Skip to content

Overlay top-layer overhaul Phase 1: primitive + tokens + guard + StatusEditor - #58

Merged
Aztec03hub merged 5 commits into
mainfrom
overlay-top-layer-phase1
Jun 26, 2026
Merged

Overlay top-layer overhaul Phase 1: primitive + tokens + guard + StatusEditor#58
Aztec03hub merged 5 commits into
mainfrom
overlay-top-layer-phase1

Conversation

@Aztec03hub

Copy link
Copy Markdown
Owner

Overlay / top-layer overhaul — Phase 1

Replaces the ad-hoc position:fixed + manual z-index overlay pattern with a single robust primitive built on the browser native top layer, and lands the marquee fix (StatusEditor). Implements design doc sections C/D/E/F, build-sequence steps 1-3. Phase 2 (the other 16 overlays, ALLOWED shrink, bits-ui audit) is intentionally out of scope.

What's in this PR

  1. z-index token scale (src/app.css :root) — additive --z-base/sticky/banner/sidebar/panel/toast (design §D). No app-wide collapse yet (Phase 2).
  2. Primitive (src/lib/top-layer.svelte.js) — the topLayer action: modal via <dialog>.showModal(), non-modal via Popover API showPopover(); anchored positioning via getBoundingClientRect re-run on capture-phase scroll + resize; light-dismiss/Esc → onClose; restoreFocus. Capability-guard (§F.3): detects jsdom's missing showPopover/showModal and degrades gracefully (skips native show, still wires events + positioning + focus) so existing component unit tests keep mounting/passing.
    • Esc is intercepted at the overlay node because the app's keyboard.svelte.js preventDefault()s window Escape, which would otherwise suppress native popover dismissal.
  3. Wrappers<Modal> (real <dialog> + native ::backdrop) and <Popover> (anchored, light-dismiss div), per §C.
  4. Tier-1 guard (web/tests/overlay-top-layer-guard.spec.js) — two ACTIVE rules (ban raw z-index outside var(--z-*); overlay-shape-without-primitive), with an explicit ALLOWED seed keeping today's unmigrated overlays green. StatusEditor is NOT allowed — it passes on merit. Phase 2 shrinks ALLOWED toward empty.
  5. StatusEditor migrated (src/components/StatusEditor.svelte + Sidebar anchor) — from position:fixed; z-index:90/91 (trapped behind the center column / panels by Sidebar's backdrop-filter stacking context) to <Popover> in the native top layer, anchored to the identity row. Existing unit + e2e coverage preserved.
  6. Tier-2 e2e (web/e2e/scenarios/14-overlay-top-layer.spec.ts) — opens StatusEditor over a backdrop-filter artifact panel and asserts visible + on-top (center hit-test) + :popover-open, plus Esc/outside-click close with focus return. Scenario 06 strengthened with a :popover-open assertion.

Verification

  • pnpm --dir web test — 100 files / 1332 tests, 0 failures, 0 skips (run 3x, stable). 0 stderr.
  • pnpm --dir web build — green.
  • Svelte MCP autofixer — clean on every edited .svelte/.svelte.js.
  • Playwright (real Chromium): 14-overlay-top-layer + 06-status-editor = 25 passed (StatusEditor on-top + :popover-open + Esc/outside-click + focus-return verified); --grep "source-level invariants" = 55 passed.
  • No version bump.

🤖 Generated with Claude Code

…Modal/Popover wrappers

Add the --z-* residual token scale to app.css :root (design §D, additive)
and the native top-layer primitive:

- src/lib/top-layer.svelte.js: the topLayer Svelte action. modal -> <dialog>
  showModal(); non-modal -> Popover API showPopover(). Anchored positioning
  via getBoundingClientRect re-run on capture-phase scroll + resize.
  Light-dismiss/Esc/cancel -> onClose; restoreFocus on close.
  CRITICAL capability-guard (design §F.3): jsdom 29.0.2 has no showPopover/
  showModal/popover/elementFromPoint, so the action detects their absence
  and degrades gracefully (skip native show, still wire events + position +
  focus) so component unit tests still mount in jsdom.
- src/components/Popover.svelte / Modal.svelte: thin wrappers per design §C.
tests/overlay-top-layer-guard.spec.js (design §E Tier-1). Two ACTIVE rules:
(1) overlay-shaped components must reference the primitive; (2) ban raw
z-index outside the var(--z-*) token scale. ALLOWED is seeded to today's
tree (all currently-unmigrated overlays + toasts/banners + docked panels +
bits-ui allowlist + app.css) so the pre-migration tree stays green.
StatusEditor is deliberately NOT in ALLOWED: it passes on merit by using
<Popover>/use:topLayer and carrying no bare z-index. Phase 2 shrinks
ALLOWED toward empty.
…arquee fix)

StatusEditor previously used position:fixed; z-index:90/91 while rendered
inside Sidebar's .sidebar-left (backdrop-filter => own stacking context),
so it painted BEHIND the center column and right-side panels. Migrate it to
<Popover> (Popover API top layer), anchored to the identity row, which
escapes every ancestor stacking context. Removes the backdrop trap, the
position:fixed, and the z-index. Sidebar binds the profile-status row as the
anchor and passes it through. Existing StatusEditor unit + e2e specs keep
passing via the action's jsdom capability-guard.
e2e/scenarios/14-overlay-top-layer.spec.ts (design §E Tier-2): open
StatusEditor while a backdrop-filter artifact panel is open and assert it is
visible AND on-top (center hit-test via expectLocatorOnTop) AND
:popover-open, plus Esc/outside-click light-dismiss closes it and focus
returns to the trigger. Strengthen 06-status-editor.spec.ts with a
:popover-open assertion confirming the primitive promoted the editor.
…Escape)

The app's keyboard.svelte.js registers a window keydown listener that calls
event.preventDefault() on Escape (universal-close), which SUPPRESSES the
browser's native popover/dialog Esc light-dismiss. Intercept Escape on the
overlay node (bubbles there before window), stopPropagation so only the
topmost overlay closes, then drive our own close + onClose. Add a notify
latch so onClose fires exactly once across the racing native signals
(keydown -> close -> hidePopover -> toggle). Verified by e2e 06 + 14 Esc
tests in real Chromium.
@Aztec03hub
Aztec03hub merged commit d6abd80 into main Jun 26, 2026
8 checks passed
@Aztec03hub
Aztec03hub deleted the overlay-top-layer-phase1 branch June 26, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant