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>
`item-disabled-opacity` hardcoded `0.55`, off the framework's canonical disabled
opacity (`--opacity-disabled` = 0.4, used by Dialog and others). dropdown-menu +
context-menu now reference the token — disabled menu items match every other
disabled surface. Browser-verified: the resolved token is 0.4.
Scoped to the disabled-opacity drift only. The other THEME-SYS-2 candidates are
left as justified non-drift: `overlay-opacity 62%` / calendar `day-outside 0.62`
are off-scale (no `--opacity-62`; the 0.05-step scale has no slot) named recipe
tokens — a deliberate scrim density, which the off-scale doctrine permits; and
select `item-description-font-size 0.85em` is a RELATIVE unit with no absolute
`--font-size-*` equivalent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- drawer-007 + popover-001 → fixed (729f3c0b).
- SYS-008 (SYS-A33-CLONE) → refuted: re-verification found NO `new Map(this.x)`
clone-and-reassign or `$state(new Map/Set)` registry in any of the four
providers (nor the shared soma layers). They track items via DOM query + the
value opt + the lifted selectedSet. The finding does not hold.
Note: the drawer SYS-5 double-write is systemic across the provider (Title
role/aria-level, Trigger type/aria-haspopup/aria-expanded share the pattern) —
a broader morfo↔soma de-dup, beyond drawer-007's Content location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both parts register with `syncAttrs: true`, so the runtime writes their
morfo-declared attrs to the DOM after render — overwriting any provider re-set
(the dialog-001 mechanism). The providers re-declared the same attrs anyway:
- drawer Content [drawer-007]: dropped manual `role: 'dialog'` + `aria-modal`.
The morfo Content declares `role: 'dialog'` and `aria-modal` (condition: the
registered `modal` source = `variant === 'overlay'` = isOverlay) — identical
values. Browser-verified: after the change the Content still has
role="dialog" + aria-modal="true" on an overlay drawer, now from the morfo.
- popover Close [popover-001]: dropped manual `type: 'button'` +
`aria-label`. The morfo Close declares `type` and `aria-label` via
`commonRef('buttons.close')`, which syncAttrs already wrote (the provider's
POPOVER_LANGS.CLOSE re-set was dead). Removed the now-orphan import.
Single authority per attribute (rule 7.7). check: 0 new errors; drawer +
popover provider tests 7/7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ~12 overlay recipes hardcoded an ad-hoc parallel z-index scale (raw integers
60–99/1200) that duplicated nothing reusable and had drifted out of order
(tooltip 76 < dropdown 80 — a tooltip painted BEHIND a dropdown). They are now a
named scale.
- `STATIC_Z_INDEX_OVERLAY` (static.ts) → emitted as `--z-index-overlay-{inline,
backdrop,content,floating,tooltip,detached,toast}`. A SEPARATE scale from the
global `--z-index-*` ladder (which orders the depth planes) — overlays portal
to <body> as siblings of modals, so they share one flat low band where each
rung sits just above the modal scrim. Mapping them to the 300–900 ladder would
hide a dropdown/select/popover opened INSIDE a dialog (dropdown 300 < modal
700); the combobox recipe already warned about this. `tooltip` now sits above
`floating` (fixes the inversion); `toast` stays above the soma FloatPanel band.
- Every overlay recipe token (`content-z`/`overlay-z`/`inline-z`/`toaster-z`/
`preview-z`) now references `var(--z-index-overlay-*)` — zero raw integers.
dialog/drawer gain an explicit `content-z` rung (drops the `calc(... + 1)`).
- Guard (contracts.test.ts, "overlay z-index against raw integers"): a recipe
`*-z` token must reference the scale, never a bare integer. Proven to catch
drift (a raw `'76'` makes it fail). Local `z-index: 0..5` (avatar/tabs/sticky)
is intra-component relative stacking — out of scope, stays.
- Docs: THEMING.md §35 rewritten to describe the consolidated scale + the
flat-band rationale + the guard; token table gains `--z-index-overlay-*`;
testing-and-tooling.md documents the catalogue guards (VG-8/SYS-1/A31/A30/
THEME-SYS-1).
Stacking order verified from the resolved CSS (deterministic z compare: content
70 < floating 80 < tooltip 90 < toast 1200; dropdown-in-dialog preserved). A
live browser check was blocked by a port conflict with another session's server.
check: 0 new type errors; the 5 guards green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>