- <ColorField.Input swatch> renders an opt-in leading colour chip showing
the field's current value, sized to the control icon (--icon-size-{size})
via a concrete token — never 1em, so it cannot recompute/jitter.
- field-segment-state: drop the active-segment font-weight 600; bold widened
the ch-based segment ~0.5px, resizing the highlight on every increment. The
accent background already marks the active segment.
- color-field stays full-width like the whole field/select family — width is
container-driven, not field-driven (no per-component exception).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the per-field / per-picker duplication the audit surfaced:
- Shared control trigger — new field/field-control-trigger.css selected by a
`data-field-control-trigger` marker: flush (no border, no shadow), UNIFORM
neutral hover tint (not the field accent), font-relative size. Migrated
date-picker + the password visibility toggle; deletes their per-picker chrome.
(date-range / time / time-range pickers still to migrate.)
- Shared segment state layer — new field/field-segment-state.css selected by a
`data-field-segment` marker (date / time / color fields): one hover (editable)
+ active (focus) rule. Fixes a specificity bug where the hover selector
out-ranked :focus, so hovering the active segment downgraded its highlight.
- Segment fixed width — min-inline-size: calc(2ch + 2*segment-px) +
font-variant-numeric: tabular-nums so a segment never reflows when its value
changes 1<->2 digits or between glyphs (no horizontal jitter). date-field done.
- date-range-picker trigger uses the same calendar icon as date-picker (was a
distinct calendar-range glyph).
- Generated element ids use the framework-neutral `uix-` prefix instead of the
layer name `soma-` (createId / useId + announce / menubar inline ids) — the
doctrine that bans data-soma-* / --soma-* keeps a layer name out of ids too.
- field.css: label + helper text scale one step below the control (font-relative
calc); optional-indicator weight to regular.
Foundation tokens added: --field-segment-hover-bg, --field-control-trigger-hover-bg.
Verified in-browser by measurement (date-field segments: states + no-jitter;
date-picker trigger: flush + uniform hover). check = 58 baseline (0 new);
recipe-css-contract clean (only pre-existing palabras WIP).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersede the "compact control md=14" archetypes (Fase 7) with a single
rule: control text scales 1:1 with its size (md=16) across all components,
so changing --font-size-md re-themes the whole system. Applied to button,
badge, breadcrumb, calendar, pagination, radio-group, toolbar, file-upload,
tag-group, stepper, toggle, tooltip (the field family was already 1:1).
Button + search-field icons now follow the font 1:1 (icon ~= font+2;
density-independent, like the type scale). Fix button icon vertical
centering: the [data-button-icon] slot didn't flex-center, so the inline
<svg> sat on the text baseline ~1px below the button center.
Exceptions left as-is (not control text): avatar (initials sized to the
circle diameter), accordion (section-heading prose scale), password-field
icon-size (it is the visibility-trigger button box, control-coupled by
design), radio-cards (icon follows the card title).
THEMING.md section 5 rewritten: 1:1 universal mapping + "density is
orthogonal to typography" (density scales space + control-height only;
font + icon ride only the global --scaling zoom).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Field font scale: input 1:1 (size N -> --font-size-N, md now 16px), label one
step below (10/12/14/16/18). Recipes: field, spin-field, date/time/color-field,
search-field, password-field, select, editable, tags-input. Segmented-field
labels via CSS calc; generic Field label via label-font-size tokens.
- Picker triggers (date/time/range): box now scales off the local
--_X-field-height + font:inherit so the 1em icon follows the field font; added
the missing xs size variant to time-picker + time-range-picker.
- color-field/time-field commit-set fires + sounds on increment (compare by
ColorValue.hex; time-field passes fallbackTarget for the double-registered
'input' part).
- color-field format-select: reverted the segment-active tint back to its focus ring.
- Docs: THEMING.md field font rule; COMPONENT_COHERENCE_AUDIT.md.
words / palabras / chronos untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Un componente coordinado puede componer la cascada (motion) con sonido/háptico
(sema) en el mismo evento. El cabo: el meta-canal visual de sema estampa
data-event-* siempre que el evento tenga algún canal (channels != []), así que
channels:['sound'] arrastraba la firma visual genérica (present-rise), que pelea
con la cascada.
Solución EN MOTION (sema intacto): eidos neutraliza su PROPIA firma visual sobre
las superficies coordinadas. render-css.ts > renderCoordinatedPresetRules emite
`[data-animation-style='cascade-X'][data-event-phase='active'] { animation: none
!important }`. La firma usa `animation` -> muere; la cascada es `transition` ->
sobrevive. Aditivo: inerte hasta que un coordinado dispare un evento sema.
Ejemplo: Reveal pasa a modo (c) — open y close declaran channels:['sound']
(emerge suena, pitch 600) + expression:'family-default'; suena al abrir y al
cerrar a la vez que la cascada, sin pelea visual. Rail queda como modo (b) puro.
Layout de /temas/animations con events:{ sound: true }; la demo /reveal explica
el modo (c).
Honestidad: la versión inicial del RFC Apéndice B afirmaba que el modo (c)
componía "gratis" — falso (verificado en engine.ts/visual.ts). Corregido: el
Apéndice B documenta ahora el acoplamiento real y el opt-out en motion.
Tests: eidos/motion 25/25 (neutralización) · morfo 70/70 · morfo:vocabulary
limpio. Audio verificado en navegador (suena al abrir y cerrar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Servicio de motion cross-layer: morfo declara superficies animables +
coordinación; soma coordina presencia/lifecycle; eidos posee lo visual;
arts/motion ejecuta por-nodo. RFC en eidos/MOTION_SERVICE_RFC.md.
- M1 — contrato MorfoPart.animation (types/schema/compile); children
como { enter?, exit? }.
- M2 — PresenceGroup (soma/layers/presence-group.ts), rune-free;
Presence.group descubre el coordinador por context.
- M3 — exit con retención de DOM (§8.1).
- M4 — interrupción/reversa (§8.3): token de generación + motion.cancel
en flip + toHandle resuelve finished en cancel (sin AbortError suelto).
- M5 — prop `animation` enrutada a las parts surface:true del morfo
compilado (routeAnimation); Panel/Item emiten data-animation-style.
- M6 — stagger auto-derivado del orden de registro (--motion-stagger-*,
inversa en exit) + presets coordinados en la librería de eidos
(MotionConfig.coordinated; cascade-slide/-fade/-scale) que reaccionan
a data-starting/ending-style, NO a data-state.
Helper Coordination (soma/layers/coordination.ts) extraído y validado
por DOS consumidores reales: Reveal (raíz virtual + Panel owner) y Rail
(raíz=owner, together). Demos en /temas/animations/{reveal,rail,
presence-group}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the icon↔type correlation (option 1): the icon follows the font the
component actually shows, no font step-up. button icon-size-sm: icon-xs→icon-sm
(follows its font); radio-cards icons follow the card title (16/16/18/18/20,
were 18-32 hand-inflated). STATIC_SIZE's font step-down stays — control-density
convention, not a bug.
Fix pre-existing button bug: the Icon's inline `width: var(--icon-size)` beat
`[data-button] svg { inline-size }`, pinning the button icon at 18px regardless
of size. button now feeds `--icon-size` from `--_button-icon-size`, so the icon
follows the button size (14/16/16/18/20). Verified in browser.
CONTINUE-icon-type-scale.md updated with the resolution + a pending audit of the
same Icon-sizing bug in other components.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Icon scale large steps now track the type scale (validated by eye in a study
harness at /uix/icon-scale-study): --icon-size-xl 24→32, --icon-size-xxl 48→52,
so the icon keeps its optical weight beside its paired text. 0 impact — nothing
consumed those steps yet.
button: icon-size per size references var(--icon-size-X) (mapped by value → same
rendered px, now density-scalable) instead of hardcoded px.
Docs: THEMING.md §5 documents the type↔icon correlation; CONTINUE-icon-type-scale.md
captures the pending 1:1 work (size = font = icon per name, dropping the font
step-down) for the next session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recipe defined per-color tonal tokens inconsistently — only fulfill/loss had
`track`/`border`, so soft/surface/outline rendered with no background or border
for the other 6 colors (primary/secondary/neutral/affirm/risk/threat):
`palette-track`/`palette-border` referenced undefined `--button-{color}-track`/
`-border`, which collapsed the background/border declarations. Added the missing
track+border to all 6 (mapped to `var(--color-{role}-track|border)` like
fulfill/loss); the recipe is now uniform (7 slots per color). Verified all 8
colors × soft/surface/outline in dark + light.
Demo: new "Gallery" tab — a 6 variants × 8 colors matrix of real <Button>s
(hierarchy via `color`, evaluative palette via `intent`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logo overlay rendered invisible. Inside a viewBox-scaled foreignObject a
fixed px padding (var(--space-1)) equals that many *modules*, overflowing the
cleared centre and collapsing the content box; inline-size/block-size also
don't size an <svg> element. Fix: proportional inline padding + physical
width/height. The logo now renders and still decodes at ECC H. Dropped the
now-orphan --qr-code-overlay-padding token + regenerated base.css.
Demo: content presets (URL / vCard / WiFi / unicode) prove the encoder takes
any UTF-8 string, not just URLs (each verified decoding via jsQR); logo on by
default; quiet-zone chips fixed to canonical [4, 8, 16] (was [2, …], below the
spec's 4-module minimum). README documents the quiet zone as modules, not
pixels — the canonical unit; for px layout spacing, pad the container in CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Own QR encoder in $libs/qr (ISO/IEC 18004: segment analysis →
Reed-Solomon over GF(256) → matrix layout → 8-mask penalty selection),
no npm dependency. Eidos <QrCode> renders the module matrix as one SVG
path with square/rounded/dots cell shapes; function patterns stay square
(via the encoder's `reserved` mask) so every shape scans. Optional logo
clears the centre + auto-boosts ECC to H. <QrCode.DownloadTrigger>
exports PNG/SVG, baking resolved colours into the clone first (a
serialized SVG loses the document's CSS custom properties). Fixed-tone
default colours for scannability; themeable via color/background.
morfo qr-code (scope: eidos; parts Provider/Pattern/Overlay/
DownloadTrigger) + 'path' added to MorfoElement; recipe with 11
--qr-code-* tokens; canonical demo + nav entry. 11 encoder unit tests;
verified end-to-end via jsQR round-trip + browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New eidos component built ON TOP of RadioGroup with zero duplicated
behaviour. RadioCards reuses RadioGroup's soma wholesale (selection,
roving focus, role=radio, native <form> via HiddenInput) and adds only
the card chrome: icon · title · description + an always-present corner
radio. Vertical, horizontal and grid layouts.
- morfo: scope:['eidos'], thin contract for presentation parts only
(Provider marker, Item-card, Indicator, Icon, Content, Title,
Description). The interactive contract + commit-select stay in
radio-group, reused via its soma. No new soma, no new sema.
- eidos: `radio-cards` recipe (card chrome) + wrappers + radio-cards.css.
radio-group.css is NOT loaded here, only its foundation tokens — the
card owns its visual, reusing --_radio-group-palette-* (inlined, since a
:root token referencing the palette is invalid-at-computed-value).
- demo: canonical structure (always-on stage + trace, tabs, chip/switch
controls, Untitled-UI example patterns) + nav entry + README.
check 0 · eidos-lint 0 invalid. Browser-verified: select (click + keyboard
roving), disabled, the three layouts, controls drive the preview.
Follow-up: icon/circle sizes are still px (mirroring radio-group's own size
tokens) — making the control-size scale density-aware is a separate,
family-wide change deferred per request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField are the same visual (a bordered field + input +
increment/decrement triggers + scrubber, split/stacked layouts, sizes/
variants/colors, themeable glyphs); only their value model differs. They
were two cloned recipes + CSS that drifted — a refinement to one (square
flush buttons, divider, contrast) didn't reach the other.
Unify into ONE shared source (the toggle-group structural-identity pattern):
- New eidos/components/spin-field: recipe key `spin-field` (`--spin-field-*`)
+ spin-field.css with all the stepper-field rules, selecting `[data-spin-
field*]`. Loaded via the foundation @import in index.css.
- number-field + css-field morfos declare structural identity (`data-spin-
field*` presence attrs on each part). The Provider emits them via syncAttrs;
the sub-parts emit them in their soma `props` getter (number-field's soma
hardcodes sub-part attrs rather than syncing the morfo).
- Removed the `number-field` / `css-field` recipe keys; their CSS files are
now stubs. A theme tints one component by scoping `[data-number-field] {
--spin-field-… }`.
- css-field thereby adopts number-field's refined steppers (square, flush,
divider) — the drift fix the user asked for, now structural (no clone).
Verified bit-for-bit in browser: number-field identical to baseline (split
flush, stacked symmetric xs..xl, RTL, glyph token/children override);
css-field now square/flush/divider. eidos-lint invalid 0; recipe contract
passes (no orphans, loads-once); check + morfo:check clean for these.
Docs: THEMING sections 33 (glyph tokens now `--spin-field-*`) + 34 (the
shared layer); number-field / css-field READMEs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField steppers gain a `stacked` compact layout and
themeable default glyphs.
- Glyphs render from recipe tokens via `:empty::before` (split plus/minus,
stacked up/down triangles); passing children overrides per instance,
a theme overrides the token. 4 tokens per component, unicode-escaped.
- CssField gains the `steppers` prop (split | stacked); its stacked layout
mirrors NumberField (self-contained — the split layout left untouched).
- NumberField split: square buttons flush to the field edge with an inner
divider and higher hover/focus contrast (control-radius 0).
- Stacked rows use minmax(0,1fr) + a height-proportional glyph so the two
arrows stay symmetric and uncut at xs.
- Demos: steppers controls + stepper-layout / sizes / themed-glyph
showcases; code snippets use the themed defaults.
- Docs: THEMING section 33, number-field and css-field READMEs.
Note: also sweeps in the pre-existing palabras WIP already in the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each input drew its focus ring differently — the foundation [data-archetype]
:focus-visible ring landing on the <input>, ad-hoc per-component input rings
(css-field/number-field), the textarea's own color-mix — so editing showed a
DOUBLE frame (inner + outer), inconsistent across components.
Unify into one theme-parameterised two-ring model:
- New --focus-ring-inner-width (0 by default), folded into --focus-ring and every
field *-focus-shadow: inner inset ring + outer ring. 0 = single outer frame;
raise it in a theme to show the inner line everywhere at once.
- Foundation [data-archetype]:focus-visible now excludes form-internal elements
(:not(input):not(textarea):not(select):not([data-archetype='segment'])) — their
focus is shown by the wrapping control.
- Removed the ad-hoc input rings (css-field/number-field); textarea uses
var(--focus-ring).
- Cleaned the now-dead per-data-color _accent-* cascade in css-field/number-field
(recorded as backlog in THEMING.md §32).
Verified with real keyboard on field/css-field/number-field/date-field/textarea:
single outer frame. Also includes the earlier textarea `resize` fix (morfo
data-resize values + soma stops inlining resize + eidos materialises it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A grabbed surface scales slightly toward the viewer (depth 'picked up' cue). This
is a cross-component primitive, not a float-panel literal:
- New --motion-scale-lift (1.02) in STATIC_MOTION.scale + MOTION_SCALE_KEYS (the
only >1 in the scale family); validated by validateEidosConfig.
- FloatPanel consumes it: [data-float-panel-content][data-dragging] { scale:
var(--motion-scale-lift) } + will-change: translate, scale, eased lift/settle,
static during the move (composited), suppressed under reduced-motion.
- Documented in eidos-motion.md §11 (primitives table) and THEMING.md §14 (new
'Superficies arrastrables: el lift de pickup' pattern) so any draggable surface
(slider thumb, sortable item, drawer) reuses the SAME token, not its own literal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New general-purpose overlay component across all 4 layers (morfo/soma/sema/eidos)
plus a shared stacking layer and i18n catalog.
- Drag (2D free, anchored→free) + resize with device-pixel-snapped, GPU-composited
1:1 movement; elevation lifts on pointerdown for the pickup metaphor.
- Stages: minimize / maximize / restore (StageTrigger, computed maximized geometry
so restore needs no saved-rect bookkeeping).
- Composable header buttons: Controls + Action (custom) + Close + StageTrigger.
- Keyboard a11y (the industry gap): grab-mode move/resize on the focusable header
and a se-corner ResizeGrip (pointer + keyboard), with debounced live-region
announcements, bounds/min-max cues, aria-roledescription/keyshortcuts.
- Stacking registry (bring-to-front/topmost), Presence motion preset (scale-fade),
es/en langs catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Motor (words): ImageBlock gana `fit` (fill/fit/crop/tile) + 7 filtros CSS
(saturate/brightness/contrast/hueRotate/grayscale/sepia/blur) + `width`/`height`
number|string (CSS-length). Render: object-fit + branch tile bg + figcaption.
Fase A: caret-carry al insertar, guard columnas-en-columnas, deleteColumnAt
siembra parrafo cuando el superviviente queda vacio. block-path.test.ts (14).
- Panel de imagen reescrito con primitivas eidos (ToggleGroup/Switch/Slider/
NumberField/Accordion), labels xs sans arriba del slider, header con icono
definitorio del bloque, secciones pinned + accordion collapsible, footer band.
- sium: `cssLength()`/`isCssLength()`/`CSS_LENGTH_REGEX` en core/refines.ts,
exportados por la fachada EngineSium. El panel valida unidades CSS (data-invalid).
- accordion: tamano `xs` (types + recipe base.ts + accordion.css + generated).
- words-activate: guard atomico (lastAtomicAt + isActiveBlockAtomic) evita que el
caret-follower degrade el bloque imagen activo a heading.
- saturate/contrast max 200% (estandar, antes 300% inventado). Caption visible:
fit-frame movido de figure a img/tile.
Sin tocar la capa sema ni la animacion (restriccion del usuario).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The block inspector mixed three control heights (chip 26 · number-field 28
· color-trigger 36px) and two fonts (chip/trigger 10px vs label/value/input
12px), so the PRIMARY control — the chip — was the smallest text in the panel.
- New recipe tokens `--words-control-height` (1.75rem — was a magic literal on
the number-field) and `--words-field-width` (7.5rem), applied uniformly to the
whole inline tier (chip · color-trigger · number-field) → one 28px height + an
aligned right-edge column.
- Chip + color-trigger lift to the 12px body tier (the ToggleGroup `xs` is kept
ONLY for its compact padding; lifting the font without the padding keeps the
four-segment groups on one line).
- Cramped long labels shortened (Predeterminada→Auto, Semi-negrita→Semi) so
Fuente/Grosor read as four even segments.
- Collapsed the dead, inconsistent font-size fallbacks (`xs, 0.6875rem` /
`xs, 0.75rem` / `sm, 0.8125rem`) to the bare declared token.
Chrome scale is now a clean 14 (section) / 12 (body) / 11 (micro). Verified in
browser: chip · color-trigger · number-field all 28px·12px, no group overflow.
19/19 recipe contract green; check adds zero errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inspector mixed THREE fonts: labels in Lora (serif!), titles in system-ui,
inputs in Instrument Sans. Root cause: eidos's "content" archetype (Accordion
content) maps to the reading serif, and the inspector's `[scope] *` rule is only
(0,1,0) — equal to the component's `[data-x]` font rule — so the Phase 3
code-split flipped the load order and the serif won.
Fix: a `--words-chrome-font` token (= --font-ui, independent of the content
`font-family` a theme may set to serif) + a rule forcing it across EVERY chrome
surface (toolbar·bubble·slash·drawer·inspector·gutter menus) with a DOUBLED
scope attribute (0,2,0) that beats any single-attribute component font
regardless of code-split order. Verified in browser: all chrome = Instrument
Sans (was Lora/system-ui/Instrument). Content keeps its reading font.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The words recipe declared a token vocabulary the CSS never used (the root of
the drift). Pruned 55 declared-but-unused tokens: 38 direct (gap-*, toolbar-*
×9, button-size-*, command-*-hover, content-px-*, status-*, rail-*,
selection-color, _accent-* ×5, line-height, max-inline-size, heading-font-size,
content-py-after-placeholder) + 17 cascaded (the intent palette *-solid/
*-solid-hover/secondary-* slots that only fed _accent-* — callouts use the
tinted track/border/text slots, kept). No wire candidates (rail had no
consumer; ::selection already uses wash-soft). The recipe contract now matches
actual usage → the orphan test goes green; full eidos suite 197/197 (all 3
words failures now fixed across increments 1/3/A). Visually inert: words.css
references nothing removed (the "declares every var" test guarantees it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The small-control radius was a soup of 8 syntaxes for the same thing:
4px / var(--radius-2,4px) / 3px / even var(--space-1) (a spacing token used
as radius). Added --words-radius-xs (= --radius-sm, 4px) — the missing
small-control step under sm — and snapped all of them to the
--words-radius-{xs,sm,md,lg} scale. Micro decorative radii (1-2px) and circles
(50%) left as-is by design. Verified radius-xs=4px, editor renders, no
regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kills the biggest visual-inconsistency soup: 18 ad-hoc color-mix opacities
(4/6/8/12/14/16/22/30/35/40/45 %) collapsed to a 4-step canonical scale
--words-wash-{faint,subtle,soft,strong} (6/16/30/44 %), applied across
words.css; geometry %s (circles, widths) untouched. Hardcoded transition/
animation durations (120/140/100 ms) collapsed to --words-transition-duration.
Declared the 2 used-but-undeclared tokens (--words-font-size-xs, --words-swatch)
→ the "declares every public var" recipe test goes green (1 of 3 words failures
fixed). Editor verified in browser, no regression. See components/words/AUDIT.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
base.css shipped all 31 Radix donor scales, but the theme's roles only
reference 9 (gray green indigo orange plum purple red slate teal). The
other 22 were dead weight on every page: no component references raw
--scale-* (they use --color-{role}-*), and runtime theming builds from JS
scale data and writes RESOLVED values (build-scheme: variables[k]=hex),
so it never reads the CSS scale tokens.
renderThemeCss now emits only role-referenced scales by default; the full
31-scale donor palette ships opt-in in generated/palette.css (new
renderColorPaletteCss). The CSS contract stays the full vocabulary
(apps can still override any scale; load palette.css to use the extra 22).
base.css 53.7→35.4 KB gz. Monolith (every page) 54.4→36.2 KB gz —
combined with Phase 3, 113.5→36.2 KB gz total (−68%). Verified: roles
resolve (primary=purple, button bg correct), pruned scales empty on
regular pages, restored on /temas/color via palette.css. Updated the
contract test to verify coverage against the full palette. check clean
(13 errors are untracked heroscrolling demo + gitignored tmp/lexical);
eidos suite at baseline (192/195, 3 pre-existing words failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shape becomes a two-moment channel: resting form (Fase 1) + the morph (event).
Folded cross-modally into the existing `press-squeeze` contact firma — one press
now drives scale (motion) + shadow-flatten (depth) + corner-firm (shape): the
`continuous` corner squares up at the press peak (--shape-smoothing 2 -> 3 -> 2).
- `@property --shape-smoothing { syntax: <number>; inherits: true }` so the number
interpolates smoothly (mirrors the motion @property pattern).
- Set only at the keyframe peak -> interpolates from/to the element underlying
smoothing (respects a theme override). No-op for non-continuous corners (they do
not read --shape-smoothing). Degrades with reduced-motion via the firma system.
Material 3 Expressive is the only other system with shape morph (bespoke, closed);
this rides the unified firma — one event, many modalities.
Verified: check 0 errors; eidos config+motion+generated 78/78. Regenerated base.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`--shape-nest-gap` token + `[data-shape-nest]` rule: a nested element derives
`border-radius: max(0px, var(--shape-outer-radius) - var(--shape-nest-gap))`, so
its corners stay parallel to the parent (concentric). The parent exposes its
radius via `--shape-outer-radius` (inherited); gap defaults to the nest token.
nestGap is optional on ShapePrimitiveSet (a shape system can omit nesting); the
token + rule are gated on its presence. Only SwiftUI (recent, platform) has
concentric corners — none on the web as a token rule.
Verified: check 0 errors; eidos config+generated 55/55. Regenerated base.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8th book channel (forma) — the one no web design system has elevated. Fase 1
lays the foundation: corner continuity + perceptual families as portable tokens.
- ShapePrimitiveSet (config-driven): `smoothing` (superellipse exponent, 1=arc ->
2=squircle) + `families` map. STATIC_SHAPE ships rounded/continuous/cut/scoop.
- Emission: `--shape-smoothing` token + `[data-shape=`{family}`]` rules setting
`corner-shape` (round / superellipse(var(--shape-smoothing)) / bevel / scoop).
Opt-in: magnitude stays in `--radius-*` (untouched), so corners degrade to the
plain border-radius arc where `corner-shape` is unsupported (progressive, like
the wide-gamut oklch of color). var() works inside superellipse() (Chrome 146).
- Validation (validateShapePrimitives) + 2 tests + regen.
Why it matters: the whole web field (Tailwind/shadcn/Chakra/Mantine/Ant/Radix
Themes/Carbon/Fluent/Spectrum/Polaris/Primer) is "radius scale + circular arc +
static". Continuity exists only in Apple (platform-locked); none on the web ships
squircle as a token. This is the first.
Verified: npm run check 0 errors; eidos config+generated 54/54. data-shape applies
in the live runtime (continuous->squircle, cut->bevel, scoop->scoop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the depth channel. Atmosphere as the channel's materials layer:
- Per-plane `blur` cue (overlay 10px, modal 16px) + an opt-in frost rule
`[data-depth=`{plane}`][data-frost]` -> translucent surface (color-mix 80%) +
backdrop-filter blur. Gated on data-frost so it never turns an opaque overlay
translucent by default; specificity 0,2,0 reliably overrides the component bg.
- buildDepth(planes) (pure) + ActiveEidos.applyDepth/clearDepth — retune any
plane cue (surface/shadow/halo/blur/scrim/z) at runtime, the depth sibling of
applyColorScheme / applyTypeScale. Exported from $uix/eidos.
Showcase: /temas/profundidad section Materiales — a frosted-glass panel over a
vivid color mesh (frost blur + shadow + halo).
The `scrim` cue stays an available token without a wired rule — modal backdrops
are component-managed.
Verified: npm run check 0 errors; eidos 185/188 (3 pre-existing `words` failures,
unrelated). New tests: frost emission + applyDepth. Regenerated generated/base.css.
Docs: DEPTH_ENGINE_RFC Fase 4 + token contract, THEMING 29.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Fase 2 rim halo now reaches real UI, not just the showcase. Each elevated
component's shadow signal (recipe tokens in recipes/base.ts + the few direct
box-shadow uses) now composes `var(--depth-{plane}-shadow), var(--depth-{plane}-halo)`.
Reaches: popover, dialog, drawer, dropdown/context/navigation-menu, menubar,
select, tooltip, card, combobox, command, link-preview, words.
Shadow signal only — z-index stays component-managed (the z bands are finer than
the 5 planos), so zero stacking risk. Verified: dialog keeps its own z-index 71
and composes drop shadow + oklab halo. Retuning a plane now retunes every
component on it.
Verified: npm run check 0 errors; eidos 183/186 (3 pre-existing `words` failures,
unrelated). Regenerated generated/base.css. THEMING 29 updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Depth engine — Fase 2 (mode-adaptive mezcla):
- New `halo` cue per plane: a top-edge rim-light computed in oklab
(color-mix(in oklab, white N%, transparent); 5/7/8% on raised/overlay/modal).
The `[data-depth]` box-shadow now composes `shadow, halo`. Invisible on light
surfaces (the drop shadow leads), the lift cue on dark surfaces (where the drop
shadow barely shows) — the mode-adaptive answer to "shadow lies in dark",
scoped to the depth channel (global --shadow-* untouched).
- Wired through config-types (DepthPlane.halo) + render-css (declare + compose)
+ config validation + STATIC_DEPTH + regenerated generated/base.css.
Showcase — /temas/profundidad to reference depth (4 -> 9 sections): matches
Material elevation catalog breadth and adds the two axes it lacks (eventful +
open cage):
- Responde a cada estado — dynamic elevation, live interactive control
- La escalera de planos — z-stack of the 5 planes
- Catalogo de planos en reposo — the resting-elevation spec table, our vocabulary
- Luz vs sombra — light/dark side-by-side showing the halo mechanism
- Accesibilidad — never the only channel, reduced-motion, forced-colors, contrast
- Fix: mirror data-theme onto <html> so :root depth tokens stay mode-aware
Docs: DEPTH_ENGINE_RFC (Fase 2 + 5 done, token contract + halo), THEMING 29.
Verified: npm run check 0 errors; depth tests 50/50 (updated the box-shadow
assertion to the shadow,halo composition). Pre-existing `words` recipe-contract
failures unrelated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The novel core: depth as something that OCCURS. The elevation dimension now lives
in the event firma (the two-moment model event moment) — leveraging the existing
signature system, NOT a parallel one:
- present-rise (emerge): the box-shadow grows from flat -> the element resting
plane shadow as it rises. A flat element (no resting shadow) is a no-op; a
raised/overlay element animates its elevation proportional to ITS plane.
- press-squeeze (contact): the shadow flattens to the surface at the press peak,
then returns (recede).
So depth + position/scale + sound + haptic all fire coordinated from one event.
Degrades with reduced-motion (the global events.css cap); a theme overrides the
keyframes/signatures (jaula abierta). No reference framework treats depth as eventful.
Fase 2 (computed mode-adaptive shadow mix) deferred — the shadows are already
mode-aware, so it is a refinement, not a gap (documented in the RFC).
Verified: check 0 errors; motion + config + generated tests pass; browser — the
present-rise keyframe is live with the box-shadow dimension.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arranca el sprint de profundidad: depth como un plano unificado, config-driven y
semántico — la base del canal "la profundidad es algo que OCURRE" del libro (el
disparo eventful por sema llega en Fase 3).
- DEPTH_ENGINE_RFC.md — la guía de diseño: estudio de los límites de los
referentes, la tesis novel, y la doctrina "default fuerte, jaula abierta".
- EidosConfig.depth.planes (DepthPlane + DepthPrimitiveSet). Set canónico:
flush · raised · overlay · modal · recessed — cada uno COMPONE los primitivos
existentes (surface/shadow/z), sin matemática nueva → cero rotura.
- emite tokens --depth-{plane}-{cue} + reglas [data-depth='{plane}'] que aplican
las señales aditivas seguras (box-shadow + z-index); surface + blur/scrim
quedan como tokens opt-in (no pisan fondos de componente).
- validado; config-driven (un tema añade/retunea planos — jaula abierta).
- arregla un punto y coma latente en la emisión de variable-fonts, cazado aquí.
Verificado: check 0 errores; tests nuevos (emisión canónica + plano custom);
generated/base.css regenerado; navegador — data-depth='overlay' aplica la sombra
overlay + z 400, 'recessed' aplica una sombra inset. (3 fallos de words pre-existentes.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the Phase 3 scales into <Text> and <Heading> + fixes a token collision the scales
introduced.
- Text/Heading props (additive): `tracking` / `leading` (reuse the existing
--_x-letter-spacing / --_x-line-height vars → override the size-derived values),
`wrap` (text-wrap: balance/pretty/nowrap), `numeric` (tabular/oldstyle →
font-feature-settings), `measure` (max-inline-size). Heading defaults to
`text-wrap: balance` (reference-grade titles); Text defaults to the CSS initial so the
axes are no-ops until a prop is set. Heading reuses Text's scale unions.
- COLLISION FIX: Phase 3a's config-driven --tracking-{tight,normal,wide,wider} collided
with a pre-existing HARDCODED tracking scale in render-css (semantic badge/ui/… +
scale tight/normal/wide/wider, all 0) emitted later → it won (everything resolved to
0, so the tracking prop did nothing). Removed the hardcoded scale lines; the config
(typography.tracking, real optical values) now owns tighter/tight/normal/wide/wider.
The semantic tracking tokens (badge/label/ui/prose/heading/display) stay (recipes use
them, e.g. card-title --tracking-tight now picks up the real -0.02em). leading/features
/measure don't collide (distinct keys).
Verified in browser: tracking-wide 0.02em -> 0.32px; tracking-tight -0.02em -> -0.32px;
heading default text-wrap balance; wrap=pretty, numeric=tabular, measure, leading all
apply. check 0 errors; eidos suite green (3 pre-existing words failures only).
Note: the pre-existing hardcoded semantic typography block (leading-ui/prose/… +
tracking-badge/…) is still hardcoded, not config-driven — a separate cleanup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 3 (engine tokens). Additive scales, pure CSS, no binaries.
- config-types: TypographyPrimitiveSet += tracking / leading / features / measure
(optional Record<string,string>).
- render-css: emits --tracking-{k} / --leading-{k} / --font-feature-{k} / --measure-{k}.
- typography.ts defaults: tracking (tighter…wider), leading (none…loose), features
(tabular = "tnum"+"lnum" for data, oldstyle/smallcaps/ligatures), measure (54/66/78ch).
+ optical tracking baked into the size tokens: small text slightly looser
(xxs +0.01em), display tighter (xxxl -0.02em) — was all 0.
- config.ts: validates the 4 scales (CSS-value maps).
- generated/base.css regenerated. test: scales + optical tracking emission.
Additive tokens (recipes/components consume var(--tracking-tight) etc.) so the scales
are zero-risk; the only rendered change is the gentle optical tracking on headings/small
text. check 0 errors; eidos suite green (3 pre-existing words failures only).
Next (Phase 3b): wire the component props (wrap: balance/pretty, numeric: tabular,
tracking/leading/measure) on <Text>/<Heading>.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 2. Fonts are theme data (each theme owns its families), so
@font-face becomes config-driven + generated — like the color palette — instead of a
separate hand-written CSS file. This matches next/font / Fontaine (config -> @font-face),
above the token-only frameworks (Radix/Tailwind/MUI) that leave loading to you.
- config-types: FontFamily += faces (FontFace[]) / axes (FontAxes) / fallback
(FontFallback, metric-override) / display / preload. Additive — the family stack still
works from `family`+`fallbacks`.
- render-css: renderFontFaceBlocks generates @font-face per face from the config, deduped
by the real font name (a font shared across slots — Lora as secondary+display — emits
once). Optional metric-override fallback @font-face (anti-CLS) injected into the stack
as `'{family} Fallback'` when declared. Emitted first in renderStaticCss.
- typography.ts: the BASE THEME's 14 @font-face migrated from fonts.css into the config
(faces). TTF today (the theme's choice); a theme swaps to woff2/variable + fallback
metrics by editing config only.
- index.css: drops `@import './themes/fonts.css'` — the @font-face now ships in
generated/base.css. (fonts.css superseded; left in place, no longer imported.)
- generated/base.css regenerated (14 @font-face, Lora deduped). test: @font-face
generation + dedup; merge-without-mutation assertion updated for the faces field.
Verified in browser: 3 families registered, files resolve (200), fonts load on demand
(swap). check 0 errors; eidos suite green (3 pre-existing words failures only).
Deferred (capability typed, theme adopts when it has the assets): variable woff2,
metric-override numbers (need fontkit/precompute), <link rel=preload> (head markup).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 1. Additive on TypographyPrimitiveSet, behind the frozen
token contract (--font-size-X keeps its name; only the value formula changes, like
color --scale-* hex -> oklch()).
- type-scale.ts (pure, isomorphic, no canvas): the Utopia clamp() formula. fluidClamp /
resolveTypeSize / isFluidSize. rem-based (a11y: scales with browser font-zoom).
- config-types: TextMetric.size accepts `string | FluidSize` ({min,max,minVw?,maxVw?}).
Plain length strings still valid -> backward-compatible.
- render-css appendTypographyDeclarations: emits calc(resolveTypeSize(size) * --scaling)
-> a fixed rem or a fluid clamp; the --scaling axis composes on top.
- config.ts: validateSizeValue accepts a FluidSize (validates min/max/minVw/maxVw) so
the base config validates (was the cascade root — FluidSize objects failed the
string-only CSS-value check).
- typography.ts: sizes in rem; headings (lg/xl/xxl/xxxl) fluid (min @480px -> max
@1280px, max = previous fixed px so desktop is unchanged); body (md) fixed. hero/h1/h2
drop the manual { base, md } responsive sizes — the clamp covers the viewport.
- generated/base.css regenerated. type-scale.test.ts (6 tests). 2 config-test assertions
updated to the new rem/clamp values.
Verified in browser: --font-size-xxxl 40px @480 -> 80px @1280; xxl 32->48; lg 18->20;
md 16 fixed. check 0 errors; eidos suite green (3 pre-existing words failures only).
canvas-text/<SText> unaffected (reads getComputedStyle real font, measures the clamp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the forced-colors a11y work. Users who request more contrast (macOS
"Increase contrast", Windows, etc.) now get strengthened neutral chrome: borders
bumped to neutral 7/8/9 (subtle/default/strong) and de-emphasized text to 12/11
(secondary/muted). Solid fills + primary text are already high-contrast, so they stay.
renderPrefersContrastBlock emits a @media (prefers-contrast: more) block using
`:root:root` (specificity 0,2,0) so it wins over the theme's :root regardless of
stylesheet order. Values reference --primitive-neutral-* (resolve from the cascade; a
theme omitting them just no-ops the declaration — graceful). Strictly additive (gated
by the media query) and strictly STRONGER, so it can't regress the default look.
Also marks audit P3-6 resolved: dispose() already routes documentElement access via
dom (#lastAttrs.target + dom.apply), no direct document access remains.
- generated/base.css regenerated. test: renderStaticCss emits the prefers-contrast
block. docs: THEMING §28 + audit P3-2/P3-6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the base light theme `overlay` was neutral-3, identical to `muted` (neutral-3),
so popovers/menus/dialogs were indistinguishable from muted panels in light mode.
Dark already had overlay=neutral-4. Light overlay -> neutral-4 makes the elevation
ladder consistent across both modes: default(1) < raised(2) < muted(3) < overlay(4).
Verified in browser (light popover): overlay L93% now distinct from muted L95.5% and
default L99% (was overlay == muted). Dark unchanged. generated/base.css regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The checkmark stroke-dashoffset draw used a hardcoded 220ms while the box fill ran at
--duration-fast (120ms) — nearly 2x, so the check read as laggy when toggling. Tokenize
it to var(--duration-fast) so the stroke draws in sync with the box (one 120ms motion).
Not the motion service (uix.motion) — this is a plain CSS transition in checkbox.css
driven by the recipe `stroke-duration` token. Verified in browser: path transition
0.22s -> 0.12s, synced with the box. generated/base.css regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the two color-quality items from THEMING_AUDIT P3.
P3-2 · forced-colors (Windows High Contrast): under @media (forced-colors: active) the
browser auto-maps borders/text/backgrounds to system colors BUT drops box-shadow — so
the box-shadow focus ring (--focus-ring) vanishes and keyboard focus disappears. The
foundation now always emits a system-colored outline fallback:
@media (forced-colors: active) {
:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}
Components that already focus via outline (e.g. Button) keep theirs by specificity; this
is the fallback for the box-shadow ones. renderForcedColorsBlock in render-css.ts.
P3-3 · role border ramp: the per-role `border` slot moved step 6 -> 7. In Radix's
functional scale 6 is a subtle separator and 7 is the UI element border; step 6 read
washed-out on real element borders (outline/surface/controls). element/hover/active
(3/4/5) stay — Radix-canonical for component bg. DEFAULT_COLOR_ROLE_SLOT_STEPS.
- generated/base.css regenerated (forced-colors block + --color-{role}-border -> step 7).
- Verified in browser: --color-primary-border now resolves to primitive-7 (oklch 0.80
0.092 vs the softer step-6 0.86 0.072); checkbox borders render defined, not broken.
- test: renderStaticCss emits the forced-colors outline block.
- docs: THEMING §28 + audit P3-1/2/3 marked resolved + README ref row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each palette step is now emitted twice: the hex as a universal fallback, then an
oklch() sibling that wins where supported (Chrome 111+/Safari 15.4+/Firefox 113+).
The token layer is now OKLCH-native and wide-gamut-ready, with NO @media and NO
config flag (it is the default behaviour).
- render-css `appendColorScaleDeclarations`: hex line + `oklch()` sibling per
`--scale-{name}-{step}`. Only opaque, parseable steps get the sibling; empty/
non-color values keep just the fallback. `--primitive-*`/`--color-*` are var()
refs (untouched); alpha scales stay color-mix/rgba.
- generated/base.css regenerated (+744 oklch sibling lines: 31 scales x 12 x 2 modes).
Honest scope: the shipped Radix palette is authored in sRGB hex, so its oklch()
siblings are sRGB-equivalent (verified: --scale-purple-9 -> oklch(0.5556 0.1829
305.86) paints #8e4ec6) -- identical today. The win is the OKLCH-native foundation:
an OKLCH-authored theme or a vivid generated scheme now renders wider on P3 with no
extra work. Making the SHIPPED palette visibly wide-gamut is Phase 3.
Tests: full eidos suite green except 3 pre-existing words-track failures (confirmed
unrelated via baseline). Docs: RFC §7 (status) + THEMING §27 + README ref row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RFC Phase 4: derive a whole-system color scheme from ONE brand seed at runtime.
Packages the demo-only builder into a first-class, tested API.
- build-scheme.ts (pure): buildScheme(seed, opts) composes the uix.color engine
(deriveScheme -> generateScale -> APCA on-solid -> compositing-inverse alpha)
into the `--primitive-{role}-*` (+ `--color-{role}-contrast`) override map.
seed -> { variables, roles }. No DOM. 6 tests.
- ActiveEidos.applyColorScheme(seed, opts) / clearColorScheme(): resolves donor
scales + background from the active theme, writes a managed `uix-eidos-scheme`
style block AFTER the theme block (wins the cascade), and RE-DERIVES on mode
change (follows light/dark). Returns BuildSchemeResult for introspection. opts:
variant (tonal|vibrant|monochrome) + temper (intent coherence, keeps hue) +
per-role overrides + selector. 4 tests (return value, intents, DOM block
ordering + clear, mode re-derivation).
- index.ts: export buildScheme + ApplyColorSchemeOptions + BuildScheme* types.
- temas/color demo: themeOverride now dogfoods buildScheme (drops the duplicated
emitRole/rgbaStr; identical output verified in-browser).
- generated/base.css: regenerated for the loss->plum role fix (binding layer
--primitive-loss-* now points at --scale-plum-*; keeps the contract test green).
- docs: THEMING.md SS26 + COLOR_ENGINE_RFC SS6.2 (status: landed) + README ref row.
Overriding the binding layer reprojects every --color-{role}-{slot} + the neutral
chrome downstream; the 31-scale palette stays put. Math in $color, composition in
eidos/lib (pure), DOM application in ActiveEidos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Radix's gold/bronze were near-identical in UI (chroma ~0.05, indistinguishable).
Re-authored via uix.color (generateScale from metallic seeds): gold #d4af37
(yellow-gold, OKLCH H91) and bronze #cd7f32 (copper, H61), chroma ~0.13 — ~2.6x
more saturated so the hue gap reads clearly (gold yellow vs bronze copper).
Verified in-browser: gold rgb(212,175,55) vs bronze rgb(205,127,50), distinct.
The other 29 scales stay exact Radix v3; header notes the exception. Palette stays
Radix-as-default (swappable) per the engine-not-hues principle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tertiary was `gray` — literally the same scale as neutral, and secondary
(slate) is a near-gray too, so the three desaturated roles blurred together.
Per Material 3's tertiary rule (rotate the primary hue ~60deg), tertiary now
uses `indigo`: purple's cool neighbor, saturated enough to be distinct, in a
hue band no intent occupies. Updated the one test that pinned tertiary=gray;
regenerated base.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relocate the motion runtime out of eidos into a runtime art (src/arts/motion,
$motion), exposed as uix.motion and consumed by BOTH soma (Presence.motion ->
motion.run) and eidos (delegates + registers presets) -- dissolving the
soma->eidos coupling. Remove DialogProps.runMotion / eidos.motionRunner; the
bridge is now EngineMotion.run (reads data-animation-style). Delete the 4
relocated dead files (lib/motion/{types,runtime,runner,presets/js}.ts); the
preset DATA (presets/css.ts) stays in eidos. Regenerate generated/base.css.
F6 - token rigor (Carbon): tokenize the raw firma durations (slower/deliberate/
emphatic/sustained holds, escalating by announce intent severity), add
--motion-distance-xl (30px shared-axis), --motion-scale-through (0.92), the
emphasized easing, and productive/expressive sets ([data-motion-set=expressive]).
F7 - extensibility + typegen: app-extensible, type-safe preset-name registry
(EidosMotionPresets, mirroring SemaChannelSignatures); MotionPresetName =
keyof EidosMotionPresets | none | (string & {}).
Also sweeps other in-progress working-tree edits (web/routes/temas/grafito).
Verify: npm run check -> 1 pre-existing error (grafito), 0 new; motion 22/22,
active-uix 25/25, Presence 2/2, Dialog 3/3. Pre-existing words-track failures
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bleed-through: the portaled [data-dropdown-menu-content] had z-index:auto, so
any positioned page element with a positive z-index (e.g. a selected
[data-toggle-group-item], z-index 1) painted THROUGH it. In the Words inspector
the Add-block menu showed the Typography toggle chips ("Predeterminada" /
"Normal") bleeding through. Added `content-z: 80` to the dropdown-menu recipe
(matches `select`, its sibling click-triggered overlay) + `z-index:
var(--dropdown-menu-content-z, 80)` on content + sub-content; regenerated
base.css. The soma floating layer reads this computed z-index and mirrors it
onto the positioner wrapper. The rest of the menu family (context-menu /
menubar / navigation-menu) shares the same latent gap — deferred.
Canonization (finishes the dom.raf rollout — zero raw requestAnimationFrame
left in src/uix):
- tabs-indicator.svelte: requestAnimationFrame/cancelAnimationFrame +
new MutationObserver + new ResizeObserver -> eidos.dom.raf /
observeMutation / observeResize (disposer pattern via ActiveEidos.require).
- tabs.svelte: new ResizeObserver -> eidos.dom.observeResize.
- resize-observer.svelte.ts: requestFrame/cancelFrame (raw handle) ->
dom.raf (disposer); fixed the stale "Debounces via requestAnimationFrame"
comment.
Browser-verified in the Words demo: column Add-block menu is opaque and on top
(elementFromPoint at the chip centers returns menu items, inMenu:true); the
menu options are collapsed + localized (one Encabezado, one Lista con viñetas);
empty-column delete works (column persists addressable with 0 children).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two audit P2 quality defects, fixed at engine level.
P2-2 - on-solid text illegible on light solids:
The `contrast` slot defaulted to `--color-content-on-solid` (white) for every
role. On light solids (amber/yellow, risk=orange ~2.3:1) white is sub-AA. The
engine now picks by WCAG contrast (gamma-linearized) of the role step-9: when
white fails (<3:1) it uses `--color-content-on-solid-contrast` (a dark, new
OPTIONAL `content.onSolidContrast` semantic, #1c1917 in base). Only risk flips
to dark in base (5.89:1); purple/red/teal/green keep white (convention, >=3:1).
An explicit `slots.contrast` override is still honored verbatim.
P2-4 - opaque tinted soft surfaces:
The soft variant tint (Button + Badge `{role}-soft-bg`) was opaque (step-1 track
+ opaque color-mix hover) so it did not composite over non-uniform backgrounds.
New derived tokens `--color-{role}-surface` (= a2) + `--color-{role}-surface-hover`
(= a3) are translucent by construction (compositing-inverse alpha). Button/Badge
soft consume them. Toast/Tabs untouched - they are cards, opacity is correct.
- config-types: optional onSolidContrast + CONTENT_COLOR_OPTIONAL_KEYS.
- config: content keySet allows the optional key; validator value-checks it.
- render-css: wcagContrastRatio/wcagRelativeLuminance; luminance pick; emit
on-solid-contrast + surface/surface-hover.
- contract: on-solid-contrast + surface tokens per role.
- themes/base: onSolidContrast #1c1917 (light + dark).
- recipes/base: Button + Badge soft-bg -> surface, soft-bg-hover -> surface-hover.
- docs: THEMING.md section 24 + section 22 item 2; audit P2-2/P2-4 resolved.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser runtime (risk 5.89:1 dark text, surfaces translucent rgba).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the synthetic step-9-at-opacity alpha ramp with a real compositing inverse: each --scale-X-aN is the translucent color that, painted over the scale's background (white for light scales, black for dark, by step-1 luminance), reproduces the opaque solid step N (Radix model). Computed in render-css.ts for ANY theme (base + custom), so alpha is consistent-by-construction with the solid scale and cannot drift. alphaScales opt-in still wins verbatim; non-hex solids fall back to the legacy color-mix ramp. Verified: --scale-purple-a9 = rgb(92 0 173 / 0.6941) over white = #8e4ec6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add 40 recipes.banner.{intent}-{slot} forwarders (= var(--color-{intent}-{slot}))
- banner.css reads --banner-{intent}-{slot} instead of foundation --color-{intent}-* directly
- Banner is now retintable per-component via recipes.banner.* like button/badge; keeps data-intent (no API change)
- Regenerated base.css; recipe contract balanced (40 declared = 40 consumed), no visual regression
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The editor used UI-cramped metrics (14px / 1.25 line-height / 8px block
gap) which read as a form field, not a document. Give the CONTENT (not
the chrome) a reading scale: body 14→16px, line-height 1.25→1.6, block
gap space-2→space-3 (headings derive 1.5x for top margin). Chrome keeps
its smaller --_words-font-size.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Decouple band width from text inset: `--_words-rail-width` (1.25rem,
the thin band) + `--_words-rail-gap` (1.25rem, breathing room) →
text is no longer glued to the band edge.
- Narrow the band: 2.5rem → 1.25rem (20px).
- Lighter tones: rail-bg #ededed → #f5f5f5, rail-border #cfcfcf →
#e3e3e3. Eidos CSS regenerated.
Verified: band 20px + 1px #e3e3e3 edge, then 20px air before text at 40px.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-directed editor rework, step 1 (left gutter):
- Remove the "+" inserter button from the block-handle gutter. Its
action moves into the drag-handle menu as a new "Insert below" item
(folded into `act('insertBelow')`); the standalone `insertBelow`
helper is gone. The separate seam inserter (words-block-inserter)
is untouched.
- Turn the left gutter into a soft silver margin band: narrow it
(`--_words-gutter-size` 3.5rem→2.5rem) and paint it via a
background-gradient on [data-words-content] — `--words-rail-bg`
#ededed band with a 1px, slightly stronger `--words-rail-border`
#cfcfcf right edge. Background only, so the contenteditable text /
selection is undisturbed. Recipe tokens softened + eidos CSS
regenerated.
Verified in browser: gutter shows only the grip on hover (no "+");
the menu reads Insert below / Move up / Move down / Duplicate / Delete
and "Insert below" inserts a paragraph (7→8 blocks) and closes the
menu; the silver band renders at 40px with the 1px border. npm run
check: 0 errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EV-G final pass + EOD documentation.
Words editor:
- Stop emitting `contact-focus` from soma. Removing the per-component
cascade rule earlier didn't fully silence the editor — the `contact`
family carries a BASE sound signature in SEMA_MAP (pitch 800, gain
0.25) that fires even without a cascade rule. The trigger call is
commented out (event no longer emitted; telemetry empty). Per spec:
"sigue apareciendo el puto sonido, el ring".
- Drag handle + inserter now cover lists AND tables. Selectors
expanded from `[data-words-node="block"][data-words-path]` to also
match `[data-words-node="list"]` and `[data-words-node="table"]`.
Top-level lists / tables get the grip + the "+" seam under them.
- Removed :hover from `[data-words-block-handle]` and
`[data-words-block-inserter-button]`. Gutter overlays are AMBIENT —
visual state via data-attrs only (`data-open`, `data-grabbed`,
`data-dragging`). Per spec: "quítale el hover al botón de añadir
bloque, si el handle no tiene este tampoco".
- Rail tokens hoisted to recipe: `words.rail-bg = #d4d4d4`,
`words.rail-border = #9a9a9a`. Fixed-tone (not theme-aware) — the
rail emulates the physical margin of a notebook; same look in
light/dark. Recipe-css-contract test was rejecting raw hex in
words.css.
Docs:
- CLAUDE.md hand-off 2026-05-28 #2 documenting the 6 EV-* Words
sprints + 5 architectural decisions with lasting effect (canvas
silent, fixed-tone tokens for physical metaphors, drag predicate
covers list/table, ambient overlays, insertParagraph empty-block
guard).
- continue.md (CONTINUE.md) rewritten for tomorrow's pickup:
TL;DR of the two halves (toggle-group structural + Words UX),
remaining bugs (drag handle outside rail on code blocks, heading
inline level change), and the heredados (POLISH-1b, F2.3-F2.6,
DRAGDROP-SOMA).
Verification: 467/467 morfo + sema + eidos + soma/words tests pass.
85/85 engine pass. `npm run check`: 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The editor body was unbounded vertically — long documents pushed the
whole page instead of confining the scroll to the editor surface.
Per spec: "la parte de la edicion deberia tener un scroll".
Recipe:
- Three new tokens `content-max-block-size-sm/md/lg` (50vh / 60vh /
70vh) mirror the existing `content-min-block-size-*`. Generated CSS
regenerated.
CSS:
- `[data-words-content]` now carries `max-block-size:
var(--_words-content-max-block-size)` and `overflow-y: auto`. The
min-block-size baseline stays as the starting height for empty
editors. When content grows past the cap, the body scrolls
internally — the host card no longer balloons.
- `scroll-padding-block: 1rem` hint so caret-into-view nudges leave
breathing room.
Block-handle / block-inserter / image-float-bar:
- Switch scroll listeners from `window.addEventListener('scroll')` to
`document.addEventListener('scroll', { capture: true })`. Scroll
events don't bubble; the previous wiring only caught window scrolls.
Now the gutter overlays (handle position, "+" snap target, image
float bar anchor) re-measure when the user scrolls INSIDE the
content area too.
Verification: dev probe at /uix/components/words confirms
`max-block-size: 600px`, `overflow-y: auto`, content scrollHeight
(615px) > clientHeight (600px), `isInternallyScrollable: true` on a
1000px viewport (60vh of 1000 = 600). 467/467 tests pass across
eidos + morfo + sema + soma/components/words.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toggle palette cascade (palette-track / palette-hover / palette-border
declarations in TSC) referenced --toggle-{X}-track, --toggle-{X}-hover,
--toggle-{X}-border for X in {primary, neutral, secondary, affirm, risk,
threat} — but the recipe only declared 5 forwarders per color (element /
solid / solid-hover / text / contrast). The 3 missing slots × 6 colors =
18 unresolved tokens made --toggle-palette-{track,hover,border} resolve
to empty for any non-neutral color (the host default DID declare them).
Visible effect, only when a non-neutral color was active:
- ghost-on-bg = var(--toggle-palette-track) → empty → transparent fallback
- ghost-on-border = color-mix(... palette-border ...) → invalid →
property dropped, border falls through to base layer
- outline-on-bg = color-mix(... palette-element ...) was the one that
worked because palette-element forwarders DID exist for all 6 colors
Surfaced today while verifying the TSC v2.2 toggle-group color cascade
fix in the browser. The toggle-group composition correctly overrode
--toggle-palette-* on items, but the empty track/hover/border slots
collapsed ghost-state styling downstream.
Verified with browser probe (preview_inspect):
- ghost + affirm: paletteTrack #0d1514, paletteHover #023b37,
paletteBorder #145750 (was "")
- ghost + threat: paletteTrack #191111, paletteHover #500f1c,
paletteBorder #72232d (was "")
- outline + primary: paletteTrack #18111b, paletteHover #3d224e,
paletteBorder #54346b (was "")
Tests: 100/100 pass in src/uix/eidos. Recipe contract guard accepts
the new forwarders (kebab-case, valid var() values).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprint completo:
- Card (eidos + sema): elevation/intent/interactive, eventos select+activate.
- Avatar: ring, AvatarBadge, AvatarGroup separados; demo Avatar standalone.
- Image: status surface (idle/loading/loaded/error) consumiendo soma ImageProvider.
- SText + STextVirtualList: canvas-text engine portado de morfo-driven-soma como
componentes separados para optimizar bundle (no se importa canvas si no se usa).
- Badge: primitivo eidos puro.
- Trans, FormatNumber, FormatDate, RelativeTime: 4 service components greppables
alrededor de uix.langs y uix.format. Nav group "Utilities".
Bugs fix arts/format/dates (engine-dates.ts):
1. Preset / per-axis conflict: Intl.DateTimeFormat rechaza dateStyle/timeStyle
con cualquier per-axis (weekday/era/year/month/day/dayPeriod/hour/minute/
second/fractionalSecondDigits) o con timeZoneName. Engine drop-ea ambos
presets por defecto cuando detecta conflict (regla estricta — un solo
per-axis hace ilegales los dos presets).
2. Per-call locale override: formatDate/Time/DateTime aceptan tercer arg
locale?: string. No muta engine state. Cache (getCachedDateFormat) keyea
por (locale x options). resolveCallHourCycle: cuando hay locale override
y pref auto, el ciclo horario se resuelve desde el locale OVERRIDE.
3. withHourCycle precedence: caller hourCycle/hour12 ahora gana sobre
preferencia del engine. Antes spread mal ordenado sobrescribia 'h12' del
caller con 'h23' del engine cuando locale resolvia a 24h.
FormatDate component vuelve a rutear siempre por uix.format.dates
(cache + hourCycle + locale reactivo), pasando locale como tercer arg.
Demos service components autonomos del toolbar global: cada uno owns su
demoLocale state con 7 chips (es-ES/en-US/en-GB/de-DE/fr-FR/ja-JP/ar-EG).
FormatDate stage filtra dateStyle/timeStyle por mode + chip hourCycle anadido.
Tests: 14/14 passing en engine-dates.test.ts (+6 nuevos: conflict handling,
locale override, hourCycle precedence).
Docs: arts/format/README.md (handoff 2026-05-25 con las 3 reglas),
README.md por service component, project_service_components_sprint_2026-05-25.md,
feedback_service_demos_own_locale.md (pattern reusable).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining
6 are all Words component (excluded per project owner — actively developed
on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout).
Key changes:
- soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component`
in `context.set()` so unit tests can instantiate providers without a
component tree (unblocked 18 picker tests in one line).
- soma/textarea: route inline style writes through `dom.apply({attrs})`
instead of direct `el.style.X = Y` (contract regex compliance).
- soma/picker-shell: add minimal Provider component + morfo so the shared
handle utility passes the morfo/component contract.
- eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the
`_layout` directory wasn't a real component) and update 8 layout
primitive imports.
- eidos/command: replace `onMount` with `$effect` (visual layer can't use
Svelte 4 lifecycle hooks per component-api contract); restructure
`Item.Icon`/`Item.Shortcut` namespace so contract regex matches.
- eidos/color-picker: extract hue spectrum hsl literals to sidecar
`color-picker-spectrum.css`; keep main recipe color-literal-free.
- eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS
vars (box, flex, grid, float, group, banner, container, section,
separator, toggle-group, pin-input, scroll-area, splitter,
navigation-menu, dropdown-menu, context-menu, date-range-field,
time-range-field, plus per-component additions to date-field,
color-picker, button, select). 197 missing → 0 non-Words.
- morfo: add `data-dir` to carousel, `data-kind` to date-picker, add
StrengthMeterLabel sub-part to password-field, register picker-shell.
- 5 pickers (date/color/date-range/time/time-range): add `internals.ts`
indirection so `exports.ts` doesn't re-export `*provider.svelte.ts`
directly (contract D); rewrite virtual root types to plain object
literals (no `WithChild`/`PrimitiveDivAttributes`).
- 4 new READMEs (button, textarea, password-field, picker-shell)
following the search-field canonical template.
Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all
Words-scope or jsdom infra timeout — explicitly out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three real visual bugs caught after browser verification:
1. **No visible border around input + meter row.** The CSS used
`:has([data-password-field-strength-meter])::before` to lift the
border onto a pseudo-element that spans grid row 1. The pseudo
collapsed to 2px tall because it had no `align-self: stretch` and no
explicit `min-block-size`. Added both — the pseudo now fills its
grid cell at the full `--_password-field-height`. Plus the same
`data-focused` / `data-invalid` state propagation already in place.
2. **Strength meter segments invisible.** Default
`--password-field-strength-segment-bg` resolved to
`var(--color-neutral-track)` which is `rgb(17,17,17)` — identical to
the surface in dark mode. Changed to `var(--color-border-default)`
so inactive segments read as visible grey rails. Also bumped segment
height from 4 → 6 px so the bar is clearly readable.
3. **Eye icon too small inside a thin trigger.** `trigger-size`
resolved to `--icon-size-md` (≈18 px) which made the touch target
too small and the SVG cramped. Promoted trigger-size to use
`--control-height-{xs..md}` so it matches the input row height per
size. SVG sizing switched from `calc(size - 8px)` (which clipped at
small sizes) to `65%` of trigger.
4. **Stage wrapper collapsed to content width.** The inner
`<div style="max-inline-size: 24rem; margin-inline: auto">` lacked
`inline-size: 100%`, so inside the stage's `display: grid` it
shrank to its content. Added the explicit `inline-size: 100%`.
5. **Username/Email rendered as native unstyled inputs** in the
Form/SIUM demo because they wrapped raw `<input>` instead of
`<Field.Input>` (Field.Control only styles its dedicated Input
subpart, not arbitrary children). Swapped to `<Field.Input>`.
6. **SIUM error refs (`#?sium.errors.min_length|...`) rendered
literal.** `Field.ErrorText` just renders the string it receives.
Added a local `issueText(form, field)` helper that calls
`uix.langs.ts(raw)` to resolve the langref and then interpolates
`{{param}}` from `form.issues[field][0].params`. Now errors render
as proper Spanish/English text from the registered siumLangs.
Verified visually via Claude_Preview: input row has a clean border with
the eye icon at the right edge, strength meter sits below with 4 visible
segments + label, all three form columns render with consistent chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Style fix — input chrome was hand-rolled with raw color tokens
(`--color-neutral-border`, `--ring-width`) instead of consuming the
recipe layer like every other text input. Now matches search-field and
number-field exactly:
- Added `password-field` recipe to `src/uix/eidos/lib/recipes/base.ts`
with the same surface as `search-field` (height, px, gap, font-size
per size; border, focus, invalid, bg, color, focus-shadow tokens).
- Regenerated `src/uix/eidos/generated/base.css`.
- Rewrote `password-field.css` to consume the new tokens — same focus
ring (`focus-shadow`), same border colors, same disabled opacity,
same transition tokens. VisibilityTrigger gets its own border/bg/
color-on-hover scheme aligned with search-field's clear button.
- Added per-size icon + caps font-size tokens so the trigger and caps
pill scale with the input.
Form + SIUM demo — answers "how does PasswordField validate?":
Added a new subsection in the Live tab with three working forms wired
through `<Form>` + `<Field>` and SIUM schemas:
1. Login (purpose='current', non-empty password)
2. Registration (purpose='new', complexity rules via regex pipes —
uppercase + digit + symbol + 8+ chars, plus the visual StrengthMeter
as a companion to the schema)
3. Password change (3 fields with a cross-field `refine()` so new ===
confirm; demonstrates SIUM's refine pattern)
The section explicitly documents that PasswordField has no built-in
complexity rules — validation is schema-first. The schema decides
what's "correct"; PasswordField's flags propagate via Field's OR-merge
of `formFieldState.isInvalid`.
svelte-check: 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`STATIC_TYPOGRAPHY.styles` already defined 11 named typography styles
(hero, h1..h6, body, prose, label, caption, code) but the renderer
only emitted families / sizes / weights — the style entries lived
as data with no CSS reflection.
Extend `appendTypographyDeclarations` so each named style emits:
- `--style-{name}-font-family` → `var(--font-family-{family})`
- `--style-{name}-font-size` → `var(--font-size-{size})`
- `--style-{name}-line-height` → literal or `var(--font-line-height-{size})`
- `--style-{name}-letter-spacing` → literal or `var(--font-letter-spacing-{size})`
- `--style-{name}-font-weight` → `var(--font-weight-{weight})` (or numeric)
- `--style-{name}-color` → `var(--color-{dot.path → dash-path})`
Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
emit the base value into `:root` and per-breakpoint
`@media (min-width: …)` blocks that override the same variables.
Breakpoint thresholds match `$libs/dom/responsive` (sm: 480, md: 768,
lg: 1024, xl: 1280, xxl: 1536).
`renderStaticCss` now iterates `STYLE_BREAKPOINT_ORDER` after the
main `:root` block and appends one media-query block per breakpoint
that has at least one responsive override. The `indentBlock` helper
preserves indentation inside the wrapper.
`base.css` regenerated. svelte-check 0 errors, `npm run component:audit`
81 / 81 PASS unchanged. This is foundation-only; no components consume
the new tokens yet — that lands in the typography port (Text + Heading
+ Display + Code + CodeBlock + Kbd + Mark + Highlight + Link).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components closing the 6-component audit batch.
number-field
- N1 RTL scrubber: invert horizontal delta sign when dir='rtl' so
dragging right decrements (visually backward) in RTL layouts. Vertical
unaffected. Added 2 RTL scrub tests; existing scrub test pinned to ltr.
- N2 stepper height: Increment/Decrement now use --_number-field-height
for inline-size/block-size/min-inline-size so they always match the
input row at every size token (xs/sm/md/lg/xl).
- N3 input focus ring: explicit [data-number-field-input]:focus-visible
with --_number-field-accent-border box-shadow, alongside the existing
Provider-level ring.
search-field
- S1 icon tokens: added --search-field-icon-size-{xs..xl} + icon-color +
icon-margin-inline-end + a Morfo Icon part + soma/eidos wrappers.
Demo uses <SearchField.Icon><Search /></SearchField.Icon>.
- S2 debounceMs prop: soma wraps onValueChange in a timers.schedule with
the provided delay. clear() cancels, submit() flushes synchronously.
- S3 loading slot: new LoadingIndicator part (morfo + soma + eidos) +
data-loading on Provider + CSS spinner via @keyframes search-field-spin
honoring prefers-reduced-motion.
combobox
- C1 ClearButton part: new morfo trigger + soma provider + eidos wrapper
with X icon. Hidden via [data-empty] (opacity + pointer-events:none).
Click clears value + inputValue and refocuses input.
- C2 allowsCustomValue: when true and no highlighted item, Enter commits
the current inputValue as the value. Single mode closes after commit;
multi appends + clears input + stays open.
- C4 Backspace removes last tag in multi mode when input is empty.
- C5 Sticky group headers: CSS position:sticky with content-overlay bg.
Plus: fixed search-field demo's brittle parts[1] index reference to a
named lookup (parts.find(p => p.kebab === 'input')).
Verified: svelte-check --threshold error → 0 errors / 0 warnings /
4584 files. All component tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops ColorPickerChannelSliderProvider / TrackProvider / ThumbProvider
(~200 lines of duplicated pointer/keyboard/ARIA handling) and rewrites
`components/color-picker-channel-slider.svelte` to compose
SliderProvider underneath. The channel slider now keeps only the
channel-aware bridge: setChannel writes the value, channelGradient
paints the track.
What the picker gains by reusing Slider
---------------------------------------
- sema events: `slider:handle-pick`, `slider:handle-drag`,
`slider:commit-set` now flow through the channel sliders (they were
silent before — the Sema layer had no perceptual hook on channel
drag/commit).
- single pointer/keyboard implementation: Arrow/Home/End/PageUp/PageDown,
multi-touch, focus trap, RTL, step-snap — all from SliderProvider.
- ARIA: role=slider, aria-valuenow/min/max, aria-orientation, tabindex
managed by the slider's thumb provider.
- recipe tokens: channel sliders consume `--slider-*` (sizes, radii,
shadows) so they stay in lockstep with the standalone Slider and
with TimePicker.HourSlider / .MinuteSlider / .SecondSlider. The
`--color-picker-channel-slider-*` tokens are removed.
Public API impact
-----------------
- `<ColorPicker.ChannelSliderTrack>` and `<ColorPicker.ChannelSliderThumb>`
remain as part names but now re-export `Slider.Range` /
`Slider.Thumb`. Consumers that compose them inline keep working.
- The inner DOM changes: `[data-slider]` / `[data-slider-thumb]` /
`[data-slider-range]` (governed by the slider morfo) replace
`[data-color-picker-channel-slider-track]` /
`[data-color-picker-channel-slider-thumb]`. The wrapper still emits
`[data-color-picker-channel-slider][data-channel]` so per-channel
CSS targeting works; the gradient is exposed via
`--cp-channel-gradient` and routed onto the slider's track ::before.
- Morfo: drops `channel-slider-track` and `channel-slider-thumb` parts
(the slider morfo owns those contracts now). `channel-slider`
remains as the wrapper marker with `data-channel`.
- Eidos: deletes `color-picker-channel-slider-track.svelte` and
`color-picker-channel-slider-thumb.svelte`; `index.ts` re-points
`ChannelSliderTrack` / `ChannelSliderThumb` at the slider eidos
components.
New norm N-8 added to PENDIENTES.md
-----------------------------------
"Reutilizar componentes ya definidos en componentes complejos siempre
que no supongan una pérdida de características demostrable." Documented
with this refactor as the worked example.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS
- svelte-check 0 errors / 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composite popover: 2D saturation/brightness area, hue + alpha channel
sliders, preset swatches, eye-dropper, and a segmented channel input
composed from ColorField. Builds on the existing soma color-picker
(23 parts) — eidos layer is mostly thin pass-throughs adding visual
chrome.
What landed
-----------
- New `src/uix/eidos/components/color-picker/`:
- Root visual (Provider) + visual-context (size/variant/color)
- Trigger with a default swatch dot tinted from
`--cp-trigger-color` (the air pattern from morfo-runtime)
- Content (popover wrapper), Area (+ Background + Thumb),
TransparencyGrid
- ChannelSlider (+ Track + Thumb) — generic per-channel slider;
default-composes Track+Thumb when no children
- SwatchGroup + SwatchTrigger + Swatch + SwatchIndicator
- EyeDropper (Pipette icon by default)
- ChannelInput + ChannelSegment + FormatSelect (composed from
ColorField parts; same provider injected automatically)
- Label, Control, ValueSwatch, ValueText, HiddenInput
- Recipe section `color-picker` in `recipes/base.ts` (regenerated
`generated/base.css`) — trigger, content, area, channel slider,
swatches, eye-dropper tokens.
- CSS reuses color-field tokens for the segmented input portion and
adds picker-specific visuals. Transparency checker on trigger swatch
+ value swatch + alpha slider track via conic-gradient pattern.
- Morfo updates: scope `['soma','sema']`, `apg: dialog-modal`, added
`commit-set` event so audit classifies it as interactive.
- Soma exports: ColorPickerProvider class so eidos Trigger can read
the current value for the swatch tint.
- Demo at `/uix/components/color-picker` with the standard 6 tabs;
live stage exercises area drag + channel sliders + preset swatches
+ eye-dropper + format switch.
- README documents the surface, comparativa vs air/Ark/Chakra, and
flags the picker contract (mode/Footer/commit/cancel) as gap to
align with date/time-picker in a follow-up.
Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS (new color-picker demo validates against
morfo DOM)
- svelte-check 0 errors / 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the date-field pattern (Provider visual root + Label / Input /
Segment / HiddenInput parts; ColorField also exposes FormatSelect).
Soma owns segment editing, ARIA spinbuttons, validation, format
switching and form submission; Eidos adds size (xs..xl), control
variant (surface/outline/ghost) and intent color recipes.
Adds time-field and color-field recipe sections to recipes/base.ts
(regenerated generated/base.css) and registers both routes in the
sidebar nav. apg=spinbutton declared on both morfos. READMEs follow
the Baseline / Superficie / Comparativa / Decisiones / Gaps /
Referencias template required by audit F-1.x.
All 67 component-audit components PASS, 38/38 morfo:check PASS,
svelte-check 0 errors/0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: setting startReadonlySegments/endReadonlySegments in the
date-range-picker demo didn't change anything visible, and the
date-picker demo had no equivalent control.
Root cause #1 (visible): the readonly segment styling was
background: var(--color-neutral-track) — primitive-neutral-3 — too
close to the surface background in both themes. Mechanically the lock
was working (data-readonly stamped, keyboard updates blocked) but the
visual delta was invisible.
Fix: add segment-readonly-color + segment-readonly-decoration tokens
to the recipe. Read-only segments now render with
- background: var(--color-surface-overlay) (clearer step from default)
- color: var(--color-content-secondary) (slightly faded)
- text-decoration: underline dotted var(--color-content-muted)
- cursor: default
both in date-field.css and the date-range-picker per-component
override.
Root cause #2 (consistency): date-picker demo lacked the segment-lock
chip group that the range demo has. Added segmentLock state +
readonly-segments derived array (year / monthYear / date presets) +
chip control + snippet integration. Now both pickers expose the prop
identically.
Verification (browser): clicking the 'monthYear' chip in date-picker
demo stamps data-readonly on the month + year segments; the dotted
underline + lighter color are visible. Range demo: clicking 'year'
under 'start segments' affects only the start year segment, end stays
editable.
0 type errors, 67/67 component:audit PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The open accordion trigger was rendering in `--color-primary-text`
(pink/magenta in dark theme), which read as a loud accent on the
heading — visually noisy and inconsistent with shadcn-svelte / Radix
/ Ark UI, which all keep the trigger text in the regular content
tone and let the rotated chevron + revealed content signal "open".
Recipe tokens updated:
- `accordion.trigger-color-open`: `--color-primary-text` →
`--color-content-primary` (no recolor on open).
- `accordion.indicator-color-open`: `--color-primary-text` →
`--color-content-primary` (subtle emphasis without the loud
accent).
`generated/base.css` regenerated. Verified in browser: open trigger
heading now matches the closed triggers' tone; the chevron rotates
upward and the content reveals as the only open affordances.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes incidencia 2026-05-20 #4 plus the R-2.1 audit finding:
- Remove raw-hex fallbacks (`#16845f`, `#106d4e`, `#2ba878`, `#ffffff`)
from `date-range-picker.css`. The `--color-affirm-*` and
`--calendar-day-selected-color` tokens are guaranteed by the
generated base.css; the safety hex hid that they weren't being
refreshed.
- Regenerate `eidos/generated/base.css` so `--calendar-day-*` tokens
(color, selected-color, today-border, etc.) materialize from the
recipe definitions in `lib/recipes/base.ts`. Fixes the stale
`keeps generated/base.css in sync` contract test.
- Start endpoint now reads `--color-affirm-*` cleanly; end follows the
picker's `data-color` accent. When the picker's accent IS in the
affirm family (`data-color="affirm"` or `"fulfill"`), the start
swaps to `secondary` so the two endpoints stay perceptually distinct.
- Both endpoints get an inset edge stripe + semibold weight so the
start vs end direction reads at a glance — addresses the user's
"indistinguible o transparente" complaint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>