The switch track is a pill (border-radius: 999px); applying data-shape made
corner-shape: bevel turn it into a hexagon (and scoop into a blob). Pills and
circles must not take shape families — same rule the avatar/thumb already
follow (pendiente.md). Dropped data-shape from the track; it stays round in
every family while the rectangles (card, button, badge, chips) still morph.
Verified: cut mode → switch corner-shape round / radius 999px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toggle changed data-theme but the preview stayed dark: applyColorScheme
resolves donor scales from the instance's EFFECTIVE mode (#resolveDonorScales
ignores the per-call mode option), and the boot had no modeSource — so the
scheme always derived dark neutrals/surfaces and overrode the foundation's
light tokens. Bound a modeSource to the `mode` state (grafito pattern) +
fire its listeners on change, so the instance mode follows the toggle and the
scheme re-derives correctly. Verified: surface oklch(0.21)↔(0.98), content
flips, both modes read cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A live theme builder: one seed → the five runtime builders (color · type ·
depth · shape · space) composed in a single uix.eidos.applyTheme() call,
scoped to [data-tema-preview] so the themed mini-app re-skins while the
controls stay put. Five presets set all axes at once (a whole personality);
per-axis sliders + swatches for fine control; live code mirrors the actual
call. Self-contained (boots its own minimal ActiveUix + ActiveEidos with
applyDom). Preview is hand-built from foundation tokens (--color-{role}-* ·
--space-* · --radius-* · --font-size-* + data-shape/data-depth) — the exact
surface applyTheme drives — since component recipes are code-split.
Verified in browser: all 5 axes retune live (color hue follows the seed,
type ratio, shape smoothing/corner-shape, depth elevation, space density),
presets apply atomically, stable (no cycling), prerenders under
adapter-static.
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>
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 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>
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>
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>
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>
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>
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>
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>
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>
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>
Two quick correctness wins for the color system:
- themes/base.ts: `loss` was `purple` -- identical to primary=purple. Map to
`plum` (canonical loss scale): a graver, more magenta violet. This closes
the last role collision in the base theme (after tertiary -> indigo).
- temas/grafito: the "override por componente" section bound raw scale names
(teal/amber/plum...) to Button.color, which only accepts the hierarchy
override (primary|secondary|neutral). That was a type error AND visually
inert (no [data-button][data-color="teal"] rule exists). Split into the two
REAL color axes a component exposes: `color` (hierarchy) + `intent`
(evaluative palette). Closes the last pre-existing svelte-check error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous split was by the text pick (white/dark) - wrong. Group the on-solid
chips by the SAME taxonomy as the Roles section: Jerarquia (primary/secondary/
tertiary) and Intents (neutral/affirm/fulfill/risk/threat/loss), reusing GROUPS.
Removed the now-unused onSolidGroups derived.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Revert ssr:false (it sidestepped the problem, did not fix it). Set the color
inputs imperatively via a `colorValue` action instead of reactive value=/bind:value.
With no Svelte-managed value, hydration no longer assigns "" (the warning) -- and
SSR stays ON. Verified: SSR HTML emits the 6 color inputs with NO value attr; the
action populates them client-side with valid hex.
- Split the "Texto on-solid" section into 2 groups by the APCA pick: "Texto blanco"
(dark solids) and "Texto oscuro" (light solids), computed from the live builder
solids (effectiveSolids) so the groups match the rendered chips. Default: white(8)
/ dark(1 = risk).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnosis: the SSR HTML already emits valid hex on all 6 <input type="color">
(verified), and the warning stack pointed to SvelteKit hydration ("await in
start"). Svelte momentarily sets a color input's value to "" while hydrating
before applying the real hex -> "specified value '' does not conform" x6. The page
is a client-only interactive tool (no SEO/SSR need), so ssr:false removes
hydration entirely -> no warning. Belt-and-suspenders with the existing value
guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The seed <input type="color"> used bind:value; switch to value + guarded oninput
so it can never receive "" (the recurring "specified value '' does not conform"
warning, fired at hydration). Both color inputs are now guarded.
- The Roles "contrast" slot IS the on-solid text color: white for dark solids
(most intents), dark only for light ones (risk). White swatches looked invisible
on the light card; add a faint inset ring to .slots .sw so they read as white,
not "no color". Behavior unchanged - only risk needs dark text (correct, APCA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 31-scale library was a flat list, so the 6 tinted grays (gray/mauve/slate/
sage/olive/sand) looked like near-duplicates. Group it like Radix: Grises (tinted
neutrals) - Colores - Brillantes (light solids) - Metales, with a fallback "Otros"
bucket so no scale is ever dropped. Intro note explains the grays cluster because
they are near-neutral (chroma ~0.01) with a subtle per-accent hue tint. Verified
in-browser: 31 scales, none dropped/duped, 4 group labels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-role override <input type="color"> took value={r.solidHex} directly; an
expression-driven color input warns ("specified value '' does not conform") if it
ever receives an empty string. r.solidHex is always a valid hex in practice, but
guard it (`|| '#000000'`) so the input can never receive '' — belt-and-suspenders
for a warning seen during live HMR editing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Gemini's sharp note: rotating an intent's HUE toward the brand (harmonize)
erodes its meaning — a red stops reading as "error". What coheres a palette is
sharing the chroma + lightness PROFILE, not the hue. New temper(color, reference,
amount) keeps the hue and lerps L+C toward the reference. The demo's intent
cohesion switches harmonize -> temper, and the slider MOVES to the "Roles
canonicos" section (next to the intents, dynamic). Verified in-browser: threat hue
stays 358 (red) at 0% and 40% temper, only chroma/lightness shift; affirm stays
teal. harmonize stays in the engine for brand accents. RFC §6.2 updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Canonical intents (red/green/...) can clash next to a brand color. Replace the
harmonize on/off toggle with a slider (0 = pure canonical -> 0.35 strong), default
a SUBTLE 12%: intents lean toward the brand enough to feel cohesive but stay
recognizable (red is still red). Verified: green seed -> threat #e35013 at 12%
(warmer red), pure #e5484d at 0. Also clarified that "La paleta" is the FIXED
library (does not derive from the seed; the seed derives the roles) to resolve the
recurring confusion. RFC §6.2 notes the subtle-default guidance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
deriveScheme gives DEFAULTS, not a cage: each role row gets a color input that
PINS that role to the designer's exact color, while the rest keep deriving from
the seed. "auto" un-pins; changing the seed re-derives only the unpinned roles.
Mirrors M3 (custom colors per role) + Radix (pick accent/gray) + the hand-authored
path (grafito maps every role). Verified in-browser: pin secondary=blue +
tertiary=gold while primary/neutral stay derived; auto reverts to derived.
Documented in COLOR_ENGINE_RFC §6.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The seed now re-themes the ENTIRE page, not just the preview: it overrides
--primitive-{role}-* (+ contrast + surface alpha) on .root, reproyecting every
--color-{role}-* and the neutral-driven surface/content/border chrome. Mode-aware
templates (light/dark). New "harmonize intents" toggle nudges the canonical intents
toward the seed's temperature (M3 blend.harmonize); off by default so intents stay
canon. The 31-scale library stays fixed. Verified in-browser: green seed -> canonical
primary / on-solid / topbar all go green; threat stays red until harmonize.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Seed color + variant (tonal/vibrant/monochrome) -> deriveScheme -> generateScale
-> live preview of the 5 hierarchy role scales (12 steps each, step 9 ringed) +
their APCA on-solid chips. The full uix.color builder pipeline, reactive and
pure. Verified in-browser: blue seed -> tertiary purple (+60deg), monochrome
collapses to one gray, primary = seed verbatim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Palette (31 scales x 12 steps), the 9 canonical roles with their slots, the
APCA on-solid pick live via uix.color, translucent role surfaces, and the
neutral chrome. Rendered through live CSS custom properties with a light/dark
toggle. Verified in-browser: swatches resolve, risk flips to dark text, modes
invert. Not linked from the /temas index (it is a system demo, not a brand
theme — like /temas/animations).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The video mode now paints a frame immediately: on `loadedmetadata` it
seeks to the initial scroll position instead of waiting for the first
scroll (the progress-0 loop guard used to leave some browsers black).
New `start` / `end` props (seconds, clamped to the real bounds) scrub
only a SEGMENT of a clip — progress 0->1 maps to `[start, end]`, `end`
defaults to the full duration. Time-based seek means frame rate is
irrelevant and duration stays browser-authoritative (read from
`loadedmetadata`, never passed by hand).
Demo: same-origin `/demos/video.mp4` sample (external URLs fail on
cross-origin range requests), live `start` / `end` / `smooth` controls,
API rows + snippet parity, README segment example.
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>
The editor had no persistent formatting bar — commands lived only in the
bubble (selection), slash (/) and drawer. Add a docked menu bar at the top
of `<Words>`, built from the existing (previously unused) soma
`Words.Toolbar` / `Words.CommandButton` primitives, the same way the bubble
composes them.
- words-menubar.svelte (new): role=toolbar with six groups — history
(undo/redo) · block style (Text/H1-3/Quote/Code via a DropdownMenu) ·
marks (B/I/U/S/code) · lists · align · insert (image/table/divider via
`api.insertBlockOfType`). Marks/align are `Words.CommandButton`s rendered
as eidos `Button`s (range restored before the command runs, active state
reflected). Right side: an Edit/Preview toggle + an inspector toggle.
- words.svelte: mounts the bar; owns `mode` ('edit'|'preview') mapped to the
provider's `readonly` (preview = contenteditable off, editing chrome —
bubble/gutter/drag — suppressed, bar stays visible). Unifies the inspector
open state (drawer open / sidebar visibility) behind one toggle the bar
drives; the floating drawer button is gone (folded into the bar).
- types.ts: `toolbar?: boolean` prop (default true).
- words.css: `[data-words-menubar]` fixed-height top bar (`--_words-bar-h`),
group dividers, active-control accent (`data-active` + `aria-pressed`), and
offsets the docked inspector panels below the bar.
- demo: drop the now-duplicate Edit/Preview toggle + serialized-HTML preview
swap (the bar owns preview as read-only); keep the inspector picker.
Verified in-browser: bar renders (role=toolbar, 44px, 6 groups, 18 controls);
Edit→Preview flips contenteditable to false, disables format buttons and hides
the chrome while the bar persists; Bold applies to a selection from the bar
(onpointerdown range-restore holds across the out-of-editor click); the
inspector toggle hides/shows the sidebar; align-left reads active for a
left-aligned block; no console errors; check clean for the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three demo/chrome fixes:
- Drawer inspector: the open toggle (SlidersHorizontal) stayed mounted
while the panel was open, colliding with the panel's own X close in the
same corner. Render it only while closed; the panel's header X closes it.
- Preview columns: serialize as <div data-words-columns><div data-words-column>,
which no preview rule matched, so they stacked. Mirror the editor's row flex.
- Preview headings: were --font-size-2xl/xl/lg at weight 700 — louder than
the editor. Reference the SAME recipe tokens (xl, lg+4, lg; semibold;
heading line-height + primary color) so Preview reads like Edit. Verified
editor + preview both render h1=28px/600, h2=24px/600.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POLISH-3. Replaces the bare Inspector-mode row in the Words demo with a real
topbar: a document breadcrumb (FileText icon + the doc's first-heading title),
a "Saved / Editing…" indicator that flips on document change and settles after
a short idle, and the Inspector-mode + Edit/Preview toggles.
Preview mode renders the document as a clean, read-only reading view via the
export-only `serializeHtml` serializer (bare semantic HTML with its own
typography — headings, blockquote, code, lists, table, links). Showcases the
export path now that table cells serialize block content (<td><p>…</p></td>).
Also exposes the export-only serializers on the public soma barrel
(`serializeHtml`, `serializeMarkdown`, `renderWordsPlainText`) — apps need them
for preview / copy / download.
Browser-verified: topbar renders; Edit↔Preview swaps the editor for the
read-only article (table + headings render); Saved badge shows. npm run check:
0 new errors (1 pre-existing grafito baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P5m. A table cell now holds full BLOCKS (paragraphs, lists, images, nested
tables) like Callout / Column, not just inline text. A fresh cell seeds one
empty paragraph; the existing generic block machinery (path walker, render,
validate, normalize) handles cells the same way it handles callout / column.
Atomic model change across the engine:
- types: TableCell.children: WordsInline[] -> WordsBlock[].
- factories / table-ops / extra-ops: cells seed [paragraph], not [text].
- normalize: normalizeTableCells recurses each child block (mirrors callout);
empty cell seeds a paragraph.
- built-ins table spec: render -> renderBlock per child; validate ->
validateBlock per child; HTML -> blockToHtml per child (<td><p>...</p></td>);
Markdown -> flatten blocks to inline joined with <br> (GFM cells are
inline-only; lossy, consistent with markdown being export-only).
- serialize-text + selection-walkers: cell text recurses block children.
- delete-ops: inline cross-cell merge (mergeTablePrev/Next) replaced by
recurseIntoTableMerge — treats the cell's blocks as a sub-document, so
backspace merges blocks WITHIN a cell; at a cell's first block it is a no-op
(cells never merge across the fixed grid — matches mainstream editors).
Caret paths into a cell are one level deeper now ([t,r,c,blockIdx,...]). All
table test fixtures wrapped (inline -> paragraph) and 13 assertions updated to
the new behavior (<p> wrappers, deeper paths, no cross-cell merge).
Browser-verified: cells render block content (path 14.0.0.0), table renders
clean. npm run check: 0 words errors (1 pre-existing grafito baseline). Words
tests: 452/452.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rename theme untitled-ui -> Grafito: route /temas/grafito, theme ids
grafito-light/dark, grafitoConfig, brand/hero/footer/index.
- Palette: Grafito now carries the full 32-scale library (spreads the
framework's 31 Radix scales + brand carbon, overriding the brand-tuned
gray/violet/blue + intents). Each directly usable as --scale-{name}-{step}.
- Roles: hierarchy explicit (primary->carbon, secondary->violet,
tertiary->blue); the 6 intents auto-derive from the palette by convention
(step 9). THEME_SCALES derived from palette keys, labelled by COLOR NAME.
- Demo Color section -> 3 groups (Paleta / Roles jerarquia / Intents
auto-derivados) + scaling control in the topbar.
Verified in browser: 32 distinct scales emitted, intents auto-derive
(affirm=teal, risk=amber, loss=plum), no validation error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three architectural pieces for inserting blocks into a column slot from
an overlay button, plus the missing DOM-selection sync that any
imperative consumer of `applyCommand` needs.
- Engine op `insertBlockInColumn` returns `{document, selection,
activeMarks}` in one transaction (Tiptap-style). Atomic blocks land
with a trailing paragraph escape hatch + caret there; text-bearing
blocks select any stub text ("Title", "List item") so the next
keystroke replaces it Notion-style.
- Provider `applyCommandWithOptions` now schedules `restoreDomSelection`
via tick when the command changes the model selection (typing-batch
excluded — the browser already placed the caret). Was the hidden gap:
overlay buttons, drag-drop, slash menu, the new column inserter, all
updated the model but the DOM caret stayed wherever the user last
clicked, breaking subsequent text editing.
- `words-column-inserter.svelte` rebuilt around a busy guard with a
hard 250ms safety timeout (the previous pendingInsert + onCloseAuto
Focus pattern could leave the `+` button dead forever if the
dropdown's teardown swallowed the close callback).
Plus type sync: `WordsProviderSnippetProps` now declares
`selectedBlockPath`, the second arg of `selectAtomicBlock`, and
`setSelection` — they were exposed by the runtime but missing from
the type, breaking typecheck on eidos consumers.
Demo carries a `columns` block in the initial doc as a permanent test
fixture for column-related fixes.
**Known issue documented in CONTINUE.md P0:** typing inside a `columns`
block does NOT insert — selection sync (`syncSelectionFromDom`) isn't
mapping nested paths (`12.0.0.0`) to the model correctly. The inserter
flow is wired correctly; once the path encoding for nested selections
lands, the full Notion-style "click + → pick Heading → type" flow
works end to end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>