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>
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>
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>
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>
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>
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>
#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>
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>
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>
The theme builder now carries REAL wide-gamut, not just sRGB reformatted. generateScale
keeps raw OKLCH (no clamp), so a seed whose chroma exceeds sRGB renders more saturated
on P3 than its hex fallback.
- build-scheme.ts: result gains `wideGamut` (oklch() per opaque step) + `roles[].stepsOklch`;
`variables` stays hex (fallback + introspection). New `schemeDeclarations(result, {fallback})`
flattens to CSS lines — dual hex+oklch stack (default) or oklch-only (fallback:false,
for inline style where the CSSOM keeps one value per prop).
- ActiveEidos.#renderSchemeCss: emits the dual stack via schemeDeclarations → the applied
scheme block is wide-gamut on P3, sRGB-safe everywhere.
- index: export schemeDeclarations + SchemeDeclarationsOptions.
- temas/color demo: new "vivacidad P3" slider pushes the seed chroma past sRGB +
a "fuera de sRGB -> P3" badge (isInSrgbGamut). themeOverride now applies oklch
(wide-gamut). Verified: vivacity x1.70 -> primary-9 chroma 0.18 -> 0.31, badge on.
- tests: wide-gamut chroma retention (stepsOklch > hex fallback) + schemeDeclarations
dual/single; active-eidos scheme block asserts oklch(). 31/31 green.
- docs: THEMING §26/§27 + RFC §6.2.
Honest scope unchanged: the AUTHORED Radix palette stays exact sRGB (no regression).
Wide-gamut lives in the generator path (vivid seeds / OKLCH-authored themes).
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>
Reconcile the theming reference with the engine as already committed
(color-model redesign, scaling axis, P2 fixes) so the doc is internally
consistent:
- TOC: add §23 (scaling), §24 (P2 corrections), §25 (color model); fix
the §21 entry to its resolved heading.
- §4: intent->scale mapping was stale (risk->amber not orange,
loss->plum not purple); note intents auto-derive via
CANONICAL_INTENT_SCALES (identity = step 9, cross-ref §25); correct
token counts (31-scale palette = 744 --scale-* tokens; 9 roles = 216
primitives).
- §3 / anti-pattern G: "30 escalas" -> 31; reconcile "never add a scale"
with §25.7 (a brand theme brings its own palette).
- §21 / §22: mark the two-level color RFC as resolved in §25 (anchor
model discarded); drop the stale present-tense "vigente" claim.
- README reference table: add §23 / §24 / §25 rows.
Docs only -- no engine or demo changes.
Co-Authored-By: Claude Opus 4.8 <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>
The runtime token-override API is setCssVariables() / clearCssVariables(); setOverrides() never existed. A dev copying §19 would hit a runtime error. (audit P2-5/A1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variants son canon del eidos, NO del theme. Decisión arquitectónica
firmemente sostenida: el vocabulario de variants (solid/outline/ghost/
soft/surface/line/pills) está fijo a nivel del framework — paralelo
a las 8 sema families del libro. Theme = retintar lo perceptualmente
fijo; cambia QUÉ color es `affirm`, no QUÉ significa `outline`.
Cambios:
- lib/types.ts: nueva constante `EIDOS_VARIANTS` con los 5 archetypes
canónicos (control / selection / chip / marker / tabs). Los 5 union
types se derivan via `[number]` indexed access — valor y tipo no
pueden desincronizarse. Nueva `EIDOS_VARIANT_VALUES` Set flat con
todos los valores canónicos + utilidades cross-component (`plain`,
`subtle`).
- recipe-css-contract.test.ts: nuevo test "variant CSS selectors per
component match the declared type union". Por cada componente:
extrae el union type de `components/{c}/types.ts` (soporta literal
unions + archetype aliases; cae a advisory mode en Extract<> y
conditional types); compara con `[data-{c}][data-variant='X']`
selectores en `{c}.css`; reporta typos y unauthorized extensions
bidireccionalmente.
- THEMING.md §19: nueva sección "Variants son canon del eidos, NO
del theme" con argumentación (portabilidad, type safety, archetypes
perceptuales paralelos a sema families), tabla de las 3 capas de
la cebolla, referencia a `EIDOS_VARIANTS`, comparación con Radix
Themes 3.x / Mantine 7 / Chakra v3 / Ark / shadcn. TOC actualizado.
- eidos/README.md: tabla de referencia ampliada con §19.
- CLAUDE.md: hand-off "2026-05-27 #6 (variants canon)".
- CONTINUE.md: nota de la decisión arquitectónica.
Variants component-specific permitidos (Banner inline/overlay/
persistent, Spinner bars/dots/ring, Button 'plain'): viven en cada
`components/{c}/types.ts` y el lint los valida contra la CSS del
componente.
Tests: 101/101 pass en `src/uix/eidos`. `npm run check`: los mismos 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>
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>