fix(walkthrough): keep sidebar crisp behind modal steps

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).
This commit is contained in:
Pouzor
2026-07-19 01:32:08 +02:00
parent 8c56921375
commit a1bf54c417
2 changed files with 41 additions and 2 deletions
@@ -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() { export function WalkthroughOverlay() {
const { isActive, stepIndex, total, setTotal, next, prev, skip } = useWalkthroughStore() const { isActive, stepIndex, total, setTotal, next, prev, skip } = useWalkthroughStore()
const actions = useWalkthroughActions() const actions = useWalkthroughActions()
@@ -99,12 +116,24 @@ export function WalkthroughOverlay() {
return () => window.removeEventListener('keydown', handleKey) return () => window.removeEventListener('keydown', handleKey)
}, [isActive, 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 if (!isActive || !step) return null
const isLast = stepIndex >= total - 1 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 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. // Cut-outs keep both the ringed control and any open modal lit.
const holes = [anchorRect, dialogRect].filter((r): r is DOMRect => r !== null) const holes = [anchorRect, dialogRect].filter((r): r is DOMRect => r !== null)
+10
View File
@@ -22,6 +22,16 @@
50% { outline: 7px solid rgba(0, 212, 255, 0); } 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) { @media (prefers-reduced-motion: reduce) {
.walkthrough-invite-enter, .walkthrough-invite-enter,
.walkthrough-card-enter, .walkthrough-card-enter,