Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado).
- M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6
son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos;
`animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana
type-safety vía el registry augmentable `MotionCoordinatedPresets` +
`CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo
tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale`
por declaration-merging — espejo de `EidosMotionPresets`.
- Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()`
(posición + velocidad por propiedad) y la reinyecta en la fase inversa vía
`MotionContext.handoff`: el `spring` continúa desde la posición/velocidad
actual en vez de reiniciar desde el `from`. Orquestado enteramente en el
engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un
run en vuelo cuando el caller no pre-cancela): cubre el camino island
(overlay-spring) y elimina un apilamiento preexistente. La reversa de los
coordinados (transiciones CSS) ya era fluida.
- Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los
presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la
página); el selector cambia `animation` en vivo y cada Button compone su
press (firma sema) con la cascada (motion coordinado).
- Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma
`--event` / state-preset `--state` / coordinado, con diagrama + ejemplo
Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y
cross-link del tercer eje desde eidos-motion.md.
Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad).
svelte-check: 0 errores en mis archivos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Servicio de motion cross-layer: morfo declara superficies animables +
coordinación; soma coordina presencia/lifecycle; eidos posee lo visual;
arts/motion ejecuta por-nodo. RFC en eidos/MOTION_SERVICE_RFC.md.
- M1 — contrato MorfoPart.animation (types/schema/compile); children
como { enter?, exit? }.
- M2 — PresenceGroup (soma/layers/presence-group.ts), rune-free;
Presence.group descubre el coordinador por context.
- M3 — exit con retención de DOM (§8.1).
- M4 — interrupción/reversa (§8.3): token de generación + motion.cancel
en flip + toHandle resuelve finished en cancel (sin AbortError suelto).
- M5 — prop `animation` enrutada a las parts surface:true del morfo
compilado (routeAnimation); Panel/Item emiten data-animation-style.
- M6 — stagger auto-derivado del orden de registro (--motion-stagger-*,
inversa en exit) + presets coordinados en la librería de eidos
(MotionConfig.coordinated; cascade-slide/-fade/-scale) que reaccionan
a data-starting/ending-style, NO a data-state.
Helper Coordination (soma/layers/coordination.ts) extraído y validado
por DOS consumidores reales: Reveal (raíz virtual + Panel owner) y Rail
(raíz=owner, together). Demos en /temas/animations/{reveal,rail,
presence-group}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New component-authoring rule, per request: when building a component (or its
demo, or any UI), compose the framework's existing soma/eidos components rather
than re-implementing primitives inline. If a needed building block does not exist
as a framework component, flag the gap so it gets built as a proper reusable
component (its own morfo + soma + eidos) and then composed — never an ad-hoc
reinvention that drifts from the system. The picker family (A27) is the canonical
example of composing existing components.
Full rule in COMPONENT_GUIDE "Before You Start" §4; one-line pointer added to
CLAUDE.md Key Conventions so every session loads it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
THEMING §5 documented "prominent icons (cards): the icon does NOT follow its
title, it bumps to xl/xxl" — the pre-continuation approach. The final decision
and the shipped code reject it: the radio-cards recipe sets icon-size to
sm/sm/md/md/lg (16/16/18/18/20, following the title) with an explicit comment
("the icon follows the title; a bigger card icon comes from a bigger title,
never from inflating the icon"). The doc had drifted from the code.
Corrected §5 to state the implemented rule: cards/titles icons follow the
title's font; prominence comes from a bigger title, never an inflated icon.
Found while verifying that CONTINUE-icon-type-scale.md's decisions actually
match THEMING before retiring it — they did not, on this point.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The bullet linked THEMING_AUDIT_2026-05-27.md, which does not exist (the
2026-06-01 audit itself flags the link as broken). Repointed to the audit that
does exist, THEMING_AUDIT_2026-06-01.md.
- eidos-motion.md was described as "propuesta motion (deferred, partially
superseded by TSC)" — the same stale contradiction fixed in §14. It is the
implemented two-moment motion system (F1–F7). Corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the renumber-safe split of THEMING.md.
- §8 "añadir componente" + §9 "definir theme" -> src/uix/eidos/THEMING_GUIDE.md
(E4 authoring guides).
- §15 "comparación con librerías" + §17 "FAQ — decisiones polémicas" ->
src/uix/eidos/THEMING_NOTES.md (E3 comparison + rationale).
THEMING.md keeps numbered pointer-stubs at §8/§9/§15/§17; §16 anti-patterns,
the ## Referencias section, and the dated §20-34 changelog stay in place. All
34 sections and every "THEMING §N" citation across the corpus + code survive.
THEMING 2571 -> 1930 lines across the three commits; it is now a leaner E1
layer reference with TSC (E2), the guides (E4) and the notes (E3) as siblings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First piece of the THEMING split, using the renumber-safe stub approach so the
many "THEMING §N" citations across the corpus + code keep resolving.
§7 (Token Scope Contract) and §18 (universal coverage, v2.2) move to a new
src/uix/eidos/TSC.md — the eidos visual canon (E2), with CANON.md-style
frontmatter. THEMING.md keeps numbered pointer-stubs at §7/§18, so section
numbers (and therefore §23/§25/§26/§27/§28 citations) are untouched. 2550 -> 2271
lines; 34 headers intact, TOC anchors still resolve.
Remaining split pieces (own commits): guides §8/§9 -> E4, comparison §15 +
FAQ §17 -> E3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
THEMING §14 described motion as "deferred / data-motion-ref does not exist /
superseded by the TSC event:* scope". eidos-motion.md says the opposite: the
two-moment model is implemented (F1-F7, 2026-06-04), the engine was relocated to
arts/motion as a service, and it is the event:* scope that is now obsolete. §14
was a stale snapshot contradicting the canonical motion doc.
Rewrite §14 as a lean pointer to eidos-motion.md (canonical, current) with a
status note flagging the obsolete data-motion-ref / event:* framing, and keep
the theming-specific bit (the --motion-scale-lift "pickup" token). Renumber-safe
— §14 stays §14, so the §N citations across the corpus are untouched.
The full stratum-split of THEMING (TSC -> E2, guides -> E4, comparison/FAQ ->
E3) is deferred: it renumbers sections that are cited by number across the
corpus + code, so it needs a dedicated pass with a citation sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "3 duplicate checklists" diagnosis was wrong. There are two legitimate,
complementary artifacts plus one redundant summary:
- COMPONENT_GUIDE.md — the build checklist (ordered authoring steps 1-40 +
rationale rules A1-A37). How to build a component.
- COMPONENT_COMPLETION_CHECKLIST.md — the acceptance matrix (machine-audited
rules with severity/applicability, bound to scripts/component-audit.ts). When
a component counts as done. Merging it into the guide would break the
script binding.
Consolidation = one source per concern, not one file. Both docs now carry a
header that states their role and cross-links the other as the complement.
soma/README §9 (the only real copy) is reduced to a pointer at both; it keeps
no checklist of its own.
Flagged out of scope: COMPONENT_COMPLETION_CHECKLIST M-3.3 still says "7
families" (canon is 8 with delegate).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two docs overlapped ~80% (purpose, layers, runtime parts, Soma class,
data-* contracts, naming, checklist). Give each a single role:
- README (1057→400 L): onboarding + authoring guide — purpose, membership
criterion, morfo contract (slim), component + composition pattern, new-
component checklist, and a "deep reference" map pointing into the
architecture doc. Inventory now points at the live directory tree instead
of a hardcoded (drifting) list.
- SOMA_ARCHITECTURE (969→1043 L): the complete deep reference. Absorbed the
few reference bits that only lived in the README — mergeProps, KEYS /
directional keys, focus / roving / scroll-lock, boolean attr helpers, the
internal context() helper and the reactive type-aliases — into a new
§8.bis. Trimmed its purpose section (now owned by the README).
README sections renumbered contiguously; cross-references verified, no other
doc links soma/README by section number.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 3 (E1), per the chosen scope. active_architecture.md should read as a
timeless architecture reference:
- §0 "Handoff 2026-05-14" → "## 0. Contratos mínimos por módulo": dropped the
session framing (estado acordado, open/closed questions, P1/P2/P3 findings,
frontend context); kept the minimum-contracts table as reference. The
ownership/degradation rules it stated are now timeless in active-uix/README.
- §10 "Estado actual (2026-05-17)" → a one-line §10 pointing at docs/process/.
The dated status snapshot moved verbatim to
docs/process/active-architecture-snapshot-2026-05.md (kept for traceability;
several of its "pendientes" are since done). Section numbering stays continuous.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 3 (E1). After Phase 1b pulled the embedded "Handoff 2026-05-14" out, the
doc still read as session process ("tras cerrar P1", "punto crítico para
mañana", a "Regla de trabajo vigente" section). Rewrote it as a clean layer
reference: what active-uix is, the two boot paths (createActiveUix /
attachActiveUix), the minimum-contracts table (linked to the executable
contracts.ts — fixed the path: it lives in src/uix/, not active-uix/), the
ownership + degradation rules as timeless invariants, and the shell-boot example.
Dropped the process framing. Spanish kept (consistent with its sibling
active_architecture.md; coordinated EN migration is a later pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 (cont.). The docs that still re-transcribe the semantic vocabulary now
name CANON.md as the single source of truth, so a reader/agent knows which copy
is authoritative when they drift:
- src/uix/README §7 reading order: CANON.md added as entry 0.
- sema/README: §intent-policy and §verbs point at CANON §4/§6 + the code.
- active_architecture §6 "Vocabularies cross-layer": points at CANON.
- morfo/README §5.5: semantic vocabulary now cited as canonical in CANON
(was pointing at GUIA).
- GUIA §1: marked as the Spanish narrative; CANON (EN) is authoritative.
Conservative on purpose: the existing lists are kept, not deleted — physically
pruning each copy down to a pointer is a separate, optional pass (it is editorial
and would remove content). Establishing the authority + the backlink already
stops silent drift, since every copy now declares who wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The §10 "Estado actual" snapshot still said "7 familias" and "66 componentes".
Per the user: the doc must state the current canon, not a frozen historical
figure — delegate makes it 8 families, and there are 115 morfos. The rest of the
dated §10 block (its caducated "Pendiente" list) is reworked / moved to
docs/process/ in the architecture pass. Docs-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1b of the doc-corpus cleanup: reference docs should read as timeless.
Pulled the inline "Handoff 2026-05-1x" blocks out of the six layer READMEs
(README, morfo, sema, soma, eidos, active-uix) into
docs/process/handoffs-2026-05.md, with docs/process/README.md explaining that
process/ holds ephemeral, time-stamped artifacts (hand-offs, audits, plans) —
never the source of truth.
README.md keeps its useful "visión de conjunto" pointer to active_architecture
+ GUIA, minus the hand-off framing.
Deferred on purpose: active_architecture §0 (embeds the "contratos mínimos"
table other docs link to — separated during the architecture pass) and
CLAUDE.md's ~10 session hand-offs (it is the per-session instructions file;
its own step). Docs-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1a of the doc-corpus cleanup: align the framework docs against the code
(the source of truth) where they had silently gone stale.
- 7 → 8 sema families (delegate was missing) in README, active_architecture,
morfo/README and GUIA_IMPLEMENTACION.
- sema/README's intent-policy section still showed the retired flat
`intentPolicy` shape; replaced with the real two-axis SEMA_FAMILY_POLICY
(intentRequirement + intentGuidance). morfo/README's "valenced families
require intent" rule corrected: only commit + signal are
intentRequirement:'required'; the rest make intent optional.
- SEMA_VERBS lists realigned to verbs.ts (adds unselect/apply/partial/block/
move/upload, signal.inform, handle.zoom, sustain.upload, + the delegate family).
- DATA_ATTRS.md: retired the misleading "Terra Data Attributes Contracts"
header (auto-generated from a now-deleted path, redundant with the morfos)
→ marked LEGACY pointing at the morfos as the source of truth.
Dated status blocks (active_architecture §10 "66 componentes / 7 familias")
are deferred to the process-extraction step. Docs-only; no code touched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`timers.schedule/interval/scheduleAt` now accept `key: null` — the engine mints a
unique `auto:N` key via an internal counter (with a `has()` collision guard) and
the caller drives the timer through the returned handle. Naming a timer stays a
feature (cancel-by-name, `replace`, dedup, scoped `cancelAll`), not a tax on every
call. Anonymous timers live in the `auto` scope. +6 engine tests; the race-safety
path is unchanged (the key is resolved before the (id,key,version) machinery).
Uses it to close the raw `setTimeout`/`setInterval` and direct `getComputedStyle`
violations the audit flagged — instead of waiving the uix.timers / $adom rules:
- code-block, relative-time: raw setTimeout/setInterval -> eidos.timers (key:null)
- cropper: raw setTimeout throttle gate -> uix.timers (key:null)
- textarea, s-text, s-text-virtual-list: getComputedStyle(el) -> dom.getWindow(el)
The earlier audit call that these were "nil functional gain" was wrong: the rule
is the rule, and the friction (manual per-instance key invention) was removed at
the framework level rather than used as grounds to skip the rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removed orphan deprecated type aliases with no real consumers: sema `IntentPolicy`
(legacy single-axis policy, superseded by intentRequirement + intentGuidance) and
its re-export; eidos `RecipeTokenObject` (→ RecipeTokenDeclaration) and the comment
that named it. Left `IntentExpectedFamily` and `SemaEventLabel` — both are still
consumed by public morfo types / sema functions, so removing them is an API rename,
not dead-code cleanup (documented in SYSTEM-AUDIT).
Translated 3 non-quote Spanish comments to English (sema/chans/types,
sema/projection/index, morfo/year-grid) per the English-comments rule. Book quotes
stay in Spanish (they're the canonical source).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9 soma component READMEs imported from the removed $lib/util/dias and
$lib/util/colors aliases → $libs/days / $libs/color (matches what the code
actually imports). morfo/README.md said "All 66 morfos" (real count ~116) →
reworded to "Every morfo in the codebase" to drop the brittle number.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the icon↔type correlation (option 1): the icon follows the font the
component actually shows, no font step-up. button icon-size-sm: icon-xs→icon-sm
(follows its font); radio-cards icons follow the card title (16/16/18/18/20,
were 18-32 hand-inflated). STATIC_SIZE's font step-down stays — control-density
convention, not a bug.
Fix pre-existing button bug: the Icon's inline `width: var(--icon-size)` beat
`[data-button] svg { inline-size }`, pinning the button icon at 18px regardless
of size. button now feeds `--icon-size` from `--_button-icon-size`, so the icon
follows the button size (14/16/16/18/20). Verified in browser.
CONTINUE-icon-type-scale.md updated with the resolution + a pending audit of the
same Icon-sizing bug in other components.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Icon scale large steps now track the type scale (validated by eye in a study
harness at /uix/icon-scale-study): --icon-size-xl 24→32, --icon-size-xxl 48→52,
so the icon keeps its optical weight beside its paired text. 0 impact — nothing
consumed those steps yet.
button: icon-size per size references var(--icon-size-X) (mapped by value → same
rendered px, now density-scalable) instead of hardcoded px.
Docs: THEMING.md §5 documents the type↔icon correlation; CONTINUE-icon-type-scale.md
captures the pending 1:1 work (size = font = icon per name, dropping the font
step-down) for the next session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Size was applied via `var(--qr-code-size-${resolvedSize})` (interpolated), so
the literal token names never appeared in source and the recipe-css-contract
test flagged --qr-code-size-{xs..xl} as orphaned. Use an explicit literal map so
each name appears verbatim; behaviour unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recipe defined per-color tonal tokens inconsistently — only fulfill/loss had
`track`/`border`, so soft/surface/outline rendered with no background or border
for the other 6 colors (primary/secondary/neutral/affirm/risk/threat):
`palette-track`/`palette-border` referenced undefined `--button-{color}-track`/
`-border`, which collapsed the background/border declarations. Added the missing
track+border to all 6 (mapped to `var(--color-{role}-track|border)` like
fulfill/loss); the recipe is now uniform (7 slots per color). Verified all 8
colors × soft/surface/outline in dark + light.
Demo: new "Gallery" tab — a 6 variants × 8 colors matrix of real <Button>s
(hierarchy via `color`, evaluative palette via `intent`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logo overlay rendered invisible. Inside a viewBox-scaled foreignObject a
fixed px padding (var(--space-1)) equals that many *modules*, overflowing the
cleared centre and collapsing the content box; inline-size/block-size also
don't size an <svg> element. Fix: proportional inline padding + physical
width/height. The logo now renders and still decodes at ECC H. Dropped the
now-orphan --qr-code-overlay-padding token + regenerated base.css.
Demo: content presets (URL / vCard / WiFi / unicode) prove the encoder takes
any UTF-8 string, not just URLs (each verified decoding via jsQR); logo on by
default; quiet-zone chips fixed to canonical [4, 8, 16] (was [2, …], below the
spec's 4-module minimum). README documents the quiet zone as modules, not
pixels — the canonical unit; for px layout spacing, pad the container in CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Own QR encoder in $libs/qr (ISO/IEC 18004: segment analysis →
Reed-Solomon over GF(256) → matrix layout → 8-mask penalty selection),
no npm dependency. Eidos <QrCode> renders the module matrix as one SVG
path with square/rounded/dots cell shapes; function patterns stay square
(via the encoder's `reserved` mask) so every shape scans. Optional logo
clears the centre + auto-boosts ECC to H. <QrCode.DownloadTrigger>
exports PNG/SVG, baking resolved colours into the clone first (a
serialized SVG loses the document's CSS custom properties). Fixed-tone
default colours for scannability; themeable via color/background.
morfo qr-code (scope: eidos; parts Provider/Pattern/Overlay/
DownloadTrigger) + 'path' added to MorfoElement; recipe with 11
--qr-code-* tokens; canonical demo + nav entry. 11 encoder unit tests;
verified end-to-end via jsQR round-trip + browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
orientation is now breakpoint-aware like size: `orientation={{ base:'vertical',
md:'horizontal' }}` resolves via Eidos.resolve and forwards a concrete value to
the soma (keyboard nav handles both axes). Documented in the README (new
"Responsive orientation" section + example) and the demo (a "responsive" chip +
note + API row) — the first consumer-facing example of the ResponsiveProp object
syntax in the repo. Browser-verified: flips at md (768px). check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hover rule out-specified the checked rule, so hovering a selected card
reverted it to the neutral (unselected) hover colour — visually undoing the
selection. Now the neutral hover excludes `[data-state='checked']`, and a
dedicated checked-hover rule keeps the active colour and just deepens it a
notch (border → --_radio-group-palette-solid-hover, tint 7% → 12%). Selection
state is never affected by hover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New eidos component built ON TOP of RadioGroup with zero duplicated
behaviour. RadioCards reuses RadioGroup's soma wholesale (selection,
roving focus, role=radio, native <form> via HiddenInput) and adds only
the card chrome: icon · title · description + an always-present corner
radio. Vertical, horizontal and grid layouts.
- morfo: scope:['eidos'], thin contract for presentation parts only
(Provider marker, Item-card, Indicator, Icon, Content, Title,
Description). The interactive contract + commit-select stay in
radio-group, reused via its soma. No new soma, no new sema.
- eidos: `radio-cards` recipe (card chrome) + wrappers + radio-cards.css.
radio-group.css is NOT loaded here, only its foundation tokens — the
card owns its visual, reusing --_radio-group-palette-* (inlined, since a
:root token referencing the palette is invalid-at-computed-value).
- demo: canonical structure (always-on stage + trace, tabs, chip/switch
controls, Untitled-UI example patterns) + nav entry + README.
check 0 · eidos-lint 0 invalid. Browser-verified: select (click + keyboard
roving), disabled, the three layouts, controls drive the preview.
Follow-up: icon/circle sizes are still px (mirroring radio-group's own size
tokens) — making the control-size scale density-aware is a separate,
family-wide change deferred per request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Batch 3 migrated both but the guard ceilings missed them, leaving the two
unprotected from drift. Pin their legit residuals (year-grid 2, file-upload
3). With this every migrated provider is guarded; the only component with
unpinned attr duplication is `words` (excluded from the rollout per request).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final 2 components of the rollout (words excluded per request). Both menu
siblings now source role/aria/data from the morfo via renderProps().
Audit total 213 → 158; both at 0 residual hits.
Unified the siblings' aria-boolean handling: aria-expanded/aria-checked
(morfo stateRef → aria-boolean string) resolve through renderProps, not a
soma boolean override. For checkbox/radio aria-checked the per-part
`checked` state source feeds the COMPUTED value — dropdown-menu's
effectiveChecked (group membership) and isChecked (value === group value)
— so renderProps emits the correct string. The migration agent had left
dropdown-menu's aria-checked as a boolean override reading effectiveChecked;
removing it without registering the state regressed a checked item to
aria-checked="false" — fixed by registering the computed source.
Tests assert the string aria-boolean shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source role/aria/data from the morfo via renderProps() across the
collection/menu families, removing the attrs the providers re-declared
("morfo declares, soma executes"). Audit total 369 → 213.
Migrated: select, listbox, carousel, tree-view, year-grid, grid-list,
tree-grid, table, file-upload, editable, combobox, command.
tree-view/table keep their per-node aria hardcoded on purpose: each tree
node / table cell computes its own aria-level/selected/rowindex/colindex/
sort, which a single renderProps can't resolve. The residual hits the
guard pins are all legit value overrides — computed aria-labels (carousel
"slide N", editable/file-upload triggers), per-instance dynamic values,
and computed refs (command aria-activedescendant/controls).
listbox/grid-list tests: aria booleans the morfo declares via stateRef
now resolve through renderProps as "true"/"false" strings (aria-boolean
mode) instead of the providers' hardcoded booleans — assert strings.
words excluded from this rollout per request (active dev track).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Source role/aria/data from the morfo via renderProps() across the
date/time/calendar/picker families, removing the attrs the providers
re-declared ("morfo declares, soma executes"). Audit total 468 → 369.
Migrated: field, date-field, time-field, color-picker, date-picker,
date-range-picker, time-picker, time-range-picker, calendar, month-grid,
range-calendar.
date-field/time-field keep the `segment` part hardcoded on purpose: it is
multi-archetype (editable segments are spinbuttons, literal separators
must NOT be) so a uniform renderProps would inject role into the literals.
The residual hits the guard now pins are all legit value overrides:
composite aria-labelledby/describedby (input), the deferred segment part,
and computed aria-labels (color-picker/calendar include the value/month).
field test: required-indicator applies aria-hidden via syncAttrs, so the
attr lives on the DOM (not the props object) — assert it via getAttribute.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Workflow-driven rollout of the morfo↔soma attr de-duplication. 16 providers
(textarea, pagination, pin-input, slider, tag-group, scroll-area, form,
search-field, drag-drop, stepper, tags-input, color-field, password-field,
link-preview, menubar, navigation-menu) now source role/aria/data from the
morfo via renderProps() instead of re-declaring them. 3 morfos gained
`ariaBoolean` on propRef-valued aria booleans (color-field, form, password-field).
Audit: 559 → 468 hits (−91). Provider tests updated to the renderProps shape:
ARIA booleans assert the string "true"/"false" (the correct ARIA shape, was a JS
boolean), and the few DOM-read assertions for parts that moved off `syncAttrs`
now read the props object (pagination). Parts still on `syncAttrs:true` keep
their imperative DOM application + tests. Each component's residue (legit value
overrides the morfo can't express) is pinned in the audit guard.
Verified centrally: 112 tests green, check clean, pagination spot-checked in the
browser (type/role/aria-label/data render correctly via props-spread).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`scripts/soma-attr-audit.ts` compiles every morfo and flags attrs a provider
hardcodes in its props getter that the morfo already declares with a resolvable
value (segmenting the provider by `runtime.part('<kebab>')` to map getter→part).
Measures the migration backlog and catches new drift.
Current backlog: 607 hits across 50 components (worst: words 49, dropdown/context
menus ~28, combobox/command/time-field ~24). The migrated spin-field family is
down to its legitimate residue — number-field 1, css-field 2 (formatted
aria-valuetext, parsed aria-valuenow — values the morfo can't express).
`soma-attr-audit.test.ts` pins each migrated component to its legit-override
ceiling so it can't regress to hardcoded attrs. Extend the ceiling map as more
components migrate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finishes the spin-field family. The 4 triggers + 2 scrubbers (number-field +
css-field) now spread renderProps() and add only soma-specific extras.
- Scrubbers: role + aria-hidden + data-spin-field-scrubber now morfo-sourced;
soma keeps data-scrubbing/disabled, cursor style, pointer handlers.
- Triggers: type + aria-label + data-spin-field + aria-disabled now morfo-
sourced. The trigger morfos' aria-disabled moves from literal-'true'+condition
to propRef('disabled') + ariaBoolean (the de-facto always-emit shape); each
trigger registers its own `disabled` source (includes atMax/atMin) so
renderProps resolves it. aria-label resolves from the morfo's
commonRef('buttons.increment/decrement') — the SAME idlangref the provider
used, so the string is unchanged ("Aumentar"/"Disminuir" in es).
- Dropped the now-orphaned NUMBER_FIELD_LANGS + boolToStr imports.
Verified in browser for all parts (type/aria-label/aria-disabled/role/aria-hidden
identical) + soma/morfo tests 95/95 (2 pre-existing palabras contract failures
only). The entire spin-field family is now morfo-sourced — zero re-declared attrs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the NumberField Input reference. CssField morfo's aria-required/disabled/
readonly/invalid gain `ariaBoolean`; the Input provider drops the hardcoded role +
data-spin-field-input + those four aria booleans — all now from renderProps().
Soma keeps the CSS-value-specific bits it genuinely owns: the parsed aria-valuenow,
soma-only aria-valuemin/max, aria-valuetext, native form attrs, handlers.
Verified in browser (role/aria/data from morfo, parsed aria-valuenow="16") +
soma/morfo tests 71/71. Both spin-field inputs are now fully morfo-sourced; the
triggers/scrubber + the other families remain on the backlog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`MorfoAriaEntry.ariaBoolean?: boolean` makes a propRef-valued ARIA attr compile
to aria-boolean mode (renders the "true"/"false" string), the same shape a
stateRef already gets. Without it, `aria-required` ← `required` prop compiled to
raw mode → renderProps() emitted a boolean (DOM-identical, but a different props
shape, and it forced the provider to keep stringifying).
- types/schema/compile wired for the flag.
- NumberField morfo: aria-required/disabled/readonly/invalid gain ariaBoolean.
- NumberField Input provider: dropped the 4 boolToStr overrides — role + every
aria + data-spin-field-input now come from renderProps(); soma keeps only the
formatted aria-valuetext, aria-describedby, native form attrs and handlers.
The Input is now fully morfo-sourced (zero re-declared attrs). Verified in
browser (aria-required="false" string, role/valuenow from morfo) + provider
tests 69/69. This is the reusable mechanism for de-duping aria booleans across
the rollout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundation for removing the morfo↔soma attribute duplication: providers that
compose attrs in their `props` getter were re-declaring role/aria/data that the
morfo already declares (a second source that drifts — the spin-field bug).
- runtime: new `SomaRuntimePart.renderProps()` — static identity + every
morfo-declared static/dynamic attr, resolved against THIS part's registered
sources. A part's getter spreads it and adds ONLY soma-specific extras
(handlers, formatted values, native attrs), instead of hardcoding role/aria.
- morfo: new `prop-defined` condition (`emitted iff prop !== undefined`) so an
optional numeric aria (aria-valuemin at min=0) emits correctly — `prop-truthy`
wrongly dropped 0. Wired through types, resolver, schema, compile dep-collect.
- NumberField Input migrated as the reference: registers value/min/max as part
sources, spreads `renderProps()`, keeps only soma extras. Removes the
hardcoded role + aria-valuenow/min/max + data-spin-field-input duplication.
ARIA booleans (aria-required/disabled/…) stay soma overrides for now — a
propRef-valued aria compiles to raw mode, so soma still stringifies them; a
future `v.ariaBool()` helper would let those resolve from the morfo too.
- COMPONENT_GUIDE: "Part props: read the morfo, don't re-declare it" doctrine.
Verified bit-for-bit in browser (role/aria/data identical incl. min=0 →
aria-valuemin="0") + provider tests 7/7. The survey found ~⅔ of components carry
this duplication (form controls 70%); this lands the pattern + the NumberField
Input reference. Family rollout is the documented backlog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField are the same visual (a bordered field + input +
increment/decrement triggers + scrubber, split/stacked layouts, sizes/
variants/colors, themeable glyphs); only their value model differs. They
were two cloned recipes + CSS that drifted — a refinement to one (square
flush buttons, divider, contrast) didn't reach the other.
Unify into ONE shared source (the toggle-group structural-identity pattern):
- New eidos/components/spin-field: recipe key `spin-field` (`--spin-field-*`)
+ spin-field.css with all the stepper-field rules, selecting `[data-spin-
field*]`. Loaded via the foundation @import in index.css.
- number-field + css-field morfos declare structural identity (`data-spin-
field*` presence attrs on each part). The Provider emits them via syncAttrs;
the sub-parts emit them in their soma `props` getter (number-field's soma
hardcodes sub-part attrs rather than syncing the morfo).
- Removed the `number-field` / `css-field` recipe keys; their CSS files are
now stubs. A theme tints one component by scoping `[data-number-field] {
--spin-field-… }`.
- css-field thereby adopts number-field's refined steppers (square, flush,
divider) — the drift fix the user asked for, now structural (no clone).
Verified bit-for-bit in browser: number-field identical to baseline (split
flush, stacked symmetric xs..xl, RTL, glyph token/children override);
css-field now square/flush/divider. eidos-lint invalid 0; recipe contract
passes (no orphans, loads-once); check + morfo:check clean for these.
Docs: THEMING sections 33 (glyph tokens now `--spin-field-*`) + 34 (the
shared layer); number-field / css-field READMEs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField steppers gain a `stacked` compact layout and
themeable default glyphs.
- Glyphs render from recipe tokens via `:empty::before` (split plus/minus,
stacked up/down triangles); passing children overrides per instance,
a theme overrides the token. 4 tokens per component, unicode-escaped.
- CssField gains the `steppers` prop (split | stacked); its stacked layout
mirrors NumberField (self-contained — the split layout left untouched).
- NumberField split: square buttons flush to the field edge with an inner
divider and higher hover/focus contrast (control-radius 0).
- Stacked rows use minmax(0,1fr) + a height-proportional glyph so the two
arrows stay symmetric and uncut at xs.
- Demos: steppers controls + stepper-layout / sizes / themed-glyph
showcases; code snippets use the themed defaults.
- Docs: THEMING section 33, number-field and css-field READMEs.
Note: also sweeps in the pre-existing palabras WIP already in the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each input drew its focus ring differently — the foundation [data-archetype]
:focus-visible ring landing on the <input>, ad-hoc per-component input rings
(css-field/number-field), the textarea's own color-mix — so editing showed a
DOUBLE frame (inner + outer), inconsistent across components.
Unify into one theme-parameterised two-ring model:
- New --focus-ring-inner-width (0 by default), folded into --focus-ring and every
field *-focus-shadow: inner inset ring + outer ring. 0 = single outer frame;
raise it in a theme to show the inner line everywhere at once.
- Foundation [data-archetype]:focus-visible now excludes form-internal elements
(:not(input):not(textarea):not(select):not([data-archetype='segment'])) — their
focus is shown by the wrapping control.
- Removed the ad-hoc input rings (css-field/number-field); textarea uses
var(--focus-ring).
- Cleaned the now-dead per-data-color _accent-* cascade in css-field/number-field
(recorded as backlog in THEMING.md §32).
Verified with real keyboard on field/css-field/number-field/date-field/textarea:
single outer frame. Also includes the earlier textarea `resize` fix (morfo
data-resize values + soma stops inlining resize + eidos materialises it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Words: lift heading level cap 1-3 -> 1-6 (engine type, WORDS_HEADING_LEVELS,
render tag union, validate message, parse-html no longer clamps h4-h6, provider
unions). Palabras panel: Nivel toggle H1...H6 wired to the theme's --style-h{n}
scale; level is a required selection.
ToggleGroup: new `deselectable` prop (default true). In single mode,
`deselectable={false}` requires a selection - re-pressing the active item is a
no-op (radio-like, like Ark/Bits). Soma provider + types + docs; eidos forwards;
palabras uses it for the heading Nivel toggle. Provider test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A grabbed surface scales slightly toward the viewer (depth 'picked up' cue). This
is a cross-component primitive, not a float-panel literal:
- New --motion-scale-lift (1.02) in STATIC_MOTION.scale + MOTION_SCALE_KEYS (the
only >1 in the scale family); validated by validateEidosConfig.
- FloatPanel consumes it: [data-float-panel-content][data-dragging] { scale:
var(--motion-scale-lift) } + will-change: translate, scale, eased lift/settle,
static during the move (composited), suppressed under reduced-motion.
- Documented in eidos-motion.md §11 (primitives table) and THEMING.md §14 (new
'Superficies arrastrables: el lift de pickup' pattern) so any draggable surface
(slider thumb, sortable item, drawer) reuses the SAME token, not its own literal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New general-purpose overlay component across all 4 layers (morfo/soma/sema/eidos)
plus a shared stacking layer and i18n catalog.
- Drag (2D free, anchored→free) + resize with device-pixel-snapped, GPU-composited
1:1 movement; elevation lifts on pointerdown for the pickup metaphor.
- Stages: minimize / maximize / restore (StageTrigger, computed maximized geometry
so restore needs no saved-rect bookkeeping).
- Composable header buttons: Controls + Action (custom) + Close + StageTrigger.
- Keyboard a11y (the industry gap): grab-mode move/resize on the focusable header
and a se-corner ResizeGrip (pointer + keyboard), with debounced live-region
announcements, bounds/min-max cues, aria-roledescription/keyshortcuts.
- Stacking registry (bring-to-front/topmost), Presence motion preset (scale-fade),
es/en langs catalog.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds external comparison table (WAI-ARIA / React Aria / Ark UI / Base UI / Bits) with sources and a Gaps table with disposition markers, bringing the css-field audit scorecard to parity with number-field (only the systemic E-2.2 css-import rule + R-1.3 readonly warning remain, both shared with the reference component). Smoke PASS for /uix/components/css-field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Motor (words): ImageBlock gana `fit` (fill/fit/crop/tile) + 7 filtros CSS
(saturate/brightness/contrast/hueRotate/grayscale/sepia/blur) + `width`/`height`
number|string (CSS-length). Render: object-fit + branch tile bg + figcaption.
Fase A: caret-carry al insertar, guard columnas-en-columnas, deleteColumnAt
siembra parrafo cuando el superviviente queda vacio. block-path.test.ts (14).
- Panel de imagen reescrito con primitivas eidos (ToggleGroup/Switch/Slider/
NumberField/Accordion), labels xs sans arriba del slider, header con icono
definitorio del bloque, secciones pinned + accordion collapsible, footer band.
- sium: `cssLength()`/`isCssLength()`/`CSS_LENGTH_REGEX` en core/refines.ts,
exportados por la fachada EngineSium. El panel valida unidades CSS (data-invalid).
- accordion: tamano `xs` (types + recipe base.ts + accordion.css + generated).
- words-activate: guard atomico (lastAtomicAt + isActiveBlockAtomic) evita que el
caret-follower degrade el bloque imagen activo a heading.
- saturate/contrast max 200% (estandar, antes 300% inventado). Caption visible:
fit-frame movido de figure a img/tile.
Sin tocar la capa sema ni la animacion (restriccion del usuario).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El "+" ya no desaparece al llenar la columna (resuelve "las columnas solo
admiten un bloque"). Ahora hay un "+" por columna: centrado si está vacía,
abajo en el borde punteado si tiene bloques. Cada click hace APPEND al final
(insertBlockInColumn -> [...col.children, block]).
El botón "+" pasa a redondo total (border-radius 999px) con borde + sombra
elevada.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Insertar bloque -> rejilla de tipos:
- palabras-block-types.ts (nuevo): lista curada de 10 (Párrafo/Encabezado/Imagen/
Lista/Separador/Cita/Código/Callout/Columnas/Tabla; headings y listas
colapsados; imagen incluida) con icono explícito por tipo (los definió el
usuario). Sale del registro del motor por id, sin duplicar create().
blockToInsert rellena placeholder en heading/list; imagen via pickImageFile.
- Iconos en el menú de acciones (Settings2/LayersPlus/ArrowBigUp/ArrowBigDown/
Trash2).
- Menú PORTALIZADO (DropdownMenu.Portal): el slot del grip tiene transform, que
lo hace bloque contenedor de los position:fixed -> la colisión de floating-ui
se desajustaba y el submenú se volteaba a la izquierda. Portalizar lo saca del
slot transformado.
Columnas (palabras.css, espejo de Words words.css):
- Un bloque de columnas vacío se renderizaba con ALTURA 0 (palabras no carga
words.css), y de ahí salían 4 bugs de la MISMA raíz: invisible, grip "se
vuelve loco" (las bandas Voronoi-on-Y de blockAtY se degeneran con altura 0),
no se podía seleccionar (0px sin área clicable) y el insert caía off-by-one.
Fix: flex-row + min-block-size 3em + borde dashed por columna (Words ya lo
documentaba: "without this, an empty columns block collapses to 0 height").
"+" por columna (palabras-column-inserter.svelte nuevo):
- Overlay "+" sobre cada columna VACÍA -> la misma rejilla (sin Columnas) ->
insertBlockInColumn(columnsIdx, colIdx, block) (comando del motor, como el
inspector de Words). Posicionado frame-relative, menú portalizado,
data-words-external-tool. El "+" desaparece al llenarse la columna.
Deuda anotada en continuar-palabras.md (decisión del usuario: al cerrar el
componente): labels -> langs, morfo de palabras. Pendiente abierto: grip "fuera
de la card" (el offsetParent del slot cae al marco exterior).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>