The positioning engine is pure collision geometry over `$adom` — a runtime
artifact, not soma-specific. Moves it out of `soma/layers/floating/engine/` into
a new `arts/floating` art (`$floating`), exactly parallel to `$motion`: both soma
(the JS positioning path) and eidos (the CSS-anchor path, future) build on it,
so the shared pure core belongs in arts, not buried in one consumer.
Moved to `$floating` (git renames, history preserved): geometry, rects, clipping,
overflow, supports, compute, auto-update, flag, types, the 7 middleware, and the
two parity test suites. `placement.ts` and the contract types `Measurable` /
`Middleware` / `MiddlewareData` move too — `$floating` is now self-contained
(depends only on `$adom`, never back on soma). New `arts/floating/index.ts` barrel
is the public surface. Alias `$floating` added to vite.config.ts + svelte.config.js.
Stays in `soma/layers/floating/` (reactive composition): use-floating.svelte.ts
(runes), floating.svelte.ts (providers/context), shell.ts, safe-polygon.ts,
utils.ts, the reactive types, index.ts. These now import `$floating`; soma's
types.ts/index.ts + soma/types/index.ts re-export the placement/contract types.
Two latent type gaps the typed `$floating` surface exposed (the soma loose-factory
shim had hidden them) are fixed: `DetectOverflowOptions` now declares `boundary`
(the middleware genuinely accept it; compute's read phase extracts it), and a
`size` test's `apply` returns void.
`@floating-ui` is UNTOUCHED — P4 deliberately NOT executed: the dep stays installed,
the fui imports + the `USE_OWN_ENGINE` flag (still OFF) remain in the wrappers, the
runtime still positions via floating-ui. Verified: 350 synthetic + 137 real-DOM
parity cases green from the new location, soma overlay providers green, check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drives the dual-system comparison suites to 487 cases (350 synthetic + 137 real-
DOM, all green) by implementing the gaps a coverage-enumeration audit
(floating-parity-coverage-gaps, 9 agents) found untested. Two more real bugs
surfaced and fixed:
- rects.ts getRectRelativeToOffsetParent passed the ELEMENT's own scale as the
getBoundingClientRect basis; floating-ui always uses the offsetParent's. They
diverge on an anisotropically-scaled or bordered offsetParent (getScale's
fallback rounds the two boxes differently) → sub-pixel reference shift. Now
passes offsetParent as the basis (the anisotropic-scale tests caught it).
- rects.ts omitted floating-ui's setLeftRTLScrollbarOffset for a window
offsetParent (offsets.x = getWindowScrollBarX(documentElement)). ~0 on a normal
LTR document, but non-zero under a left-side scrollbar / writing-mode:vertical-rl
→ wrong flip decision (the writing-mode test caught it). Also ports the
getViewportRect scrollbar-gutter correction + SCROLLBAR_MAX, and the isTopLayer
short-circuit in getClippingRect (native popover / modal <dialog> escape clip).
New coverage (synthetic): flip bestFit/fallbackStrategy/crossAxis-alignment/
multi-step, limitShift offset-object/axis-toggles/origin-side, size single-axis
shift + symmetric shrink, offset crossAxis under RTL, hide boundary + numeric
offsets, arrow padding clamp, the reset loop (bare-true preserve + MAX_RESET_COUNT),
padding-object × offsetScale, shift data delta.
New coverage (real-DOM): document scrollbar, reference-clip ≠ floating-clip
(altBoundary), anisotropic + scaled-and-scrolled offsetParent, scaled clipping
ancestor, multi-element collisionBoundary, different offsetParents, non-default
flip options, modal <dialog>, body-as-scroller, nested table chain, transformed
<html> scrolled, thick asymmetric border, writing-mode vertical-rl, shadow
crossing, floating in two scrollers, fixed×scaled sweep.
Documented as deliberate non-features (out of the frozen consumer API): derivable
function options, virtual-anchor contextElement, rootBoundary:'document',
visualViewport pinch-zoom, deep cross-iframe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two suites asserting the in-house engine == @floating-ui (flag stays OFF;
no removal, no migration). Together they exercise the whole pipeline and caught
9 real bugs in the P1 DOM-read layer; all fixed and re-verified.
Suites:
- engine.test.ts — 232 synthetic math cases: runMiddleware vs @floating-ui/core
over identical synthetic rects. Full 12-placement × 7-edge matrix (full chain),
flip-at-edges, shift (main/cross × limiter), arrow (+ alignmentOffset), size,
hide, extremes (oversized/zero-size/fractional/negative-padding/huge-offset),
and scroll/scale/rtl variants. Pixel-identical.
- engine-dom.svelte.test.ts — 60 real-DOM cases (chromium/client project):
BOTH systems' computePosition on the SAME real elements — the only suite that
exercises rects.ts + clipping.ts. Nested scroll, transformed + CSS-scaled
offsetParent, fixed strategy, scrolled page, individual-transform containing
block, static-table-cell offsetParent, position:fixed escaping a scroll
container, arrow + size.
Bugs fixed (independently confirmed by the floating-engine-parity-audit workflow,
25 agents):
- rects.ts getRectRelativeToOffsetParent: inverted scroll/offset signs (+ missing
htmlOffset) — broke every scrolled page / positioned offsetParent.
- rects.ts convert: early-returned for a window offsetParent (=== win instead of
=== documentElement) leaving viewportDelta = 0, plus inverted signs, plus the
offsetParent rect must be RAW (includeScale:false) and scale must be applied.
- overflow.ts: scale the element rect by offsetScale (rect·scale + viewportDelta)
so CSS-scaled offsetParents detect overflow correctly; synthetic platform.convert
updated to model rect·scale too.
- rects.ts isContainingBlock: add translate/scale/rotate, gate filter/backdrop
behind !isWebKit(), drop container-type (matches shipped floating-ui), widen
the willChange regex. New isWebKit()/isTableElement() helpers.
- rects.ts getOffsetParent: skip static td/th, not just table.
- clipping.ts: getClippingElementAncestors (+ hasFixedPositionAncestor) — drop the
body and any overflow ancestor the positioned element escapes via a fixed/absolute
containing block; getViewportRect gates the visual-viewport offset on isWebKit();
getOverflowAncestors includes win.visualViewport for auto-update zoom tracking.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New value-type-agnostic Picker — the shared transactional core that will
later replace the five system pickers' duplicated coordinators (date /
color / time / range), parameterised by TValue.
- morfo/components/picker.ts — provider part only; open/close delegated to
the composed Popover (no dialog semantics, no events).
- soma/components/picker — PickerProvider<TValue> + root wrapper (composes
Popover). Exposes workingValue (draft-or-bound), commit/cancel/clear and a
PickerShellHandle for the shared footer.
- web/routes/uix/components/picker — interactive deferValue testbed.
Unified transaction (the deferValue axis):
- deferValue=false: workingValue IS the bound value (live); cancel() reverts
to the open-edge snapshot.
- deferValue=true: workingValue routes to an internal draft; the bound value
only updates on commit() (Accept); any close that is not an explicit accept
discards. The form / onValueChange only ever see accepted values.
Verified: npm run check 0 new errors. Browser — deferred edit/accept/cancel/
dismiss-discard + live edit + live cancel-revert all correct, no console errors.
Not committed (shared files carry a parallel session's WIP): the soma/components
barrel export + the docs sidebar entry. Demo imports the picker via subpath so
the commit is self-contained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finishes the in-house positioning engine that replaces @floating-ui. The DOM-read
core (rects + clipping ancestor-walk) was already committed; this adds the rest:
- engine/overflow.ts — detectOverflow, PURE over the pre-read clipping +
viewportDelta/offsetScale (no DOM read in the middleware phase).
- engine/middleware/{offset,shift,flip,arrow,size,hide,limit-shift}.ts — each a
factory with @floating-ui's call signatures + index barrel.
- engine/compute.ts — one dom.measure read phase → the pure, exported
runMiddleware loop (base coords → chain → flip/arrow reset, capped at 50).
- engine/auto-update.ts — ancestor scroll/resize via $adom listen, element
resize via ResizeObserver, optional rAF loop; no raw window/getBoundingClientRect.
- MiddlewareState gains viewportDelta/offsetScale/rtl (precomputed upfront so the
middleware stay pure); MiddlewareReturn.reset widened to boolean | {placement}.
- geometry.ts gains getOppositeAxis/getAlignmentSides/getExpandedPlacements/
getOppositeAxisPlacements; clipping.ts exports getOverflowAncestors.
- Middleware de-vendored from @floating-ui: engine/types owns it, ../types
re-exports it as the public surface.
- engine/flag.ts — USE_OWN_ENGINE A/B switch, default OFF (runtime still
@floating-ui). use-floating + floating.svelte branch on it and thread `dom`.
Verified: engine/engine.test.ts — 13-case parity guard proving runMiddleware is
pixel-identical to @floating-ui/core on synthetic rects (offset, flip with/without
overflow, shift, limitShift, arrow centerOffset + alignmentOffset, size, hide,
full chain with viewportDelta/scroll, rtl). npm run check at baseline (0 new).
Note: the engine/ dir keeps its P1 no-semicolon style (pre-existing), so it does
not match the repo's prettier semi:true; left as-is to avoid reformatting the
committed P1 files. P2/P3 (broad foreground-browser A/B) + P4 (@floating-ui
removal) remain — see engine CONTINUE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last neutral component on a bespoke hover. tabs trigger (transparent base) moves to
the shared state layer:
background: var(--tabs-trigger-bg-hover)
-> background-image: linear-gradient(var(--state-hover), var(--state-hover))
Overlay (background-image) keeps it filled-safe for the segmented/pill variant. Removed
the now-orphan --tabs-trigger-bg-hover recipe token + regenerated base.css.
tabs.css is concurrently being reworked by the other session (segmented variant + focus
outline migration, uncommitted). Staged ONLY the hover line via line-level staging
(checkout HEAD + sed the single line) so their uncommitted segmented/focus work stays
untouched + unstaged in the working tree.
Completes the --state-* neutral-hover unification: every neutral component now uses the
state layer; only valenced (palette) hovers keep their own.
Verified: tabs.css uses --state-hover, token gone from base.css, segmented tokens intact;
eidos suite 15 failed (pre-existing) / 255 passed.
color-picker.css is clean (committed in 72e8fb57), so the deferred color-picker fold is
unblocked. The trigger + eye-dropper neutral hovers move from bespoke
--color-picker-{trigger,eye-dropper}-hover-bg to the shared state layer:
background: var(--color-picker-trigger-hover-bg)
-> background-image: linear-gradient(var(--state-hover), var(--state-hover))
The trigger is filled (--color-picker-trigger-bg base), so the tint overlays via
background-image (not background, which would replace the base). Kept the border-color /
text-color hover shifts. Removed the 2 now-orphan recipe tokens + regenerated base.css.
Leaves tabs as the only neutral component still on a bespoke hover (blocked by the other
session's tabs.css WIP).
Verified: color-picker hover tokens gone from base.css; color-picker.css uses --state-hover
x2; eidos suite 15 failed (pre-existing, unchanged) / 255 passed.
Self-contained CONTINUE.md to resume the positioning-layer rebuild cold: the
decisions (CSS-anchor primary + full-parity JS engine via $adom, reimplement-not-
vendor, P5 deferred), what's done (P0 + P1 foundation + the DOM-read/clipping core),
the read-phase architecture, the ordered remaining work (overflow + middleware +
compute + auto-update + wiring + dep deletion), the exact @floating-ui removal
surface, and the A/B verification plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The synchronous read layer of the own engine — invoked only from the single
dom.measure read phase (post-layout, coalesced), reimplemented from floating-ui's
platform/dom (spec):
- engine/rects.ts — getOffsetParent (containing-block aware), scale-aware rects,
reference-rect-relative-to-offsetParent, viewport↔offsetParent conversion.
- engine/clipping.ts — getClippingRect: the FULL overflow-ancestor walk (option 2),
intersection of every scroll/clip ancestor + the viewport — what keeps an overlay
inside a nested scroll container. isOverflowElement / getOverflowAncestors.
Single-document fidelity (deep cross-iframe accumulation simplified — the rare
case). Node helpers reused from $adom. Compiles clean (0 new errors); not wired yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of the own positioning engine (replacing @floating-ui's math). Pure,
no-DOM foundation, not wired yet:
- engine/types.ts — internal positioning types (Rect/Coords/ElementRects/
ClippingContext/MiddlewareState/MiddlewareReturn/Middleware/ComputePosition*).
Read-phase model: one coalesced dom.measure reads rects + the full clipping
ancestor-walk + arrow dims upfront; middleware then run purely.
- engine/geometry.ts — pure placement/coords math (getSide/getAlignment,
getOppositePlacement, getOppositeAlignmentPlacement, computeCoordsFromPlacement)
reimplemented from the floating-ui algorithm.
Compiles clean (0 new errors). The DOM-read core (rects + clipping ancestor-walk)
is the substantial next slice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First step of removing @floating-ui (the last external dep). Move the simple
positioning types off the dependency into our own placement.ts/types.ts and
scaffold the CSS-anchor capability gate — runtime still calls floating-ui's
computePosition (no behaviour change).
- placement.ts: own Placement (Side | `${Side}-${start|end}`, center = bare side)
+ Strategy.
- types.ts: own FloatingElement / ReferenceElement / MiddlewareData (only the
arrow/hide/transformOrigin keys we actually read). Middleware stays from
@floating-ui until P1 (de-vendored with the engine that defines its State/Return).
- use-floating / floating / safe-polygon: repoint type imports to ./placement | ./types.
- engine/supports.ts: supportsCssAnchor(win) — behavioural twin of the eidos
@supports gate (unused yet).
Verify: npm run check back to baseline (0 new errors); popover+tooltip tests 7/7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
decisions.md (the E3 design-rationale entry point) gains a cross-cutting entry
for the read-timing & token-resolution work — including the decision to reject a
static grep-guard in favour of the runtime `uix.perf` detector — pointing to where
the full argument lives (active_architecture §7 + arts/adom + arts/perf READMEs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface the layout-read + token-resolution work across the doc corpus, beyond the
per-artifact READMEs already shipped (adom / color / perf):
- CLAUDE.md: `$perf` alias + perf/color in the arts list (the read-timing doctrine
bullet landed in abf76332).
- arts/README.md: perf + color rows in the artifact map; adom row notes post-layout
read scheduling (`measure`); `$perf` + `$color` in the aliases block.
- active_architecture.md §7 (Reglas duras): the read-timing discipline — layout
reads run post-layout (`dom.measure` / `dom.raf`), never sync-after-write; theme
token → colour via `eidos.resolveToken`; `uix.perf` detects violations. The
framework now governs READS like `dom.apply` governs writes.
- eidos/README.md: `ActiveEidos.resolveToken` / `resolveTokens` in "Runtime activo".
- COLOR_ENGINE_RFC.md: `uix.color` realized (`EngineColor`) + `resolveToken` consumer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add the read-timing rule to Key Conventions: layout-forcing reads
(getComputedStyle / getBoundingClientRect / offset* / scroll* / client*) run
post-layout via `dom.measure` (or a `dom.raf` callback), NEVER synchronously
after a write; a theme token → concrete colour via `eidos.resolveToken`, not a
`getComputedStyle` probe; `uix.perf` detects violations at runtime.
- Also lands a pre-existing uncommitted behavioral-guidelines preamble
(Think Before Coding / Simplicity First / Surgical Changes / Goal-Driven).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- floating: read the resolved content z-index via `dom.measure` instead of a raw
`requestFrame` + manual `cancelFrame` — coalesced post-layout, disposer returned
directly. The canonical `dom.measure` adopter.
- color-picker demo: resolve preset-swatch token colours via `eidos.resolveTokens`
(config + `$color`, pure JS) instead of the `getComputedStyle(probe)` round-trip
that forced the reflow. Restores the Clear footer button (`{#if showClear}`,
was a stray `{#if false}` debug edit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `arts/perf` — a dev-only forced-reflow detector on the Long Animation Frames
API. Turns Chrome's opaque "[Violation] Forced reflow while executing JavaScript
took Nms" into an attributed report: which script forced how much synchronous
style+layout (`forcedStyleAndLayoutDuration`). It catches the actual runtime bug
regardless of static pattern — what a grep guard can't do (the codebase has ~120
legitimate layout reads across ~48 components; the fault is the temporal
sync-read-after-write ordering, not the read itself).
`createActivePerf({ threshold, onReport, log })` owns the only PerformanceObserver
the framework creates; inert where LoAF is unsupported (non-Chromium). Discoverable
as `uix.perf`, opt-in via `createActiveUix({ reflowDetector: import.meta.env.DEV })`;
`ActivePerf` (stateful → Active*) is disposed by the composition root.
- src/arts/perf/{types,active-perf,index}.ts + README + 6 tests
- $perf alias (vite.config.ts + svelte.config.js)
- ActiveUix.perf getter + reflowDetector option
- disabled-dom stub gains measure() (completes the dom.measure interface)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `ActiveDom.measure(read, node?)` — schedule a layout-forcing read
(getBoundingClientRect / getComputedStyle / offset* / scroll*) in a coalesced
animation frame instead of synchronously. All reads queued in one turn run
together in a single rAF per window, so a read never forces a synchronous
reflow mid-write-turn — the cause of "[Violation] Forced reflow while executing
JavaScript". Returns the same idempotent disposer shape as `raf`; `dispose()`
cancels pending frames. Reads-only by design (writes sequence through `apply`).
The sanctioned home for layout reads in components: it owns *when* the read
runs (post-turn, coalesced), not *which* element.
- active-dom.svelte.ts — measure() + per-window queue + dispose cleanup
- test/active-dom.test.ts — 6 tests (defer / coalesce / dispose / throw-isolation)
- README.md — API + dated decision entry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `ActiveEidos.resolveToken(token)` / `resolveTokens(tokens)` — resolve a
colour token (`--scale-{name}-{step}` / `--primitive-{role}-{step}`) to a
concrete sRGB hex purely in JS (eidos config + the `$color` engine), with NO
DOM read. This replaces the `getComputedStyle(probe)` round-trip consumers
used to reach a token's value, which forces a synchronous reflow. Role
primitives honour an applied `applyColorScheme` override (override-first);
palette scales resolve theme-scoped with the primitive palette as fallback.
Expose the `$color` engine at runtime as `uix.color` (`EngineColor` — stateless,
so `Engine*` not `Active*`) — a discoverable accessor next to `uix.motion` /
`uix.timers`. eidos keeps importing `$color` directly for build/SSR.
- src/uix/eidos/lib/resolve-token.ts — pure parseColorToken + normalizeToHex
- ActiveEidos.resolveToken/resolveTokens + ParsedColorToken export
- ActiveUix.color getter + EngineColor type
- arts/color README documents the uix.color surface
- tests: 16 (pure + integration against the base config)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The --state-* rollout (7de6c76b) migrated 19 neutral components' hovers to the state
layer but left their now-unused bespoke --{x}-bg-hover recipe tokens in base.ts. The
concurrent session's base.ts work is now committed (72e8fb57), clearing the entanglement
that blocked this. Removed the 16 confirmed-orphan tokens (0 var() consumers, verified
per-token against the consumed set + the component->line map):
accordion-trigger, breadcrumb-ellipsis, collapsible-trigger, spin-field-control,
calendar-control, calendar-day, pagination-control, radio-cards, select-trigger,
toolbar-control, file-upload-button, tag-group-item, tag-group-remove, editable-trigger,
tags-input-action, stepper-trigger
Kept every consumed/valenced hover (tag-group/toast/toggle palette, pagination-selected,
menu-item accent) + tabs-trigger + color-picker (still consumed). tabs stays bespoke
(blocked by the other session's tabs.css WIP); color-picker fold deferred. The existing
recipe-css-contract "does not leave declared public recipe variables orphaned" test is
the standing guard -- no new guard needed.
Verified: eidos suite 15/239 IDENTICAL with and without this change (stash-compared) --
the 15 failures are the concurrent session's pre-existing WIP, none from this. base.css
regenerated (palette.css unchanged).
- New <ColorSwatch> eidos primitive: transparency-checker base + colour
overlay, opt-in inset ring, inherits the container corner-shape.
- ColorPicker presets tab + saved colours: `presets` prop, bindable
`savedColors`, ColorPicker.Presets / SavedSwatches / SaveAction parts
(canonical hsvToHex comparison; save/delete component-owned).
- Readonly: the trigger no longer opens the popover when readonly — gated
via a PopoverTrigger `disabled` opt, rolled out across the color / date /
date-range / time / time-range pickers.
- ColorField swatch respects its container (no bg/border, inherits shape via
corner-shape); picker trigger swatch matches the button shape.
- Recipe: popover content-width tokens.
Documents this session's reference-grade coherence work in the canonical theming docs
(framework changes land in the framework's own docs, same pass):
- New §38 "Capa de estado (state-layer) --state-*": the MD3 token set
(hover 8% / press+selected 12%, currentColor -> theme-adaptive), the neutral-vs-valenced
doctrine, the overlay mechanism (transparent base vs filled gradient), the rollout
(accordion pilot + 19 comps + transversal fold of the archetype trigger/item rules),
the deferred cleanup. The ~168-hover unification had no doc until now.
- §32 (focus ring): new subsection "Outline en superficies" -- why box-shadow dies under
forced-colors/overflow + the hardcoded surface-default gap (misaligned halo on planes),
the 5 migrated components (ab62cca7), fields kept on box-shadow (inner-ring), the now
orphan --focus-ring token.
- TOC completed (§36 / §37 / §38 were missing) + revision date -> 2026-06-29.
- eidos/README.md reference table: focus-ring / touch-target / state-layer rows.
The --size-{tier}-font-size bundle (STATIC_SIZE) still encoded the superseded coupled
model -- each tier used the font ONE STEP DOWN (md->sm=14px, lg->md, xl->lg, xxl->xl),
the "compact control text" doctrine. But THEMING.md section 5 already revoked that on
2026-06-17 ("1:1 universal: control text follows the type scale; md-control is 16px,
not 14") and flagged the bundle's 14px as a stale fossil that "no longer reflects the
rule". This realigns the orphan bundle to the already-decided canon:
STATIC_SIZE md/lg/xl/xxl fontSize -> their own tier (1:1)
=> --size-md-font-size = var(--font-size-md) = 16px (was var(--font-size-sm) = 14px)
No component changes -- the bundle is orphan (0 recipe consumers; recipes already
declare 1:1 directly). This fixes the orphan reference + the docs so the size primitive
stops contradicting the live canon. Also realigns the paired line-height + letter-spacing.
- static.ts: 4 tiers fontSize -> 1:1
- active-eidos-config.test.ts: assertion --font-size-sm -> --font-size-md
- THEMING.md section 5: value, the revoked "two md / compact" box, the orphan note
- base.css: regenerated (--size-* hunks only; the concurrent session's base.ts work
left unstaged)
Verified: eidos suite 15/239 unchanged; no new type errors.
The box-shadow token --focus-ring hardcodes var(--color-surface-default) as its
gap-fill (render-css.ts:184), so a control focused inside a raised/overlay/filled
plane showed a mismatched halo; box-shadow rings are also clipped by overflow:hidden
and die under forced-colors. Migrate the last 5 box-shadow consumers to the canonical
outline pattern already used by button/card/~40 components:
outline: var(--focus-ring-width) solid var(--focus-ring-color)
outline-offset: var(--focus-ring-offset) (or 0, flush, for input + scrollbar)
- command-input (offset 0, keeps border-color shift) - collapsible-trigger - toggle -
splitter-resize-trigger (offset var) - scroll-area-scrollbar (offset 0)
Outline follows border-radius on every evergreen browser, isn't clipped by overflow,
and the forced-colors fallback already maps outline. The gap-color mismatch disappears
-- no --focus-ring-surface token needed.
NOTE: the box-shadow token --focus-ring now has zero CSS consumers (only THEMING.md +
a palabras doc reference it). Left in place -- it's a public foundation token; removing
it is a separate API decision (bundle with the --size-* orphan cleanup).
Verified: grep 0 box-shadow focus rings in component CSS; eidos suite 15/239 unchanged;
served rule confirmed in browser ([data-toggle]:focus-visible -> outline). Visual look
on keyboard focus worth a live Tab-through (CDP screenshot hung all session).
Folds the two TRANSVERSAL neutral-interaction rules in archetypes.css into the
canonical state layer (completes the --state-* unification at the archetype level):
- [data-archetype='trigger']:hover — opacity-dim (0.85) -> the state-layer tint
(background-image gradient of --state-hover), so bare triggers feel like the rest
instead of fading their own text.
- [data-archetype='item'/'option'] highlight (hover/focus/highlighted/selected) -
surface-raised swap -> --state-hover, so every menu/list active row (dropdown,
context, select, combobox, listbox, command) reads via the shared layer.
These were the LAST two neutral-hover idioms (canonical cross-component rules). No
base.ts touched -> no collision with the concurrent session.
Verified: folded rules served (state-layer gradient), old opacity/surface-raised gone;
eidos suite unchanged (15 pre-existing failures, none from this). Visual note: the 8%
tint ~ surface-raised (step-2) in light; menu-row highlight worth a live hover/keyboard
check (CDP screenshot hung all session).
Two fixes surfaced by the ColorPicker demo's
"[Violation] 'setTimeout' handler took 75ms".
Overlay open was gated behind the ~240ms perceptual hold. Popover
`present` / Dialog `open` / Drawer `present` flip `open` in the trigger
HANDLER but declared `sequence: 'pre'`, so the runtime awaited the emit
(and its hold) BEFORE mounting the content: the overlay opened ~240ms
late and its heavy first render ran inside the hold's setTimeout turn —
the violation. Switched these open events to `sequence: 'post'`
(mount-then-signal); `close` stays `pre` (signal must precede unmount).
Measured popover open 291ms -> 20ms, longtask 77ms -> 0.
Sema scheduled its hold / haptic delay / earcon completion with raw
setTimeout and the engine never received the timers service. Added a
`timers` port (semaDelay, src/uix/sema/timers.ts) forwarded through the
engine into all three channels; active-uix and defineEngineSemantic
inject uix.timers. Perceptual timing is now cancellable on dispose,
observable, and fake-clock deterministic; raw setTimeout survives only as
a unit-test fallback. Verified live: the hold now flows
VisualChannel.handle -> semaDelay -> uix.timers.schedule -> clock.
Docs: sema/README.md (timing via uix.timers + the post-open doctrine).
Propagates the state-layer unification (after the accordion pilot a13ca387): each
neutral :hover swaps its bespoke --{x}-bg-hover for the canonical MD3 state layer via
`background-image: linear-gradient(var(--state-hover), var(--state-hover))` — a
translucent currentColor overlay that works UNIFORMLY on transparent (tint shows) AND
filled (base bg survives under the overlay) bases. Keeps each component's `color:` text
shift; leaves the VALENCED palette hovers (pagination-selected, tag-group per-color,
toast, toggle, menu-item highlight) untouched.
Components: accordion, breadcrumb, calendar, collapsible, editable, field-control-trigger,
field-segment, file-upload, month-grid, pagination(control), radio-cards, range-calendar,
select(filled base), spin-field, stepper, tag-group(item+remove), tags-input, toolbar,
year-grid.
Verified: the gradient resolves to currentColor at 8% (`srgb .../0.08`) + the base bg
survives under it (computed probe); NO new test failures (the 6 failing eidos test files
are identical with and without the migration, and --state-hover/press/selected appear in
NONE of the failures — those are pre-existing card-group / palabras / dialog / icon).
Deferred: tabs + color-picker carry another session's uncommitted work (migrate once they
commit). Orphan --{x}-bg-hover recipe-token cleanup + guard test + the transversal
trigger/item fold follow.
Documents the 3 touch-target passes (a265d39a/c0904fc6/2750b9ce): 44px tap targets
gated on @media (pointer: coarse), controls + list rows + radio label-row, the
:not(lg)(xl) double-duty, markers via the React Aria label-row (not a Compose-style
visual restructure), and the WCAG AA-via-spacing conformance for bare grouped markers.
First step of the state-archetype unification (the audit's Arq.8 / the plugin's #1
rec): collapse the ~168 bespoke hover idioms into a canonical MD3 state layer. Adds
--state-hover / --state-press / --state-selected to archetypes.css — a translucent
`currentColor` overlay (8% / 12% / 12%), the single source for NEUTRAL interaction
feedback. Per-variant accent hover (solid -> palette-solid-hover, soft -> element, …)
stays in the recipes; the state layer owns the neutral / ghost tier.
Pilot: accordion-trigger:hover now uses var(--state-hover) instead of its bespoke
--accordion-trigger-hover-bg. Verified (computed): the token resolves to an 8%
currentColor tint on a real trigger (`srgb 0.125 / 0.08`); the color-mix + currentColor
chain confirmed directly. (Visual screenshot blocked by a renderer-hang in the tooling;
hover it live to see the tint.)
Rollout pending a design call: accordion has a TRANSPARENT base so a `background` swap
works; FILLED surfaces need a layered overlay (not a bg replace). The other ~30
neutral-hover components + the recipe orphan cleanup (--*-hover-bg) follow.
Marker touch-target, done the reference-grade WEB way (React Aria + WCAG), NOT a
Compose-style visual restructure. The clickable label ROW [data-radio-group-row]
(dot + text) becomes the 44px tap target on COARSE pointers; the dot stays small.
This mirrors React Aria (indicator + label wrapped as one target) + WCAG 2.5.5's
Equivalent exception (the larger row is the conformant target). Desktop (fine
pointer) keeps the compact 20px row.
The bare dot is intentionally NOT grown: grouped markers already clear WCAG 2.5.8
(24px AA) via the spacing exception — checkbox (12px gap) and radio (12/16px gap)
keep the 24px circles from intersecting (verified).
Verified: coarse rule serves; desktop row stays 20px (min-block-size auto), gate
doesn't leak.
Checkbox/switch have no built-in label-row (bare boxes; labels come from Field /
consumer) — their AAA label-row is a Field-level follow-up; grouped bare boxes are
already AA-conformant.
Second touch-target pass (after the button-like controls, a265d39a). On COARSE
pointers (touch), menu / select / listbox / command rows grow to the 44px mobile
minimum. `--list-item-height` (list-surface.css) is the row's min-block-size floor
that every list surface bridges to, so lifting it for the small sizes (xs/sm/md,
all < 44) grows every list row at once; lg (44) / xl (52) already clear it. Desktop
(fine pointer) keeps its compact rhythm.
Verified: coarse rule serves; md list-surface matches (->44), lg doesn't; desktop
md row stays calc(36px).
The framework's only axis below industry (ARCHETYPE_COHERENCE_AUDIT flagged it
RED: 0 components guaranteed 44/48px, 0 hit-area, md=36 < the mobile minimum). On
COARSE pointers (touch), grow button-like controls to the 44px target (WCAG 2.5.5
AAA / Apple HIG 44pt / Material 48dp); the desktop (fine pointer) keeps its compact
density — gated out, zero regression.
Transversal rule in archetypes.css: the trigger / close / action archetypes + the
standalone [data-button]. `:not([data-size='lg']):not([data-size='xl'])` does double
duty — skips the already->=44 sizes (so min-* only GROWS xs/sm/md, never shrinks
lg/xl) AND lifts specificity to 0,2,0 to beat the recipes' own min-block-size. Both
axes, so icon-only / short-label controls reach 44x44.
Verified: rule serves; md button + sm trigger match, lg button + item rows don't;
desktop button stays 36px (gate doesn't leak).
Follow-up passes: item/option rows (list-surface height), field-trigger, thumb, and
the small markers (checkbox/radio/switch — their clickable label often already
provides the target).
Presets tab reorganized into 4 labeled groups: Sistema (3 roles) + Intent
(6, incl. neutral) side by side, Paleta (31 scales) + Guardados (savedColors)
full width. Role/scale tokens resolved to rgb() via a color-mix probe so the
SwatchTrigger can commit on click. Labels follow the field canon (one step
below the input, regular weight). Preset swatches drop the recipe checker +
border (opaque). Bottom padding equalizes the two tabs' height.
A "Guardar color" button in the footer (ghost/neutral, matching Borrar/
Cancelar) saves the current value to Guardados: enabled only when a color is
selected, not already saved, and under the 10-color cap.
KNOWN ISSUE: the footer overflows with 4 buttons (Listo clipped) -- the save
button needs to become icon-only. See color-picker/CONTINUE.md.
Materializes the framework's signature corner. SURFACES (overlay panels +
non-floating cards) carry the continuous (squircle) corner by default; CONTROLS
stay arc. At surface-scale radii (>=~10px) arc and squircle visibly diverge
(premium); at control radii they're indistinguishable, so the split costs no
coherence.
The foundation rule (renderShapeBlocks) ENUMERATES the tier — NOT a
`[data-archetype='content']` hook, which also marks tabs/accordion/collapsible/
table content (non-surfaces) and would over-apply (verified per-attr). `:where()`
(specificity 0) lets a `[data-shape]` override win, so the `shape` prop flips from
opt-IN (activate squircle) to opt-OUT (`shape='rounded'` escapes to arc) on
surfaces. Theme knob `--shape-surface-default` reverts the whole tier; degrades to
arc where `corner-shape` is unsupported (border-radius magnitude is universal).
- Tier A — floating panels [data-{c}-content] (pickers inherit via Popover) + [data-command]
- Tier B — [data-card], [data-banner], [data-radio-cards-item]
- Guard test: surfaces resolve to squircle, controls/rows/pills stay arc
- THEMING.md §30 updated; pilot moved out of card.css into the foundation
Verified live (Chrome 150 renders the squircle); per-attr computed coverage
matches the taxonomy.
box-shadow is a single property, so the interactive hover shadow replaced the
selection ring on hover (the ring vanished). Compose both — ring first, hover
shadow second — for soft and solid variants.
The audit that kicked off the radius-decoupling / archetype-:where / floating-gap
canon work — per-component matrix for the 5 inheritance rules (scaling, density,
ambient size, sub-component scale, concentric corners) + the corrected v2 doctrine
(radius = Radix decoupled-from-size, corners = Apple concentric, ambient = Ant).
The per-menu panel anchors to the in-bar trigger, but the trigger is INSET from
the bar edge by the bar's padding PLUS its 1px vertical centering (the trigger's
`min-block-size` subtracts 2px → 1px each side). So the canonical `menu` gap (4px
from the trigger) left the panel flush with the BAR. Add that full inset back per
size to the content's `--floating-gap` so the visible gap reads exactly --space-1
(4px) FROM THE BAR. Also stamp a `data-menubar-content` marker on the eidos content
so the rule can target the menubar panel (it only carried `data-dropdown-menu-content`
before, so the existing `[data-menubar-content]` selector matched nothing).
Verified: File menu shows gapFromBar=4 (was -1, flush).
Canon-read fix verified live (menubar + dropdown show the 4px canonical gap on
real interaction); menu demos cleaned (commit 9c48164f). Notes the demo-only
stale-anchor artifact (dropdown auto-opens before layout settles).
dropdown/context/select demos passed an explicit sideOffset that BYPASSED the
--floating-gap canon (showed 0/4/6 instead of the canonical --space-1). Default
to undefined so the canon (which now reaches portaled menus via the
FloatingContent $derived fix) applies; the sideOffset slider still overrides.
Verified live: menubar + dropdown show the 4px canonical gap on real interaction
(the canon-read $derived returns 4 → Floating UI offset 4).
Demo (web/routes/uix/components/color-picker): tabbed layout
(Seleccion | Presets) as a flat segmented band, a tools row (outline
eyedropper + two 12px sliders inset by the thumb diameter), and the real
<ColorField> as the value input (size-synced, value swatch, system
format-select). Icons sized canonically via --icon-size-{size}.
Framework:
- color-picker.css: popover viewport scrollbar-gutter stable->auto
(tabbed content fits; the reserved gutter only skewed the panel right).
- ChannelInput/ChannelSegment gain visual parity with <ColorField>
([data-color-field] root context + data-field-segment marker + state CSS).
Presets 4-group split (Sistema/Intent/Paleta/Guardados) + eyedropper
button-size decision pending; see color-picker/CONTINUE.md.
- floating-gap canon: --floating-gap-menu 0 -> var(--space-1); FloatingContent
reads --floating-gap via a reactive $derived (was a one-shot rAF that never
reached portaled menus -> they fell back to sideOffset, flush); nav-menu wired
to the token via CSS.
- focus-ring: exclude item/option/content from the universal archetype ring; menu
rows use the canonical highlight on :focus-visible too (no thick keyboard border,
no ring framing the whole float).
- overscroll-behavior: contain on the ScrollArea viewport + 7 overlay scroll regions
(no scroll chaining to the page).
- archetypes.css: default rules wrapped in :where() so component recipes always win.
- radius decoupled from size (Radix model) in toggle/textarea/menubar/nav-menu/
virtual-list/grid; toggle `rounded` prop.
Canon-read + token reach portaled menus pending a fresh-load check (the dev tab
cached the floating module) -- see CONTINUE-floating-gap-canon.md.
Value input: channel separators (",", "/") render as full-height 1px vertical
dividers; the format prefix/suffix glyphs ("#", "%") keep their text. Static
delimiters get transition:none so divider <-> glyph format switches don't flash.
Format switcher: now the design-system <Select>, not a native <select>. The soma
FormatSelect exposes the format state via its `child`
(WithChildNoChildrenSnippetProps); the eidos renders <Select> from it. Borderless
ghost trigger + chevron; popover follows the control width (matchAnchorWidth,
list-surface min-width dropped), panel gap, field-matched corner radius.
CardGroup rebuilt as an eidos-only composition shell — selection delegated to
ToggleGroup, disclosure to Collapsible, item = card via structural identity
(data-card), title = system Button. Eidos owns only grid / depth / concentric /
cascade / size propagation. The bespoke card-group morfo + soma were removed.
ToggleGroup now owns the canonical selection cardinality: one knob
enabledSelections (number | [min, max]) + whenFull + a commit-block event on
rejection, consuming $libs/selection. Backward-compatible: type / deselectable
derive to the limit (tests 5/5).
Replace the v1 demo template (which only exposed variant/size/role) with a
9-tab layout that surfaces the whole system: the full 31-scale palette on the
real `color` prop, border/shape/radius, the foundation axes
(density/scaling/mode/dir/border), the motion catalog, and the sema firma with
audible sound.
- Button `color` accepts the 31 palette scales as a REAL prop (THEMING §25.5,
the Radix `color="teal"` per-instance override), gated by the intent↔color
doctrine. PALETTE_SCALES/PaletteScale/ComponentColor/HierarchyColorRole in
eidos lib/types; soma `color` widened (layer-pure); eidos `color?:
ComponentColor`; recipe `palette-*` extended with the 31-scale cascade via
paletteScaleDecls/paletteContrastDecls (graceful fallback to primary so apps
without generated/palette.css degrade, not break). Pilot of
pendiente_color_demos.md; base.css regenerated.
- Shared harness in web/routes/uix/lib/: harness.svelte.ts (constants +
DemoTrace + signatureFor), PalettePicker, SystemAxes, MotionPanel, SemaPanel
+ uix.css primitives (swatches/firma/motion).
- Button canary rewritten to the 9-tab layout; DEMO_AUTHORING_GUIDE rewritten.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full handoff for the remaining GradientBuilder work (eidos visual editor +
GradientPicker + sema + demo + translations): the shared model, the soma API to
compose, the eidos file plan (template = color-picker eidos), the design decisions
from the editor research, and the session's gotchas (rAF freeze in the unfocused
tab, dev-server flakiness, ACTIVE_DEV_TRACK, pre-existing contracts failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the headless layer of the GradientBuilder (still ACTIVE_DEV_TRACK):
- Providers expose `.props` (renderProps() + handlers) and register per-part
sources for the morfo's value-bound aria (aria-valuenow / valuetext / label on
each stop) — every stop is a keyboard-accessible role=slider thumb.
- Svelte wrappers: Provider (the editor root, bindable `value: Gradient`), Track
(click-empty-to-add drag surface), Stop (the slider thumb) + types + barrel.
The eidos composes Preview / AddStop / AngleDial / KindSwitch over these via
GradientBuilderProvider.require(). check: 0 new errors, type-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gradient axis — Phase 1 (engine + dogfood):
- $libs/gradient: canonical Gradient model + gradientToCss serializer, pure and
zero-dep, shared by the theming axis and the (WIP) GradientBuilder. Stops
reference color ROLES → a --gradient-{name} re-tints with the seed and flips
light/dark for free. Default `in oklch` interpolation.
- build-gradient: role-derived factories (deepen/sheen/halo/aurora mesh) +
ActiveEidos.applyGradients() (the 6th runtime builder, in ThemeSeed/applyTheme).
- shimmer migrated to `in oklch`. Dogfood: /demos/cristal replaces its ~12
hardcoded gradients with applyGradients role tokens.
Opacity = function of elevation (depth cue `translucency`):
- New translucency depth-plane cue, sibling of shadow/blur: frost opacity now
scales with elevation (foundation overlay 68% / modal 80%; cristal 52→66→80)
via --depth-{plane}-translucency, consumed by the frost rule. base.css regenerated.
GradientBuilder component — Phase 2 (scaffold, WIP):
- morfo (contract) + soma provider state machine over the Gradient model: stops
add/move/remove/recolor, kind, angle, pointer drag; every stop a keyboard-
accessible role=slider thumb. Marked ACTIVE_DEV_TRACK until eidos/picker/demo land.
Docs: $libs/gradient README + THEMING §gradient-axis / §translucency + token table.
Demo cristal: scroll-reveal via uix.motion spring + hover glow; Select z-index
ladder; aurora/title → role-derived tokens.
contracts.test: ACTIVE_DEV_TRACK now filtered uniformly in both soma collectors.
check: 0 new errors. Tests: $libs/gradient + build-gradient + base.css sync green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All 5 pickers (date / date-range / time / time-range / color) modelled
themselves as standalone APG dialogs while ALSO composing a Popover that
already provides the dialog — producing nested role="dialog" (the
calendar/clock was a role=dialog child of the popover-content dialog).
Reconcile per soma SOMA_ARCHITECTURE.md §2 (the Popover IS the authoritative
dialog): drop apg:dialog-modal, focus.trap, the content part's role:'dialog',
the trigger's redundant aria-controls, and the own open/close events from each
morfo; providers stop firing their own close (commit/cancel just toggle open).
time-picker/time-range-picker Clock made morfo-driven (renderProps).
Also lands this session's picker UX:
- composable <DatePicker.Time /> / <DateRangePicker.Time /> + shared
picker-time helpers/row
- TimeRangePicker: canonical column gap + vertical divider, slider sizing,
AM/PM ToggleGroup, granularity-aware sliders
Verified: all 5 pickers show a single dialog in the browser; morfo:vocabulary
clean; svelte-check 0 new picker errors; provider tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the SYS-3 test gap for the two virtual-focus overlays — the exact paths
the combobox-001 / select-003 focus bugs lived in. Provider-level tests (the
accordion sub-provider harness pattern): create the Trigger/Input provider, set
a contentRef with items, dispatch `onkeydown`, assert the result.
- select (9): ArrowDown/Up/Home/End move highlightedId via the shared nextIndex
helper; ArrowUp from no-selection seeds the LAST item (the n-2 off-by the
helper fixes); clamp vs loop; Enter selects + closes; Escape closes; typeahead;
and DOM focus stays on the trigger (never an item).
- combobox (8): same nav + seed + clamp/loop; Backspace removes the last value
in multi mode; Enter commits a custom value; and DOM focus stays on the INPUT
(the combobox-001 invariant — APG combobox keeps focus on the textbox).
Each is proven to have teeth: breaking the seed (`nextIndex(0,…)`) fails the nav
tests; re-adding `dom.focus(item)` fails the virtual-focus tests. The item
fixtures carry `tabindex="-1"` so a stray `.focus()` actually moves focus —
without it the focus assertion is a jsdom no-op and the test is vacuous.
check: 0 new type errors; both files green (23 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverts 729f3c0b, 2f02fdd9, 6c5616be at the user's request — the phase was
flawed: the SYS-INERT investigation grepped the per-picker providers and missed
that the shared picker-shell already emits `open`/`presence`, so the proposed
wiring would have double-fired (the color-picker click bounce). Reverting the
whole phase to a clean state before re-approaching it with the component READMEs
read first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>