astra
alpha-0.1-background
alpha-0.1-dir-prefs
alpha-0.1-sec-dom
menubar-v4-safe
active-uix
morfo-runtime
morfo-driven-soma
semantuix
glm-5
main
sium-v1.0
${ noResults }
58 Commits (044221e4c79b917dbc3e9410271b1b6ecf17610f)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
044221e4c7 |
feat(eidos): 33-color palette + palette-first theme builder + color guards
Palette: - Add `fuchsia` (~H334) and `steel` as the 32nd/33rd named scales; rename radix-scales.ts -> color-scales.ts and drop the "Radix parity" framing (seeded from Radix, but the palette is ours). - generatePalette: per-family nearest-hue donor (L-per-hue) so bright hues (yellow/amber/lime) stay vivid instead of landing muddy. Theme builder (web/routes/temas): - estudio: new "palette-first" color mode — character (vivacity/tone/neutral) regenerates the 33 families; roles are selected by hue harmony or manually and applied live to real components via --primitive-* overrides. - paleta: palette generator + role harmony chooser (the model, visualized). Color guards (THEMING.md §25.2/§25.4): - G1: completeColorRoleMap throws when a hierarchy alias and a valenced intent share a scale; the builder picker excludes intent-occupied scales. - G2: re-expose role slots bg2*2 / separator*6 / borderHover*8 / textStrong*12 (13 slots) — real UI/a11y needs the original nine had dropped. - G3: document the CVD / WCAG 1.4.1 rule (an intent needs a non-color cue) plus the affirm/fulfill activation distinction; audit-grounded. - G4: palette-invariant.test.ts checks the delta-E floor on every generator's OUTPUT (multi-step, monochrome-exempt); clamp builder tone to +0.08. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
56eb9bd265 |
feat(ethereal): finalize — own engine is the runtime + dual-engine native path
Two parts: the cutover (own JS engine replaces @floating-ui as the runtime, dep out of the library) and the native CSS Anchor Positioning path layered on top. Cutover (P4, reconciled): - Flip USE_OWN_ENGINE -> true, then delete it (flag.ts removed). Collapse the A/B in use-floating + floating.svelte to own-only (drop the fui imports, the factory shim, the `as unknown as` casts). useFloating's `dom` is now required. - @floating-ui moved dependencies -> devDependencies: the LIBRARY (src/arts/ethereal + src/uix/soma) imports it NOWHERE; it stays only for the visual demo + the parity oracle. Zero-dep doctrine met for consumers. Dual engine (P2 — built + proven end-to-end): - `selectPositioningStrategy` ($ethereal/strategy.ts, 5 tests) picks native vs JS per instance on discriminators (arrow / shift / virtual-anchor / explicit-boundary; sticky is subsumed by shift). The behavioural twin of supportsCssAnchor. - eidos `renderFloatingNativeBlock`: @supports (anchor-name) and (position-area) with position-area from data-side/align (logical axes — validated against the spec), per-side gap margin, position-try-fallbacks for flip. base.css regenerated. - soma wrapper: when native, stamps data-floating-native + an inline anchor-name<-> position-anchor link and PAUSES the JS positioner (no computePosition, no autoUpdate). - New `avoidCollisions: 'flip'` (flip-only, no slide) decouples flip from shift so an overlay can flip natively. Popover demo: avoidCollisions = full / flip / off. - Proven in a real browser: a flip-only arrowless popover routes to native, positions via position-area, and flips natively (flip-block) at an edge — zero JS transform. Honest scope: shift has no native primitive, so native lights up only for non-shift overlays; the JS engine stays the default for collision-avoiding ones. data-side doesn't track a native flip, so native is gated to the arrowless case. Verified: 537 tests green (ethereal + 7 overlays + floating + generated-css), 0 new check errors. Docs: CONTINUE.md, PERF.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
3 months ago |
|
|
151873d795 |
feat(uix): squircle is the surface-tier default (hybrid shape doctrine)
Materializes the framework's signature corner. SURFACES (overlay panels +
non-floating cards) carry the continuous (squircle) corner by default; CONTROLS
stay arc. At surface-scale radii (>=~10px) arc and squircle visibly diverge
(premium); at control radii they're indistinguishable, so the split costs no
coherence.
The foundation rule (renderShapeBlocks) ENUMERATES the tier — NOT a
`[data-archetype='content']` hook, which also marks tabs/accordion/collapsible/
table content (non-surfaces) and would over-apply (verified per-attr). `:where()`
(specificity 0) lets a `[data-shape]` override win, so the `shape` prop flips from
opt-IN (activate squircle) to opt-OUT (`shape='rounded'` escapes to arc) on
surfaces. Theme knob `--shape-surface-default` reverts the whole tier; degrades to
arc where `corner-shape` is unsupported (border-radius magnitude is universal).
- Tier A — floating panels [data-{c}-content] (pickers inherit via Popover) + [data-command]
- Tier B — [data-card], [data-banner], [data-radio-cards-item]
- Guard test: surfaces resolve to squircle, controls/rows/pills stay arc
- THEMING.md §30 updated; pilot moved out of card.css into the foundation
Verified live (Chrome 150 renders the squircle); per-attr computed coverage
matches the taxonomy.
|
3 months ago |
|
|
61ea97b5bb |
feat(uix): menu gap canon + keyboard focus-ring fix + overscroll + archetype :where
- floating-gap canon: --floating-gap-menu 0 -> var(--space-1); FloatingContent reads --floating-gap via a reactive $derived (was a one-shot rAF that never reached portaled menus -> they fell back to sideOffset, flush); nav-menu wired to the token via CSS. - focus-ring: exclude item/option/content from the universal archetype ring; menu rows use the canonical highlight on :focus-visible too (no thick keyboard border, no ring framing the whole float). - overscroll-behavior: contain on the ScrollArea viewport + 7 overlay scroll regions (no scroll chaining to the page). - archetypes.css: default rules wrapped in :where() so component recipes always win. - radius decoupled from size (Radix model) in toggle/textarea/menubar/nav-menu/ virtual-list/grid; toggle `rounded` prop. Canon-read + token reach portaled menus pending a fresh-load check (the dev tab cached the floating module) -- see CONTINUE-floating-gap-canon.md. |
3 months ago |
|
|
c474fc6bae |
feat(eidos): gradient axis (6th builder) + elevation-scaled opacity + GradientBuilder scaffold
Gradient axis — Phase 1 (engine + dogfood):
- $libs/gradient: canonical Gradient model + gradientToCss serializer, pure and
zero-dep, shared by the theming axis and the (WIP) GradientBuilder. Stops
reference color ROLES → a --gradient-{name} re-tints with the seed and flips
light/dark for free. Default `in oklch` interpolation.
- build-gradient: role-derived factories (deepen/sheen/halo/aurora mesh) +
ActiveEidos.applyGradients() (the 6th runtime builder, in ThemeSeed/applyTheme).
- shimmer migrated to `in oklch`. Dogfood: /demos/cristal replaces its ~12
hardcoded gradients with applyGradients role tokens.
Opacity = function of elevation (depth cue `translucency`):
- New translucency depth-plane cue, sibling of shadow/blur: frost opacity now
scales with elevation (foundation overlay 68% / modal 80%; cristal 52→66→80)
via --depth-{plane}-translucency, consumed by the frost rule. base.css regenerated.
GradientBuilder component — Phase 2 (scaffold, WIP):
- morfo (contract) + soma provider state machine over the Gradient model: stops
add/move/remove/recolor, kind, angle, pointer drag; every stop a keyboard-
accessible role=slider thumb. Marked ACTIVE_DEV_TRACK until eidos/picker/demo land.
Docs: $libs/gradient README + THEMING §gradient-axis / §translucency + token table.
Demo cristal: scroll-reveal via uix.motion spring + hover glow; Select z-index
ladder; aurora/title → role-derived tokens.
contracts.test: ACTIVE_DEV_TRACK now filtered uniformly in both soma collectors.
check: 0 new errors. Tests: $libs/gradient + build-gradient + base.css sync green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
3 months ago |
|
|
8606232125 |
fix(eidos): THEME-SYS-1 — consolidate overlay z-index into a named scale (+ guard + docs)
The ~12 overlay recipes hardcoded an ad-hoc parallel z-index scale (raw integers
60–99/1200) that duplicated nothing reusable and had drifted out of order
(tooltip 76 < dropdown 80 — a tooltip painted BEHIND a dropdown). They are now a
named scale.
- `STATIC_Z_INDEX_OVERLAY` (static.ts) → emitted as `--z-index-overlay-{inline,
backdrop,content,floating,tooltip,detached,toast}`. A SEPARATE scale from the
global `--z-index-*` ladder (which orders the depth planes) — overlays portal
to <body> as siblings of modals, so they share one flat low band where each
rung sits just above the modal scrim. Mapping them to the 300–900 ladder would
hide a dropdown/select/popover opened INSIDE a dialog (dropdown 300 < modal
700); the combobox recipe already warned about this. `tooltip` now sits above
`floating` (fixes the inversion); `toast` stays above the soma FloatPanel band.
- Every overlay recipe token (`content-z`/`overlay-z`/`inline-z`/`toaster-z`/
`preview-z`) now references `var(--z-index-overlay-*)` — zero raw integers.
dialog/drawer gain an explicit `content-z` rung (drops the `calc(... + 1)`).
- Guard (contracts.test.ts, "overlay z-index against raw integers"): a recipe
`*-z` token must reference the scale, never a bare integer. Proven to catch
drift (a raw `'76'` makes it fail). Local `z-index: 0..5` (avatar/tabs/sticky)
is intra-component relative stacking — out of scope, stays.
- Docs: THEMING.md §35 rewritten to describe the consolidated scale + the
flat-band rationale + the guard; token table gains `--z-index-overlay-*`;
testing-and-tooling.md documents the catalogue guards (VG-8/SYS-1/A31/A30/
THEME-SYS-1).
Stacking order verified from the resolved CSS (deterministic z compare: content
70 < floating 80 < tooltip 90 < toast 1200; dropdown-in-dialog preserved). A
live browser check was blocked by a port conflict with another session's server.
check: 0 new type errors; the 5 guards green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
3 months ago |
|
|
1590684c2e |
feat(floating): token-driven canonical gap — foundation + popover pilot
floating-ui's numeric `sideOffset` can't take a token, so the trigger->panel gap was a hardcoded per-component number (0/4/6/8, inconsistent). Canonize it: register a `--floating-gap` <length> @property, set it per archetype via a foundation rule (`--floating-gap-menu: 0`, `--floating-gap-panel: --space-1-5`), and have the shared positioner (soma FloatingContent) read the resolved px off the content and feed it as the offset -- token-driven (follows density x scaling) and arrow-safe (the arrow rides with the offset). A content opts in by stamping `data-floating-gap`; otherwise the positioner falls back to its numeric `sideOffset`, so the rest migrates one at a time. Pilot: Popover.Content stamps `data-floating-gap="panel"`. Verified in-browser: overriding `--floating-gap-panel` to 20px moves the popover gap to 18px (20 minus the popover's ~2px border overlap); the default 6px is a no-op vs the old sideOffset=6. check 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
3ae9ff339a |
fix(eidos): --radius-default sm → md (radius archetype default)
Reconsidered Decisión 2 against the reference systems: the default radius should equal the eidos control center-of-gravity (button / field / select-trigger = md / 6px), like Bootstrap / Chakra / Radix-medium. `sm` (4px) would leave the default tighter than the controls themselves — backwards — and would shrink any control that later adopts the token; md makes that adoption a no-op. `--radius-default` = var(--radius-md). Verified in-browser: resolves to 6px. check 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
845d6579d1 |
feat(eidos): radius foundation — --radius-default + --radius-factor + scaling-responsive (A2)
First increment of the A2 (radius) phase. The radius scale was fixed px; now: - `--radius-default` (= var(--radius-sm)) — the archetype default radius for components without a radius reason of their own (Decisión 2 / §13). - `--radius-factor` (default 1) — a global Radix-style roundness knob; a theme sets it on :root to dial all corners up/down at once. - Radius responds to `--scaling`: each real radius emits `calc(base * var(--radius-factor) * var(--scaling))`. `none` (0) and `full` (the pill sentinel) stay verbatim. At factor=1 + scaling=100 both resolve to 1 → identical output (zero regression). Verified in-browser: md=6px default · --radius-default=4px · factor 1.5→9px (at :root) · scaling 1.1→6.6px · full=9999px. check 0. Pending (A2 increment 2): components consume --radius-default + adopt [data-shape-nest] (drop the hand-rolled calc(--radius-md − space)); panel-radius sub-decision (modal xl vs overlay lg). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
fe597f3fa9 |
fix(motion): audit follow-ups — spring reduced-motion, Motion+loop exit, loop tokens
Three fixes from the motion-ecosystem audit:
- `spring()` honours `ctx.reduced`: snap to the target and settle instead of running
the physics. The engine only short-circuits `reduce: 'instant'`, the driver ignored
`ctx.reduced`, and Presence never applied the declared CSS `fallback` — so spring-pop
played the full bounce under `prefers-reduced-motion`.
- `<Motion>` leave(): a loop preset is infinite — detect `animation-iteration-count:
infinite` and unmount promptly instead of holding the node for one loop iteration
(1–2s, still looping) on removal.
- Loop durations are now canonical `:root` tokens (`--motion-loop-{name}`), declared
like the `--duration-*` scale instead of living only as a `var()` fallback.
Tests: engine-motion (spring reduced) + motion (loop tokens); check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
9e4187bc3c |
feat(motion): loops + state-domain + staggered-exit + debug-stagger
Close the content domain of the universal `motion` prop (RFC §D.12/§D.13):
- State domain: a dedicated `data-motion-state` attribute lets a component with
its OWN data-state machine (Card selected/idle) drive a preset on its transition
without clobbering its semantic `data-state`. Enter-only (animate-on, snap-off).
New emphasis preset `select-pop` (no opacity-from-0, for still-visible elements).
- Loops (content domain, infinite): Tailwind's canonical `spin`/`pulse`/`ping`/
`bounce`, emitted as un-gated `[data-animation-style='X'] { animation: … infinite }`
rules. Themeable per loop via `--motion-loop-{name}`; reduced motion stops them.
- Staggered-exit bridge: the container-driven cascade now covers exit too —
`[data-stagger][data-state='closed'] > …` cascades children OUT in reverse
(`--motion-stagger-index-rev`, last leaves first). The container is retained for
the stagger window then unmounts (pragmatic CSS bridge for bounded lists; the
parent-waits-for-children version still needs the retired PresenceGroup).
- `[data-debug-stagger]`: opt-in dev affordance — a CSS counter badge per child
showing its `:nth-child - 1` index (the analog of UIX_DEBUG_MOTION).
- Fix `<Motion>` exit: a `{ duration }`-only Svelte transition never completes (the
node sticks at the exit frame, a re-show reuses the dead node); add a `tick` no-op
so it times the unmount.
Generated base.css regenerated; motion.test 27/27; docs (eidos-motion + RFC) updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
5a132ead22 |
feat(eidos): button audit — focus canon, press archetype, intrinsic box, plain pure-emphasis, spinner via Spinner
- focus ring: adopt canonical `--focus-ring-*` (outline, HCM-safe, primary-tint) — drops the per-color fork - press archetype: new `STATIC_PRESS` primitive (`--press-scale: 0.985` / `--press-duration: 80ms`) emitted from foundation. Button switches from hardcoded `translateY(1px)` to canonical `scale(var(--press-scale))`. Card recipe repointed to the shared token — single press signature across the system - intrinsic box: `inline-size`/`block-size: fit-content` defends the Button against flex/grid stretch (was filling 400×160 in a grid; now 94×36). `data-block` still opts into width-stretch, `data-icon-only` still square - variant=emphasis only: `plain` no longer mutates layout (`min-block-size: auto` + `padding-inline: 0` removed) — same control box as the other 5 variants. Inline link-like actions belong to <Link>, not here - spinner composition: Button drops its bespoke SVG ring + `@keyframes button-spinner-spin` + `--button-spinner-duration: 720ms` and composes the canonical <Spinner variant="ring"> via two new modes: `presentation` (no role=status/aria-live, marks aria-hidden — Button owns the a11y) + `color="inherit"` (currentColor + 25% track). One spinner across the system, unified duration (0.9s) and reduced-motion behaviour Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
9d40ebe9c8 |
feat(motion): event-driven item cascade — first real consumer (DropdownMenu)
Productizes the event-driven cascade on a real component (not the explicit `<Cascade>` orchestrator). When a DropdownMenu opens, its items fade in staggered by structural order — the appearance flows from the `open` (emerge) event, nothing in the app wires it. Architecture: a +1 GENERIC selector branch in the motion generator, NOT a per-item `data-state`. A `menuitem` isn't open/closed — the menu is — and eidos writing a state attr is a smell. The new container-driven form `[data-stagger][data-state='open'] > [data-animation-style='X']` fires each child's ENTER off the container's `data-state` (which soma already writes via the morfo `commits`); items carry only `data-animation-style`. ENTER only: the coordinated EXIT cascade (retain the container until children finish) is the deferred soma PresenceGroup work, so a closing container can't drive a child exit that would be cut off on unmount. - render-css.ts: `renderCssPresetRules` adds the container-driven enter branch (same declarations, shared comma selector) — reuses the existing presets + the foundation `:nth-child` stagger index; zero new system. - DropdownMenu: Content marks itself `[data-stagger]` + `--motion-stagger-each: var(--motion-stagger)` (20ms, themeable, no magic number); item/checkbox-item/radio-item carry `data-animation-style="fade"` (overridable). `fade` so item opacity doesn't fight the panel's own scale. - regen generated/base.css (8 css presets gain the container-driven enter). Verified in browser (frozen frame): index from `:nth-child`, `delay = idx × each`, opacity gradient in flight (Ruler 0.98 → Grid 0.89 → Guides 0.69, Log out 0.00). Honest limit (documented): only DIRECT-child items cascade — items in a <Group>/<RadioGroup> DOM wrapper aren't `:nth-child` of the panel, so they appear instantly (the "transparent intermediate"). Docs: dropdown README §Motion, cascade README §2 (concrete consumer), RFC §D.11.4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
27012a5c68 |
feat(motion): structural stagger index + Cascade orchestrator on the verified model
The cascade is no longer a separate system — it's the existing state-presets + the Material stagger + one foundation rule. Retires the --cascade-* reinvention. - render-css: foundation structural-index writer — `[data-stagger] > *:nth-child` → `--motion-stagger-index` (forward) / `:nth-last-child` → `--motion-stagger-index-rev` (reverse). Nobody writes the index; soma never writes a visual var. Every preset's enter/exit rule consumes it (parallel = `--motion-stagger-each` 0, cascade = N). - Cascade rewritten as a thin EXPLICIT orchestrator: marks `[data-stagger]`, reflects open→data-state, passes the preset to items via context; the `out:` retention flips to data-state=closed (reusing the exit preset), reading the duration via dom.getWindow. Drops the whole `--cascade-*` namespace + bespoke keyframes. Docs: MOTION_SERVICE_RFC §D.11 — the final model (three orthogonal axes sema/motion/ eidos, the full lifecycle, the realization ladder snap->transition->animation->JS, the firma-vs-realization seam, the cascade = existing system + one rule, the motion service as the engine). eidos-motion aligned. Verified: browser (enter scale-in/fade-in + nth-child index, exit reverse, zero inline writes), npm run check 0 mine, motion.test 22/22, eidos-lint 0 invalid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
6307d03055 |
refactor(motion): retire parallel orchestration motor (Plan A) + verified model + Fase 1 prototype
Remove the parallel motion-coordination service per the redesign: animation is a channel of the EVENT's firma (sema owns it across all channels), not a parallel axis. Code is gone; the RFC stays as historical record with a retirement banner. - morfo: drop MorfoPart.animation + its schema/compile/types/exports - arts/motion: drop CoordinatedPreset / MotionConfig.coordinated - eidos: drop BUILTIN_COORDINATED_PRESETS / renderCoordinatedPresetRules / the animation:none neutralization + registry block; regen generated/base.css - soma: Presence reduced to a single-surface island; delete presence-group / dom-cascade / coordination; dropdown-menu loses the cascade wiring - delete Reveal / Rail (morfo + soma + demos) Docs: MOTION_SERVICE_RFC gains the retirement banner + §D.8 (retirada) + §D.9 (verified model: morfo->soma->sema->eidos pipeline + the two hard rules — soma never writes a visual --var; data-event-* is a single-target stamp, not a bus) + §D.10 (Fase 1 prototype). Fix stale "5 canales" claim in GUIA §11; eidos-motion + dropdown README aligned. Fase 1 prototype (web/routes/temas/animations/panel-cascade): validates the model end to end — panel->cards cascade (enter/exit), dynamic removal with Svelte out: retention, nested cascade — all via :nth-child + custom-property inheritance, with zero JS visual writes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
b8637976a0 |
feat(uix): elevation archetype (A1) + coherence audit + popover/dialog/option-list polish
Coherence audit (ARCHETYPE_COHERENCE_AUDIT_2026-06-19): 8 archetypes + 8 closed decisions, the §13 construction contract, per-component deviation matrix (88/117 deviate). Master guides (soma/COMPONENT_GUIDE, eidos/components/README) point to it. A1 — elevation bundle via `data-depth`: the plane paints surface·border·shadow·halo (added `border` to the depth bundle). z-index is NOT painted — stacking is a positioning concern owned by the floating-wrapper. Popover migrated to the overlay plane (panel + arrow consume it); recipe orphans pruned. Popover fixes: arrow un-clipped via an inner [data-popover-viewport] (Radix-style; the absolute arrow escapes the scroll clip), sized via density tokens (not soma's hardcoded 10×5), border-seam overlap keyed on data-side, hover-bounce guard in scheduleHoverOpen (ignore hover-open when already open). Dialog.Close composes IconButton/Button (neutral) — closes the Lote ② debt. Option lists: density-driven padding/radius via --space-* + concentric radius, ScrollArea adoption, emerge open/close events (combobox/select/command), and the canonical option-highlight in archetypes.css. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
e0bb7ed5f5 |
feat(eidos): shared field control-trigger + segment state/width layers; uix- id prefix
Consolidate the per-field / per-picker duplication the audit surfaced: - Shared control trigger — new field/field-control-trigger.css selected by a `data-field-control-trigger` marker: flush (no border, no shadow), UNIFORM neutral hover tint (not the field accent), font-relative size. Migrated date-picker + the password visibility toggle; deletes their per-picker chrome. (date-range / time / time-range pickers still to migrate.) - Shared segment state layer — new field/field-segment-state.css selected by a `data-field-segment` marker (date / time / color fields): one hover (editable) + active (focus) rule. Fixes a specificity bug where the hover selector out-ranked :focus, so hovering the active segment downgraded its highlight. - Segment fixed width — min-inline-size: calc(2ch + 2*segment-px) + font-variant-numeric: tabular-nums so a segment never reflows when its value changes 1<->2 digits or between glyphs (no horizontal jitter). date-field done. - date-range-picker trigger uses the same calendar icon as date-picker (was a distinct calendar-range glyph). - Generated element ids use the framework-neutral `uix-` prefix instead of the layer name `soma-` (createId / useId + announce / menubar inline ids) — the doctrine that bans data-soma-* / --soma-* keeps a layer name out of ids too. - field.css: label + helper text scale one step below the control (font-relative calc); optional-indicator weight to regular. Foundation tokens added: --field-segment-hover-bg, --field-control-trigger-hover-bg. Verified in-browser by measurement (date-field segments: states + no-jitter; date-picker trigger: flush + uniform hover). check = 58 baseline (0 new); recipe-css-contract clean (only pre-existing palabras WIP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
8f84b36f6d |
feat(eidos): field-family font-scaling coherence (input 1:1 + label step-down)
- Field font scale: input 1:1 (size N -> --font-size-N, md now 16px), label one step below (10/12/14/16/18). Recipes: field, spin-field, date/time/color-field, search-field, password-field, select, editable, tags-input. Segmented-field labels via CSS calc; generic Field label via label-font-size tokens. - Picker triggers (date/time/range): box now scales off the local --_X-field-height + font:inherit so the 1em icon follows the field font; added the missing xs size variant to time-picker + time-range-picker. - color-field/time-field commit-set fires + sounds on increment (compare by ColorValue.hex; time-field passes fallbackTarget for the double-registered 'input' part). - color-field format-select: reverted the segment-active tint back to its focus ring. - Docs: THEMING.md field font rule; COMPONENT_COHERENCE_AUDIT.md. words / palabras / chronos untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
dc62a0268b |
feat(eidos): canonize theming scales + tokenize magic numbers
Audit-driven canonization of the eidos theming layer: a canonical scale that components bypass with literals drifts into N variants. Every axis is now retunable-by-theme AND consumed via token, never a bare literal. Bloque A/B (scales): --blur-* (Tailwind-aligned, depth planes consume it); inner-shadow --shadow-inset-* (mode-aware) for the recessed plane; gradient angle/named tokens; --breakpoint-* sourced from ActiveDom (runtime, dev-settable) + container queries; opacity dual numeric+semantic scale; border-width none/thin/medium/thick/heavy (adds real 3px); tracking caps/widest. Fase 7: 3 size archetypes documented + coherence guard (no px/rem in recipe font-size/icon-size). Bloque C (magic numbers): z-index 80/50/99 (combobox/nav-menu/drag-drop) -> recipe content-z/preview-z tokens (overlay micro-band soma mirrors); on-scale durations -> var(--duration-*) (dialog fast/slow, card slow); 12 single border/ring widths -> var(--border-width-*), value-preserving. Off-scale kept only as justified recipe tokens (continuous spinner/loading periods, sub-fast press). Self-contained dimensional scales (avatar ring, ring-thickness) left whole. Docs: THEMING.md SS35 + SS6 + SS29, THEMING_NOTES, README, TSC. Excludes words/palabras/chronos (active dev tracks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
4ff9f0d277 |
feat(motion): modo (c) — composición sema + motion (opt-out visual en eidos)
Un componente coordinado puede componer la cascada (motion) con sonido/háptico
(sema) en el mismo evento. El cabo: el meta-canal visual de sema estampa
data-event-* siempre que el evento tenga algún canal (channels != []), así que
channels:['sound'] arrastraba la firma visual genérica (present-rise), que pelea
con la cascada.
Solución EN MOTION (sema intacto): eidos neutraliza su PROPIA firma visual sobre
las superficies coordinadas. render-css.ts > renderCoordinatedPresetRules emite
`[data-animation-style='cascade-X'][data-event-phase='active'] { animation: none
!important }`. La firma usa `animation` -> muere; la cascada es `transition` ->
sobrevive. Aditivo: inerte hasta que un coordinado dispare un evento sema.
Ejemplo: Reveal pasa a modo (c) — open y close declaran channels:['sound']
(emerge suena, pitch 600) + expression:'family-default'; suena al abrir y al
cerrar a la vez que la cascada, sin pelea visual. Rail queda como modo (b) puro.
Layout de /temas/animations con events:{ sound: true }; la demo /reveal explica
el modo (c).
Honestidad: la versión inicial del RFC Apéndice B afirmaba que el modo (c)
componía "gratis" — falso (verificado en engine.ts/visual.ts). Corregido: el
Apéndice B documenta ahora el acoplamiento real y el opt-out en motion.
Tests: eidos/motion 25/25 (neutralización) · morfo 70/70 · morfo:vocabulary
limpio. Audio verificado en navegador (suena al abrir y cerrar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
48b183672a |
feat(motion): servicio de coordinación de presencia (M1–M6)
Servicio de motion cross-layer: morfo declara superficies animables +
coordinación; soma coordina presencia/lifecycle; eidos posee lo visual;
arts/motion ejecuta por-nodo. RFC en eidos/MOTION_SERVICE_RFC.md.
- M1 — contrato MorfoPart.animation (types/schema/compile); children
como { enter?, exit? }.
- M2 — PresenceGroup (soma/layers/presence-group.ts), rune-free;
Presence.group descubre el coordinador por context.
- M3 — exit con retención de DOM (§8.1).
- M4 — interrupción/reversa (§8.3): token de generación + motion.cancel
en flip + toHandle resuelve finished en cancel (sin AbortError suelto).
- M5 — prop `animation` enrutada a las parts surface:true del morfo
compilado (routeAnimation); Panel/Item emiten data-animation-style.
- M6 — stagger auto-derivado del orden de registro (--motion-stagger-*,
inversa en exit) + presets coordinados en la librería de eidos
(MotionConfig.coordinated; cascade-slide/-fade/-scale) que reaccionan
a data-starting/ending-style, NO a data-state.
Helper Coordination (soma/layers/coordination.ts) extraído y validado
por DOS consumidores reales: Reveal (raíz virtual + Panel owner) y Rail
(raíz=owner, together). Demos en /temas/animations/{reveal,rail,
presence-group}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
4 months ago |
|
|
ded673ad0f |
fix(eidos): unify input focus into a parameterised two-ring model
Each input drew its focus ring differently — the foundation [data-archetype] :focus-visible ring landing on the <input>, ad-hoc per-component input rings (css-field/number-field), the textarea's own color-mix — so editing showed a DOUBLE frame (inner + outer), inconsistent across components. Unify into one theme-parameterised two-ring model: - New --focus-ring-inner-width (0 by default), folded into --focus-ring and every field *-focus-shadow: inner inset ring + outer ring. 0 = single outer frame; raise it in a theme to show the inner line everywhere at once. - Foundation [data-archetype]:focus-visible now excludes form-internal elements (:not(input):not(textarea):not(select):not([data-archetype='segment'])) — their focus is shown by the wrapping control. - Removed the ad-hoc input rings (css-field/number-field); textarea uses var(--focus-ring). - Cleaned the now-dead per-data-color _accent-* cascade in css-field/number-field (recorded as backlog in THEMING.md §32). Verified with real keyboard on field/css-field/number-field/date-field/textarea: single outer frame. Also includes the earlier textarea `resize` fix (morfo data-resize values + soma stops inlining resize + eidos materialises it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
95be3b6cb1 |
perf(eidos): prune base.css to role-referenced scales (Phase 5) — −18 KB gz
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>
|
4 months ago |
|
|
7255a52a0a |
feat(eidos): shape Fase 3 — eventful corner morph (the differentiator)
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>
|
4 months ago |
|
|
e04c098787 |
feat(eidos): shape Fase 2 — concentric nested-radius harmony
`--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> |
4 months ago |
|
|
7e26608ee0 |
feat(eidos): shape engine — Fase 1 (continuity + families) + SHAPE_ENGINE_RFC
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>
|
4 months ago |
|
|
6998c5bde2 |
feat(eidos): depth Fase 4 — atmosphere (frost) + applyDepth runtime builder
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>
|
4 months ago |
|
|
0e0fbc05d7 |
feat(eidos): depth Fase 2 (oklab rim halo) + reference-grade /temas/profundidad
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> |
4 months ago |
|
|
290f5d0c12 |
feat(eidos): depth engine — Fase 1 (plano unificado) + DEPTH_ENGINE_RFC
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>
|
4 months ago |
|
|
4fe2ee27df |
feat(eidos): close typography hygiene — semantic tokens config-driven + font preloads
#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> |
4 months ago |
|
|
6a848b50e3 |
feat(eidos): wire variable-font axes (weight range + optical sizing)
`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>
|
4 months ago |
|
|
ecc514951f |
feat(eidos): typography component props (Phase 3b) — wrap/tracking/leading/numeric/measure
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>
|
4 months ago |
|
|
d85c2fd376 |
feat(eidos): typography scales — tracking/leading/features/measure + optical tracking (Phase 3a)
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>
|
4 months ago |
|
|
f53299d42b |
feat(eidos): config-driven @font-face (Phase 2) — fonts as theme data (next/font model)
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>
|
4 months ago |
|
|
667aa49331 |
feat(eidos): fluid typography engine (Phase 1) — Utopia clamp, rem, fluid headings
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>
|
4 months ago |
|
|
afa15aea59 |
chore(eidos): close theming-engine backlog — P3-5 guard + P3-8 API tidy + triage
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> |
4 months ago |
|
|
21b2329a66 |
feat(eidos): prefers-contrast: more — stronger neutral chrome a11y (P3-2 follow-up)
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> |
4 months ago |
|
|
4eab3306b8 |
feat(eidos): forced-colors focus a11y (P3-2) + role border ramp 6->7 (P3-3)
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>
|
4 months ago |
|
|
909ab7f944 |
feat(eidos): wide-gamut OKLCH output, default-on (RFC Phase 2, strategy A)
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>
|
4 months ago |
|
|
8e3221f158 |
feat(color): APCA-driven on-solid contrast pick in render-css (RFC Phase 1)
First consumer of uix.color. The theme generator's on-solid text pick (white vs dark) now decides by APCA (|Lc| >= 60) instead of WCAG 2 (< 3:1), with a WCAG 2 ratio kept as a conservative cross-check — white must clear BOTH or the contrast slot flips to onSolidContrast. APCA is accurate in the mid-tones where WCAG 2 mis-estimates (the risk=orange case). Reproduces the documented base behavior (only risk flips) via a better metric; generated/base.css unchanged (the pick lives in the runtime theme block). - render-css: import apcaLc / oklchToGammaRgb / safeParseColor / wcagContrastRatio from $color; replace the local WCAG pick; drop the now-orphaned local wcagRelativeLuminance + wcagContrastRatio. - color: add safeParseColor (null instead of throw for var()/color-mix theme values the engine can't introspect). - wire $color alias (vite.config.ts + svelte.config.js + CLAUDE.md). Verified: color 20/20, eidos 162/165 (3 pre-existing words failures), active-eidos-config contrast asserts pass, npm run check 0 new errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
4 months ago |
|
|
be6bd540aa |
feat(motion): motion engine as arts/motion service + F6 tokens + F7 typegen
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>
|
4 months ago |
|
|
152d2ad960 |
Theming: Radix-parity palette + intent auto-derivation + color-model docs
Two-level color model settled (THEMING.md section 25), replacing the anchor RFC:
the palette is the source (scales, directly usable, designable); hierarchy roles
alias scales explicitly; intents auto-derive from the palette by the book's
canonical convention.
- Palette library expanded 12 -> 31 scales at Radix Colors parity (exact values):
radix-scales.ts (19 added: mauve/sage/olive/sand/tomato/ruby/crimson/plum/
violet/iris/indigo/jade/grass/brown/sky/mint/lime/gold/bronze) spread into
base.ts. Each directly usable as --scale-{name}-{step}.
- Intent auto-derivation: CANONICAL_INTENT_SCALES (neutral->gray, affirm->teal,
fulfill->green, risk->amber, threat->red, loss->plum) + completeColorRoleMap.
Intents omitted from a theme role map fill from the convention (identity =
step 9); slots derive normally; override optional. ColorRoleMap: hierarchy
required, intents optional.
- Validation: hierarchy roles required; omitted intents validate the canonical
scale exists in the palette.
- index: export ScalingKey / SCALING_KEYS.
- docs: THEMING.md section 25 (full color model + decisions), section 21 marked
resolved, COLOR_MODEL_RFC resolved (anchor rejected).
- test: base library asserts 31 scales x 12 steps.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser (intents auto-derive: affirm=teal #0E9384, risk=amber #DC6803,
loss=plum #7A3AAD at step 9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
66ce4f354d |
Theming: on-solid contrast by luminance (P2-2) + translucent role surfaces (P2-4)
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>
|
4 months ago |
|
|
bceaef41e0 |
Theming: add `scaling` zoom axis (Radix parity), separate from density
Introduce a global zoom axis independent of density, in parity with Radix
Themes' `scaling` (90/95/100/105/110%). Scaling zooms px metrics INCLUDING
typography (font-size, icon-size, space, control-height); density only moves
layout rhythm + control height and leaves text fixed. The two axes compose
multiplicatively.
- config-types: SCALING_KEYS / ScalingKey / DEFAULT_SCALING; DensityPrimitiveSet
drops the dead `scale` + `contentScale` (kept spaceScale, controlScale).
- primitives/static: STATIC_SCALING (0.9..1.1).
- render-css: appendScaledMetricDeclarations wraps metrics in
calc(<raw>[ * var(--density-x-scale)] * var(--scaling)); appendScalingDeclarations
emits --scaling-{key} + --scaling default; renderScalingBlocks emits
[data-scaling] blocks. line-height/radius/border/shadow excluded.
- config + contract: prune the removed density scalars.
- active-eidos: `scaling` / `scalingSource` options, getScaling() on the
preference source, data-scaling projection + dispose cleanup.
- docs: THEMING.md section 23 + 20.1 reconcile; README density/scaling; SCALING_RFC.md.
Verified: npm run check (0 new errors), vitest eidos (0 new regressions),
browser cascade at 90/100/110 scales font/space/control x0.9/x1.1 and leaves
radius/border fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
4 months ago |
|
|
98d6799ad0 |
Theming: generate alpha scales via compositing inverse (P1-1)
Replace the synthetic step-9-at-opacity alpha ramp with a real compositing inverse: each --scale-X-aN is the translucent color that, painted over the scale's background (white for light scales, black for dark, by step-1 luminance), reproduces the opaque solid step N (Radix model). Computed in render-css.ts for ANY theme (base + custom), so alpha is consistent-by-construction with the solid scale and cannot drift. alphaScales opt-in still wins verbatim; non-hex solids fall back to the legacy color-mix ramp. Verified: --scale-purple-a9 = rgb(92 0 173 / 0.6941) over white = #8e4ec6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
ef9f589d4c |
docs(theming): clarify synthetic alpha-scale model + alphaScales opt-in (P1-1)
Alpha steps (--scale-*-aN) are unconsumed today (only a test + README ref). The step-9-at-opacity synthesis is a valid 'accent-at-opacity' ramp default, not a bug; it is not a Radix-style reproduction of the solid scale. Documented the model, its limitation, and the already-wired alphaScales opt-in in appendColorAlphaScaleDeclarations. A build-time compositing-inverse generator is deferred until alpha is actually consumed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
218ccfcc5e |
Theming: untitled-ui demo theme, engine fixes, audit + P0/P1 fixes
- New untitled-ui demo theme + theming showcase route (web/routes/temas) - Engine: density tokens scale via calc(var(--density-*-scale)); contrast slot defaults to on-solid - Eidos theming audit (THEMING_AUDIT_2026-06-01.md) + two-tier color-model RFC (COLOR_MODEL_RFC.md) - P0: replace phantom foundation tokens in recipes + component CSS — shadows -> semantic scale (subtle/raised/overlay), add --space-7, --border-width-strong -> thick; neutral solid contrast -> step 12 (AA in light + dark) - P1: validate hex color values + per-theme semantic completeness; regression tests for contrast, density, dark output, and TSC multi-part/composition emission Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
4 months ago |
|
|
9c6375c96e |
feat(eidos): TSC v2.2 (parts + composition) + universal theming coverage
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>
|
5 months ago |
|
|
b504c1c4b7 |
feat(eidos foundation): vertebrate typography via named-style aliases + R-2.7
Single source of truth for typography values that the recipe layer
consumes. The foundation aliases `--font-ui` and `--leading-ui` (read
by ~30 recipe tokens in `lib/recipes/base.ts`) now derive from the
canonical `label` named style instead of carrying duplicate literals:
--style-label-font-family: var(--font-family-primary);
--style-label-line-height: 1.25;
--font-ui: var(--style-label-font-family, var(--font-family-primary));
--leading-ui: var(--style-label-line-height, 1.25);
Chain: typography.ts styles → --style-{name}-* → --leading-ui / --font-ui
→ recipe tokens → component CSS. Editing
`STATIC_TYPOGRAPHY.styles.label.lineHeight` now propagates to every
recipe in one go.
Why not push recipes to consume `--style-{name}-*` directly:
- t-shirt sizes (xs/sm/md/lg/xl) don't map to four semantic buckets
- per-component matices (description/caption/hint) need their own
color / weight / letter-spacing
- ref libraries (Radix Themes, Mantine, MUI, Chakra) all keep
numerical scale for component internals; semantic layer is only for
user-facing typography primitives (`<Text variant="body2">`)
Audit rule R-2.7 (warn): detects literal font-size / font-weight /
line-height / letter-spacing in eidos component CSS. Escape valves:
var(...), numeric identities (0/0px/1), keywords (inherit/initial/
unset), or trailing `/* literal: <reason> */` comment. Current run
flags 6 components with letter-spacing/font-size literals (all
intentional micro-tracking and em-relative; can be annotated case by
case).
Documentation:
- src/uix/eidos/README.md § "Vertebración tipográfica" — two-layer
architecture rationale, alias chain diagram, comparison vs Radix
Themes / Chakra / Mantine / MUI, escape valves
- web/routes/uix/lib/COMPONENT_AUDIT_GUIDE.md § 4.11 — pointer to
R-2.7 + cross-link to the foundation doc
Verified end-to-end in browser at /uix/components/field:
--font-ui → 'Instrument Sans', system-ui, sans-serif
--style-label-font-family → 'Instrument Sans', system-ui, sans-serif
--leading-ui → 1.25
--style-label-line-height → 1.25
computed [data-field-label].line-height → 17.5px (= 14 × 1.25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |
|
|
015bd761bd |
feat(eidos foundation): emit --style-{name}-* tokens for named typography styles
`STATIC_TYPOGRAPHY.styles` already defined 11 named typography styles
(hero, h1..h6, body, prose, label, caption, code) but the renderer
only emitted families / sizes / weights — the style entries lived
as data with no CSS reflection.
Extend `appendTypographyDeclarations` so each named style emits:
- `--style-{name}-font-family` → `var(--font-family-{family})`
- `--style-{name}-font-size` → `var(--font-size-{size})`
- `--style-{name}-line-height` → literal or `var(--font-line-height-{size})`
- `--style-{name}-letter-spacing` → literal or `var(--font-letter-spacing-{size})`
- `--style-{name}-font-weight` → `var(--font-weight-{weight})` (or numeric)
- `--style-{name}-color` → `var(--color-{dot.path → dash-path})`
Responsive sizes (e.g. `hero.size = { base: 'xxl', md: 'xxxl' }`)
emit the base value into `:root` and per-breakpoint
`@media (min-width: …)` blocks that override the same variables.
Breakpoint thresholds match `$libs/dom/responsive` (sm: 480, md: 768,
lg: 1024, xl: 1280, xxl: 1536).
`renderStaticCss` now iterates `STYLE_BREAKPOINT_ORDER` after the
main `:root` block and appends one media-query block per breakpoint
that has at least one responsive override. The `indentBlock` helper
preserves indentation inside the wrapper.
`base.css` regenerated. svelte-check 0 errors, `npm run component:audit`
81 / 81 PASS unchanged. This is foundation-only; no components consume
the new tokens yet — that lands in the typography port (Text + Heading
+ Display + Code + CodeBlock + Kbd + Mark + Highlight + Link).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
5 months ago |