You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/CONTINUE-floating-gap-canon.md

6.1 KiB

Continuation — Floating-gap canon + menu focus-ring (2026-06-28)

Where we left off. Spun out of the eidos inherit/radius audit; converged on canonizing the trigger→panel gap of menus and killing the keyboard focus-ring that framed the whole menu panel.

✅ RESOLVED 2026-06-29

The canon-read fix WORKS — verified live: menubar (no sideOffset) and dropdown show the 4px canonical gap on real interaction (the $derived returns 4 → Floating UI offset 4). The keyboard focus-ring fix was already live. Menu demos (dropdown / context / select) cleaned to follow the canon — commit 9c48164f. The canon-read + token are in commit 61ea97b5.

Minor known artifact (demo-only, NOT a real-world bug): the dropdown demo opens on mount (open: on), so Floating UI positions the panel against the trigger's TRANSIENT initial-layout rect and doesn't re-measure on the settle (autoUpdate watches scroll/resize, not arbitrary layout shifts) → it shows ~36px until you close+reopen. Real menus open on a user click (settled layout) → 4px. To make the demo pixel-perfect, give Floating UI a layout-shift re-measure on open.

The original (now historical) handoff follows.

✅ DONE + VERIFIED LIVE (CSS — HMR is reliable)

  1. Archetype :where() systemic fix (src/uix/eidos/archetypes.css) — every DEFAULT archetype rule wrapped in :where() (specificity 0,0,0) so any component recipe (0,1,0) wins regardless of load order. Kept at full specificity ON PURPOSE: the :has([data-archetype='field-trigger']) padding override + the canonical item highlight.
  2. Overscroll containment — overscroll-behavior: contain added to the shared ScrollArea viewport (scroll-area.css) + 7 overlay scroll regions (context-menu, dialog, drawer, dropdown-menu, float-panel, popover, tooltip). Fixes scroll chaining to the <body>. Verified on select + dropdown.
  3. Focus-ring fix (archetypes.css) — the universal [data-archetype]:focus-visible box-shadow ring now EXCLUDES item, option, AND content:
    • Menu/listbox ROWS keyboard-focus via roving tabindex → drew the thick ring on keyboard but only a subtle bg on hover (inconsistent). Now rows use the canonical highlight for BOTH :hover AND :focus-visible.
    • Menu/popover/dialog PANELS (data-archetype='content') take focus for key capture → drew the ring around the WHOLE float (the thick purple frame the user hated). Now excluded; the panel's own elevation (border + shadow) is the boundary.
    • Verified live: loaded rule tail = …:not([item]):not([option]):not([content]); highlight selector includes :focus-visible.

⏳ DONE but NEEDS A FRESH-LOAD CHECK (JS — the tab cached the old module)

  1. --floating-gap-menu token: 0px → var(--space-1) (4px) in render-css.ts + regenerated generated/base.css. Menus get a small gap now, not flush.
  2. nav-menu wired to the canon (navigation-menu.css) — both orientations now calc(100% + var(--floating-gap, 0px)) (was hardcoded --space-1). nav-menu is CSS-positioned (not portaled) so it consumes the canon through CSS. Verified 4px early, before the cache trouble — reliable.
  3. Canon-read FIX (src/uix/soma/layers/floating/floating.svelte.ts) — THE fix:
    • FloatingContent.canonicalGapPx was a $state filled by a one-shot rAF (requestFrame) inside a watch(contentRef). For PORTALED menus that rAF callback never fired (instrumented: watch passes the guard, frame scheduled, never run, not cancelled — requestFrame/resolveWindow at arts/adom/active-dom.svelte.ts:481 suspected). So canonicalGapPx stayed null → menus fell back to sideOffset (flush). The token change alone never reached them.
    • Fix: canonicalGapPx is now a $derived reading --floating-gap off contentRef.current directly (reactive, no rAF). The rAF keeps only the zIndex read. --floating-gap is a style value (no layout) so reading on mount is safe.
    • Type-clean (svelte-check passed) but NOT visually verified — see CACHE below.

⚠️ CACHE GOTCHA — READ FIRST (it ate most of the session)

  • Editing a deeply-imported .svelte.ts (e.g. floating.svelte.ts) does NOT reliably HMR, AND the connected tab serves that module from a STRONG cache. navigate and a ?nocache= query do NOT revalidate the module (it's fetched by path).
  • Tell-tale: a gap: 12 from a hardcode I'd reverted long ago persisted through edits + a full dev-server restart → the tab was running a frozen old module.
  • Detect stale: temporarily set mainAxis: 12 in the offset middleware → if the gap isn't 12, the code is fresh.
  • Only reliable bust: browser HARD reload (Ctrl+Shift+R). Not navigate.
  • CSS (archetypes.css, recipes) DOES hot-update fine — only the .svelte.ts module is the problem.

▶️ FIRST ACTIONS TOMORROW

  1. Hard-reload (Ctrl+Shift+R) the menubar/dropdown page.
  2. Confirm the canon-read fix: menubar (passes no sideOffset) should now show a ~4px gap (no longer pegado). If still flush → re-instrument; you're guaranteed fresh.
  3. Clean the menu demos so they follow the canon (now that the read works). These pass explicit sideOffset that BYPASSES the canon:
    • web/routes/uix/components/dropdown-menu/+page.svelte → sideOffset = $state(4)
    • web/routes/uix/components/context-menu/+page.svelte → sideOffset = $state(0)
    • web/routes/uix/components/select/+page.svelte → hardcoded <Select.Content sideOffset={6}> Default each to undefined so the token retunes all. Slider pattern to avoid the number|undefined bind error: value={sideOffset ?? 0} oninput={(e) => (sideOffset = e.currentTarget.valueAsNumber)} + label {sideOffset === undefined ? 'auto' : sideOffset + 'px'}.
  4. If the canon-read fix DIDN'T take on fresh load, isolate why requestFrame's rAF never fires for portaled menu content — but the $derived should sidestep it entirely.

Dev server

Restarted fresh on 5199: npm run dev -- --port 5199 --strictPort (the bare dev script uses the default port, so pass the flags).

Powered by TurnKey Linux.