Full-bleed lost the inset frame entirely — "¿dónde está el radio
interior?". A 16:10 cover can't be concentric on all 4 corners at full
(its height caps every corner to ~half-height), but its TOP corners CAN
match the card if the bottom stays square — the iOS-player geometry.
CSS can't read the card's dimension-capped radius (computed value is
still 9999px; the cap is a *used* value), so measure it via
ResizeObserver (min(w,h)/2 − gap) and apply to the cover's top corners;
bottom corners square, meeting the metadata. The inset frame is kept on
top/sides so the inner radius reads.
Finite radii unchanged (full 4-corner concentric). Toggle at full shows
"auto: solo top". Docs reconciled in SHAPE_ENGINE_RFC §5 + THEMING §30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous fix dropped the cover to a clean --radius-xl, but a sharp
16px corner floating inside the card's 169px pill curve still read as
broken. The only way a child's corner can match a `full` parent (whose
radius caps per-dimension, so concentric math is impossible) is to let
the parent CLIP it: at `full` the cover now bleeds edge-to-edge with no
radius of its own, and the card's overflow:hidden + real corner round
its top to match exactly. Updated SHAPE_ENGINE_RFC §5 accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
At `full` (--radius-full = 9999px) the border-radius caps to half each
element's shorter dimension. A tall card and its 16:10 cover cap to
different values, so they can never be concentric — the nest inset broke
("el borde interior no sigue el radio exterior").
Concentric nesting requires FINITE radii; `full` is for leaf elements
(chips, buttons, avatars, pill inputs). So at `full` the demo now:
- gates `[data-shape-nest]` off and dims the "radios concéntricos" toggle
with «n/a a full — geometría de hoja»
- drops the cover to a clean --radius-xl (.cover-full) instead of a
mismatched pill
Documented the finite-only constraint in SHAPE_ENGINE_RFC §5 + THEMING §30.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The radius scale ships `--radius-full` (9999px = pill / totally-rounded); the
/temas/forma magnitude chips just omitted it (sm/md/lg/xl). Added `full` so the demo
exposes totally-rounded. Magnitude (rounded / --radius-*) is orthogonal to the shape
family (corner-shape).
Verified: check 0 errors; card --sample-radius -> 9999px on `full`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Overcorrected the prior fix into "todo es sema / eidos solo hace visible", which
flattened eidos and skipped morfo + soma. Reframed to the real layer chain, each with
a role: morfo declares the event (family · intent) -> soma triggers (writes data-state,
calls sema.emit) -> sema emits (executes sound + haptic; projects the visual channel as
data-event-* tokens) -> eidos materializes (reads data-state + data-event-* and renders
via its OWN token engines: color · motion · depth · shape · space). Sema = the QUE
(semantic), eidos = the COMO (visual vocabulary) — co-layers, eidos the sole visual
owner, not a shim.
Applied to CHANNELS_SYNTHESIS §3 + the /temas/orquesta footnote + top comment.
Verified: check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refine the doctrine per review: the two moments are the producer/consumer split —
the SEMA moment (it emits: evaluates the occurrence and stamps data-event-* tokens;
sound + haptic it executes there; no DOM/CSS) and the EIDOS moment (it reads those
tokens + data-state and materializes the visual channel in CSS). The token is the
contract — sema writes, eidos reads — which is why sema is DOM-agnostic and eidos is
the sole visual owner. The temporal axis (state persistent / event transient) runs on
top: which token, read by eidos as presets / signatures.
Applied to CHANNELS_SYNTHESIS §1 + the /temas/orquesta "two moments" section.
Verified: check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes:
- Sound + haptic now fire from the REAL EngineSemantic.emit (channels filtered by the
mixer; `visual` excluded so no data-event-* conflict with the toggleable facets) —
replacing the Web Audio / Vibration approximation. One real emit, the chosen channels.
- Correct the architecture framing (was conflating LAYER with CHANNEL): the channels are
SEMANTIC (sema). Sema projects the `visual` channel (-> eidos materializes color ·
motion · depth · forma) and executes sound + haptic. motion/depth/shape/color are NOT
"eidos channels" — they are sema visual channel, made visible by eidos. Mixer now groups
"canal visual · sema -> eidos" vs "canales runtime · sema"; dropped the misleading
per-track eidos/sema tag. CHANNELS_SYNTHESIS reframed: 8 book expression channels -> 3
sema runtime channels (visual / sound / haptic).
Verified: check 0 errors; browser — two mixer groups render, real engine.emit fires
without error, visual facets compose.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The book thesis, playable: ONE press fires the firma across channels, with a mixer to
mute each and watch them layer. The 4 visible channels (color · motion · depth · shape)
compose from one event with REAL eidos tokens — verified: press -> primary->affirm-solid
+ scale(0.93) + shadow overlay->raised + --shape-smoothing 2->3.4. Sound + haptic via
Web Audio / Vibration (in-app the sema engine emits them from the same event). + the
two-moment model section.
CHANNELS_SYNTHESIS.md ties the five channel RFCs: two moments x N channels from one
event, the layer ownership (sema runs sound/haptic + projects data-event-*; eidos
materializes the visual channels), the cross-modal firma, the runtime-builder quintet.
Verified: check 0 errors. Browser: all four visible channels compose on press.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live structural workshop: a real settings panel whose every gap/padding is
var(--space-*), retuned by four controls — densidad (data-density) · zoom
(data-scaling) · unidad base · fluido — composing on the panel via the preserved
calc(value * density * scaling). The space ladder (buildSpaceScale steps) visualizes
the modular rhythm + the fluid grow range. Plus THEMING 31 (three structural axes;
space as rhythm).
Verified: check 0 errors. Browser: panel --space-4 = calc(16px * density * scaling);
compact -> 0.84, base/growth re-derive the ladder live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The space scale was the one structural primitive without a builder — density and
scaling were already strong, but the base space scale stayed flat / static / arbitrary.
buildSpaceScale (pure) + ActiveEidos.applySpacing/clearSpacing regenerate the
--space-{key} ladder from one base unit, optionally FLUID (growth > 1 -> each step
clamp()s with the viewport, reusing the type scale fluidClamp), PRESERVING the
density x scaling composition (calc(value * --density-space-scale * --scaling)).
Opt-in over the authored STATIC_SPACE, same posture as applyTypeScale. Completes the
runtime-builder quintet (color/type/depth/shape/space).
Thesis (STRUCTURE_ENGINE_RFC): space is rhythm, not a flat px lookup table — modular,
fluid, composed with density x scaling from a seed. Structural = state-only (no
two-moment; honest).
Verified: check 0 errors; eidos config 58/58 (incl. modular + fluid space tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shape prop was usable but invisible — add a live `shape` chip control
(rounded/continuous/cut/scoop) to each demo, wired to the live preview + code
snippet, plus a "shape families" patterns row in Badge and a tag-shape control on
TagsInput items. The family system is now discoverable, not just documented.
Verified live (Chrome 146): badge cut -> bevel, button continuous -> squircle,
both flow from the control to the rendered component.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`<TagsInput.Item shape="cut">` — typed `shape` (rounded/continuous/cut/scoop) on the
tag chip, the case the family system was made for. Emits `data-shape` -> the
foundation rule.
Also clarified (pendiente.md): every component that spreads `{...rest}` already
accepts `data-shape="…"` directly (verified on inputs + tags), so the families are
usable everywhere today; the typed `shape` prop is ergonomic sugar on the common
surfaces (Badge / Card / Button / TagsInput.Item).
Verified: check 0 errors. Browser: real tag chip data-shape=cut -> bevel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes the shape families usable per-instance, not just in the demo. New canonical
SHAPE_FAMILIES + ShapeFamily in lib/types (canon, like the variant archetypes).
Badge / Card / Button gain a typed `shape` prop (rounded - continuous - cut - scoop)
that emits `data-shape` -> the foundation [data-shape] corner-shape rule. Orthogonal
to `rounded` (radius magnitude); default = arc (no attr); degrades to arc where
corner-shape is unsupported.
A differentiator for chips/badges (<Badge shape="cut">) and a premium touch for
buttons (<Button shape="continuous"> = squircle). For cut/scoop prefer filled
variants (concave/angular corners seam with borders) — documented per prop + pendiente.md.
Verified: check 0 errors; eidos 190/193 (3 pre-existing `words` failures, unrelated).
Browser: real Badge data-shape=cut -> bevel, real Button data-shape=continuous ->
squircle, radius magnitude preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scoop family chip cast its box-shadow INTO the concave notches and the 1px
border seamed at the concave corners (native corner-shape, not clip-path) — looked
broken. Flat-filled chips render every family cleanly and are the right home for
cut/scoop (chips/badges), matching the per-component idea below.
Add pendiente.md: per-component shape family (chips/badges differentiator), general
shape adoption, corner-shape support (canon = degrade to arc, no polyfill), depth
scrim cue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuilt from abstract tiles to a real "now playing" card whose every corner is
driven by a control panel — switch family / smoothing / magnitude / concentric and
the whole card retunes live. The avatar stays a true circle (families do not deform
it, by design). Plus: de-cerca round-vs-squircle, the 4 families on one chip, the
press-morph buttons, and a "what only Eidos does" strip.
Fix: the cover no longer sets its own border-radius, so the [data-shape-nest]
concentric rule owns it — the cover inset is now truly concentric (16 - 8 = 8px).
Verified: check 0 errors; browser (Chrome 146) — the family switch retunes card +
cover + play + tag + input live, avatar stays circular, cover concentric.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the runtime-builder quartet (color, type, depth, shape). buildShape
(pure) + ActiveEidos.applyShape/clearShape: dial `smoothing` (live continuity /
squircle intensity) + `nestGap`, or override/add `families`, written as a managed
block that wins over the foundation. Exported from $uix/eidos.
Verified: check 0 errors; eidos config 56/56.
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 first cut was weak (grey tiles + a "dispárame" box — it did not sell depth).
Rebuilt around real UI that responds:
- Reacciona — metric cards that lift on hover (raised -> overlay: shadow grows +
translateY): depth responds to interaction (state moment).
- Asciende — an app mockup whose trigger rises a dialog over a scrim; the dialog
shadow grows from flat -> its modal plane as it ascends (the present-rise firma,
fired by stamping data-event-*) and the backdrop recedes (event moment).
- Cada plano, un rol — the 5 planes as the real components they are for:
chips (flush) / card (raised) / menu (overlay) / dialog (modal) / input (recessed).
Self-contained, dark editorial aesthetic. Verified: check 0 errors; browser — the
dialog rises with the firma over a scrim (firmaFired + scrim true), archetypes render
with their plane shadows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-facing summary of the depth system (DEPTH_ENGINE_RFC remains canonical):
the two moments (data-depth resting plane + the present-rise/press-squeeze firma),
the config-driven planes composing existing primitives, the jaula-abierta escape
hatches, and the deferred refinements + component-adoption follow-up. Closes Fase 5 docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showcase for the depth channel (mirrors /temas/color and /temas/tipografia):
- ESTADO: the 5 planes (flush/raised/overlay/modal/recessed) as elevated tiles
composing surface + shadow + z (`data-depth`), with a light/dark toggle showing
the mode-aware mix.
- EVENTO: stamping `data-event-*` (as sema would during the hold) fires the firma
live — emerge -> the shadow grows (rise); contact -> it flattens (recede).
Depth occurs.
Self-contained (eidos foundation + data-theme), distinctive dark/editorial aesthetic.
Verified: check 0 errors; browser — 5 planes render with their resting shadows;
firing emerge applies the present-rise firma (firmaFired true).
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>
Captures the alignment raised in review: depth respects the framework two-moment
model — `data-depth` is the STATE moment (resting plane, Fase 1); the rise/recede
signature on `data-event-*` is the EVENT moment (Fase 3). The depth channel is
expressed through the SAME model motion already uses (evento != estado, never collapsed).
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>
#2 Semantic leading/tracking config-driven: the per-role `--leading-{role}`
(ui/prose/text/heading/display) + `--tracking-{role}` (badge/label/ui/prose/
heading/display) tokens were hardcoded in render-css; moved to
`typography.semanticLeading` / `semanticTracking` (STATIC_TYPOGRAPHY), emitted
config-driven + validated, so a theme can retune them. Byte-identical output
(generated/base.css unchanged — same values, same order).
#3 Font preloads: `collectFontPreloads(typography)` (pure) + `eidos.fontPreloads()`
surface `<link rel=preload>` descriptors for families flagged `preload: true`
(the engine emits CSS, not head markup), for the app `<svelte:head>`. Inert
until a theme opts in.
Docs: TYPOGRAPHY_ENGINE_RFC fases marked closed + Fase 4 (the two-zone scale is
intentional — documented, not rewired); THEMING §10 — applyColorScheme +
applyTypeScale system builders.
`npm run check` 0 errors; new font-preload tests + config/generated tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typography showcase mirroring /temas/color: dogfoods the engine through
live CSS tokens.
- Escala: live `buildTypeScale` ladder — base + modular ratio (+ fluid
ratioMax/viewport) re-derive all 8 sizes; the named styles (hero/h1-h6/
body/caption) re-scale too (they reference --font-size-* via var()).
- Familias: the 4 config-driven @font-face slots in their own fonts.
- Ejes: tracking / leading / measure / wrap / numeric driven live via the
--_text-* per-instance vars the recipe reads.
Self-contained (eidos foundation CSS + raw recipe data-attrs + data-theme),
same posture as the color demo. Editorial Lora-display aesthetic.
Verified in browser: live scale responds (xxxl 16->77px at ratio 1.6);
4 real fonts load (Instrument Sans / Lora / Azeret Mono); axes apply
(measure narrow = 54ch). `npm run check` 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Typography analogue of applyColorScheme: derive a whole `--font-size-*`
ladder from one modular ratio + base, optionally fluid (`ratioMax` grows
the scale on wide screens), applied as a managed `:root` block that
overrides the theme authored sizes at runtime.
- buildTypeScale(seed) — pure, in eidos/lib; mirrors build-scheme. Steps
the 8 named sizes (xxs..xxxl) off `md`=base via the ratio; reuses
fluidClamp; composes with `--scaling`.
- ActiveEidos.applyTypeScale(seed, opts) / clearTypeScale() — managed
block written last so it wins over the static sizes.
- exported from $uix/eidos (buildTypeScale, typeScaleDeclarations, types).
`npm run check` 0 errors; buildTypeScale + applyTypeScale tests pass. The
managed-block DOM path is the same mechanism as applyColorScheme; the
/temas/tipografia showcase will dogfood it live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`FontFamily.axes` ({ wght, opsz }) was declared but never consumed — the
TYPOGRAPHY_ENGINE_RFC §5 promise was unmaterialized. Wire it:
- axes.wght -> when a face omits `weight`, @font-face emits the range
(`font-weight: 100 900`) so one variable face spans the whole axis
- axes.opsz -> `:root { font-optical-sizing: auto }` so the optical-size
axis tracks the rendered font-size
Engine-only, inert until a theme declares axes (shipped fonts are static
TTFs — same posture as wide-gamut color: ready, not yet exercised by
assets). Zero change to generated/base.css (no family declares axes).
`npm run check` 0 errors; new test asserts the emission; the generated-css
test confirms base output unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finishes the move started in 9f007a84, which left the `id` concern split
across TWO folders (createId in soma/id, useId in active-uix/id) — worse
than one library, and the right call you pushed for. Both functions now
live in a single `$active-uix/id`; `soma/id` is deleted.
+ src/uix/active-uix/id/{create-id,use-id,index}.ts (one authority)
~ 441 soma imports repointed: relative ../../id and ../../../id -> $active-uix/id
- src/uix/soma/id/ (removed)
~ COMPONENT_GUIDE / README examples updated
createId (wraps $props.id(), SSR/ARIA ids) and useId (client-only counter)
sit together; any UIX layer — soma, eidos, demos, apps — shares one id
authority without coupling to soma. Output format unchanged
(`soma-{component}-{n}`) -> zero behavior change.
`npm run check` 0 errors; accordion (createId+useId) verified on a fresh
dev server, console clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The soma `id` module conflated two concerns: `createId` (wraps Svelte
`$props.id()` for SSR/ARIA element ids — ~200 soma importers, intrinsic to
the headless layer) and `useId` (a generic monotonic counter — 6 importers).
Only the counter is a cross-layer utility.
Move `useId` to a new `$active-uix/id` subpath (mirroring the existing
`$active-uix/prefs` that soma already imports), so any UIX layer — soma,
eidos, demos, apps — can mint client-only ids from a cheap, collision-free
counter without coupling to soma. `createId` stays in soma (its domain).
+ src/uix/active-uix/id/index.ts (useId)
~ soma/id now exports createId only
~ 6 useId importers repointed to $active-uix/id
(floating, toaster, date-field, time-field, accordion-item, internal/arrow)
Output format unchanged (`soma-{component}-{n}`) -> zero behavior change.
`npm run check` 0 errors; toast/accordion/popover verified in browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The event-trace inspector in each demo keyed its `{#each}` by a
`Date.now()` timestamp (`entry.at` / `h.at`). A single interaction can
stamp `data-event` on several elements within the same millisecond, so
two trace entries share the key and Svelte throws `each_key_duplicate`
(reported on dropdown-menu).
The trace is an ephemeral, 3-item, text-only log with no transitions or
stateful children, so positional reconciliation is correct: drop the
timestamp key (the each becomes unkeyed). The `fmtTime(...at)` display is
left intact.
Swept all 22 demos carrying the pattern (21 keyed by `entry.at` + announce
by `h.at`): dropdown-menu, context-menu, menubar, navigation-menu, listbox,
grid-list, table, tree-view, tree-grid, feed, command, carousel, drag-drop,
clipboard, announce, alert-dialog, button, color-field, link-preview,
range-calendar, time-field, time-range-field.
Verified: `as \w+ (\w+.at)` -> 0 occurrences site-wide; `npm run check`
0 errors; browser repro on dropdown-menu + table (burst of events, no
each_key_duplicate, console clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eidos-native components have NO soma layer, yet each demo rendered a
misleading "soma" code-snippet block ("n/a · eidos-native — equivalent
markup shown") fed by an orphan `somaSnippet` derived. Sweep the fix
already verified on text/heading across all 26:
layout primitives box flex grid stack container auto-grid wrap group
section aspect-ratio float
typography/inline code code-block kbd mark highlight link badge separator
visual leaves avatar banner skeleton spinner icon display scroll-frames
For each: remove the `somaSnippet` declaration + the soma `data-uix-code`
block, leaving only the real eidos snippet (and dropping its now-unneeded
inline margin-top).
Skipped (correctly): card / avatar-group / image (soma mentioned only in
prose, no fake code block — image reads a real ImageProvider) and all
genuinely soma-backed components.
Verified: `npm run check` 0 errors; 0 orphan somaSnippet refs in swept
files; browser spot-check (box, scroll-frames) shows one eidos code
block, zero soma badges.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Text and Heading are eidos-native (no soma layer), but their demos:
1. wrapped the live preview in a content-sized box (no inline-size),
so `align` had no room to render inside the centered stage area;
2. rendered a "soma" code-snippet block ("n/a · eidos-native") with
non-real equivalent markup, muddying the layer story.
Fix both: the live wrapper now `inline-size: 100%` (fills up to its
max-inline-size cap, 36/38rem) so alignment is visible; and the soma
code block + its now-orphan `somaSnippet` derived are removed, leaving
only the real eidos snippet.
Verified in browser: text box 576px / heading box 608px (filling the
918px stage); align=end renders; one eidos code block, zero soma badges.
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>
Mirrors the color RFC approach (audit -> compare -> extend additively behind the frozen
token contract, by phases). Covers: the current state + the gap vs Utopia/Tailwind v4/
Material 3/Apple/Carbon; the inclusion model (extend TypographyPrimitiveSet, never
rename tokens); the fluid clamp() formula (rem-based, with --scaling composing on top);
woff2 + variable fonts + anti-CLS metric-override fallbacks; tracking/leading/features/
measure/text-wrap; the <SText> measurement link; canon-vs-theme doctrine; token contract
additions; and a 5-phase plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes all 5 items of fix-stext.md for the <SText> canvas-measured text engine. None
were implemented before; T1 was a P0 correctness bug, T2 a P1 memory leak.
- T1 (P0) font-load invalidation: `invalidateFont` / `invalidateFontFamily` in
measurement.ts (granular, vs the old all-or-nothing clear) + `useFontReady(dom)` hook
that subscribes to `document.fonts` `loadingdone` via `ActiveDom.listen` (iframe/popup
-safe, no raw listener), evicts the loaded family's cache FIRST, then bumps a reactive
`epoch`. s-text.svelte folds `epoch` into the layout getter. Fixes the line count
lying after a web font swaps in (getComputedStyle reports the requested family,
unchanged on load, and the cache is keyed by the font string).
- T2 (P1) bounded caches: `LruCache` (Map-backed, move-to-recent + evict-oldest) caps
the per-font segment cache at 4096 entries and at most 24 fonts. invalidateFont reuses
it cleanly.
- T3: single getComputedStyle per reactive pass (merged `font` + `lineHeightPx`).
- T4: hydration flash documented in s-text.svelte.
- T5: bidi levels opt-in via `PrepareOptions.computeBidiLevels` (default false) — the
walker never consumed `segLevels`, so it was wasted compute on every prepare.
- tests: new canvas-text.test.ts (9 tests, deterministic canvas stub) covering LruCache
semantics, invalidateFont/Family, per-font bounding, and bidi opt-in + line-count
invariance. (The engine had ZERO tests before.)
Invariants kept: no change to line-break decisions, no canvas painting, pure files
(measurement/layout) stay Svelte-free, SSR-safe, strict TS. check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found stale color docs while verifying currency:
- arts/color/README.md: said "Status — Phase 0 ... nothing consumes it yet" (false —
consumed at build via render-css + runtime via applyColorScheme) and used
`ActiveEidos.setCssVariables` as the theme-builder mechanism (the real API is
applyColorScheme; setCssVariables is for contract knobs). Updated status, added
deriveScheme/temper/harmonize to the API table, documented temper as the canonical
intent-cohesion tool (vs harmonize for brand accents), fixed the builder pipeline to
buildScheme + applyColorScheme, and corrected the wide-gamut note (strategy A is
live + default-on, not "deferred").
- COLOR_ENGINE_RFC.md: 4 remaining `setCssVariables` references for the runtime
white-label builder -> applyColorScheme (only §6.2 was fixed earlier). Marked
Fase 4-bis (runtime generation) as IMPLEMENTED.
COLOR_MODEL_RFC.md verified current (RESUELTO; loss->plum correct; the anchor-hex
examples are the documented-discarded proposal = history). THEMING/audit/README were
already synced in their own commits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the session hand-off up to date with the work after the first write:
- checkbox 244ms lag fix (sequence pre->post) + the new doctrine (handler-gated
control state needs 'post'; call-site state tolerates 'pre');
- P3-11 surface ladder (light overlay -> neutral-4);
- prefers-contrast: more a11y;
- theming-engine backlog close (P3-5/6/8 fixed, P3-4/7/9 deferred with rationale,
P2 halves deferred).
Marks the prior "pendiente" engine-hygiene list as resolved-or-decided in the
continuation block. Final state: color/theming fully triaged, check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finishes the THEMING_AUDIT P3 backlog.
P3-5 (fixed) · appendScaledMetricDeclarations: the `parseFloat(raw) === 0` guard let
non-numeric values (auto / var() / calc()) fall into `calc(x * …)` = invalid CSS. Now
only finite, non-zero numbers are scaled; zero + non-numeric emit verbatim. No change
to the base config output (all values numeric) — pure robustness.
P3-8 (fixed) · index.ts no longer re-exports the raw render-* fns. The public render
API is the ActiveEidos class (gated by assertValid() + active config); ./lib/render-css
stays reachable for internal/tooling use. Redirected the one internal consumer
(active-eidos-config.test.ts) to import renderThemeCss from the module.
Triaged the rest with rationale (audit updated):
- P3-4 deferred · density wins by deterministic source order (stable); the :where(:root)
restructure to also support scoped density is high-cost for a theoretical nit.
- P3-6 already resolved · dispose() routes documentElement via dom (no direct access).
- P3-7 deferred · ActiveEidos reactivity is callback-driven (apply() on pref change) by
design; a full runes conversion is a risky refactor with no bug to justify it.
- P3-9 deferred · orphan _accent forwarders — low-value recipe surgery with cascade risk.
All P3 now fixed-or-decided; only the P2 secondary halves (contract pruning + bare
identifier color validation, both edge-case) remain, deferred as low-value.
check 0 errors · eidos suite green (3 pre-existing words-track failures unrelated).
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 REAL cause of the slow checkbox (not the stroke duration). The checkbox provider
flips its checked state inside the trigger HANDLER, and the morfo declared the commit
events with sequence: 'pre' — so the runtime ran `await runEmit()` BEFORE the handler.
emit() awaits the visual-channel hold (~240ms), so the functional state change (and
thus data-state) waited the full perceptual hold before flipping. Measured: click ->
data-state='checked' took 244ms.
Fix: commit-toggle-check / commit-toggle-uncheck -> sequence: 'post' (handler runs
FIRST, state flips immediately, the celebratory pulse emits after). This is the
doctrine for control commits (runtime.svelte.ts §577: "toggle's commit-toggle... the
pulse arrives AFTER the state has flipped"). Toggle + Switch were already 'post';
checkbox was the outlier. Measured after: 244ms -> 46ms.
Verified the siblings are NOT affected: radio-group (47ms) and tabs (31ms) are also
'pre' but flip state in the call-site (not the handler), so no lag — left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>