@@ -20,10 +20,14 @@ import {
2020 ZOOM_LEVELS ,
2121 DEFAULT_ZOOM_INDEX ,
2222 COLOR_PALETTE ,
23+ DEFAULT_MIN_YEAR ,
24+ DEFAULT_MAX_YEAR ,
25+ DRAG_THRESHOLD_PX ,
2326} from "./constants" ;
2427import {
2528 parseDate ,
2629 formatDate ,
30+ normalizeDateString ,
2731 addDays ,
2832 getDaysDiff ,
2933 getMonthYear ,
@@ -44,13 +48,43 @@ import { calculateTrackLanes } from "./utils/eventLanes";
4448
4549// -- Main App Component --
4650
51+ const normalizeEventDates = ( event : IEvent ) : IEvent => {
52+ const startDate = normalizeDateString ( event . startDate ) || event . startDate ;
53+ const endDate =
54+ normalizeDateString ( event . endDate || event . startDate ) ||
55+ event . endDate ||
56+ event . startDate ;
57+ return {
58+ ...event ,
59+ startDate,
60+ endDate,
61+ } ;
62+ } ;
63+
64+ const normalizeEventList = ( events : IEvent [ ] ) =>
65+ events . map ( ( event ) => normalizeEventDates ( event ) ) ;
66+
4767export default function App ( ) {
4868 // -- State --
4969 const [ data , setData ] = useState < ITimelineData > ( ( ) => {
5070 const saved = localStorage . getItem ( "chrono_data" ) ;
51- return saved
52- ? JSON . parse ( saved )
53- : { tracks : DEFAULT_TRACKS , events : DEFAULT_EVENTS } ;
71+ if ( saved ) {
72+ try {
73+ const parsed = JSON . parse ( saved ) ;
74+ if ( parsed ?. tracks && parsed ?. events ) {
75+ return {
76+ tracks : parsed . tracks ,
77+ events : normalizeEventList ( parsed . events ) ,
78+ } ;
79+ }
80+ } catch {
81+ // fall through to defaults
82+ }
83+ }
84+ return {
85+ tracks : DEFAULT_TRACKS ,
86+ events : normalizeEventList ( DEFAULT_EVENTS ) ,
87+ } ;
5488 } ) ;
5589
5690 const [ zoomIndex , setZoomIndex ] = useState ( DEFAULT_ZOOM_INDEX ) ;
@@ -124,11 +158,11 @@ export default function App() {
124158 // fall through to defaults
125159 }
126160 }
127- return { minYear : 2020 , maxYear : 2030 } ;
161+ return { minYear : DEFAULT_MIN_YEAR , maxYear : DEFAULT_MAX_YEAR } ;
128162 } ) ;
129163 const [ boundsDraft , setBoundsDraft ] = useState < IViewBounds > ( ( ) => ( {
130- minYear : 2020 ,
131- maxYear : 2030 ,
164+ minYear : DEFAULT_MIN_YEAR ,
165+ maxYear : DEFAULT_MAX_YEAR ,
132166 } ) ) ;
133167
134168 const timelineRange = useMemo ( ( ) => {
@@ -185,6 +219,26 @@ export default function App() {
185219 }
186220 } , [ zoomIndex , timelineRange , pixelsPerDay ] ) ;
187221
222+ // Scroll to center on today's date on initial load
223+ const hasScrolledToTodayRef = useRef ( false ) ;
224+ useLayoutEffect ( ( ) => {
225+ if ( ! hasScrolledToTodayRef . current && scrollContainerRef . current ) {
226+ const container = scrollContainerRef . current ;
227+ const today = new Date ( ) ;
228+ today . setHours ( 0 , 0 , 0 , 0 ) ; // Normalize to midnight
229+
230+ // Calculate days from timeline start to today
231+ const daysFromStart = getDaysDiff ( timelineRange . min , today ) ;
232+ const todayPixel = daysFromStart * pixelsPerDay ;
233+
234+ // Center the view on today
235+ const newScrollLeft = todayPixel - container . clientWidth / 2 ;
236+ container . scrollLeft = Math . max ( 0 , newScrollLeft ) ;
237+
238+ hasScrolledToTodayRef . current = true ;
239+ }
240+ } , [ timelineRange , pixelsPerDay ] ) ;
241+
188242 // -- Panning Logic --
189243 useEffect ( ( ) => {
190244 const handleGlobalMouseMove = ( e : MouseEvent ) => {
@@ -240,7 +294,7 @@ export default function App() {
240294 }
241295 } ;
242296
243- const handleGlobalMouseUp = ( ) => {
297+ const handleGlobalMouseUp = ( e : MouseEvent ) => {
244298 // Panning Cleanup
245299 if ( panStateRef . current . isActive ) {
246300 panStateRef . current . isActive = false ;
@@ -254,13 +308,27 @@ export default function App() {
254308 // Event Dragging Cleanup
255309 const state = dragStateRef . current ;
256310 if ( state . isDragging && state . eventId ) {
257- const deltaX = state . currentX - state . startX ;
258-
259- if (
260- Math . abs ( deltaX ) > 2 ||
311+ // Use e.clientX directly to avoid race condition with React state updates
312+ const deltaX = e . clientX - state . startX ;
313+ const deltaY = e . clientY - state . startY ;
314+ const movedEnough =
315+ Math . abs ( deltaX ) > DRAG_THRESHOLD_PX ||
261316 ( state . mode === "move" &&
262- state . targetTrackId !== state . initialTrackId )
263- ) {
317+ Math . abs ( deltaY ) > DRAG_THRESHOLD_PX ) ;
318+
319+ // Detect target track from actual mouse position at release
320+ let finalTargetTrackId = state . initialTrackId ;
321+ if ( state . mode === "move" ) {
322+ const el = document . elementFromPoint ( e . clientX , e . clientY ) ;
323+ const trackRow = el ?. closest ( "[data-track-id]" ) ;
324+ if ( trackRow ) {
325+ finalTargetTrackId =
326+ trackRow . getAttribute ( "data-track-id" ) ||
327+ state . initialTrackId ;
328+ }
329+ }
330+
331+ if ( movedEnough ) {
264332 setSuppressClick ( true ) ;
265333 setTimeout ( ( ) => setSuppressClick ( false ) , 50 ) ;
266334
@@ -281,8 +349,7 @@ export default function App() {
281349 parseDate ( state . initialEndDate )
282350 ) ;
283351 newEndDate = addDays ( newStartDate , duration ) ;
284- if ( state . targetTrackId )
285- newTrackId = state . targetTrackId ;
352+ newTrackId = finalTargetTrackId ;
286353 } else if ( state . mode === "resize-start" ) {
287354 newStartDate = addDays (
288355 parseDate ( state . initialStartDate ) ,
@@ -381,6 +448,7 @@ export default function App() {
381448 event : IEvent ,
382449 mode : DragMode
383450 ) => {
451+ if ( e . button !== 0 ) return ;
384452 e . stopPropagation ( ) ;
385453 // Note: Don't call e.preventDefault() here as it blocks double-click events
386454
@@ -471,6 +539,26 @@ export default function App() {
471539 }
472540 } ;
473541
542+ const handleReorderTracks = ( fromIndex : number , toIndex : number ) => {
543+ if ( fromIndex === toIndex ) return ;
544+
545+ setData ( ( prev ) => {
546+ const sortedTracks = [ ...prev . tracks ] . sort (
547+ ( a , b ) => a . order - b . order
548+ ) ;
549+ const [ movedTrack ] = sortedTracks . splice ( fromIndex , 1 ) ;
550+ sortedTracks . splice ( toIndex , 0 , movedTrack ) ;
551+
552+ // Update order property for all tracks
553+ const updatedTracks = sortedTracks . map ( ( track , index ) => ( {
554+ ...track ,
555+ order : index ,
556+ } ) ) ;
557+
558+ return { ...prev , tracks : updatedTracks } ;
559+ } ) ;
560+ } ;
561+
474562 const handleAddEvent = ( trackId : string , dateStr ?: string ) => {
475563 const startDate = dateStr ? parseDate ( dateStr ) : new Date ( ) ;
476564 const endDate = addDays ( startDate , 7 ) ; // Default 7-day duration
@@ -489,17 +577,17 @@ export default function App() {
489577
490578 const handleEditEvent = ( event : IEvent ) => {
491579 if ( suppressClick ) return ;
492- setEditingEvent ( { ... event } ) ;
580+ setEditingEvent ( normalizeEventDates ( event ) ) ;
493581 setActiveModal ( ModalType . EDIT_EVENT ) ;
494582 } ;
495583
496584 const handleSaveEvent = ( ) => {
497585 if ( ! editingEvent ) return ;
498586 // Ensure endDate is always set (default to startDate if empty)
499- const eventToSave = {
587+ const eventToSave = normalizeEventDates ( {
500588 ...editingEvent ,
501589 endDate : editingEvent . endDate || editingEvent . startDate ,
502- } ;
590+ } ) ;
503591 setData ( ( prev ) => {
504592 const exists = prev . events . find ( ( e ) => e . id === eventToSave . id ) ;
505593 if ( exists ) {
@@ -543,7 +631,10 @@ export default function App() {
543631 try {
544632 const imported = JSON . parse ( evt . target ?. result as string ) ;
545633 if ( imported . tracks && imported . events ) {
546- setData ( imported ) ;
634+ setData ( {
635+ tracks : imported . tracks ,
636+ events : normalizeEventList ( imported . events ) ,
637+ } ) ;
547638 } else {
548639 alert ( "Invalid JSON format" ) ;
549640 }
@@ -850,6 +941,13 @@ export default function App() {
850941 if ( ! dragState . isDragging || ! dragState . eventId ) return null ;
851942
852943 const deltaX = dragState . currentX - dragState . startX ;
944+ const deltaY = dragState . currentY - dragState . startY ;
945+ const movedEnough =
946+ dragState . mode === "move"
947+ ? Math . abs ( deltaX ) > DRAG_THRESHOLD_PX ||
948+ Math . abs ( deltaY ) > DRAG_THRESHOLD_PX
949+ : Math . abs ( deltaX ) > DRAG_THRESHOLD_PX ;
950+ if ( ! movedEnough ) return null ;
853951 const deltaDays = Math . round ( deltaX / pixelsPerDay ) ;
854952 const initialStart = parseDate ( dragState . initialStartDate ) ;
855953 const initialEnd = parseDate ( dragState . initialEndDate ) ;
@@ -966,6 +1064,12 @@ export default function App() {
9661064 [ data . events , data . tracks ]
9671065 ) ;
9681066
1067+ // Sort tracks by order for consistent display
1068+ const sortedTracks = useMemo (
1069+ ( ) => [ ...data . tracks ] . sort ( ( a , b ) => a . order - b . order ) ,
1070+ [ data . tracks ]
1071+ ) ;
1072+
9691073 const isTrackModalOpen = activeModal === ModalType . EDIT_TRACK ;
9701074 const isEventModalOpen = activeModal === ModalType . EDIT_EVENT ;
9711075 const isDataModalOpen = activeModal === ModalType . IMPORT_EXPORT ;
@@ -987,14 +1091,15 @@ export default function App() {
9871091 < div className = "flex-1 overflow-hidden relative flex flex-col cursor-default" >
9881092 < div className = "flex-1 flex overflow-hidden" >
9891093 < TrackSidebar
990- tracks = { data . tracks }
1094+ tracks = { sortedTracks }
9911095 trackLaneInfo = { trackLaneInfo }
9921096 onAddTrack = { handleAddTrack }
9931097 onEditTrack = { handleEditTrack }
1098+ onReorderTracks = { handleReorderTracks }
9941099 />
9951100
9961101 < TimelineCanvas
997- tracks = { data . tracks }
1102+ tracks = { sortedTracks }
9981103 trackLaneInfo = { trackLaneInfo }
9991104 dragState = { dragState }
10001105 isPanning = { isPanning }
0 commit comments