@@ -1979,7 +1979,13 @@ function SimulationPanel({
19791979 { showProgressDetail && (
19801980 < div className = "simulation-panel__progress-detail" aria-live = "polite" >
19811981 { progressEntries . map ( ( entry ) => (
1982- < p key = { entry . role } className = "simulation-panel__progress-line" >
1982+ < p
1983+ key = { entry . role }
1984+ className = { [
1985+ 'simulation-panel__progress-line' ,
1986+ entry . isComplete ? 'is-complete' : 'is-waiting' ,
1987+ ] . filter ( Boolean ) . join ( ' ' ) }
1988+ >
19831989 • { entry . label } ({ entry . done } /{ entry . total } )
19841990 </ p >
19851991 ) ) }
@@ -2350,6 +2356,14 @@ function App() {
23502356 const [ goodsPreset , setGoodsPreset ] = useState ( { term : '' , nonce : 0 } ) ;
23512357 const simulationEventRef = useRef ( null ) ;
23522358 const simulationPollRef = useRef ( null ) ;
2359+ const simulationColumnRef = useRef ( null ) ;
2360+ const simulationDockBaseTopRef = useRef ( null ) ;
2361+ const simulationDockOffsetRef = useRef ( null ) ;
2362+ const simulationDockRafRef = useRef ( null ) ;
2363+ const simulationDockTickingRef = useRef ( false ) ;
2364+ const simulationDockCurrentTopRef = useRef ( null ) ;
2365+ const simulationDockTargetTopRef = useRef ( null ) ;
2366+ const simulationDockAnimatingRef = useRef ( false ) ;
23532367 const copy = useMemo ( ( ) => getLandingCopy ( language ) , [ language ] ) ;
23542368 const tourCopy = useMemo ( ( ) => TOUR_CONTENT [ language ] || TOUR_CONTENT . ko , [ language ] ) ;
23552369 const tourSteps = tourCopy . steps || [ ] ;
@@ -2396,6 +2410,108 @@ function App() {
23962410 } ;
23972411 } , [ ] ) ;
23982412
2413+ useEffect ( ( ) => {
2414+ const updateDockMetrics = ( recalcBase = false ) => {
2415+ const column = simulationColumnRef . current ;
2416+ if ( ! column ) {
2417+ return ;
2418+ }
2419+ const rect = column . getBoundingClientRect ( ) ;
2420+ const root = document . documentElement ;
2421+ const container = column . closest ( 'main.container' ) || document . querySelector ( 'main.container' ) ;
2422+ let top = 32 ;
2423+ if ( recalcBase || simulationDockBaseTopRef . current == null ) {
2424+ simulationDockBaseTopRef . current = rect . top ;
2425+ if ( container ) {
2426+ const containerRect = container . getBoundingClientRect ( ) ;
2427+ simulationDockOffsetRef . current = rect . top - containerRect . top ;
2428+ } else {
2429+ simulationDockOffsetRef . current = rect . top ;
2430+ }
2431+ }
2432+ const baseTop = simulationDockBaseTopRef . current ?? rect . top ;
2433+ const baseOffset = simulationDockOffsetRef . current ?? 0 ;
2434+ const viewportHeight = window . visualViewport ?. height || window . innerHeight || rect . height ;
2435+ const centeredTop = Math . round ( ( viewportHeight - rect . height ) / 2 ) ;
2436+ if ( container ) {
2437+ const containerRect = container . getBoundingClientRect ( ) ;
2438+ const minTop = Number . isFinite ( containerRect . top )
2439+ ? containerRect . top + baseOffset
2440+ : top ;
2441+ const maxTop = Number . isFinite ( containerRect . bottom )
2442+ ? containerRect . bottom - rect . height - baseOffset
2443+ : minTop ;
2444+ if ( maxTop < minTop ) {
2445+ top = minTop ;
2446+ } else {
2447+ const targetTop = containerRect . top < 0 ? centeredTop : baseTop ;
2448+ top = Math . min ( Math . max ( targetTop , minTop ) , maxTop ) ;
2449+ }
2450+ } else {
2451+ top = baseTop ;
2452+ }
2453+ root . style . setProperty ( '--sim-panel-left' , `${ rect . left } px` ) ;
2454+ root . style . setProperty ( '--sim-panel-width' , `${ rect . width } px` ) ;
2455+ root . style . setProperty ( '--sim-panel-height' , `${ rect . height } px` ) ;
2456+ simulationDockTargetTopRef . current = top ;
2457+ if ( simulationDockCurrentTopRef . current == null ) {
2458+ simulationDockCurrentTopRef . current = top ;
2459+ }
2460+ if ( ! simulationDockAnimatingRef . current ) {
2461+ simulationDockAnimatingRef . current = true ;
2462+ const animate = ( ) => {
2463+ const current = simulationDockCurrentTopRef . current ?? top ;
2464+ const target = simulationDockTargetTopRef . current ?? top ;
2465+ const delta = target - current ;
2466+ if ( Math . abs ( delta ) < 0.5 ) {
2467+ simulationDockCurrentTopRef . current = target ;
2468+ root . style . setProperty ( '--sim-panel-top' , `${ Math . round ( target ) } px` ) ;
2469+ simulationDockAnimatingRef . current = false ;
2470+ return ;
2471+ }
2472+ const next = current + delta * 0.35 ;
2473+ simulationDockCurrentTopRef . current = next ;
2474+ root . style . setProperty ( '--sim-panel-top' , `${ Math . round ( next ) } px` ) ;
2475+ simulationDockRafRef . current = requestAnimationFrame ( animate ) ;
2476+ } ;
2477+ simulationDockRafRef . current = requestAnimationFrame ( animate ) ;
2478+ }
2479+ } ;
2480+ const handleResize = ( ) => {
2481+ requestAnimationFrame ( ( ) => updateDockMetrics ( true ) ) ;
2482+ } ;
2483+ const handleScroll = ( ) => {
2484+ if ( simulationDockTickingRef . current ) {
2485+ return ;
2486+ }
2487+ simulationDockTickingRef . current = true ;
2488+ simulationDockRafRef . current = requestAnimationFrame ( ( ) => {
2489+ updateDockMetrics ( false ) ;
2490+ simulationDockTickingRef . current = false ;
2491+ } ) ;
2492+ } ;
2493+ updateDockMetrics ( true ) ;
2494+ window . addEventListener ( 'resize' , handleResize ) ;
2495+ window . addEventListener ( 'scroll' , handleScroll , true ) ;
2496+ const visualViewport = window . visualViewport ;
2497+ if ( visualViewport ) {
2498+ visualViewport . addEventListener ( 'resize' , handleResize ) ;
2499+ visualViewport . addEventListener ( 'scroll' , handleScroll ) ;
2500+ }
2501+ return ( ) => {
2502+ window . removeEventListener ( 'resize' , handleResize ) ;
2503+ window . removeEventListener ( 'scroll' , handleScroll , true ) ;
2504+ if ( visualViewport ) {
2505+ visualViewport . removeEventListener ( 'resize' , handleResize ) ;
2506+ visualViewport . removeEventListener ( 'scroll' , handleScroll ) ;
2507+ }
2508+ if ( simulationDockRafRef . current ) {
2509+ cancelAnimationFrame ( simulationDockRafRef . current ) ;
2510+ }
2511+ simulationDockAnimatingRef . current = false ;
2512+ } ;
2513+ } , [ ] ) ;
2514+
23992515 const startTutorial = useCallback ( ( ) => {
24002516 if ( tutorialSnapshotRef . current ) {
24012517 return ;
@@ -3355,7 +3471,7 @@ function App() {
33553471 </ div >
33563472 </ section >
33573473 </ div >
3358- < div className = "simulation-column" >
3474+ < div className = "simulation-column" ref = { simulationColumnRef } >
33593475 < SimulationPanel
33603476 hasResults = { Boolean ( response ) }
33613477 imageCount = { selectedImageCount }
0 commit comments