@@ -202,6 +202,8 @@ const DEFAULT_VIEWPORT: Viewport = { x: -22, y: 42, zoom: 1.24 };
202202const GRAPH_FONT_SCALE_STORAGE_KEY = 'acn-ui-graph-font-scale' ;
203203const CANVAS_VIEWPORT_STORAGE_KEY = 'acn-ui-canvas-viewport' ;
204204const SIDEBAR_WIDTH_STORAGE_KEY = 'acn-ui-sidebar-width' ;
205+ const CAPTION_CARD_SCALE_STORAGE_KEY = 'acn-ui-caption-card-scale' ;
206+ const CAPTION_OFFSET_STORAGE_KEY = 'acn-ui-caption-offset' ;
205207const CONTINUOUS_PLAY_STORAGE_KEY = 'acn-ui-continuous-play' ;
206208const PLAY_SPEED_STORAGE_KEY = 'acn-ui-play-speed' ;
207209const DEFAULT_SIDEBAR_WIDTH = 487 ;
@@ -210,6 +212,10 @@ const MAX_SIDEBAR_WIDTH = 560;
210212const DEFAULT_GRAPH_FONT_SCALE = 1.2 ;
211213const MIN_GRAPH_FONT_SCALE = 0.9 ;
212214const MAX_GRAPH_FONT_SCALE = 1.6 ;
215+ const DEFAULT_CAPTION_CARD_SCALE = 1 ;
216+ const MIN_CAPTION_CARD_SCALE = 0.85 ;
217+ const MAX_CAPTION_CARD_SCALE = 1.35 ;
218+ const DEFAULT_CAPTION_OFFSET = { x : 0 , y : 0 } ;
213219const GRAPH_ZOOM_STEP = 1.03 ;
214220const CONTINUOUS_PLAY_GATE_DELAY_MS = 1000 ;
215221const DEFAULT_PLAY_SPEED = 1 ;
@@ -273,6 +279,7 @@ const CATALOG_KEY_ALIASES: Record<string, string> = {
273279 showScript : 'ui.settings.showScript' ,
274280 hideScript : 'ui.settings.hideScript' ,
275281 graphTextSize : 'ui.settings.graphTextSize' ,
282+ captionCardSize : 'ui.settings.captionCardSize' ,
276283 graphTextScaleValue : 'ui.settings.graphTextScaleValue' ,
277284 playSpeed : 'ui.settings.playSpeed' ,
278285 playSpeedValue : 'ui.settings.playSpeedValue' ,
@@ -381,6 +388,36 @@ function getInitialCanvasViewport(): Viewport {
381388 }
382389}
383390
391+ function getInitialCaptionCardScale ( ) {
392+ if ( typeof window === 'undefined' ) {
393+ return DEFAULT_CAPTION_CARD_SCALE ;
394+ }
395+ const raw = Number . parseFloat ( window . localStorage . getItem ( CAPTION_CARD_SCALE_STORAGE_KEY ) ?? '' ) ;
396+ if ( ! Number . isFinite ( raw ) ) {
397+ return DEFAULT_CAPTION_CARD_SCALE ;
398+ }
399+ return Math . min ( MAX_CAPTION_CARD_SCALE , Math . max ( MIN_CAPTION_CARD_SCALE , raw ) ) ;
400+ }
401+
402+ function getInitialCaptionOffset ( ) {
403+ if ( typeof window === 'undefined' ) {
404+ return DEFAULT_CAPTION_OFFSET ;
405+ }
406+ try {
407+ const raw = window . localStorage . getItem ( CAPTION_OFFSET_STORAGE_KEY ) ;
408+ if ( ! raw ) {
409+ return DEFAULT_CAPTION_OFFSET ;
410+ }
411+ const parsed = JSON . parse ( raw ) as Partial < typeof DEFAULT_CAPTION_OFFSET > ;
412+ if ( typeof parsed . x !== 'number' || typeof parsed . y !== 'number' ) {
413+ return DEFAULT_CAPTION_OFFSET ;
414+ }
415+ return { x : parsed . x , y : parsed . y } ;
416+ } catch {
417+ return DEFAULT_CAPTION_OFFSET ;
418+ }
419+ }
420+
384421function getInitialSidebarWidth ( ) {
385422 if ( typeof window === 'undefined' ) {
386423 return DEFAULT_SIDEBAR_WIDTH ;
@@ -2325,6 +2362,8 @@ function Dashboard() {
23252362 const [ lastBackendPollAt , setLastBackendPollAt ] = useState < number | null > ( null ) ;
23262363 const [ canvasViewport , setCanvasViewport ] = useState < Viewport > ( ( ) => getInitialCanvasViewport ( ) ) ;
23272364 const [ graphFontScale , setGraphFontScale ] = useState ( ( ) => getInitialGraphFontScale ( ) ) ;
2365+ const [ captionCardScale , setCaptionCardScale ] = useState ( ( ) => getInitialCaptionCardScale ( ) ) ;
2366+ const [ captionOffset , setCaptionOffset ] = useState ( ( ) => getInitialCaptionOffset ( ) ) ;
23282367 const [ playSpeed , setPlaySpeed ] = useState ( ( ) => getInitialPlaySpeed ( ) ) ;
23292368 const [ continuousPlay , setContinuousPlay ] = useState ( ( ) => getInitialContinuousPlay ( ) ) ;
23302369 const [ backendEnabled , setBackendEnabled ] = useState ( ( ) => ! initialDebugMode ) ;
@@ -2357,6 +2396,8 @@ function Dashboard() {
23572396 const playtimeAccumulatedRef = useRef ( 0 ) ;
23582397 const reactFlowRef = useRef < ReactFlowInstance < Node , Edge > | null > ( null ) ;
23592398 const sidebarResizeStartRef = useRef < { pointerX : number ; width : number } | null > ( null ) ;
2399+ const captionDragStartRef = useRef < { pointerX : number ; pointerY : number ; x : number ; y : number } | null > ( null ) ;
2400+ const [ captionDragging , setCaptionDragging ] = useState ( false ) ;
23602401
23612402 useEffect ( ( ) => {
23622403 scriptRef . current = scriptDoc ;
@@ -2419,6 +2460,18 @@ function Dashboard() {
24192460 }
24202461 } , [ graphFontScale ] ) ;
24212462
2463+ useEffect ( ( ) => {
2464+ if ( typeof window !== 'undefined' ) {
2465+ window . localStorage . setItem ( CAPTION_CARD_SCALE_STORAGE_KEY , String ( captionCardScale ) ) ;
2466+ }
2467+ } , [ captionCardScale ] ) ;
2468+
2469+ useEffect ( ( ) => {
2470+ if ( typeof window !== 'undefined' ) {
2471+ window . localStorage . setItem ( CAPTION_OFFSET_STORAGE_KEY , JSON . stringify ( captionOffset ) ) ;
2472+ }
2473+ } , [ captionOffset ] ) ;
2474+
24222475 useEffect ( ( ) => {
24232476 if ( typeof window !== 'undefined' ) {
24242477 window . localStorage . setItem ( PLAY_SPEED_STORAGE_KEY , String ( playSpeed ) ) ;
@@ -2446,20 +2499,36 @@ function Dashboard() {
24462499 useEffect ( ( ) => {
24472500 const handlePointerMove = ( event : PointerEvent ) => {
24482501 const start = sidebarResizeStartRef . current ;
2449- if ( ! start ) {
2502+ if ( start ) {
2503+ const nextWidth = start . width + ( start . pointerX - event . clientX ) ;
2504+ setSidebarWidth ( Math . min ( MAX_SIDEBAR_WIDTH , Math . max ( MIN_SIDEBAR_WIDTH , nextWidth ) ) ) ;
2505+ return ;
2506+ }
2507+ const captionStart = captionDragStartRef . current ;
2508+ if ( ! captionStart ) {
24502509 return ;
24512510 }
2452- const nextWidth = start . width + ( start . pointerX - event . clientX ) ;
2453- setSidebarWidth ( Math . min ( MAX_SIDEBAR_WIDTH , Math . max ( MIN_SIDEBAR_WIDTH , nextWidth ) ) ) ;
2511+ setCaptionOffset ( {
2512+ x : captionStart . x + ( event . clientX - captionStart . pointerX ) ,
2513+ y : captionStart . y + ( event . clientY - captionStart . pointerY ) ,
2514+ } ) ;
24542515 } ;
24552516
24562517 const handlePointerUp = ( ) => {
2457- if ( ! sidebarResizeStartRef . current ) {
2458- return ;
2518+ let released = false ;
2519+ if ( sidebarResizeStartRef . current ) {
2520+ sidebarResizeStartRef . current = null ;
2521+ released = true ;
2522+ }
2523+ if ( captionDragStartRef . current ) {
2524+ captionDragStartRef . current = null ;
2525+ setCaptionDragging ( false ) ;
2526+ released = true ;
2527+ }
2528+ if ( released ) {
2529+ document . body . style . cursor = '' ;
2530+ document . body . style . userSelect = '' ;
24592531 }
2460- sidebarResizeStartRef . current = null ;
2461- document . body . style . cursor = '' ;
2462- document . body . style . userSelect = '' ;
24632532 } ;
24642533
24652534 window . addEventListener ( 'pointermove' , handlePointerMove ) ;
@@ -2651,17 +2720,36 @@ function Dashboard() {
26512720 document . body . style . userSelect = 'none' ;
26522721 } , [ sidebarCollapsed , sidebarWidth ] ) ;
26532722
2723+ const startCaptionDrag = useCallback ( ( event : React . PointerEvent < HTMLDivElement > ) => {
2724+ if ( event . button !== 0 ) {
2725+ return ;
2726+ }
2727+ captionDragStartRef . current = {
2728+ pointerX : event . clientX ,
2729+ pointerY : event . clientY ,
2730+ x : captionOffset . x ,
2731+ y : captionOffset . y ,
2732+ } ;
2733+ setCaptionDragging ( true ) ;
2734+ document . body . style . cursor = 'grabbing' ;
2735+ document . body . style . userSelect = 'none' ;
2736+ } , [ captionOffset . x , captionOffset . y ] ) ;
2737+
26542738 const resetViewState = useCallback ( ( ) => {
26552739 setSidebarCollapsed ( false ) ;
26562740 setSidebarWidth ( DEFAULT_SIDEBAR_WIDTH ) ;
26572741 setCanvasViewport ( DEFAULT_VIEWPORT ) ;
2742+ setCaptionCardScale ( DEFAULT_CAPTION_CARD_SCALE ) ;
2743+ setCaptionOffset ( DEFAULT_CAPTION_OFFSET ) ;
26582744 const instance = reactFlowRef . current ;
26592745 if ( instance ) {
26602746 void instance . setViewport ( DEFAULT_VIEWPORT , { duration : 160 } ) ;
26612747 }
26622748 if ( typeof window !== 'undefined' ) {
26632749 window . localStorage . removeItem ( CANVAS_VIEWPORT_STORAGE_KEY ) ;
26642750 window . localStorage . removeItem ( SIDEBAR_WIDTH_STORAGE_KEY ) ;
2751+ window . localStorage . removeItem ( CAPTION_CARD_SCALE_STORAGE_KEY ) ;
2752+ window . localStorage . removeItem ( CAPTION_OFFSET_STORAGE_KEY ) ;
26652753 }
26662754 } , [ ] ) ;
26672755
@@ -2983,6 +3071,8 @@ function Dashboard() {
29833071 const viewportDisplay = `x ${ Math . round ( canvasViewport . x ) } , y ${ Math . round ( canvasViewport . y ) } , z ${ canvasViewport . zoom . toFixed ( 2 ) } , panel ${ Math . round ( sidebarWidth ) } px` ;
29843072 const graphFontScalePercent = Math . round ( graphFontScale * 100 ) ;
29853073 const graphFontScaleDisplay = t ( locale , 'graphTextScaleValue' ) . replace ( '{percent}' , String ( graphFontScalePercent ) ) ;
3074+ const captionCardScalePercent = Math . round ( captionCardScale * 100 ) ;
3075+ const captionCardScaleDisplay = t ( locale , 'graphTextScaleValue' ) . replace ( '{percent}' , String ( captionCardScalePercent ) ) ;
29863076 const playSpeedDisplay = t ( locale , 'playSpeedValue' ) . replace ( '{speed}' , playSpeed . toFixed ( playSpeed % 1 === 0 ? 0 : 2 ) . replace ( / \. 0 0 $ / , '' ) . replace ( / ( \. \d ) 0 $ / , '$1' ) ) ;
29873077 const playtimeDisplay = formatPlaytime ( playtimeElapsedMs ) ;
29883078 const playbackControlTitle = playback . phase === 'running'
@@ -3094,6 +3184,21 @@ function Dashboard() {
30943184 < span className = "settings-range-value" > { graphFontScaleDisplay } </ span >
30953185 </ div >
30963186 </ label >
3187+ < label className = "settings-field" >
3188+ < span className = "settings-label" > { t ( locale , 'captionCardSize' ) } </ span >
3189+ < div className = "settings-range-row" >
3190+ < input
3191+ className = "settings-range-input"
3192+ type = "range"
3193+ min = { MIN_CAPTION_CARD_SCALE }
3194+ max = { MAX_CAPTION_CARD_SCALE }
3195+ step = { 0.05 }
3196+ value = { captionCardScale }
3197+ onChange = { ( event ) => setCaptionCardScale ( Number . parseFloat ( event . target . value ) ) }
3198+ />
3199+ < span className = "settings-range-value" > { captionCardScaleDisplay } </ span >
3200+ </ div >
3201+ </ label >
30973202 < label className = "settings-field" >
30983203 < span className = "settings-label" > { t ( locale , 'playSpeed' ) } </ span >
30993204 < div className = "settings-range-row" >
@@ -3218,10 +3323,21 @@ function Dashboard() {
32183323 < section
32193324 className = "canvas-area"
32203325 onWheel = { handleCanvasWheel }
3221- style = { { '--graph-font-scale' : String ( graphFontScale ) } as CSSProperties }
3326+ style = { {
3327+ '--graph-font-scale' : String ( graphFontScale ) ,
3328+ '--caption-card-scale' : String ( captionCardScale ) ,
3329+ '--caption-font-scale' : String ( captionCardScale ) ,
3330+ } as CSSProperties }
32223331 >
32233332 { canvasCaptionTitle && (
3224- < div className = { cn ( "canvas-caption-card" , canvasCaptionLeaving && "canvas-caption-card-leaving" ) } >
3333+ < div
3334+ className = { cn ( "canvas-caption-card" , captionDragging && "canvas-caption-card-dragging" , canvasCaptionLeaving && "canvas-caption-card-leaving" ) }
3335+ onPointerDown = { startCaptionDrag }
3336+ style = { {
3337+ left : `calc(50% + ${ captionOffset . x } px)` ,
3338+ top : `${ 18 + captionOffset . y } px` ,
3339+ } }
3340+ >
32253341 < span className = "canvas-caption-glow" aria-hidden = "true" />
32263342 < span className = "canvas-caption-aura" aria-hidden = "true" />
32273343 < span className = "canvas-caption-edge" aria-hidden = "true" />
0 commit comments