- 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>
date/time/color-field assigned `field.inputId.current = opts.id.current` inside
a `$effect` — the A30 anti-pattern (an effect for a one-shot id registration is
wasteful + order-fragile). `opts.id` is available at construction and the
parent Field context resolves synchronously, so this is a plain constructor
assign. Mirrors the 12 field-family providers that already do it right (the fix
is byte-equivalent to number-field:583-587 / css-field). Verified by that
equivalence + the field providers' 15/15 tests (incl. color-field's
input/label wiring test); a live browser check was blocked by a port conflict
with another session's dev server.
Guard (A30, contracts.test.ts): flags `$effect(... inputId.current = opts.X.current ...)`.
- Proven to catch drift: reverting date-field to the effect form makes it fail.
- Scoped to an `opts.*` source on purpose: file-upload registers
`this.hiddenInputId` (empty '' until a child part mounts) inside a $effect —
that source is genuinely async, so the effect is JUSTIFIED, and the `opts.*`
requirement leaves it green without an exception list. (The guard surfaced
file-upload during development, which is how the distinction was found.)
check: 0 new type errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more catalogue invariants in contracts.test.ts, locking in audit fixes:
- VG-8: every component morfo must be declared `as const satisfies Morfo`,
never `: Morfo` (the annotation widens the literal so the schema can't check
it — how alert-dialog-001 shipped). Green; proven to catch drift (annotating
toggle as `: Morfo` makes it fail).
- A31: a per-item membership predicate (`isSelected` / `isItemPressed` / …) must
not do `.current.includes(...)` — that is O(N²) per render. Lift a Set, use
`.has()`.
The A31 guard immediately surfaced a real violation the human audit missed:
`toolbar-provider` ToolbarGroupProvider.isItemPressed did
`this.opts.value.current.includes(value)`, called from the per-item `isPressed`
$derived — the exact O(N²) pattern, in a component not on the audit's 9-item
A31 list. Fixed with the same lift (`pressedSet` + `.has()`). That catch IS the
guard's proof.
Both guards exclude the active-dev-track set so they stay green for the
maintained catalogue. check: 0 new type errors; toolbar provider tests 3/3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A catalogue invariant in contracts.test.ts: every maintained component that
ships an eidos recipe (a `.svelte` wrapper or `.css` under
`eidos/components/{kebab}/` — README-only dirs don't count) MUST declare
`'eidos'` in its morfo `scope`. This is the prevention for the 62-component
drift the audit found and just fixed — turns "an agent has to re-audit" into
"the build goes red".
- Green now (scope-drift = 0 after 212624e0).
- Proven to catch drift: temporarily removing `'eidos'` from toggle's scope
makes the guard fail naming `toggle`.
- Excludes the active-dev-track set (words/palabras/chronos) so the guard stays
green for the maintained catalogue — a regression guard is only useful while
green, so a NEW drift turns it red.
Note: contracts.test.ts has pre-existing local failures from the untracked
`chronos` dev-track component (incomplete soma chrome trips the soma catalogue
guards); those are unrelated to this guard, which adds zero failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SYS-001 (SUMMARY) resolved + 46 per-component scope-drift findings set to
fixed. The other drifted components annotate the drift in their report header
(not a fix-status finding) or capture it only via the systemic SYS-001, so they
have no per-component line to flip. scroll-area's data-overflow finding is a
distinct undeclared-attr issue (not scope) and stays open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Decision (the one definitional call SYS-1 required): morfo `scope` enumerates
the layers that IMPLEMENT the component (per the glossary), so a component with
an `eidos/components/{c}/` recipe + wrapper MUST list `'eidos'`. This matches the
62 components that already declared it correctly (dropdown-menu, accordion, tabs,
checkbox, radio-group, splitter, …) and makes "which components have eidos"
answerable from the morfo. Chosen over the implicit-eidos alternative for
explicitness.
Added `'eidos'` to the morfo-level `scope` of every drifted component — the 62
with a real eidos recipe (.svelte/.css, not a README-only dir) whose scope
omitted it. After this, all 124 audited morfos declare scope consistently;
scope-drift = 0.
`scope` is purely documentary (the only readers are the schema validator —
`layerSchema` already accepts `'eidos'` — and the 2-of-3 doctrine; nothing keys
behavior off it), so this is a behavior-safe documentation alignment: every diff
is exactly one `scope:` line, `npm run check` adds zero errors.
words/palabras/chronos excluded. calendar included (its morfo is clean; the
view-switch refactor lives in soma/eidos, untouched here).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix-status: open -> fixed (<commit>) for every HIGH whose code fix landed this
session, across 12 component reports:
- 92f988e7: listbox-001, grid-list-001/002, tree-view-002, tree-grid-001, tag-group-001 (SYS-7 A31)
- 98954a7c: alert-dialog-001, breadcrumb-001, navigation-menu-006, select-001, dialog-002
- db33f66c: select-002, dialog-001, collapsible-NEW-001
- bac67315: combobox-001, select-003
The HIGH tier is now code-complete; the 2 remaining HIGH (select-004,
combobox-002) are test-coverage gaps for Phase 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit both virtual-focus list components to a single keyboard model and a
shared, unit-tested index helper (the EX-1 extraction).
- keyboard/directional: new pure `nextIndex(curr, key, len, {loop,nextKey,
prevKey})` — the next/prev/Home/End math every roving/virtual list route
duplicated. Seeds `curr === -1` to first (next) / LAST (prev) BEFORE the
loop/clamp, which kills the off-by where `-1 + prev + loop` wrapped to
`len-2`. 11-case boundary unit suite.
- combobox [combobox-001]: the Input opened then `dom.focus(items[0])` and the
Content route moved real DOM focus onto options — while the Input advertised
`aria-activedescendant`. That's the A17 virtual+real focus mix and breaks the
APG combobox pattern (focus must stay on the textbox). Rewrote the Input route
to own all navigation via a `moveHighlight` helper (highlightedId only, never
`dom.focus(item)`); deleted the real-focus Content route. Browser-verified on
a 19-item list: focus stays on the input, ArrowDown/Up/Home/End drive
aria-activedescendant.
- select [select-003]: the Trigger route now uses the shared `nextIndex`; the
dead Content route (read `activeElement` in a virtual-focus component, carried
the loop off-by) is deleted — verified that activeElement === trigger while
open, so the route never fired. Browser-verified: Trigger route navigates
(Down/Up/End) correctly.
Verified: combobox 3/3, select 3/3, directional 11/11, svelte-check 0 errors in
the touched files, browser-verified keyboard nav for both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- select: drop the dead `focus` block from the morfo. Select uses VIRTUAL focus
(focus stays on the trigger via aria-activedescendant; items are never
DOM-focused), so the declared `trap: true` / `initial: 'first-focusable'` was
unimplemented and contradicted the behavior. Matches Combobox (no focus
block). [select-002]
- dialog: stop double-writing the Content `role`. The morfo declared
`role: 'dialog'` (literal) AND the provider set `role = variant` — syncAttrs'
dom.apply (which runs after Svelte render) overwrote the provider's
`alertdialog` back to `dialog`, so AlertDialog rendered the wrong a11y role.
Remove `role` from the morfo (it's variant-dependent, which a literal can't
express) → the provider is the sole authority. Also drop the dead
`aria-roledescription: undefined` (syncAttrs wrote the morfo value anyway).
Browser-verified: alertdialog content -> role='alertdialog'. [dialog-001]
- collapsible: `collapse` event -> `sequence: 'post'`. The provider sets
`open = false` in the runtime HANDLER, so `pre` deferred the close behind the
full perceptual hold — measured 250ms dead delay before the content closed
(the Checkbox-lag class). With `post` the content closes immediately; the CSS
exit animation is keyed on data-state. Browser-measured: 250ms -> 1ms. [collapsible-NEW-001]
Verified: dialog/alert-dialog/select/collapsible provider tests green,
morfo:check passes for all four (the 5 unrelated fails — chronos/combobox/fab/
menu-dial/words — are pre-existing and untouched), svelte-check 0 errors in
touched files, browser-verified dialog role + collapsible latency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- alert-dialog: `: Morfo` -> `as const satisfies Morfo` (restores createAttrs
key narrowing; the only morfo using the loose annotation). [alert-dialog-001]
- breadcrumb: drop `archetype:'item'` from the display <li> Item part — it
pulled cursor:pointer + hover from archetypes.css onto a non-clickable
container (false affordance). Matches Timeline's pattern. [breadcrumb-001]
- navigation-menu: replace the leaked `$effect.root` (never disposed) with a
bare constructor `$effect` that auto-disposes with the component scope. [navigation-menu-006]
- select: `scrollSelectedIntoView` now reuses `resolveItemEl` (which CSS.escapes
the consumer value) instead of an inline unescaped querySelector. [select-001]
- dialog: delete the dead `[data-dialog-trigger]` chrome envelope (the trigger
composes <Button>, so it fought Button's recipe on the same element) + its
orphaned `trigger-font-size` recipe token; regen eidos css. [dialog-002]
Verified: 12/12 provider tests, morfo:check pass, svelte-check 0 errors in
touched files, eidos recipe contract unchanged (3 failures pre-existing/palabras),
browser-verified dialog trigger (solid Button) + breadcrumb (li cursor:auto).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Constant-resistance circles + constant-reactance arcs (clipped to the unit
disk), with normalized impedance points mapped through Γ = (z − 1)/(z + 1)
and a z + Γ hover tooltip. The matched load (z = 1) sits at the centre.
Configurable grid (resistanceCircles / reactanceArcs), labels and fade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New eidos-native Metrics component (morfo scope sema+eidos, soma runtime
for the live signal). Parts: featured Icon, Label, Value, Delta (direction
⟂ valence via goodTrend), Trend row (delta + caption inline), Chart
(composes Sparkline), Progress + Gauge (compose Meter), Actions, Description.
Opt-in `live` turns a value change into `signal-notify-update` — flashes the
value, pings sema, announces the new value.
Reference-grade hierarchy: the value is rebased ~2 steps UP the canonical
type scale (md→font-xl, lg→font-xxl) so the small-label → big-number jump
reads as a KPI (the 1:1 mapping collapsed it). Tight rhythm (label→value 8px,
value→trend 14px); featured icon = fixed 48px soft rounded-square.
Demo at /uix/components/metrics: Live (interactive) + Gallery (14 real
compositions, outline cards).
Also: fix Skeleton light-mode visibility (neutral-track is ~white in light,
so the placeholder vanished on a white surface). Defer exposing the full
31-scale palette in component `color` + demos to pendiente_color_demos.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Heatmap is now one component, two modes by data shape: matrix {x,y,value}
(columns × rows) and calendar {date,value} (GitHub-style weeks × weekdays,
with granularity year/month, orientation, locale-aware week start, fade).
New standalone charts: Gauge, Funnel (multicolor + labelPosition), Bubble
(size-encoded, axis-padded), PolarArea (Nightingale, padAngle gap between
sectors), CalendarHeatmap (internal renderer behind Heatmap).
Funnel/Bubble/Heatmap/PolarArea gain hover tooltips + dim. CalendarHeatmap
week start + month/weekday labels follow the uix langs locale reactively.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stacking: a `stack` prop (boolean | 'expand') on the bar + area marks, reusing
$libs/plots `shape-stack`; the frame sums the stacked series for the y-domain.
Entrance animations via an `animate` prop (content domain, reduced-motion-free);
each mark plays its idiomatic motion:
- bars grow from the baseline (new `grow` preset), staggered
- lines draw themselves (new `draw` preset: stroke-dashoffset + pathLength=1)
- area grows up from the baseline (`grow`)
- BarList/BarSegment grow from the left (new `grow-x` preset), staggered
- Pie slices + Radar polygons grow RADIALLY from the chart centre (new `bloom`
preset: scale 0->1 + fade, transform-box: view-box), staggered — so each pie
slice grows out and every radar vertex travels from the origin to its place
- scatter + the area's line edge fade/draw; Sparkline inherits
Four reusable, type-safe, reduced-motion-aware presets added to the eidos motion
engine (grow / draw / grow-x / bloom) + the F7 typegen test. `animate` controls
in every chart demo. Roadmap + MUI X comparison in components/chart/IMPROVEMENTS.md.
Note: generated/base.css is regenerated; it carries one inert `--metrics-*` token
from the metrics recipe that co-lives in the shared generated file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>