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>
The attr audit segmented each provider by `runtime.part('X')` and scanned the
whole segment, so it false-positived on type annotations, per-part source
registrations (`props: { role: () => … }`), and non-part helper getters (a
hidden `inputProps` with its own `type`). Now it matches only inside
`.assert({ … })` blocks — the real part props getters.
Backlog corrected: 607/50 → 559/44 (switch, announce, etc. were pure false
positives — 0 real duplication). number-field (1) / css-field (2) residue
unchanged; guard still green.
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>
El grip pasa a ser un DropdownMenu con acciones (Propiedades, Mover
arriba/abajo, Insertar, Eliminar bloque). Arreglado el "parpadeo" del menú,
que eran CUATRO fallos compuestos (por eso cada fix suelto no lo resolvía):
- disabled reactivo (leía api.html) re-creaba el item enfocado al abrir ->
cierre/reapertura en bucle. Fix: index/count congelados en el objeto grip +
grip congelado mientras el menú está abierto.
- pointerdown sobre el grip llegaba a la capa Dismissal del menú, que lo veía
"fuera del contenido" -> cerraba, y el click reabría. Fix: stopPropagation en
el pointerdown del trigger SOLO con el menú abierto.
- preventScroll (default true) bloqueaba el scroll del body al abrir -> el
scrollbar desaparecía y la página saltaba. Fix: preventScroll={false}.
- doble animación de entrada (present-rise de sema/motion durante el hold +
dropdown-menu-enter del recipe) reiniciaba la opacidad a 0 al acabar el hold;
como el menú no está portalizado, se veían los bloques detrás. Fix: fijar una
sola animación con !important en palabras.css.
Nuevo palabras-block-actions.ts (moveBlock / insertParagraphBelow /
deleteBlockAndReanchor). words-provider: marcador genérico
data-words-external-tool en isInsideWordsTool para que abrir el menú no dispare
el bucle blur->refoco del editor. Diagnóstico completo en continuar-palabras.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El hover-follow resolvia el bloque con closest('[data-words-id]'), que da null
cuando el cursor esta sobre el carril (el rail no es un bloque), asi que el grip
se congelaba ahi. Ahora onMove, cuando no hay bloque directo bajo el cursor,
resuelve el bloque por la Y del cursor (blockAtY: banda vertical Voronoi-on-Y de
cada bloque), de modo que el grip se desplaza igual por el gutter que por los
bloques.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El piloto envolvia cada parrafo en <div data-words-node=block><p>, pero
el unico beneficio neto (drag vertical de raiz) ya esta cubierto por el
`dropZone` del sink, y el borde activo punteado funciona igual sobre el
elemento pelado. paragraphSpec.render vuelve a emitir un <p> plano.
Tests del render del motor en verde otra vez (engine de words 430/430).
palabras verificado en navegador: render, grip, drag y borrado intactos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Estado funcional del editor palabras (verificado en navegador) como
punto de retorno antes de revertir el piloto del wrapper.
- Grip: framework <Button> draggable copiado del gutter de Words
(ghost/xs/iconOnly + dots svg). dragstart escribe el indice en el MIME
canonico; click abre el inspector via onPress; cursor grab/grabbing.
- Drag vertical: <WordsBlockDrag> acepta `dropZone` opcional y escucha
dragover/drop en toda la fila (rail+canvas) en vez de solo en content,
asi un arrastre recto hacia abajo sobre el carril ya engancha.
- Borrado de bloque (palabras-panel): mueve el activo al bloque
superviviente leyendo el documento YA actualizado (cubre el caso del
ultimo bloque, que deja un parrafo en blanco), cierra el popup, y el
grip se re-ancla en vez de quedar fantasma.
- active-eidos: getter `timers` (ActiveTimers) sobre el uix service.
- Piloto wrapper: paragraphSpec.render emite <div data-words-node=block>
envolviendo un <p data-words-inner> (deja tests del render del motor
por consolidar/revertir).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The block inspector mixed three control heights (chip 26 · number-field 28
· color-trigger 36px) and two fonts (chip/trigger 10px vs label/value/input
12px), so the PRIMARY control — the chip — was the smallest text in the panel.
- New recipe tokens `--words-control-height` (1.75rem — was a magic literal on
the number-field) and `--words-field-width` (7.5rem), applied uniformly to the
whole inline tier (chip · color-trigger · number-field) → one 28px height + an
aligned right-edge column.
- Chip + color-trigger lift to the 12px body tier (the ToggleGroup `xs` is kept
ONLY for its compact padding; lifting the font without the padding keeps the
four-segment groups on one line).
- Cramped long labels shortened (Predeterminada→Auto, Semi-negrita→Semi) so
Fuente/Grosor read as four even segments.
- Collapsed the dead, inconsistent font-size fallbacks (`xs, 0.6875rem` /
`xs, 0.75rem` / `sm, 0.8125rem`) to the bare declared token.
Chrome scale is now a clean 14 (section) / 12 (body) / 11 (micro). Verified in
browser: chip · color-trigger · number-field all 28px·12px, no group overflow.
19/19 recipe contract green; check adds zero errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inspector mixed THREE fonts: labels in Lora (serif!), titles in system-ui,
inputs in Instrument Sans. Root cause: eidos's "content" archetype (Accordion
content) maps to the reading serif, and the inspector's `[scope] *` rule is only
(0,1,0) — equal to the component's `[data-x]` font rule — so the Phase 3
code-split flipped the load order and the serif won.
Fix: a `--words-chrome-font` token (= --font-ui, independent of the content
`font-family` a theme may set to serif) + a rule forcing it across EVERY chrome
surface (toolbar·bubble·slash·drawer·inspector·gutter menus) with a DOUBLED
scope attribute (0,2,0) that beats any single-attribute component font
regardless of code-split order. Verified in browser: all chrome = Instrument
Sans (was Lora/system-ui/Instrument). Content keeps its reading font.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression from the onMount→$effect conversion (increment 3): eidos.langs.extend
both reads AND mutates the reactive langs schema, so the $effect tracked that
read, the mutation retriggered it, and it looped — re-extending the bundles
thousands of times, each logging a DEV leaf-overwrite warning (~90k warnings,
hung the editor page). Wrapped the body in untrack ⇒ the effect has no deps ⇒
runs exactly once on mount, like the old onMount. Verified: words page loads
fast, zero console warnings, editor + inspector render. api-contract test still
green (untrack is not flagged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The words recipe declared a token vocabulary the CSS never used (the root of
the drift). Pruned 55 declared-but-unused tokens: 38 direct (gap-*, toolbar-*
×9, button-size-*, command-*-hover, content-px-*, status-*, rail-*,
selection-color, _accent-* ×5, line-height, max-inline-size, heading-font-size,
content-py-after-placeholder) + 17 cascaded (the intent palette *-solid/
*-solid-hover/secondary-* slots that only fed _accent-* — callouts use the
tinted track/border/text slots, kept). No wire candidates (rail had no
consumer; ::selection already uses wash-soft). The recipe contract now matches
actual usage → the orphan test goes green; full eidos suite 197/197 (all 3
words failures now fixed across increments 1/3/A). Visually inert: words.css
references nothing removed (the "declares every var" test guarantees it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
words-inspector.svelte carried an intentionally-empty <style> (the styles
live in words.css, imported by the root) — removed. words.svelte used the
legacy onMount only to flip `mounted` + register langs bundles (no reactive
reads) → converted to a run-once $effect, dropped the lifecycle import.
component-api-contract test now green; eidos suite 196/197 (only the words
orphan-token test remains → Phase A).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The small-control radius was a soup of 8 syntaxes for the same thing:
4px / var(--radius-2,4px) / 3px / even var(--space-1) (a spacing token used
as radius). Added --words-radius-xs (= --radius-sm, 4px) — the missing
small-control step under sm — and snapped all of them to the
--words-radius-{xs,sm,md,lg} scale. Micro decorative radii (1-2px) and circles
(50%) left as-is by design. Verified radius-xs=4px, editor renders, no
regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kills the biggest visual-inconsistency soup: 18 ad-hoc color-mix opacities
(4/6/8/12/14/16/22/30/35/40/45 %) collapsed to a 4-step canonical scale
--words-wash-{faint,subtle,soft,strong} (6/16/30/44 %), applied across
words.css; geometry %s (circles, widths) untouched. Hardcoded transition/
animation durations (120/140/100 ms) collapsed to --words-transition-duration.
Declared the 2 used-but-undeclared tokens (--words-font-size-xs, --words-swatch)
→ the "declares every public var" recipe test goes green (1 of 3 words failures
fixed). Editor verified in browser, no regression. See components/words/AUDIT.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Soma is clean (95/100, headless). The mess is words.css drifting from its
own token contract: ~117 tokens declared, ~42 used, 41 orphaned, the same
concept expressed 3-4 ways (radius, mix%, focus ring, sizes, durations,
padding), 3 duplicated color palettes, rail hardcoded over orphaned tokens.
Phased plan A-E; pilot = inspector. Working doc, updated per phase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real fix, not a dodge. Corner families act on CORNERS; a pill (radius 999)
has none — its ends are continuous curves — so cut/scoop eat the whole end
into a hexagon. The correct behavior is to give the control corners: under an
angular family the switch track adopts a FINITE radius (a coherent cut-capsule)
while round/continuous keep it a pill. The thumb stays a round dot. Verified:
cut → track radius 6px / corner-shape bevel; rounded/continuous → 999px pill.
Refined the pendiente doctrine accordingly: angular families on pill/circle
controls aren't vetoed — the control either adopts a finite radius to express
the family, or stays shape-neutral by deliberate choice. The eidos Switch
recipe should follow the same pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
applyTheme(seed) covers only the 5 visual channels. Recorded the complete
theme model by layer (visual=eidos, sema=sound/haptic separate+nullable,
iconography=art, variants=canon/preview-only), the architectural note that a
full cross-layer theme belongs at active-uix (uix.applyBrand) not eidos, and
the framing debt: /temas/tema overclaims "un tema" — it's the visual channels.
Three reopen options (reframe copy / richer page / applyBrand). Deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The switch track is a pill (border-radius: 999px); applying data-shape made
corner-shape: bevel turn it into a hexagon (and scoop into a blob). Pills and
circles must not take shape families — same rule the avatar/thumb already
follow (pendiente.md). Dropped data-shape from the track; it stays round in
every family while the rectangles (card, button, badge, chips) still morph.
Verified: cut mode → switch corner-shape round / radius 999px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toggle changed data-theme but the preview stayed dark: applyColorScheme
resolves donor scales from the instance's EFFECTIVE mode (#resolveDonorScales
ignores the per-call mode option), and the boot had no modeSource — so the
scheme always derived dark neutrals/surfaces and overrode the foundation's
light tokens. Bound a modeSource to the `mode` state (grafito pattern) +
fire its listeners on change, so the instance mode follows the toggle and the
scheme re-derives correctly. Verified: surface oklch(0.21)↔(0.98), content
flips, both modes read cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A live theme builder: one seed → the five runtime builders (color · type ·
depth · shape · space) composed in a single uix.eidos.applyTheme() call,
scoped to [data-tema-preview] so the themed mini-app re-skins while the
controls stay put. Five presets set all axes at once (a whole personality);
per-axis sliders + swatches for fine control; live code mirrors the actual
call. Self-contained (boots its own minimal ActiveUix + ActiveEidos with
applyDom). Preview is hand-built from foundation tokens (--color-{role}-* ·
--space-* · --radius-* · --font-size-* + data-shape/data-depth) — the exact
surface applyTheme drives — since component recipes are code-split.
Verified in browser: all 5 axes retune live (color hue follows the seed,
type ratio, shape smoothing/corner-shape, depth elevation, space density),
presets apply atomically, stable (no cycling), prerenders under
adapter-static.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Composes the five runtime builders (color · type · depth · shape · space)
from one ThemeSeed { color?, type?, depth?, shape?, space? } in a SINGLE
managed write — vs five separate apply* calls. Atomic whole-theme
semantics: axes you provide are set, axes you omit revert to the authored
foundation; clearTheme() reverts all. Per-axis apply{Color,Type,…} stay
for surgical tweaks. Returns ApplyThemeResult for per-axis introspection.
New: ThemeSeed, ApplyThemeOptions, ApplyThemeResult (exported from
$uix/eidos); also exported the previously-omitted ApplySpacingOptions.
2 tests (composition + atomic-replace). Closes the "jaula abierta" arc —
no referent reunites the 5 perceptual axes under one runtime theme builder.
Updated CHANNELS_SYNTHESIS §5 (quintet → sextet). check clean (the 28
errors are the untracked heroscrolling WIP demo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
base.css pruned to the 9 role-referenced scales (full palette opt-in):
113.5 → 36.2 KB gz per page total (−68% from the original monolith).
Non-Gregorian calendar lazy-loading (5b) deliberately deferred — it needs
a sync→async change to the vendored date core for a narrow date-pages-only
win; documented the trade-off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
base.css shipped all 31 Radix donor scales, but the theme's roles only
reference 9 (gray green indigo orange plum purple red slate teal). The
other 22 were dead weight on every page: no component references raw
--scale-* (they use --color-{role}-*), and runtime theming builds from JS
scale data and writes RESOLVED values (build-scheme: variables[k]=hex),
so it never reads the CSS scale tokens.
renderThemeCss now emits only role-referenced scales by default; the full
31-scale donor palette ships opt-in in generated/palette.css (new
renderColorPaletteCss). The CSS contract stays the full vocabulary
(apps can still override any scale; load palette.css to use the extra 22).
base.css 53.7→35.4 KB gz. Monolith (every page) 54.4→36.2 KB gz —
combined with Phase 3, 113.5→36.2 KB gz total (−68%). Verified: roles
resolve (primary=purple, button bg correct), pruned scales empty on
regular pages, restored on /temas/color via palette.css. Updated the
contract test to verify coverage against the full palette. check clean
(13 errors are untracked heroscrolling demo + gitignored tmp/lexical);
eidos suite at baseline (192/195, 3 pre-existing words failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "imports every component CSS recipe from the Eidos entrypoint" test
encoded the OLD invariant (index.css aggregates all 104 recipes), which
Phase 3 intentionally retired. Replaced it with the new contract: every
component recipe loads EXACTLY ONCE — foundation @import (layout
primitives) XOR self-import in its own .svelte. This also enforces the
no-double-load invariant from Phase 0. Eidos suite back to its
pre-existing baseline (192/195; the 3 remaining failures are the
known words-track issues, unrelated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>