src/uix/README.md (485 L, Spanish) translated to English as the book's
opening chapter. Two already-decided reconciliations applied in passing:
the 'authoritative GUIA' pointer now reflects its historical status
(phase-6 decision) and the frozen 2026-05-17 migration status became the
timeless statement (every component ships an eidos wrapper; inventory =
tree + component:audit). Thin stub at the old path; docs/README entry,
strata table and reading order repointed. docs:check 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User decision (2026-07-02): the corpus stops being a MAP over dispersed
layer docs — the layer reference MOVES into docs/ with book order, in
English, superseding the kickoff-era 'hybrid structure' decision. The 189
component READMEs stay in-place (E5, linked).
- docs/process/PLAN-docs-book.md: the phase-7 plan — target tree
(architecture/ canon/ theming/ rfcs/ guides/ decisions/), the per-doc
migration pattern (translate -> Write new path -> stub at old path ->
link sweep -> docs:check -> commit per batch), batches F7.1-F7.7 with
volumes, and the known risks (sN citations get a map in the stub; the
legacy RFC renames finally become safe because the stub keeps the old
name alive for provenance citations).
- Pilot migrated end-to-end: docs/architecture/active-uix.md (English,
frontmatter, links repointed) with a thin stub at
src/uix/active-uix/README.md; docs/README map, glossary and
active_architecture s0 repointed. Validates the pattern for F7.2+.
docs:check: 0 errors (244 docs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing pass of PLAN-docs-reconciliation:
- docs/building-a-component.md 'Known traps' table emptied — every trap
was fixed by phases 1a-1d/2 and docs:check guards the recurrence
classes (this was the phase's visible success indicator).
- PLAN checkboxes ticked + 'Resultado del cierre' appended (commits,
deviations from the plan, out-of-scope findings pending user decision).
- CONTINUE-docs-corpus.md Fase 6 marked HECHA.
- testing-and-tooling.md row reworded so the guard doesn't flag its own
description (the phantom-field literal).
Final state: npm run docs:check = 0 errors, 13 warns (all known foreign/
user-decision targets: THEMING_AUDIT deleted in worktree, PENDIENTES.md
deleted in d68d2c45, MOTION_SERVICE_RFC demo routes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 3 of PLAN-docs-reconciliation (fable_audit D5: the 'link, don't
copy' law had no mechanical guard). New scripts/docs-check.ts + npm run
docs:check with six invariants, each born from a verified drift:
- I1 vocabulary counts vs SEMA_FAMILIES/INTENTS/ARCHETYPE_VOCABULARY
(context-gated: font/shape families, variant archetypes and evaluative
subsets don't false-positive; section numbers and quoted historical
mentions skipped).
- I2 phantom fields: translations: in morfo/soma docs, defaultSemantic
outside LIBRO_VARIACIONES (rename/rejection mentions allowed).
- I3 dependency claims: soma dep lists must cite real package.json
dependencies; corpus-wide @floating-ui mentions need their devDep-only
context.
- I4 SHARED_VARIANT_VOCAB (component-audit) == EIDOS_VARIANTS (textual
mirror comparison).
- I5 checklist<->audit: every Enforcement:audit rule exists in the
script, every script rule ID is declared.
- I6 relative links resolve (WARN until the known dangling targets get
their user decision: THEMING_AUDIT deleted in worktree, PENDIENTES.md
deleted 2026-06-07, MOTION_SERVICE_RFC demo routes).
Chronicle docs (process/, changelogs, audits, CONTINUEs, status:
historical frontmatter, the book) are exempt from truth-tracking.
Also: THEMING s2 per-layer table realigned to the real entrypoint (the
fragile rule-count column dropped); stale adom link in SOMA_ARCHITECTURE
fixed; validator documented in docs/testing-and-tooling.md.
Current output: 1 error (building-a-component known-traps row — cleared
in the closing pass) + 13 warns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 of PLAN-docs-reconciliation (fable-eidos-audit D1: 'THEMING.md es
referencia + changelog fusionados'). New eidos/THEMING_CHANGELOG.md holds
the dated sprint records verbatim (s13 superseded body + s20-s38, same
section numbers). THEMING keeps a numbered stub per section — same title,
the living decision in a few lines, and pointers to the living sources
(per-channel RFCs: SCALING/COLOR_MODEL/COLOR_ENGINE/DEPTH/SHAPE/STRUCTURE;
config/generator symbols; demos) — so every historical sN citation across
the corpus and CLAUDE.md still resolves. Canon-heavy sections (s25 color
model, s29-s31 depth/shape/structure, s32 focus, s35 scale canon) got
richer stubs carrying the invariant + source instead of prose copies
(anti-copy law). THEMING.md 2603 -> 1499 lines; all 38 numbered headers +
TOC anchors intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1d of PLAN-docs-reconciliation.
- soma/README s3 + SOMA_ARCHITECTURE s2/s6/s12 + popover/link-preview
READMEs: positioning is the in-house engine (layers/floating +
$ethereal); @floating-ui is devDep-only. Layers table gains Stacking /
AxialDrag / ZoomPan / ImageProvider / ListSelection (popper/ is an empty
leftover dir — not documented). clsx documented as it really is: NOT
declared but still imported by props/props.ts via a svelte transitive
(phantom dep — flagged for a decision, chip spawned).
- COMPONENT_COMPLETION_CHECKLIST: new Enforcement column on every rule
table (audit | tool:X | manual) so declared-but-unchecked rules are
explicit; morfo rule IDs renamed M-* -> A-* to grep-match what
component-audit.ts emits; stale rows fixed against the corrected script
(D-1.2 v2 9-tab, D-3.1 single eidosSnippet, E-2.2 wrapper import);
implemented-but-undeclared rules added (R-2.7 literal typography,
A-3.4b verb/family pairing); A-3.3 family count decopied; sI now
requires an honest Enforcement value for new rules.
- Sema cascade numbering unified to 1 family - 2 intent - 3 morfo -
4 runtime - 5a packs - 5b app across sema/README, engine.ts,
resolver.ts, resolver.test.ts and soma/runtime.svelte.ts (three
divergent schemes coexisted; CLAUDE.md already uses the chosen one).
- '24 archetypes' copied lists replaced by pointers to
ARCHETYPE_VOCABULARY in active_architecture s6 + morfo/README.
- TriggerOptions.semantic JSDoc: phantom defaultSemantic removed
(implemented shape is additive allowedFamilies, D.11); 'warns via
logger' claim corrected — the override is silently ignored on
non-polymorphic events (no warn exists in code).
npm run check: 61 errors = pre-existing baseline, 0 new.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1c of PLAN-docs-reconciliation (fable-eidos-audit D1/D3/D4, B6/B7).
- THEMING s1 vs s2 ('5 layers' vs '6 capas'): s2 owns the layer story,
retitled count-free and rewritten against the REAL index.css imports
(generated incl. font-faces / archetypes / events / aggregated recipes;
code-split recipes + shared partials + runtime theme blocks noted);
s1 links s2. TOC updated.
- THEMING s3 capa 3 + s6 rule 7: hardcoded slot lists (13 and 9 — both
wrong, code has 12 after border-hover removal) replaced by pointers to
COLOR_ROLE_SLOTS + DEFAULT_COLOR_ROLE_SLOT_STEPS.
- THEMING s4: physical-scale mapping table (primary=indigo, risk=amber —
both stale) replaced by pointer to THEME_BASE_COLOR_ROLES with the
convention-vs-authorship note (CANONICAL_INTENT_SCALES != base theme).
- eidos/README: 'Cambios 2026-05-21' + 'Estado actual (2026-05-17)'
(frozen wrapper list ~20 vs 137 real) extracted to
docs/process/handoffs-2026-05.md; pointer left.
- eidos/components/README: frozen migration table (2026-05-20) extracted
likewise; live inventory = components tree + component:audit.
- COMPONENT_AUDIT_GUIDE: stale v1 s12.7/s12.8/s12.9 citations re-pointed
at DEMO_AUTHORING_GUIDE v2 s6 / the eidos composition doctrine.
- eidos-motion s15: title contradicted its own body (the firma moved to
EidosConfig.motion signatures in F2) — retitled, TOC synced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1b of PLAN-docs-reconciliation (fable_audit D2). The guide contradicted
the code in >=5 verified points while CLAUDE.md declared it authoritative.
Decision: degrade, don't reconcile — frontmatter (status: historical) + banner
pointing at docs/CANON.md + code as the ruling sources, and surgical fixes on
the dangerous points only:
- s2: real inventory is 9 roles (tertiary) with --color-{role}-{slot} naming;
pointer to THEMING s4 + THEME_BASE_COLOR_ROLES.
- s4.1: table annotated as doctrinal; Dialog row now records the implemented
emerge (open/close polymorphic, no shift in allowedFamilies).
- s5.3: phantom defaultSemantic shape replaced by the implemented additive
allowedFamilies (real dialog.ts excerpt; LIBRO_VARIACIONES D.11).
- s6.2: diverged holds table replaced by pointers to SEMA_MAP + holds.ts.
- s14: marked historical (migration plan already executed).
CLAUDE.md citations untouched (own deferred pass).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1a of PLAN-docs-reconciliation (fable_audit D1). The Morfo field is
texts (idlangrefs); the multilingual catalog lives in langs/components/*.
Fixed every example and mechanism claim that still documented the removed
translations: field: morfo/README (contains-list, anatomy, Step 4.5),
SOMA_ARCHITECTURE (Texto funcional + checklist), soma/README (morfo field
list + runtime registration), COMPONENT_GUIDE (A1/A3 + common mistakes +
smoke fixes), COMPONENT_COMPLETION_CHECKLIST M-1.3, active-uix/README,
uix/README, 4 component READMEs, and 3 stale JSDoc blocks in code
(morfo/types.ts, morfo/schema.ts invariant 7, active-uix/types.ts
registerDefaultLangs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fable audit follow-through (fable_audit.md + fable-eidos-audit.md):
- RECIPE_CONTRACT.md (E2 canon): the transversal systems every recipe must
consume, enforced by component-audit R-4.1-4.6 (all at error; escape
valves /* literal */ + /* functional */; WIP tracks excluded). Stale
audit rules fixed against the current architecture (E-2.2 wrapper
imports, D-1.2 v2 9-tab union, D-3.1 single snippet, TabsVariant
mirror) - verdicts went 0/117/15 -> 75/50/5.
- Motion migration 15/15: recipes off local @keyframes onto the channel -
preset stamps (dropdown/context/select/combobox/tooltip/link-preview/
clipboard), new expand/collapse + value-flash signatures, shared-axis
reverse pair, delayed-open open-alias in the preset trigger
(PRESET_STATE_ALIASES), materials pattern for irreducible triggers
(card/timeline/tabs/nav-menu/metrics). Duration/scale hooks keep every
recipe's tuned values.
- buildScheme (fase C): full a1..a12 alpha ramp per role (was a2/a3 -
stale alphas after applyColorScheme), intentSeeds so temper starts from
the ACTIVE theme's intent mapping (risk stays orange), alpha background
self-derived from the scheme's own neutral step 1 (was hardcoded
#fff/#111); mode now forces the donor variant.
- Inventory decisions (fase D): semanticTracking axis removed (all-zero),
border-hover slot dropped (0 consumers), separator slot adopted across
line dividers (step 6, Radix divider tone), size-bundle consumption
pilot on toggle (canonical coordinates consumed, deliberate deviations
kept visible).
- Docs: building-a-component.md (the one door, 9-phase route + known
traps), PLAN-docs-reconciliation.md (fase 6 kickoff for a fresh
session), THEMING wiring updates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Palette:
- Add `fuchsia` (~H334) and `steel` as the 32nd/33rd named scales; rename
radix-scales.ts -> color-scales.ts and drop the "Radix parity" framing
(seeded from Radix, but the palette is ours).
- generatePalette: per-family nearest-hue donor (L-per-hue) so bright hues
(yellow/amber/lime) stay vivid instead of landing muddy.
Theme builder (web/routes/temas):
- estudio: new "palette-first" color mode — character (vivacity/tone/neutral)
regenerates the 33 families; roles are selected by hue harmony or manually
and applied live to real components via --primitive-* overrides.
- paleta: palette generator + role harmony chooser (the model, visualized).
Color guards (THEMING.md §25.2/§25.4):
- G1: completeColorRoleMap throws when a hierarchy alias and a valenced intent
share a scale; the builder picker excludes intent-occupied scales.
- G2: re-expose role slots bg2*2 / separator*6 / borderHover*8 / textStrong*12
(13 slots) — real UI/a11y needs the original nine had dropped.
- G3: document the CVD / WCAG 1.4.1 rule (an intent needs a non-color cue) plus
the affirm/fulfill activation distinction; audit-grounded.
- G4: palette-invariant.test.ts checks the delta-E floor on every generator's
OUTPUT (multi-step, monochrome-exempt); clamp builder tone to +0.08.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top blocks already say FINALIZED, but the body still read as a live P1→P4
plan ("flag OFF", "runtime still @floating-ui", "P4 pending") — stale and
contradictory now that the finalize + flip rollout shipped. Replace the old
"Status: P1 COMPLETE" block with a clear HISTORICAL marker so the remaining plan
reads as the completed build record, not a to-do. Own engine is the runtime;
@floating-ui is out of the library; native path + avoidCollisions:'flip' shipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A perceptible line showed across the caret base on every side but bottom: the
panel's border draws across the caret's base edge, "cutting" it off from the panel.
Add the popover's per-side fix — pull the caret base into the panel by the border
width (margin toward the panel, keyed on data-side) so the fill covers that border
and the caret's side strokes continue the panel outline seamlessly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single-direction drop-shadow on the caret is wrong: the caret sits at the panel
edge pointing AT the trigger, so on a bottom-side tooltip (caret at the top) the
downward shadow falls onto the panel below it — a dark smudge over the content.
Remove it. The caret keeps the panel-bg fill + border edges (no black blob, no
smudge). A shadow that reads correctly would have to wrap the whole panel+caret
silhouette (content-level drop-shadow), not the caret alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the un-clip fix (c3def5d6). With the caret no longer clipped it was
visible, but it rendered in `currentColor` (the dark text colour) — a black triangle
on a light panel. Colour it to the panel bg (a seamless caret) + the panel border on
the two exposed edges, and give it its own `drop-shadow` so the matched (light) caret
still reads against the page — the panel's box-shadow is rectangular and never reaches
the caret. This is the "matched + visible" caret (the drop-shadow-on-the-silhouette
idea) now that overflow:visible lets it show.
VERIFICATION CAVEAT: confirmed the colour at the computed-style level (polygon fill ==
panel background, was the dark text colour). Could NOT capture a stable screenshot —
the demo tooltip closes on every programmatic/cursor interaction, so the visual pop of
the drop-shadow is unverified here. If the matched caret reads too faint, bump the
drop-shadow alpha or fall back to the darker caret.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tooltip content is `position: relative; overflow: auto`, so the caret
(`position: absolute`, containing block = the content) that pokes past the panel
edge was CLIPPED away — invisible regardless of its colour. Confirmed by measuring:
the caret sits fully outside the content box, and the content clips its overflow.
Change the content to `overflow: visible` — the same reason the popover content is
`overflow: visible` (its comment literally says "fixes the arrow that overflow:auto
used to clip"). Tooltips are short, so no inner scroll viewport is needed.
Verified in a real browser: the dark caret is now clearly visible pointing at the
trigger. Combined with a40d1573 (caret colour back to currentColor) + the earlier
`display: block` (correct arrow height → correct trigger gap), the tooltip arrow is
visible and correctly placed again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
My previous commit (4afadfd7) over-corrected: it changed the caret fill from
`currentColor` (the panel's dark text colour — long-standing, visible) to the
panel background, aiming for a "seamless extension". On a light-on-light tooltip
that makes the caret INVISIBLE — the panel reads only because of its box-shadow,
which the separate caret element doesn't share. The user had a visible caret
before; the "correct" colour regressed it.
Revert the polygon/path colour rules (caret returns to `currentColor` = visible)
and keep ONLY the `display: block` on the SVG — that was the fix that mattered: it
removes the ~21px of inline-baseline phantom height that both misaligned the caret
and, because the positioner adds the measured arrow height to the gap, pushed the
panel too far from the trigger. So: caret visible again + panel sits at the correct
~13px gap. A properly matched-and-visible caret (bg fill + its own drop-shadow) is
deferred until it can be verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tooltip arrow SVG carries `fill="currentColor"` / `stroke="currentColor"`
presentation attributes, so setting `fill`/`stroke` on the wrapping `<span>`
(`[data-tooltip-arrow] { fill: … }`) never reached the shapes — an element's own
presentation attribute beats an inherited value. The caret therefore painted with
`color` (the dark text colour) instead of the panel background, and the `<svg>` was
left `display: inline`, adding phantom baseline height (26px box for a 5px caret)
that misaligned it. Net effect: the caret didn't read as a caret.
Fix mirrors the popover recipe: colour the `polygon` (fill = panel bg) + `path`
(stroke = panel border) directly, and set the `<svg>` to `display: block`. The
tooltip caret is now byte-identical in treatment to the popover's (fill = content
bg, 1px border-coloured edge). Also fixes the outline variant, which had the same
span-vs-shape mistake.
Pre-existing bug, unrelated to the avoidCollisions:'flip' rollout. Verified in a
real browser: polygon fill now equals the content background, svg is block, caret
positioned correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Widens `avoidCollisions` from `boolean` to `boolean | 'flip'` on the 6 overlays
that expose it (combobox, context-menu, dropdown-menu, link-preview, select,
tooltip) — the FloatingContent engine already decoupled flip from shift, so
'flip' (flip-only, no slide) works everywhere and lets an arrowless, non-shift
overlay take the native CSS-anchor path. Additive: `boolean` is unchanged.
- Public prop types + provider opts widened (types.ts + *-provider). JSDoc on
the 3 descriptive ones (combobox/select/tooltip) documents the 3 modes; the
terse `@default true` menu types keep their inline style (the type shows 'flip').
- Demos: the 4 that had an avoidCollisions checkbox (dropdown-menu, context-menu,
link-preview, tooltip) now expose a 3-way full/flip/off control.
Verified in a real foreground browser (Claude-in-Chrome):
- dropdown-menu + avoidCollisions='flip' + no arrow → routes to NATIVE
(data-floating-native, position-anchor link to trigger, zero JS transform,
placed correctly).
- context-menu correctly STAYS on JS with 'flip' — it anchors to the cursor
(a Measurable), which the dispatcher's virtualAnchor gate forces to JS. The
discriminator works as designed.
- tooltip / link-preview 3-way controls render.
0 new check errors; 41 overlay + floating tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified in-browser that a borderless filled diamond anchored to the trigger with
mirrored position-try flips natively in sync with the content (the symmetric rotated
square needs no per-side logic), so hasArrow COULD be dropped to let arrow overlays
go native. Not built: the popover caret is bordered (border on the 2 side-facing
edges, drawn per resolved side), and a native flip can't reproduce that — no API
exposes the resolved side. The flip-safe arrow is filled-only (loses the bordered
caret outline) and the win is narrow (shift forces JS anyway). Decision: keep the
bordered caret on JS; the filled-diamond pattern stays a documented, ready option for
solid-fill overlays (tooltips). Captured in strategy.ts + CONTINUE.md so it isn't
re-explored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two parts: the cutover (own JS engine replaces @floating-ui as the runtime, dep
out of the library) and the native CSS Anchor Positioning path layered on top.
Cutover (P4, reconciled):
- Flip USE_OWN_ENGINE -> true, then delete it (flag.ts removed). Collapse the A/B
in use-floating + floating.svelte to own-only (drop the fui imports, the factory
shim, the `as unknown as` casts). useFloating's `dom` is now required.
- @floating-ui moved dependencies -> devDependencies: the LIBRARY (src/arts/ethereal
+ src/uix/soma) imports it NOWHERE; it stays only for the visual demo + the parity
oracle. Zero-dep doctrine met for consumers.
Dual engine (P2 — built + proven end-to-end):
- `selectPositioningStrategy` ($ethereal/strategy.ts, 5 tests) picks native vs JS
per instance on discriminators (arrow / shift / virtual-anchor / explicit-boundary;
sticky is subsumed by shift). The behavioural twin of supportsCssAnchor.
- eidos `renderFloatingNativeBlock`: @supports (anchor-name) and (position-area)
with position-area from data-side/align (logical axes — validated against the spec),
per-side gap margin, position-try-fallbacks for flip. base.css regenerated.
- soma wrapper: when native, stamps data-floating-native + an inline anchor-name<->
position-anchor link and PAUSES the JS positioner (no computePosition, no autoUpdate).
- New `avoidCollisions: 'flip'` (flip-only, no slide) decouples flip from shift so an
overlay can flip natively. Popover demo: avoidCollisions = full / flip / off.
- Proven in a real browser: a flip-only arrowless popover routes to native, positions
via position-area, and flips natively (flip-block) at an edge — zero JS transform.
Honest scope: shift has no native primitive, so native lights up only for non-shift
overlays; the JS engine stays the default for collision-avoiding ones. data-side
doesn't track a native flip, so native is gated to the arrowless case.
Verified: 537 tests green (ethereal + 7 overlays + floating + generated-css), 0 new
check errors. Docs: CONTINUE.md, PERF.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
24h picker driven by a horizontal daylight gradient — drag the sun/moon
thumb across the night→day→night sky, jump with moment chips, or nudge
with hour/minute/second steppers. Inline panel + trigger/popover variants,
built on the generic Picker (deferValue) composing Slider, Button,
PickerShell, Popover and Tooltip.
- granularity (hour|minute|second) + optional band props; steppers keep a
fixed 3-column grid; shape/size propagate to panel, trigger and chips.
- fixed-tone sky palette as named recipe tokens (theme-independent).
- fix soma Tooltip.Arrow (empty children slot hid the default SVG) and
make PickerShell footer labels locale-reactive.
The in-house positioning engine (relocated to its own art in 58683b03) is
renamed floating -> ethereal: src/arts/floating -> src/arts/ethereal, alias
$floating -> $ethereal (vite + svelte config), all soma wrappers + the demo
repointed. The reactive wrappers stay in soma/layers/floating (soma's
floating-overlay concept); the *library* is ethereal.
Also lands the cross-browser validation + perf characterization the rename
batch was verified against:
- engine-dom parity suite runs on Chromium + WebKit + Firefox via
UIX_CROSS_BROWSER=1 (env-gated in vite.config; default chromium). 411 runs
green — exercises the isWebKit-gated paths Chromium never ran.
- PERF.md: honest write-up. Bundle measured (own 6.1 KB vs @floating-ui
8.2 KB gzip -> ~2 KB net win). Speed numbers labelled CPU-work, NOT
perceived wall-clock; the +1-frame latency documented as a real cost.
- web/routes/demos/ethereal: visual A/B demo vs @floating-ui.
@floating-ui untouched — flag still OFF, P4 not executed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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).