@@ -6,6 +6,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
66
77import { ParseError , parseDeck } from '@/ir/parse' ;
88import { planDeck } from '@/ir/plan' ;
9+ import { reorderSlide } from '@/ir/source-edit' ;
910import type { Brand , Deck , Density , Mode , ThemeRef } from '@/ir/schema' ;
1011import { createAsset , assetSrc } from '@/storage/asset-store' ;
1112import { getDeck , type StoredDeck , updateDeck } from '@/storage/deck-store' ;
@@ -164,6 +165,11 @@ export function Editor({ deckId }: Props) {
164165 setSelectedSlide ( index ) ;
165166 } , [ ] ) ;
166167
168+ const handleReorderSlide = useCallback ( ( from : number , to : number ) => {
169+ setSource ( ( s ) => reorderSlide ( s , from , to ) ) ;
170+ setSelectedSlide ( to ) ;
171+ } , [ ] ) ;
172+
167173 const handleInsert = useCallback ( ( snippet : string ) => {
168174 insertRef . current ?.( snippet ) ;
169175 } , [ ] ) ;
@@ -315,6 +321,7 @@ export function Editor({ deckId }: Props) {
315321 deck = { result . deck }
316322 selectedSlide = { selectedSlide }
317323 onSelectSlide = { handleSelectSlide }
324+ onReorderSlide = { handleReorderSlide }
318325 />
319326 ) : (
320327 < div className = "editor__error" >
@@ -426,10 +433,12 @@ function PreviewStage({
426433 deck,
427434 selectedSlide,
428435 onSelectSlide,
436+ onReorderSlide,
429437} : {
430438 deck : Deck ;
431439 selectedSlide : number ;
432440 onSelectSlide : ( i : number ) => void ;
441+ onReorderSlide : ( from : number , to : number ) => void ;
433442} ) {
434443 const total = deck . slides . length ;
435444 const safeIndex = Math . min ( Math . max ( selectedSlide , 0 ) , Math . max ( total - 1 , 0 ) ) ;
@@ -461,7 +470,12 @@ function PreviewStage({
461470 < DeckRenderer deck = { visibleDeck } />
462471 </ div >
463472 </ div >
464- < ThumbStrip deck = { deck } selectedIndex = { safeIndex } onSelect = { onSelectSlide } />
473+ < ThumbStrip
474+ deck = { deck }
475+ selectedIndex = { safeIndex }
476+ onSelect = { onSelectSlide }
477+ onReorder = { onReorderSlide }
478+ />
465479 </ div >
466480 ) ;
467481}
@@ -470,13 +484,17 @@ function ThumbStrip({
470484 deck,
471485 selectedIndex,
472486 onSelect,
487+ onReorder,
473488} : {
474489 deck : Deck ;
475490 selectedIndex : number ;
476491 onSelect : ( i : number ) => void ;
492+ onReorder : ( from : number , to : number ) => void ;
477493} ) {
478494 const total = deck . slides . length ;
479495 const activeRef = useRef < HTMLButtonElement > ( null ) ;
496+ const [ dragIndex , setDragIndex ] = useState < number | null > ( null ) ;
497+ const [ overIndex , setOverIndex ] = useState < number | null > ( null ) ;
480498
481499 const singles = useMemo < Deck [ ] > (
482500 ( ) => deck . slides . map ( ( slide ) => ( { ...deck , slides : [ slide ] } ) ) ,
@@ -510,7 +528,26 @@ function ThumbStrip({
510528 index = { i }
511529 single = { singles [ i ] }
512530 active = { active }
531+ dragging = { dragIndex === i }
532+ over = { overIndex === i && dragIndex !== null && dragIndex !== i }
513533 onSelect = { onSelect }
534+ onDragStart = { ( ) => setDragIndex ( i ) }
535+ onDragEnter = { ( ) => {
536+ if ( dragIndex !== null ) setOverIndex ( i ) ;
537+ } }
538+ onDragOver = { ( e ) => {
539+ if ( dragIndex !== null ) e . preventDefault ( ) ;
540+ } }
541+ onDrop = { ( e ) => {
542+ e . preventDefault ( ) ;
543+ if ( dragIndex !== null && dragIndex !== i ) onReorder ( dragIndex , i ) ;
544+ setDragIndex ( null ) ;
545+ setOverIndex ( null ) ;
546+ } }
547+ onDragEnd = { ( ) => {
548+ setDragIndex ( null ) ;
549+ setOverIndex ( null ) ;
550+ } }
514551 />
515552 ) ;
516553 } ) }
@@ -523,20 +560,58 @@ type ThumbProps = {
523560 index : number ;
524561 single : Deck ;
525562 active : boolean ;
563+ dragging ?: boolean ;
564+ over ?: boolean ;
526565 onSelect : ( i : number ) => void ;
566+ onDragStart ?: ( ) => void ;
567+ onDragEnter ?: ( ) => void ;
568+ onDragOver ?: ( e : React . DragEvent < HTMLButtonElement > ) => void ;
569+ onDrop ?: ( e : React . DragEvent < HTMLButtonElement > ) => void ;
570+ onDragEnd ?: ( ) => void ;
527571 ref ?: React . Ref < HTMLButtonElement > ;
528572} ;
529573
530- const Thumb = memo ( function Thumb ( { index, single, active, onSelect, ref } : ThumbProps ) {
574+ const Thumb = memo ( function Thumb ( {
575+ index,
576+ single,
577+ active,
578+ dragging,
579+ over,
580+ onSelect,
581+ onDragStart,
582+ onDragEnter,
583+ onDragOver,
584+ onDrop,
585+ onDragEnd,
586+ ref,
587+ } : ThumbProps ) {
588+ const cls = [
589+ 'thumb-strip__item' ,
590+ active ? 'thumb-strip__item--active' : '' ,
591+ dragging ? 'thumb-strip__item--dragging' : '' ,
592+ over ? 'thumb-strip__item--over' : '' ,
593+ ]
594+ . filter ( Boolean )
595+ . join ( ' ' ) ;
531596 return (
532597 < button
533598 ref = { ref }
534599 type = "button"
535600 role = "tab"
536601 aria-selected = { active }
537602 aria-label = { `Slide ${ index + 1 } ` }
538- className = { `thumb-strip__item ${ active ? ' thumb-strip__item--active' : '' } ` }
603+ className = { cls }
539604 onClick = { ( ) => onSelect ( index ) }
605+ draggable
606+ onDragStart = { ( e ) => {
607+ e . dataTransfer . effectAllowed = 'move' ;
608+ e . dataTransfer . setData ( 'text/plain' , String ( index ) ) ;
609+ onDragStart ?.( ) ;
610+ } }
611+ onDragEnter = { onDragEnter }
612+ onDragOver = { onDragOver }
613+ onDrop = { onDrop }
614+ onDragEnd = { onDragEnd }
540615 >
541616 < span className = "thumb-strip__num" > { String ( index + 1 ) . padStart ( 2 , '0' ) } </ span >
542617 < div className = "thumb-strip__frame" >
0 commit comments