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>
Lands the eidos `<WordsColorRow>` (Text / Background pickers inside the
block inspector) on top of the new base-Block words engine + drops 3
obsolete audit MDs.
ColorPicker fixes surfaced while wiring it into the inspector — all
documented in `soma/components/color-picker/README.md` §Integration
pitfalls:
1. Eidos wrapper now declares `format = $bindable('hex')` (matches
soma's default). Without it, `bind:format={undefined}` threw
`props_invalid_value` on every mount → render loop.
2. Eidos wrapper now forwards `ref` to `ColorPickerProvider.create`.
Without it, `attachRef` was never built and any `runtime.trigger`
targeting `provider` threw `SomaRuntimeTargetError`.
3. `triggerClose` falls back to the picker provider's own DOM when
`runtime.partRef('content')` returns null (content is registered on
the Popover's runtime, not the picker's — re-exported part).
4. WordsColorRow draft-pattern: `$effect` reads draft inside `untrack`
so it doesn't overwrite mid-drag value, and `onValueChange` catches
the `Clear` programmatic transition (`onValueChangeEnd` doesn't fire
on Clear).
CSS: `contain: layout style` on `[data-color-picker-area]` and
`scrollbar-gutter: stable` on the picker popover content — stops the
area from shaking during drag when the trigger's ValueText hex changes
width or the popover scrollbar toggles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the user-directed rework. The engine is rebuilt around a single
base `Block` carrying the common properties (id, align, margin,
padding, background, text color, border), with each specialized block
(paragraph/heading/quote/code/list/table/image/divider/callout — plus
list-item/table-row/table-cell) EXTENDING it. The old `visual` sidecar
+ per-type `Pick<>` whitelist + loose `textAlign` are gone; common
props are first-class and universal.
Naming: dropped the `v2` noise everywhere — `operations-v2/`→
`operations/`, `types-v2`/`render-v2`/`validate-v2`/`serialize-*-v2`→
clean names, `*V2` symbols→bare, `WordsEvalIntent`→`WordsIntent`.
Sema: removed entirely from the editor — deleted `sema/components/
words.ts` + `engine/sema-parity`, dropped the registration from the
sema barrel + the demo layout, no `runtime.trigger`/semantic events.
Visual layer: deleted `eidos/components/words/*` (the bespoke chrome)
and stubbed the demo — both rebuilt next on the new model (F2 render
by components, F3 gutters/bubble/drawer, all eidos).
Engine tests 400/400 green; `npm run check` 0 errors (5142 files).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Six user-reported issues in one pass.
EV-F1 — canvas silence
Remove the contact-focus entry from the sema cascade. The event still
fires (telemetry / a11y can read the data-event* stamps) but no sound
/ haptic plays. Per spec: "no puede emitir eventos es demencial, una
vez que está trabajando con el no tiene sentido". The editor surface
is now perceptually silent on focus; only explicit commits (save,
format, set-link, etc.) make noise.
EV-F2 — rail visual
`--_words-rail-bg` becomes flat silver `#d4d4d4` and the dot
pattern is gone. New `--_words-rail-border` (`#9a9a9a`) lays a
1px hairline on the rail's inline-end so it reads as a separate
gutter ("paper edge" feel). Fixed-tone (not theme-aware) — the rail
is meant to look like the physical margin of a notebook regardless
of the editor theme. Left-side drawer flips the border to inline-
start.
EV-F3 — direct undo/redo in toolbar
Demo presets + custom config now use `['undo', 'redo', ...]` as
direct command strings instead of `['history-menu', ...]` (which
opened a popover to pick from two items — wasted real estate +
two clicks). `link-menu` and `tools-menu` stay as popovers because
they hold 2-3 items each. `insert-menu` is OUT — block construction
lives in the drawer's empty-state Insert panel and the slash menu.
`demoToolbarFamilyItems.insert` collapsed to `[]` accordingly.
EV-F4 — popover widths
Base `[data-words-toolbar-family-panel]` width 12.5rem → 16rem so
content fits without horizontal scroll. Per-family overrides:
tools → 17rem (descriptive labels), link → 20rem (URL input +
button grid). The default panel CSS no longer carries a max-block-
size + scroll — a vertical scroll inside these popovers is a
misdesign signal; size the panel, don't scroll it.
EV-F5 — link popover (covered by EV-F4)
The vertical scroll the user saw was a side-effect of the
12.5rem width forcing the link-editor input + button to wrap, then
the panel running out of vertical room. The new 20rem width
restores the natural one-row layout and no scroll appears.
EV-F6 — Enter on empty block no longer multiplies
`insertParagraph` engine op now guards the default case
(paragraph/heading): if the block is collapsed AND empty (children
length 0, both halves of split empty), Enter on a paragraph is a
no-op and Enter on a heading demotes it to a paragraph (canonical
Notion / Google Docs UX). For non-empty paragraphs/headings the
existing split-and-create-paragraph behavior stands.
Verification: 467/467 tests pass across morfo + sema + eidos +
soma/components/words. 85/85 engine tests pass (insertParagraph
guard non-regressive on existing fixtures). `npm run check` still 0
errors. DOM probe at /uix/components/words confirms toolbar = 5
items (undo, redo, Links, Tools, Find/replace), rail bg
`rgb(212,212,212)` + 1px `rgb(154,154,154)` right border, link
popover 320px wide with no scroll.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four user-requested fixes to the Words editor surface — the body
should not emit perceptual cues during editing, the top toolbar must
not duplicate the drawer, and the gutter widgets need clearer anchor
points.
EV-A (events spam):
- Move `contact-focus` event target from `content` to `provider` in
the morfo + sema cascade selector. Focus is a Words-component-level
signal; firing it from the content body conflates "user entered the
editor" with "user is editing the body". The wasFocused guard in
soma already throttles to one emit per real focus transition.
- Expand `isInsideWordsTool` selector list to cover the four
companion overlays added after the original list was written:
`data-words-drawer`, `data-words-block-handle`,
`data-words-block-handle-menu`, `data-words-block-inserter`,
`data-words-image-float-bar`. Before this fix every click on a
companion overlay fired commit-save-content + contact-focus on the
blur/refocus cycle (two perceptual cues per tool interaction).
EV-B (toolbar trim):
- Demo's full / formatting / minimal presets + custom config now host
only GLOBAL actions: history (undo/redo), insert (creates new
blocks), link (selection-bound flow), tools (find/replace + clear),
find-replace in its own group. text-menu / block-menu / align-menu
/ list-menu / table-menu moved out because the drawer already owns
contextual formatting per the HIER-2 split.
EV-C (block-handle drag UX):
- Drop the `setDragImage(hoverBlockEl)` call. The browser now uses
its default snapshot (the grip button itself) as the drag ghost —
the ghost travels with the cursor while the bar in the gutter
stays fixed as a visual anchor. New `data-dragging` attr +
`[data-words-block-handle][data-dragging]` CSS rule fades the
static anchor to 0.35 opacity so it reads as "drag origin" while
the ghost is the moving part.
EV-D (inserter at block bottom):
- Seam positions are now pinned to the BOTTOM EDGE of the preceding
block (`a.bottom`) instead of the midpoint of the gap between two
blocks. The "+" reads as "insert AFTER this block" anchored to
that block's lower edge, per spec — el botón de añadir bloque
debe aparecer en el límite inferior del área en relación al
bloque.
- Tighten the "cursor inside block band" check to a half-open
interval `[top, bottom)` so the exact bottom-edge pixel belongs to
the seam below (otherwise the seam at `y === bottom` is shadowed
by the block and the inserter never snaps).
Verification: dev server, /uix/components/words, DOM probe confirms
handle centered in the rail column (left=48 inside the 32-wide rail
starting at ~46), inserter snaps to block 1's bottom (`top=569.94px`
when block 1 bottom = 570px). 366/366 tests pass in morfo + sema +
soma/components/words. `npm run check` still 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cierre de los hallazgos P0/P1 del audit Codex sobre Words + fix del
evento contact-focus molesto reportado por el usuario.
F1.1 — Fix evento foco (UX):
El handler onfocus disparaba `runtime.trigger('contact-focus')` cada vez
que el editor recibía foco, generando sound + haptic + visual stamp en
cada re-focus desde popovers internos (bubble menu / link editor / find
panel / slash menu / toolbar dropdowns). En sesión normal de edición el
foco entra/sale del content docenas de veces, lo que producía fatiga
perceptual.
Cambios en words-provider.svelte.ts:
- onfocus: emit solo en transición real (was-not-focused → focused).
Re-focus desde popover interno NO emite.
- onblur: si el related target es interno al Words tool tree
(isInsideWordsTool), mantener focused=true (Words se trata como un
solo scope de foco). Skip commit('blur') también para evitar
commits espurios entre popover hops.
F1.2 — Declarar 8 data-* hardcoded en morfo (audit P0-1):
Añadidos al Content part:
- data-words-find-match, data-words-find-active (mark highlights)
- data-words-indent (list items con indent > 0)
- data-language (alias parsing-only para interop con HTML externo
emitido por Prism/highlight.js)
Nuevas 6 parts compound (Eidos wrappers):
- HeadingPicker + HeadingPickerPanel (data-words-heading-picker /
-panel)
- CodeLanguagePicker + CodeLanguagePanel
- ToolbarFamilyMenu + ToolbarFamilyPanel
F1.3 — Rename i18n keys a kebab-case (audit P0-1):
4 keys camelCase eran ilegales según contracts.test.ts. Renamed:
- components.words.bubbleMenu → bubble-menu
- components.words.slashMenu → slash-menu
- components.words.linkEditor → link-editor
- components.words.findReplace → find-replace
Propagado a: morfo declarations (8 refs), provider tslangs calls
(4 refs), catalog langs/components/words.ts (4 keys).
F1.4 — Canonizar 9 event names a forma {family}-{verb}[-{variant}]
(audit P2-1):
Renombrados para parsear con el morfo:vocabulary script. El verb
declarado en cada semantic ahora aparece explícito en el name:
- commit-content → commit-save-content (verb: save)
- commit-format → commit-set-format (verb: set)
- commit-link → commit-set-link
- commit-unlink → commit-reset-link (verb: reset)
- commit-slash-command → commit-set-slash
- commit-check → commit-set-check
- commit-history → commit-restore-history (verb: restore)
- commit-clear → commit-reset-content (verb: reset)
- signal-invalid-input → signal-warn-invalid (verb: warn)
Propagado a: sema cascade selectors, soma provider trigger calls,
provider + content tests, demo route. 0 conflictos con event names
de otros componentes (file-upload/tag-group/etc. usan namespaces
propios sin colisión con words).
Tests: 161/161 pass en src/uix/{contracts,soma/components/words}.
Suite full eidos+morfo+soma+sema pendiente de verificar tras commit.
contracts.test.ts ahora verde — cierra P0 audit Codex.
morfo:vocabulary 0 event-name warnings (los 107 data-attr enum
warnings son pre-existentes, separate scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Token Scope Contract universal — no más excepciones arquitectónicas.
Los 3 componentes que vivían fuera de TSC v2.1 (select, avatar,
toggle-group) ahora están dentro del contrato via dos extensiones nuevas.
TSC v2.2 extensiones (lib/config-types.ts + render-css.ts + config.ts):
- `parts: readonly string[]` en RecipeTokenMultiDeclaration — emite
selectores comma-separados (`[data-{c}-x], [data-{c}-y]`) para
componentes con data-color cascadeado per-part. Consumer: select.
- `composition: { foreignRecipe: { targetSelector, tokens } }` sibling
key — overrides cross-recipe scoped a la cascade del host. Consumer:
toggle-group modifica `--toggle-palette-*` en sus items.
Migraciones:
- select: 3 private `_accent-{track,border,text}` con parts: ['trigger',
'content'] + 7 cascades color:X each. Removed orphan `_accent-solid`
(CSS no consumía).
- avatar: 6 tokens via composite scopes `['variant:X', 'color:Y']` con
matrix helper inline. Badge usa parts: ['badge']. Reemplaza 24 bloques
CSS × 2 partes.
- toggle-group: composition block con 8 palette tokens × 4 colors.
Reemplaza 4 bloques CSS per-color.
Bug toggle-group post-composition (encontrado y arreglado):
Tras la composition migration, el cascade del toggle-group seguía roto
porque los tokens derivados (`--toggle-solid-on-bg`, `--toggle-outline-fg`,
etc.) viven en scope `[data-toggle]`. El `[data-toggle-group-item]` es
sibling (no descendant), así que `var(--toggle-solid-on-bg)` resolvía
undefined. Fix: inlined derivation expressions directamente en
`[data-toggle-group-item]` y sus variant cascades (solid/outline/ghost),
referenciando palette tokens en su propio scope local.
Validador + emisor + contract:
- `validateRecipeComposition` valida el shape `{ targetSelector, tokens }`
y rechaza composition entries con scope='root'.
- `stripCompositionKey` + `emitComposition` separan el pipeline.
- `appendRecipeContractTokens` skip-list para `composition` (no aparece
como fake `--{c}-composition` knob).
- `tokenKeys`/`tokenEntries` helpers en recipe-css-contract.test.ts
filtran composition en todos los iteradores.
Documentación:
- THEMING.md §18 reescrito como "Cobertura universal de TSC". §7
extendido con subsecciones "Multi-part scope" y "Cross-recipe
composition" + ejemplos completos. TOC actualizado.
- eidos/README.md tabla de referencia ampliada con TSC v2.2 + §18.
- CLAUDE.md gana hand-off "2026-05-27 #5" (TSC v2.2 + cobertura universal).
- CONTINUE.md reescrito al estado actual de la sesión.
Working tree también incluye sprint Words en paralelo (multiple authors):
slash menu, find/replace regex, code language picker, table audit,
toolbar family menu, code highlight engine.
Tests: 786/786 pass en src/uix/{eidos,morfo,soma,sema}. `npm run check`:
6 errores pre-existentes (lib/_demo, soma/components/internal,
web/routes/active) no relacionados.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three overlay components each had five `close-*` events (close-save /
close-cancel / close-dismiss / close-dismiss-outside / close-after-fail)
with per-event prewrite arrays setting `data-last-action`. Per the book
§5.3 polymorphism canon, collapse to a single `close` event:
semantic: {
family: 'emerge', verb: 'close', target: ...,
sequence: 'pre', persistence: 'transient',
allowedFamilies: ['emerge', 'commit', 'signal']
}
NO prewrite — the provider sets data-last-action imperatively before
trigger because polymorphic events can't bind a single prewrite per call.
**Provider pattern** (Dialog / Drawer / Popover):
- New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail }
→ { lastAction, semantic: { family, verb, intent? } }.
- `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })`
then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`.
- `triggerClose` is now private; previously-public 2-arg signature collapsed
into the cause-driven internal helper.
- All internal callsites (escape, outside-click, hover-close, close button)
migrated to `dismissWith`.
- `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion`
is set (today only for the 'fail' case), the live region announces it.
**Schema validator relaxed** (`src/uix/morfo/schema.ts`): the
"every declared value of `data-last-action.values[]` must be prewritten
by some event" check was DROPPED — provider-imperative writes can't
satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict.
**Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`):
- `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'`
- `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: {
attr: 'data-last-action', value: 'dismissed-outside' }`
**Eidos CSS**: not touched — already reads `data-last-action` directly.
**Public API preserved**: `dismissWith` signature + observable behaviour
identical. External consumers see no difference.
Tests updated for the new event name. 680/680 pass across sema + morfo +
soma + adom scopes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Los 11 packs sema shipped en commit 2d562f37 quedaban dormantes
porque soma no llamaba runtime.trigger en ningún sitio de estos 7
componentes. Esta es la cabling que los activa.
Patrón: emisión en el state-mutator central (no en handlers
individuales) cuando es posible, pasando fallbackTarget al elemento
DOM apropiado para que el cascade selector matchee la instancia
correcta (no la "más reciente registrada").
- toggle-group: `commit-toggle` desde Item.onclick con item button
como fallbackTarget.
- menubar: `commit-select` desde Provider.open(value), usando
triggerRegistry para resolver value→element. Cubre click, keyboard,
hover-follow y arrow-nav en una sola emisión.
- navigation-menu: `commit-select` desde Provider.openNow(value),
usando triggerRefs existente. Closest() del trigger sube al
item ancestro.
- dropdown-menu: `open` y `close` desde handleOpen/handleClose
(sequence: 'pre' — antes de la mutación de state); `commit-select`
desde MenuItemProvider.onclick + onkeydown.
- context-menu: `open` desde openAt(), `close` desde handleClose,
`commit-select` desde ContextMenuItemProvider.onclick + onkeydown.
- tree-view: `emerge-expand`/`emerge-collapse` desde toggleExpand
(lookup branch via querySelector). `commit-select` desde select()
con parámetro fromEl pasado por BranchControl/ItemProvider.
- tree-grid: análogos via lookup de row por data-value.
Pack tree-view actualizado: añade cascade rule para commit-select
sobre `branch` (mismo tuning que item) — branches y leaves comparten
firma perceptual.
Fix colateral: tag-group test esperaba intent='neutral' para
commit-unselect, pero el canon (f3641347) lo declara como 'affirm'.
Test actualizado.
Verificación:
- npm run morfo:vocabulary → EXIT 0
- npx vitest run src/uix/sema src/uix/morfo → 195/195 pass
- npx vitest run src/uix/soma → 419/419 pass (74 files)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Morfo declaraba target='item' para emerge-expand y emerge-collapse,
pero los items son hojas — no se expanden. El stamp del data-event-*
debe aterrizar en el branch (data-tree-view-branch), que es el part
que realmente revela/oculta contenido.
- Morfo: target: v.partRef('item') → v.partRef('branch') en ambos
events de emerge. commit-select sigue en item (selección aplica
tanto a leaves como a branches).
- Pack: selector `onItem(emerge-*)` → `onBranch(emerge-*)`. Las dos
cascadas de emerge ahora matchean el elemento correcto en DOM.
tree-grid no afectado: su Row es a la vez item y branch (data-has-
children + data-expanded en el mismo part), así que target='row'
ya estaba correcto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes in one commit:
1. **Doctrina del autor sobre el documento de variaciones**
The author evaluated LIBRO_VARIACIONES_Y_EXTENSIONES.md and gave per-
entry verdicts. The doc is now rebranded, restructured, and classified:
- Renamed: "Registro de desviaciones entre implementación y canon
editorial". Disclaimer at top: not the book, internal registry only.
- Formal status classification (BOOK_CANON / PROJECT_CANON / CANDIDATE
/ LOCAL_EXTENSION / DEPRECATED / ALIAS / IMPLEMENTATION_CONTRACT).
- Per-entry verdicts assigned per author's evaluation. Author's
doctrinal texts included verbatim where given.
- A.6 split per verb (was a single bullet for apply/partial/block/
move/upload; now each has its own status: apply=BOOK_CANON,
move=BOOK_CANON, upload/partial/block=CANDIDATE).
- A.6/A.7 ordering fixed (commit.unselect now A.11 — terminal).
- C.3 split into C.3.a (renames mecánicos) y C.3.b (correcciones
doctrinales — command intent shift).
- E (resumen ejecutivo) actualizado: clear/unselect movidos a
resueltos (estaban contradiciéndose). Pendientes reales listados.
- F + new section G ("Anti-mezclas") con disciplina sobre el doc.
2. **Family policy: split `intentPolicy` en `intentRequirement` +
`intentGuidance`** (D.3 del doc).
La policy actual `'allowed' | 'expected' | 'optional'` mezclaba dos
ejes que el autor pidió separar:
- `intentRequirement: 'required' | 'optional' | 'forbidden'` —
compile-time type constraint.
- `intentGuidance: 'expected' | 'contextual' | 'discouraged'` —
guía doctrinal sin efecto en tipos.
Policy nueva:
| family | requirement | guidance |
|----------|-------------|--------------|
| contact | optional | discouraged | (Cap 22 §11)
| commit | required | expected |
| signal | required | expected |
| handle | optional | contextual |
| emerge | optional | contextual |
| shift | optional | contextual |
| sustain | optional | contextual |
| delegate | optional | contextual | (Cap 29 §4)
`IntentExpectedFamily` deprecated → `IntentRequiredFamily` (alias
mantenido). `IntentPolicy` type también deprecated.
Consumers actualizados: event.ts (isSemaEvent usa requirement),
validation.ts (validateSemaEvent usa requirement), event.test.ts
(tests usan ambos campos), dialog.ts (comment doc update).
3. **Bug menor encontrado en `SEMA_TRANSITIONAL_FAMILIES`**: faltaba
`delegate`. Añadido. (Era inconsistente con SemaTransitionalFamily
type que sí lo incluye.)
Verification:
- vitest src/uix/sema src/uix/morfo: 195/195 pass
- morfo:vocabulary: 7 warns (todos words/*, separate dev track)
- EXIT 0
Pendiente futuro (no en este commit):
- Añadir flag `emission` a MorfoEvent type per D.2 (eventos declarados
pero no emitidos)
- Re-evaluar verbos CANDIDATE (acknowledge/confirm/upload/partial/
block) cuando aparezcan más casos de uso
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the last cluster of name-shape drift in the non-words morfos. 11
are pure cosmetic renames (declared verb already canonical); 1
includes a doctrinal correction.
Renames (verbo declarado canónico, solo el name):
- carousel.shift-slide → shift-navigate-slide
- feed.shift-focus-item → shift-navigate-focus-item
- feed.commit-load-more → commit-submit-load-more
- file-upload.commit-add → commit-set-add
- file-upload.signal-reject → signal-warn-reject
- form.signal-invalid → signal-warn-invalid
- number-field.handle-scrub → handle-drag-scrub
- range-calendar.commit-start → commit-select-start
- range-calendar.commit-range → commit-select-range
- tags-input.commit-add → commit-set-add
- tags-input.signal-reject → signal-warn-reject
Doctrinal correction:
- command.commit-invoke (declared submit + fulfill)
→ commit-submit-invoke + submit + AFFIRM (not fulfill)
Per book cap. 22 §8 ("Error típico: contact.press + fulfill — esto
hace que el sistema diga 'ya terminó' cuando solo ha empezado.
Contact inicia. Commit resuelve."), the user submitting a command
in the palette does NOT mean the command has completed. Same
precedent as Button: celebrate-before-time is wrong. Affirm
acknowledges the submission; the command's actual completion
(success/failure) fires downstream from whichever morfo owns it.
Consumers updated:
- sema/components/{file-upload,form,number-field,tags-input}.ts
- soma/components/{file-upload,form,number-field,range-calendar,tags-input}/*.ts
- sema/resolver.test.ts (event name + data-event selector)
Verification:
- morfo:vocabulary: warns 19 → 7 (-12), the 7 remaining are all words.*
(separate dev track, excluded). EXIT 0.
- vitest src/uix/sema src/uix/morfo: 195/195 pass
Doc src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md C.3 marked RESUELTO
with full list of renames + the command intent correction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User doctrinal decision (2026-05-25): add `unselect` as canonical verb of
the commit family. The pair `select` / `unselect` covers results applied
on the selection state of an item — distinct from `toggle` (binary
control flip) and `remove` (retirement from a collection).
Quoting the author:
"Seleccionar y deseleccionar son resultados aplicados sobre el estado
de selección de un elemento. Eso es commit, porque el resultado queda
aplicado.
- No es remove: no estás eliminando el item ni sacándolo de una
colección funcional; solo estás cambiando su estado de selección.
- No es necesariamente toggle: toggle describe mejor el mecanismo
binario o el control, pero no expresa tan bien el resultado
semántico concreto."
Changes:
- src/uix/sema/verbs.ts: add `unselect` to SEMA_VERBS.commit after
`select` (natural pair). Header comment documents the doctrinal
distinction with toggle and remove.
- 6 morfos updated: calendar, combobox, grid-list, listbox, select,
tag-group. Each changes `commit-unselect` from
`verb: 'remove' + intent: 'neutral'` to
`verb: 'unselect' + intent: 'affirm'`. Names preserved (already
canonical now). Intent `affirm` because "the action applied
correctly" (sobrio, not celebratory).
- src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md: C.2 marked RESUELTO with
the full doctrinal reasoning verbatim. A.3 (remove vs delete) updated
to reflect the three-way distinction now formalized
(unselect/remove/delete). New A.7 entry for `unselect` itself with
"para el libro" note (add to Cap 23 §5).
Verification:
- morfo:vocabulary: warns 25 → 19 (-6 unselect cluster), EXIT 0
- vitest src/uix/sema src/uix/morfo: 195/195 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User directive: clearing a field is `commit.reset` per book cap. 23
("vuelve a estado inicial") — fits all 10 components in the cluster
since each goes back to empty-initial-state, not discarding meaningful
defaults.
Components updated (morfo + soma trigger + sema cascade + test):
- color-picker, date-field, date-picker, date-range-picker
- file-upload, range-calendar, tag-group, tags-input
- time-picker, time-range-picker
range-calendar.commit-clear was declaring `verb: 'remove'` — corrected
to `'reset'` along with the name change.
All event names now: `commit-reset` (family commit, verb reset, intent
neutral). No variant suffix needed; no ambiguity of multiple resets in
any single component.
words.commit-clear excluded — separate dev track.
Doc updated: src/docs/LIBRO_VARIACIONES_Y_EXTENSIONES.md C.1 marked as
RESUELTO with author's disposition.
Verification:
- morfo:vocabulary: warns 35 → 25 (-10), EXIT 0
- vitest src/uix/sema src/uix/morfo + tag-group tests: 199/199 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves family-level misclassifications across 9 morfos. Each cluster
maps a doctrinally wrong family to the book's literal prescription:
CLUSTER 4 — announce (cap. 24 §5):
- commit-announce-polite (commit.submit) → signal-announce (signal.announce + neutral)
- commit-announce-assertive (commit.submit) → signal-alert (signal.alert + threat)
Announcing is the canonical signal verb. Was misclassified as commit.
CLUSTER 5 — checkbox (cap. 22 §10, option B per user):
- commit-check (commit.select) → commit-toggle-check (commit.toggle + affirm)
- commit-uncheck (commit.remove) → commit-toggle-uncheck (commit.toggle + neutral)
Book literal: "Toggle: contact.press → commit.toggle + affirm". Two events
retained (option B) so check/uncheck carry different intents; same canonical
verb `toggle` per book.
CLUSTER 1 — tree-view + tree-grid (cap. 26):
- shift-expand (shift.navigate) → emerge-expand (emerge.expand)
- shift-collapse (shift.navigate) → emerge-collapse (emerge.collapse)
Book literal: "emerge.expand — una región crece y revela contenido". Tree
node expansion reveals content; it's not a context shift.
CLUSTER 3 — drag-drop (cap. 25 §4):
- commit-drag-start (commit.submit) → handle-pick (handle.pick)
- commit-drop (commit.submit) → handle-drop (handle.drop)
- commit-cancel verb 'reset' → verb 'cancel' (was inconsistent with name)
Book literal pick → carry → drop. The commit (reorder/delete/move) lives
downstream in whichever morfo owns the moved entity, not on drag-drop.
CLUSTER 6 — table (cap. 23, 26):
- shift-sort (shift.navigate) → commit-set-sort (commit.set + neutral)
Sort sets a criterion; the reorder is consequence. Not regime change.
- shift-expand-row (shift.navigate) → emerge-expand-row (emerge.expand)
Same argument as tree.
CLUSTER 2 — virtual-list + virtual-grid (cap. 25, 27, 23):
- shift-scroll, shift-scroll-row, shift-scroll-column (shift.navigate)
→ handle-scroll, handle-scroll-row, handle-scroll-column (handle.scroll)
User-initiated scroll = direct manipulation of viewport. `scroll` extends
the book's literal handle verb list per Cap 8 §1.
- shift-scroll-to-index, shift-scroll-to-cell (shift.navigate)
→ shift-navigate-to-index, shift-navigate-to-cell (shift.navigate)
Programmatic scroll = the SYSTEM moves the viewport, not the user. Still
shift.navigate, but name now surfaces family+verb canonical shape.
- shift-resize (shift.navigate) → commit-set-resize (commit.set + neutral)
System applies a new container/data size. Useful for "items loaded" cues.
CLUSTER 7 — clipboard (cap. 23, user directive):
- commit-copy (commit.submit + affirm) → commit-save-copy (commit.save + affirm)
Value persisted to clipboard = save per book.
- commit-copy-error (commit.submit + threat) → commit-fail-copy (commit.fail + risk)
Failure is fail + risk (correctable, not threat which is exigent).
- shift-reset (shift.navigate) → REMOVED
Timer-driven internal flag flip; no subscriber; not a perceptive event per
user directive ("eliminar si no es perceptivo"). The visible label revert is
signalled by `data-copied` attribute.
Verification:
- `npm run morfo:vocabulary`: warns 57 → 35 (-22), EXIT 0
- `npx vitest src/uix/sema src/uix/morfo`: 195/195 pass
- checkbox provider tests: 4/4 pass
- `npm run check`: no new errors from these changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The canon includes two multi-word shift verbs: `enter-mode` and
`exit-mode`. `validateEventName` was splitting at the first dash and
checking the canon for `enter` / `exit` alone — those aren't canon,
so legitimately canonical events like `shift-enter-mode` were being
flagged as drift.
Fixed by matching the longest canonical verb prefix against the tail
(family-verb shape) or against the name (bare verb shape). Handles
trailing variants too — `shift-enter-mode-edit` recognises
`enter-mode` as the verb and `edit` as the variant.
Verification:
- 1 new test covering multi-word verb shapes; 19/19 pass
- `npm run morfo:vocabulary`: 58 → 57 warns (editable.shift-enter-mode
no longer falsely flagged), EXIT 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codifies the doctrinal source for the semantic system per the user's
book at src/docs/Disenando_lo_que_ocurre_manuscrito_completo_revisado_v2.docx.
Canon (book cap. 8 + cap. 22-29 + cap. 10):
- 8th family `delegate` added (offer/plan/authorize/act/review/escalate/return)
- `signal.inform` added (book cap. 24 listed 7 signal verbs; we had 6)
- Commit contextual verbs from case studies: apply, partial, block, move, upload
- `sustain.upload` added
- SemaFamily union, SEMA_FAMILY_POLICY, sema-map, visual chan, morfo schema
all extended to include delegate (intentPolicy: 'optional' — no intent by
default per cap. 29 §4)
Lint (scripts/morfo-vocabulary-check.ts):
- Hard-fails on declared `semantic.verb` not in family canon (the doctrinal
contract: subscribers query by family+verb)
- Soft-warns on event name shape drift (declared verb is the truth, name is
informational)
- Allowlists `button.commit-action` with TODO referencing Plan B commit 2
(Button's intent vs contact question per cap. 22 §11 needs design sign-off)
3 mechanical violation fixes:
- textarea: `shift-count-overflow` (family shift, verb limit) → `signal-warn-count-overflow`
(signal.warn + risk per cap. 24 — corregible warning, not context shift)
- password-field: `shift-toggle-visibility` → `commit-toggle-visibility`
(commit.toggle + affirm per cap. 23 — state fixation, not context shift)
- password-field: `shift-caps-state` → `signal-notify-caps-state`
(signal.notify + neutral per cap. 24 — system orients attention without urgency)
- month-grid + year-grid: `nav-step` → `shift-navigate-step` (family/verb
already canonical, name aligned to {family}-{verb}-{variant})
Verification:
- npm run morfo:vocabulary → EXIT 0 (1 allowlisted, 59 pre-existing name-shape
warns documenting visible drift for future sprints)
- npx vitest src/uix/sema src/uix/morfo → 194/194 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dedicated password input component — matches the Ark UI / Chakra v3 /
Mantine / PrimeReact / Ant Design pattern of treating PasswordField as
its own first-class component (not a TextField variant). Includes one
feature that NO surveyed library ships: a CapsLock indicator driven by
KeyboardEvent.getModifierState('CapsLock').
Anatomy (5 parts, 4 events):
- Provider (root, value + visibility state)
- Input (the textbox; type flips between password / text)
- VisibilityTrigger (eye toggle button; aria-pressed + aria-controls)
- CapsLockIndicator (status live region; data-active toggles via soma
listening to keydown/keyup on Input)
- StrengthMeter (composable; consumer provides 0-4 score; UIX bundles
no zxcvbn-style algorithm — dependency-free by design)
Events:
- commit-input (every keystroke)
- commit-submit (Enter)
- shift-toggle-visibility (eye click)
- shift-caps-state (Caps Lock state flips while focused)
Key behaviors:
- `purpose: 'current' | 'new'` drives native autocomplete attribute
('current-password' for login / 'new-password' for registration).
- Field integration: disabled/readonly/required/invalid OR-merged.
- VisibilityTrigger gets tabindex=-1 so it doesn't break the form's
natural tab order — mouse / programmatic focus only, matching Ark UI.
- Caps Lock indicator is aria-live="polite" — announces on activation
while focused, silent when inactive.
- Letter-spacing increased on the input for legibility while masked.
Reference library survey performed (10 libraries) before implementation —
all consensus around dedicated component, all have built-in toggle,
ZERO have caps lock warning. Documented in the demo's API tab with a
feature comparison table.
Demo follows DEMO_AUTHORING_GUIDE canonical 6-tab depth: Live (controls
grouped by layer + code snippets at the bottom), API (props + reference
comparison), Morfo (field/value + Parts + Events tables), Sema (events
table with ▶ play), Recipe (selectors with morfo/eidos owner tags), A11y
(concern/contract table).
Also includes external work in words/ from a separate session
(unrelated to PasswordField) — not touching Words is the standing rule,
but the diff was already in the working tree.
svelte-check: 0 errors in PasswordField files (Words errors are tracked
by its own development branch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: 'la caja del color seleccionado en el pickercolor no se
actualiza, compruébalo.'
Root cause
----------
The Eidos recipe had an `[data-color-picker-value-swatch]::after`
overlay that painted `var(--cp-trigger-color, currentColor)` on top of
the swatch — a CSS variable set ONLY on the trigger button by the
Eidos wrapper. The popover content is portaled outside the trigger's
subtree, so the var never propagated into the ValueSwatch element.
The overlay therefore stayed at its fallback (`currentColor`) and
covered the soma-set inline `background-color`, hiding the live colour.
Fix
---
1. **Soma `ColorPickerValueSwatchProvider.backgroundStyle`** now emits
BOTH `background-color: <hex>` and `--cp-current-color: <hex>` so:
- Consumers without the eidos recipe still see the colour (inline
background-color, as before).
- Eidos recipes / external CSS can read `--cp-current-color` as a
reactive CSS variable on the element itself.
2. **Eidos CSS** drops the `::after` overlay. Instead the checker
(transparency pattern) lives on a `::before` pseudo with
`z-index: -1` inside an isolated stacking context, so the soma's
inline `background-color` paints ON TOP and the checker shows
through only where alpha < 1. No more colour stomping.
Checks: 67/67 component-audit PASS, svelte-check 0 errors / 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural correction. Morfo was importing Svelte runes, $adom, and
$uix/sema — all violations of "morfo is pure declarative DNA". The
runtime that interprets a CompiledMorfo lives where it executes: soma.
Move:
- src/uix/morfo/runtime.svelte.ts → src/uix/soma/runtime.svelte.ts
- src/uix/morfo/runtime.svelte.test.ts → src/uix/soma/runtime.svelte.test.ts
Rename (consistent with the move; "soma's runtime", per the doctrine
"morfo declares, soma executes"):
- class MorfoRuntime → SomaRuntime
- createMorfoRuntime() → createSomaRuntime()
- type MorfoRuntimeSources → SomaRuntimeSources
- error prefixes [morfo-runtime] → [soma-runtime]
Sema is now genuinely ornamental:
- ActiveUix.semantic returns EngineSemantic | undefined (no longer
throws when no engine is configured). Components in SSR / headless
tests / audio-disabled environments stay functional.
- SomaRuntime.trigger() skips the emit step AND the target-resolution
check when no engine is present — only requires a DOM target when
something will actually be emitted.
- Soma.semantic getter returns | undefined too (was non-optional).
Consumers updated:
- src/uix/active-uix/active-uix.svelte.ts — imports createSomaRuntime
from $soma; semantic getter returns | undefined
- src/uix/active-uix/types.ts — interface SomaRuntime, semantic | undefined
- 8 providers (toggle, switch, collapsible, dialog, drawer, popover,
toast, avatar) — type SomaRuntime, import path $soma
- src/uix/soma/index.ts — re-exports createSomaRuntime + types
- src/uix/morfo/index.ts — drops the runtime re-exports (morfo is pure)
- src/uix/sema/signal.ts — JSDoc reference updated
- src/uix/soma/core/soma.svelte.ts — semantic getter | undefined
Test added: SomaRuntime.trigger() succeeds without a semantic engine
(skips emit + target check, runs handler directly).
Docs aligned to the new shape:
- active_architecture.md — runtime locations, sema as ornamental
- soma/SOMA_ARCHITECTURE.md, soma/README.md — name + import paths
- morfo/README.md — file listing now flags morfo as PURE TS, points
at soma/runtime.svelte.ts for the interpreter
- sema/README.md — SomaRuntime references
- README.md — root architecture diagram
- CLAUDE.md — hand-off note
Tests 218/218 green. npm run check holds at the 39-error baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 (docs alignment with the canonical guide):
- active_architecture.md — Dialog example refactored to the new event
shape (target inside semantic, explicit verb + sequence). Verbs
table grouped by family with the post-canon vocabulary (select/
toggle moved to commit, edit removed from handle, new verbs added).
Section 10 (Estado actual) updated to 2026-05-08 with current
progress: doctrina cerrada, eidos en producción, SoundChannel
eager-init, switch migrated, etc. "Para profundizar" §14 points at
the canonical guide.
- morfo/README.md — Step 5.5 added covering the doctrinal event
shape with all fields (semantic.family, .verb, .target, .sequence,
.intent) and the two naming shapes ({verb}-{variant} and
{family}-{verb}).
- sema/README.md — verbs section restructured to Record-by-family
matching the runtime; documents the verbs that moved between
families per the canon. Naming shapes section updated with both
forms and validateEventName signature.
- CLAUDE.md — "Session hand-off" rewritten with the post-canon
state. Pointer at GUIA_IMPLEMENTACION_SEMAUIX.md as the canonical
source. Pending list refreshed: switch is the immediate next
component, persistence/a11ySemantic/polymorphism deferred until
real consumers appear.
Switch migration to the eidos wrapper pattern:
- morfo/components/switch.ts — adds events: [{ commit-toggle }] with
the doctrinal shape (verb=toggle, sequence=post, intent fromProp
binding to the 4 supported intents). New data-* (data-color with
the 6-token Toggle/Switch subset, data-readonly, data-invalid,
data-required) and corresponding aria-*. scope expanded to
['soma', 'sema'].
- soma/components/switch/types.ts — adds intent, color, readonly,
invalid, aria-labelledby props with full JSDoc. Subset constrained
per guide §3.1 (Toggle/Switch row).
- soma/components/switch/switch-provider.svelte.ts — runtime-direct,
Field integration via FieldProvider.get(), isDisabled/isReadonly/
isRequired/isInvalid OR-merge derivations, intent ↔ color
resolution (intent wins when evaluative), runtime.trigger(
'commit-toggle') in onclick.
- soma/components/switch/components/switch.svelte — wires the new
props through bindProps.
- eidos/components/switch/{switch.css, switch.svelte, types.ts,
index.ts, README.md} — full subdirectory pattern, mirroring
toggle. Recipe drops legacy success/warning/danger/info selectors;
declares the 6 doctrinal data-color values directly. Wrapper
auto-renders Switch.Thumb inside the track and exposes an optional
`thumb` snippet for in-thumb glyphs.
- eidos/components/switch.css (legacy flat) deleted; eidos/index.css
updated to import the subdirectory recipe.
- web/routes/switch/+page.svelte — interactive testbed with all soma
props as live controls + tabs (Live, Soma, Morfo, Sema, Eidos,
Notes). Mirrors toggle's structure.
npm run check holds at the 39-error baseline. Tests 217/217 green.
Pending:
- Migrate collapsible / dialog / drawer / popover / toast / avatar
to the eidos wrapper pattern.
- Persistence + holds-by-intent — defer until first signal.warn /
signal.alert consumer.
- a11ySemantic + polymorphic events — defer until concrete need.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>