Skip to content

Commit 53f063e

Browse files
committed
Refine simulation panel dock motion
1 parent 990898b commit 53f063e

File tree

2 files changed

+158
-6
lines changed

2 files changed

+158
-6
lines changed

frontend/src/App.jsx

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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}

frontend/src/index.css

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
--card-bg: rgba(255, 255, 255, 0.97);
1212
--muted-border-color: rgba(32, 58, 123, 0.12);
1313
--logo-size: 118px;
14+
--sim-panel-left: 0px;
15+
--sim-panel-width: 100%;
16+
--sim-panel-height: auto;
17+
--sim-panel-top: 2rem;
1418
}
1519

1620
html,
@@ -2019,7 +2023,7 @@ main.container {
20192023
box-shadow: 0 18px 32px rgba(32, 58, 123, 0.16);
20202024
padding: 1rem 1.2rem 1.25rem;
20212025
width: 100%;
2022-
transition: all 0.25s ease;
2026+
transition: box-shadow 0.25s ease, border-color 0.25s ease, background-color 0.25s ease;
20232027
pointer-events: auto;
20242028
display: flex;
20252029
flex-direction: column;
@@ -2031,8 +2035,11 @@ main.container {
20312035
}
20322036

20332037
.simulation-panel--dock {
2034-
position: sticky;
2035-
top: 2vh;
2038+
position: fixed;
2039+
top: var(--sim-panel-top, 2rem);
2040+
left: var(--sim-panel-left, 0px);
2041+
width: var(--sim-panel-width, 100%);
2042+
height: var(--sim-panel-height, auto);
20362043
}
20372044

20382045
.simulation-panel.is-visible {
@@ -2276,6 +2283,35 @@ main.container {
22762283
color: #1f2937;
22772284
}
22782285

2286+
.simulation-panel__progress-line.is-waiting {
2287+
background-image: linear-gradient(
2288+
90deg,
2289+
rgba(15, 23, 42, 0.45) 0%,
2290+
rgba(15, 23, 42, 0.9) 35%,
2291+
rgba(15, 23, 42, 0.45) 70%
2292+
);
2293+
background-size: 220% 100%;
2294+
background-position: 100% 50%;
2295+
-webkit-background-clip: text;
2296+
background-clip: text;
2297+
-webkit-text-fill-color: transparent;
2298+
color: transparent;
2299+
animation: progressShimmer 1.6s ease-in-out infinite;
2300+
}
2301+
2302+
.simulation-panel__progress-line.is-complete {
2303+
color: #1f2937;
2304+
}
2305+
2306+
@keyframes progressShimmer {
2307+
0% {
2308+
background-position: 120% 50%;
2309+
}
2310+
100% {
2311+
background-position: -120% 50%;
2312+
}
2313+
}
2314+
22792315
.simulation-panel__elapsed {
22802316
font-size: 0.78rem;
22812317
color: #475467;
@@ -3581,7 +3617,7 @@ main.container {
35813617
font-size: 0.95rem;
35823618
}
35833619

3584-
@media (max-width: 1100px) {
3620+
@media (max-width: 768px) {
35853621
.simulation-panel--dock {
35863622
position: static;
35873623
width: 100%;

0 commit comments

Comments
 (0)