From a1bf54c41703bce6240e597c6dbe6c43196d0db4 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sun, 19 Jul 2026 01:32:08 +0200 Subject: [PATCH] fix(walkthrough): keep sidebar crisp behind modal steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neutralize the app modal backdrop's dim/blur while the tour runs so a ringed sidebar/toolbar control behind an open modal (steps 2, 10) stays readable — the overlay's SVG spotlight is now the single source of dimming. Add a walkthrough-running body class toggled by the overlay and a CSS rule stripping [data-slot=dialog-overlay] background/backdrop-filter. Also refine coach-card placement: dialogCardPosition drops the card into the empty bottom-right corner for large modals (inventory, scan history) and beside small modals, so it never covers modal content (steps 3, 4). --- .../src/walkthrough/WalkthroughOverlay.tsx | 33 +++++++++++++++++-- frontend/src/walkthrough/walkthrough.css | 10 ++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/frontend/src/walkthrough/WalkthroughOverlay.tsx b/frontend/src/walkthrough/WalkthroughOverlay.tsx index 7d08002..05e5359 100644 --- a/frontend/src/walkthrough/WalkthroughOverlay.tsx +++ b/frontend/src/walkthrough/WalkthroughOverlay.tsx @@ -36,6 +36,23 @@ function cardPosition(rect: DOMRect | null, placement: StepPlacement): { left: n } } +/** + * Position the card relative to an OPEN MODAL (the step's real subject). + * Large modals (inventory, scan history at ~90vw) leave no room beside them, so + * the card drops into the empty bottom-right corner instead of covering content. + * Small modals get the card beside them, on whichever side has room. + */ +function dialogCardPosition(rect: DOMRect): { left: number; top: number } { + const vw = window.innerWidth + const vh = window.innerHeight + const clampT = (t: number) => Math.max(8, Math.min(t, vh - CARD_H_EST - 8)) + const isLarge = rect.width > vw * 0.6 || rect.height > vh * 0.7 + if (isLarge) return { left: vw - CARD_W - 16, top: vh - CARD_H_EST - 16 } + if (rect.left >= CARD_W + GAP * 2) return { left: rect.left - GAP - CARD_W, top: clampT(rect.top) } + if (vw - rect.right >= CARD_W + GAP * 2) return { left: rect.right + GAP, top: clampT(rect.top) } + return { left: Math.max(8, Math.min(rect.left, vw - CARD_W - 8)), top: clampT(rect.bottom + GAP) } +} + export function WalkthroughOverlay() { const { isActive, stepIndex, total, setTotal, next, prev, skip } = useWalkthroughStore() const actions = useWalkthroughActions() @@ -99,12 +116,24 @@ export function WalkthroughOverlay() { return () => window.removeEventListener('keydown', handleKey) }, [isActive, handleKey]) + // Flag the tour so app-modal backdrops drop their own dim/blur (see + // walkthrough.css) — the spotlight then dims consistently everywhere. + useEffect(() => { + if (!isActive) return + document.body.classList.add('walkthrough-running') + return () => document.body.classList.remove('walkthrough-running') + }, [isActive]) + if (!isActive || !step) return null const isLast = stepIndex >= total - 1 - // Ring / card follow the anchor if the step has one, else the open modal. + // Ring the step's anchor (sidebar/toolbar button) if it has one, else the modal. const ringRect = anchorRect ?? dialogRect - const pos = cardPosition(ringRect, step.placement ?? 'auto') + // The card follows the OPEN MODAL when there is one (that's what the user reads), + // otherwise the step's anchor. Keeps the card off the modal's content. + const pos = dialogRect + ? dialogCardPosition(dialogRect) + : cardPosition(anchorRect, step.placement ?? 'auto') // Cut-outs keep both the ringed control and any open modal lit. const holes = [anchorRect, dialogRect].filter((r): r is DOMRect => r !== null) diff --git a/frontend/src/walkthrough/walkthrough.css b/frontend/src/walkthrough/walkthrough.css index 448a18f..7db09fa 100644 --- a/frontend/src/walkthrough/walkthrough.css +++ b/frontend/src/walkthrough/walkthrough.css @@ -22,6 +22,16 @@ 50% { outline: 7px solid rgba(0, 212, 255, 0); } } +/* While the tour runs, app modals must not add their own dim/blur — the overlay's + SVG spotlight is the single source of dimming, so a ringed sidebar/toolbar + control behind an open modal stays crisp instead of being double-dimmed and + blurred by the modal's own backdrop. */ +body.walkthrough-running [data-slot="dialog-overlay"] { + background: transparent !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + @media (prefers-reduced-motion: reduce) { .walkthrough-invite-enter, .walkthrough-card-enter,