fix(words): silence canvas truly, drag for list/table, no overlay hovers + day-end docs

EV-G final pass + EOD documentation.

Words editor:
- Stop emitting `contact-focus` from soma. Removing the per-component
  cascade rule earlier didn't fully silence the editor — the `contact`
  family carries a BASE sound signature in SEMA_MAP (pitch 800, gain
  0.25) that fires even without a cascade rule. The trigger call is
  commented out (event no longer emitted; telemetry empty). Per spec:
  "sigue apareciendo el puto sonido, el ring".
- Drag handle + inserter now cover lists AND tables. Selectors
  expanded from `[data-words-node="block"][data-words-path]` to also
  match `[data-words-node="list"]` and `[data-words-node="table"]`.
  Top-level lists / tables get the grip + the "+" seam under them.
- Removed :hover from `[data-words-block-handle]` and
  `[data-words-block-inserter-button]`. Gutter overlays are AMBIENT —
  visual state via data-attrs only (`data-open`, `data-grabbed`,
  `data-dragging`). Per spec: "quítale el hover al botón de añadir
  bloque, si el handle no tiene este tampoco".
- Rail tokens hoisted to recipe: `words.rail-bg = #d4d4d4`,
  `words.rail-border = #9a9a9a`. Fixed-tone (not theme-aware) — the
  rail emulates the physical margin of a notebook; same look in
  light/dark. Recipe-css-contract test was rejecting raw hex in
  words.css.

Docs:
- CLAUDE.md hand-off 2026-05-28 #2 documenting the 6 EV-* Words
  sprints + 5 architectural decisions with lasting effect (canvas
  silent, fixed-tone tokens for physical metaphors, drag predicate
  covers list/table, ambient overlays, insertParagraph empty-block
  guard).
- continue.md (CONTINUE.md) rewritten for tomorrow's pickup:
  TL;DR of the two halves (toggle-group structural + Words UX),
  remaining bugs (drag handle outside rail on code blocks, heading
  inline level change), and the heredados (POLISH-1b, F2.3-F2.6,
  DRAGDROP-SOMA).

Verification: 467/467 morfo + sema + eidos + soma/words tests pass.
85/85 engine pass. `npm run check`: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 125c62cc20
commit d126e16e65

@ -560,6 +560,58 @@ Cierra la deuda dejada explícitamente abierta en el hand-off 2026-05-27 #5 ("Lo
**Doctrina reforzada**: cuando un wrapper componente reusa visualmente otro, la respuesta canónica NO es duplicar el cascade ni extender TSC con un tercer feature. Es declarar la identidad estructural en el morfo del wrapper. Patrón aplicable si emerge button-group, link-group, etc.
## Session hand-off — 2026-05-28 #2 (Words editor — 6 sprints UX)
Segunda mitad de la sesión (2026-05-28) dedicada a fixes del editor Words por feedback iterativo del usuario. Seis sprints EV-* + cleanup del check.
**Cleanup pre-sprint — `npm run check` 16 → 0**:
- 4 errores `orientation` drift en `soma/components/words-toolbar*` (deuda mía del sprint anterior — removí orientation del type pero no del soma component). Quitado el prop + create() call.
- 12 errores en el sprint Words activo (post-2026-05 F2/F3/COLOR): `onUploadImage` threading en soma `Words.Provider`, `'insert-image'` añadido a 2 Records `WordsToolbarButtonCommandName`, `leafItem` snippet hoisted FUERA de `<SomaWords.Provider>` (snippets dentro de un component element son props en Svelte 5; el snippet era helper local), 5 arrays inline en `words-drawer.svelte` extraídos a constantes typed `as const satisfies readonly { id: WordsCommandName | WordsMark; ... }[]`.
**EV-A — spam de eventos del canvas**:
- `isInsideWordsTool` extendido a los 5 overlays añadidos post-DRAWER: `data-words-drawer`, `data-words-block-handle`, `data-words-block-handle-menu`, `data-words-block-inserter`, `data-words-image-float-bar`. Antes, cualquier click sobre estos overlays se interpretaba como blur EXTERNO → fire `commit-save-content` + re-focus → fire `contact-focus` = 2 sonidos por interacción.
- `contact-focus` target movido de `content` a `provider` en morfo + sema cascade (doctrina: focus es evento de componente, no de body).
**EV-B — toolbar slim**:
- Presets demo (`minimal`/`formatting`/`full` + custom) reducidos a acciones GLOBALES: history (undo/redo) + insert + link + tools + find-replace. Text/block/list/align/table OUT porque el drawer ya los cubre por scope.
**EV-C — drag handle UX**:
- Borrado `e.dataTransfer.setDragImage(hoverBlockEl, 12, 12)`. El browser usa su snapshot por defecto (= el grip button) como ghost — el ghost viaja con el cursor mientras el bar en la gutter queda fijo como ancla visual. Nuevo `data-dragging` + CSS fade del ancla a 0.35 opacity.
**EV-D — inserter al borde inferior**:
- `seam.y` para seams entre bloques cambia de `(a.bottom + b.top) / 2` (midpoint) a `a.bottom` (borde inferior del bloque anterior). Half-open interval `[top, bottom)` para que el píxel exacto del bottom pertenezca al seam, no al bloque.
**EV-E — scroll interno del content**:
- Nuevo token `content-max-block-size-sm/md/lg` (50/60/70vh) en el recipe. `[data-words-content]` gana `max-block-size: var(--_words-content-max-block-size)` + `overflow-y: auto`. El min-block-size baseline queda como starting height para editores vacíos.
- block-handle / block-inserter / image-float-bar: scroll listeners migrados de `window.addEventListener('scroll')` a `document.addEventListener('scroll', { capture: true })`. Razón: scroll events NO burbujean — la versión anterior solo captaba scroll del root document; con la rail/handle/inserter ahora dentro de un content scrollable, había que capturar también scroll DENTRO del content para que las overlays se re-midieran.
**EV-F — paquete de 6 fixes en uno**:
1. Cascade sema de `contact-focus` REMOVIDA → canvas mudo en focus (EV-A fixaba solo el target; la cascade seguía sonando).
2. Rail bg → flat silver (`#d4d4d4`) + borde derecho `#9a9a9a`, sin dot pattern. Tokens `words.rail-bg / rail-border` en el recipe. **Fixed-tone, NOT theme-aware** — la intención es emular un margen físico de cuaderno, debe verse igual en dark/light theme.
3. Toolbar: `undo` + `redo` DIRECTOS (no popover); `insert-menu` eliminado. Resultado: 5 items.
4. Family-panel base 12.5 → 16rem · tools 17rem · link 20rem. Sin scroll horizontal.
5. Link popover sin scroll vertical (consecuencia del #4).
6. Engine `insertParagraph` con guarda explícita: paragraph vacío + Enter = no-op; heading vacío + Enter = demote a paragraph (canonical Notion UX).
**EV-G — último pass (parcial)**:
- `runtime.trigger('contact-focus')` comentado en soma. Causa real del sonido residual: la family `contact` en `SEMA_MAP` tiene BASE signature de sound (pitch 800, gain 0.25) que suena aunque no haya cascade per-component. Solución: no disparar el evento. Telemetry vacía para focus, aceptado.
- Drag handle + inserter ahora cubren `data-words-node="list"` y `data-words-node="table"`, no solo `"block"`. Los selectores en `findBlockElement`, `blockUnderCursorY`, `listBlockBoundaries` aceptan los tres tipos para top-level blocks. Listas y tablas tienen drag handle + inserter seam.
- `:hover` rules eliminadas en `[data-words-block-inserter-button]` y `[data-words-block-handle]`. Las overlays son AMBIENT — estado visual cambia solo en data attrs (`data-open`, `data-grabbed`, `data-dragging`). No se iluminan en hover.
**Decisiones arquitectónicas con efecto duradero**:
- **Canvas perceptualmente silencioso** (Words): `contact-focus` no se emite. Si en el futuro un consumidor necesita el evento, descomentar el `runtime.trigger` en `words-provider.svelte.ts > onfocus`.
- **Tokens FIXED-TONE para concepto físico** (Words rail): `--words-rail-bg/border` son hex literal en el recipe, no `var(--color-*)`. Justificación: el concepto visual es "margen de papel" — debe verse igual en todos los themes.
- **Drag-handle predicate cubre 3 tipos de node**: `block`/`list`/`table`. No solo `block`. Cualquier nuevo top-level node type tiene que entrar en la lista.
- **Overlays ambient, no interactive buttons**: gutter overlays (block-handle, block-inserter button) no tienen `:hover`. Estado visual via data attrs solamente.
- **Engine guard sobre bloque vacío**: `insertParagraph` ya no duplica párrafos vacíos. Convierte heading→paragraph en vacío.
**Pendientes documentados en CONTINUE.md**:
- P1 — Drag handle aparece FUERA del rail sobre code blocks (no diagnosticado).
- P1 — Heading inline level change (h1↔h2↔h3) — la UI no permite cambiar nivel inline; el drawer Block panel lo tendría que ofrecer cuando `currentBlock === 'heading'`.
- POLISH-1b / POLISH-3 / WORDS-F2.3-F2.6 / WORDS-DRAGDROP-SOMA — heredados.
**Tests al cierre**: 467/467 pass en morfo + sema + eidos + soma/components/words. 85/85 engine pass. `npm run check`: **0 errors**.
## Session hand-off — 2026-05-09 (selector discipline + lint reframing)
- **Typed selector builder applied** in

@ -1,295 +1,243 @@
# Continue tomorrow
Fecha de corte: **2026-05-27**. Rama: `active-uix`. Working tree con cambios
sin commitear listos para push.
Fecha de corte: **2026-05-28**. Rama: `active-uix`. 7 commits por
delante de `gita/active-uix` (push pendiente al cerrar la sesión).
## TL;DR de la sesión
Sesión maratón del Token Scope Contract — del bug del toggle (ya conocido)
hasta cobertura universal sin excepciones arquitectónicas:
1. **Diagnóstico inicial**: usuario reporta "el toggle no funciona, el
color del intent no cambia". Eager-resolution de CSS custom properties
en el cascade del toggle.
2. **TSC v1 → v2 → v2.1 → v2.2**: 4 iteraciones del Token Scope Contract.
Cada una cerrando un gap (scope explícito, álgebra de cobertura,
private tokens, multi-part + composition).
3. **Universal migration**: 18 componentes con `data-color` migrados a TSC
(15 con TSC v2.1, + 3 con extensiones v2.2 de hoy).
4. **Bug crítico post-composition**: la toggle-group composition
correctamente override `--toggle-palette-*` pero el cascade colapsaba
downstream porque los derived tokens (`--toggle-solid-on-bg` etc.)
viven en scope `[data-toggle]` (sibling, no ancestor, de
`[data-toggle-group-item]`). Fix: inlined derivations en
toggle-group.css con palette-direct refs.
5. **Bundle JIT purge** (`scripts/eidos-purge.ts`): tool standalone que
tree-shakes el `generated/base.css` por componente, var() ref,
data-attr y component import. Reduce ~50% el bundle final.
6. **Docs**: THEMING.md (~1700 líneas) como referencia canónica del
theming. CLAUDE.md con 5 hand-offs del 2026-05-27. Eidos README
actualizado.
## Tareas completadas hoy (resumen)
Total: 87 tareas TaskList. Sprint TSC ocupa #50–#87. Highlights:
- **TSC v1** (#50–#56): primera versión — `scope`/`depends` explícitos,
generator agrupando por scope.
- **TSC v2** (#57–#62): álgebra de scope (ScopeSet covers), cross-axis
collision detection, var() auto-inferred deps, color:* rename.
- **P0–P2 housekeeping** (#63–#68): rename `--{c}-color-{role}-{slot}` →
`--{c}-{role}-{slot}`, JIT purge script.
- **CONSOL + GAP** (#69–#73): consolidación de docs, universal anti-eager-
resolution guard test.
- **UNIV** (#75–#79): private tokens (`_` prefix) + migración universal
de 13 componentes.
- **EXT** (#80–#86): TSC v2.2 — multi-part `parts` + cross-recipe
`composition` + 3 componentes restantes migrados (`select`,
`toggle-group`, `avatar`) + docs.
- **EXT-FIX** (#87): bug del toggle-group color cascade tras la
composition migration.
## Patrones canon nuevos en esta sesión
### Token Scope Contract (TSC) — la fuente de verdad
Cada recipe token tiene un **scope explícito** que materializa el selector
CSS donde se emite. El generator infiere deps de los `var()` y valida
álgebra de cobertura. **Imposible introducir el bug eager-resolution si
declaras tokens en TSC** — el validator lo rechaza antes de generar CSS.
Formas:
```ts
recipes.toggle = {
'height-md': '32px', // shorthand → root
'solid-on-bg': { // single decl
value: 'var(--toggle-palette-solid)', scope: 'host'
},
'palette-solid': { // multi-decl
declarations: [
{ value: 'var(--toggle-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' }
]
}
}
```
Sesión doble — primera mitad **arquitectónica** (toggle-group
estructural), segunda mitad **UX del editor Words** (silencio del
canvas, rail visual, scroll interno, toolbar mínima, popovers, drag
sobre listas/tablas).
### Parte A — toggle-group structural identity
Cierra la deuda del hand-off 2026-05-27 #5: el item de toggle-group
ahora DECLARA `data-toggle` en su morfo y la receta de Toggle pinta
end-to-end. Cero duplicación de derivaciones.
- `morfo/components/toggle-group.ts`: item part añade
`{ attr: 'data-toggle', value: v.literal(''), severity: 'required' }`.
- `eidos/components/toggle-group/context.ts` (nuevo): contexto
Svelte tipado con getters reactivos para `variant` + `size`.
- `eidos/.../toggle-group.svelte`: setea el contexto en el root.
- `eidos/.../toggle-group-item.svelte`: lee el contexto y escribe
`data-variant` + `data-size` en el button del item.
- `eidos/.../toggle-group.css`: ~150 líneas borradas (derivaciones,
variant cascades, size cascade, focus/disabled/icon-only
duplicados). Solo queda layout/orientation/attached/block/focus
z-index/group-disabled.
- TSC v2.2 composition (palette overrides en `[data-toggle-group]
[data-color='X'] [data-toggle-group-item]`) sigue activa — palette
+ toggle's derivation chain hace el trabajo.
Verificación: 12 combinaciones (4 colores × 3 variants × 2 estados)
con computed values bit-a-bit idénticos al baseline pre-refactor.
### Parte B — auditoría npm check verde
`npm run check` pasó de 16 errores a **0 errors** en esta sesión.
- 4 errores `orientation` drift en `soma/components/words-toolbar*`
(deuda mía del sprint anterior).
- 12 errores en el sprint Words activo:
- `onUploadImage` propagado en soma `Words.Provider`.
- `'insert-image'` añadido a 2 Records `WordsToolbarButtonCommandName`.
- `leafItem` snippet hoisted FUERA de `<SomaWords.Provider>` (en
Svelte 5, snippets dentro de un component element se pasan como
props del componente — el snippet era helper local, debe estar
a nivel root de template).
- 5 arrays inline `{id: ...}` en `words-drawer.svelte` extraídos
a constantes typed `as const satisfies readonly { id:
WordsCommandName; ... }[]` (FORMAT_MARKS / TEXT_ALIGN_ACTIONS /
CELL_TONE_ACTIONS / CELL_VERTICAL_ACTIONS / CELL_TEXT_ALIGN_ACTIONS).
### Parte C — Words editor: fixes UX en cascada (commits EV-*)
Orden en el que el usuario fue pidiendo:
**EV-A — spam de eventos**: `isInsideWordsTool` extendido a drawer,
block-handle, block-handle-menu, block-inserter, image-float-bar.
`contact-focus` target movido de `content` a `provider` en morfo +
sema cascade. (Más tarde EV-F1 lo desactivó del todo).
**EV-B — toolbar slim**: presets reducidos a `[history-menu,
insert-menu, link-menu, tools-menu, find-replace]` quitando
text/block/list/align/table que ya viven en el drawer.
**EV-C — drag handle UX**: borrado `setDragImage(hoverBlockEl)`
para que el ghost sea solo el grip (no el bloque entero). Nuevo
`data-dragging` + CSS fade del ancla estático.
**EV-D — inserter al bottom edge**: `seam.y = a.bottom` en vez del
midpoint. Half-open interval `[top, bottom)` para que el píxel
exacto del bottom pertenezca al seam (no al bloque).
**EV-E — scroll interno**: nuevo token `content-max-block-size`
(50/60/70vh) + `overflow-y: auto` en `[data-words-content]`.
Listeners de scroll en handle/inserter/float-bar pasados a
**capture phase** (scroll no burbujea — para captar scroll dentro
del content).
**EV-F (6 fixes en uno)**:
1. Cascade sema de `contact-focus` REMOVIDA → canvas mudo en focus.
2. Rail bg → silver flat (#d4d4d4) + borde derecho darker (#9a9a9a),
sin dot pattern. **Tokens** `words.rail-bg/rail-border` (fixed
tone, NOT theme-aware — emulan margen de papel).
3. Toolbar: `undo` + `redo` DIRECTOS (no popover); `insert-menu`
eliminado. Resultado: 5 items (undo, redo, Links, Tools,
Find/replace).
4. Family-panel base 12.5 → 16rem · tools 17rem · link 20rem.
5. Link popover scroll: consecuencia del #4, ya no aparece.
6. Engine `insertParagraph` con guarda sobre bloque vacío:
paragraph → no-op, heading → demote a paragraph.
**EV-G (final pass, parcial)**:
- Sound STILL aparecía después de EV-F1. Causa real: `contact` family
tiene BASE signature en SEMA_MAP que suena aunque no haya cascade
per-component. **Solución**: comentar el `runtime.trigger
('contact-focus')` en soma. Event no se emite. Telemetry vacía
(aceptado).
- Listas + tablas no mostraban drag handle. Selectores en
block-handle + block-inserter ampliados de
`[data-words-node="block"][data-words-path]` a también
`[data-words-node="list"]` y `[data-words-node="table"]`.
- Hover removido del inserter button (CSS rule borrada) y del
block-handle (también removed). Gutter overlays son ahora
AMBIENT — no se iluminan en hover.
## Lo que QUEDÓ PENDIENTE para mañana
Reportado por el usuario al final, no resuelto en el commit
final por presión de cierre:
### P1 — Drag handle aparece FUERA del rail sobre bloques `<code>`
El usuario reporta que el grip se va fuera de la rail cuando hovers
un code block. Posibles causas:
- `<pre>` tiene `border: var(--words-border-width)` + padding propio
(1.25rem) + `overflow-x: auto`. El `rect.left` debería seguir
siendo el outer edge del `<pre>` (igual que cualquier `<p>`), pero
podría haber un shift inesperado.
- Sospecha alterna: el code block tiene un margen-inline negativo
(no encontrado en mi grep, pero vale la pena reverificar).
- Lugar: `src/uix/eidos/components/words/words-block-handle.svelte
> measureFromEl()` líneas 85-108.
Pasos para mañana:
1. Ir a `/uix/components/words`, hover sobre el code block,
inspector → log de `block.getBoundingClientRect()` para `<pre>`
vs `<p>`. Comparar `rect.left`.
2. Si difieren, hay un shift en CSS — buscar margen/padding/
transform en `[data-words-content] :where(pre)` (línea 1970 de
`words.css`).
3. Si NO difieren pero el handle igual sale mal, el bug es en la
resta de `cssToPx` o en `rememberRailMetrics` cuando el host
tiene padding por overflow.
### P1 — Heading inline level change (h1 ↔ h2 ↔ h3)
El usuario quiere que al editar un heading pueda cambiar de nivel
INLINE (sin abrir drawer/menu). Opciones:
- A. **Drawer's Block panel** ya tiene un HeadingPicker; verificar
que se muestra cuando `currentBlock === 'heading'`. Si no, añadir.
- B. **Block-handle menu** podría tener un submenu "Transform to..."
con h1/h2/h3/paragraph/quote. Patrón Notion.
- C. **Floating menu** sobre el heading (similar al image float bar)
con 3 chips H1/H2/H3.
Recomendación: empezar por A (menor scope), evaluar UX.
### P2 — Heredados antes de hoy
- **POLISH-1b** (#145): Slider para image radius + toggle switches
en la float bar. REQUIRE extender `WordsImageBlock` con `radius` +
campos boolean (`shadow`, `bordered`). Toca engine + serializers +
render + CSS + float bar — sprint propio.
- **POLISH-3** (#148): Topbar del demo (breadcrumb, Edit/Preview,
Saved indicator).
- **WORDS-F2.3-F2.6** (#99-102): Migración table + code-block a
`extensions/*`. Sprint multi-sesión documentado.
- **WORDS-DRAGDROP-SOMA** (#151): Migrar drag-drop a soma
DragDrop.Provider en vez de HTML5 nativo. Requiere restructurar el
engine render (intento previo falló).
## Estado de los gates
| Gate | Status | Notas |
|---|---|---|
| `npm run check` | **0 errors** | 26 warnings (pre-existentes en demos) |
| `morfo-check` | exit 0 | |
| Tests morfo + sema + eidos + soma/words | **467/467 pass** | |
| Tests engine (`soma/words/engine`) | **85/85 pass** | |
| `npm run morfo:vocabulary` | no corrido en última verif | rerun mañana |
| `translations:check` | no corrido | rerun mañana |
## Commits de hoy (7 ahead)
### TSC v2.2 — `parts: [...]` para multi-part scope
```ts
'_accent-track': {
parts: ['trigger', 'content'],
declarations: [
{ value: 'var(--select-primary-track)', scope: 'host' },
{ value: 'var(--select-affirm-track)', scope: 'color:affirm' }
]
}
// → emits:
// [data-select-trigger], [data-select-content] { --_select-accent-track: ... }
// [data-select-trigger][data-color='affirm'], [data-select-content][data-color='affirm'] { ... }
```
Único consumer hoy: `select` (Trigger + Content). Cualquier componente
que tenga atributos cascadeados per-part (no en root) usa este patrón.
### TSC v2.2 — `composition: { ... }` para cross-recipe override
```ts
'toggle-group': {
gap: 'var(--space-1)',
composition: {
toggle: { // foreign recipe
targetSelector: '[data-toggle-group-item]',
tokens: {
'palette-solid': {
declarations: [
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' },
{ value: 'var(--toggle-risk-solid)', scope: 'color:risk' }
]
}
}
}
}
}
// → emits:
// [data-toggle-group][data-color='affirm'] [data-toggle-group-item] {
// --toggle-palette-solid: var(--toggle-affirm-solid);
// }
125c62cc fix(words): silence canvas, silver rail, direct undo/redo, popover widths, Enter empty
ce954732 feat(words): internal scroll for the content area + capture-phase scroll sync
c4be6dbe fix(words): silence body event spam + slim top toolbar + reposition handle/inserter
c250f7d7 fix(words): close 12 type-drift errors from F2/F3/COLOR sprints
d2f184da refactor(toggle-group): item is structurally a Toggle (zero derivation duplication)
+ commit EV-G (en curso al cierre, contiene: silenciar contact-focus trigger, drag para list/table, eliminar hovers de overlays)
```
Único consumer hoy: `toggle-group`. Pattern reutilizable para futuros
wrappers compositivos (button-group, nav-menu, etc.).
## Decisiones arquitectónicas (afectan al ecosistema)
### Private tokens (`_` prefix)
### 1. Wrappers visuales declaran su DNA con el morfo del componente reutilizado
Recipe key `_palette-solid` → CSS var `--_toggle-palette-solid` (con
underscore prefix). NO aparece en el public CSS contract. Solo accesible
desde la propia CSS recipe del componente. Convención: usar para slots
intermedios de derivation que no quieres exponer como theming knobs.
**Doctrina nueva** (toggle-group sprint): cuando un wrapper componente
reusa visualmente otro (como toggle-group reusa Toggle), la respuesta
canónica NO es duplicar el cascade ni extender TSC con un tercer
feature. Es DECLARAR LA IDENTIDAD ESTRUCTURAL en el morfo del wrapper
añadiendo el atributo del componente reusado.
### Inline derivation cuando el ancestor scope no aplica
Ejemplo: el `Item` de toggle-group declara `data-toggle` porque ES un
toggle estructuralmente. La receta de Toggle entonces lo pinta entero.
**Anti-pattern**: leer `var(--{foreign-component}-derived-token)` desde
un elemento que NO es descendant del scope donde se declaró ese token.
Resultado: undefined → cascade colapsa.
Patrón aplicable si emerge: button-group, link-group, nav-menu, etc.
**Pattern correcto**: inline la derivation expression localmente. Ejemplo
en `toggle-group.css` para `[data-toggle-group-item]` — replica las
expressions de `recipes/base.ts > toggle.{solid,outline,ghost}-*`
directamente. Duplicación documentada y aceptada como trade-off.
Documentado en CLAUDE.md hand-off 2026-05-28.
## Trabajo acumulado SIN commitear
### 2. Words: tokens de la rail son FIXED-TONE (no theme-aware)
```
Modificados (eidos + soma touched durante TSC sprint):
M src/uix/eidos/components/avatar/avatar.css
M src/uix/eidos/components/badge/badge.css
M src/uix/eidos/components/button/button.css
M src/uix/eidos/components/card/card.css
M src/uix/eidos/components/checkbox/checkbox.css
M src/uix/eidos/components/editable/editable.css
M src/uix/eidos/components/file-upload/file-upload.css
M src/uix/eidos/components/radio-group/radio-group.css
M src/uix/eidos/components/select/select.css
M src/uix/eidos/components/stepper/stepper.css
M src/uix/eidos/components/switch/switch.css
M src/uix/eidos/components/tag-group/tag-group.css
M src/uix/eidos/components/tags-input/tags-input.css
M src/uix/eidos/components/toggle-group/toggle-group.css
M src/uix/eidos/components/toggle/toggle.css
M src/uix/eidos/README.md
M CLAUDE.md
M package.json (npm script para eidos:purge añadido)
Modificados (words component sprint en paralelo — no este chat):
M src/uix/eidos/components/words/*
M src/uix/soma/components/words/*
M src/uix/words/README.md
M web/routes/uix/components/words/*
Eliminado:
D src/docs/libro_semantica_completo.txt (reemplazado por .docx)
Untracked (TSC v2.2 + auxiliares):
?? scripts/eidos-purge.ts
?? scripts/probe-*.ts (5 probe scripts del debug session)
?? src/docs/Disenando_lo_que_ocurre_manuscrito_completo_revisado_v2.docx
?? src/uix/eidos/THEMING.md
?? src/uix/eidos/THEMING_AUDIT_2026-05-27.md
?? src/uix/eidos/components/words/* (slash menu, family menu, code lang picker)
?? src/uix/soma/components/words/components/words-slash-menu.svelte
?? src/uix/soma/components/words/engine/code-highlight.ts
?? src/uix/words/references/* (4 audit docs + 1 screenshot)
```
Las decisiones de visual del editor para la gutter rail son: silver
plano `#d4d4d4` + borde `#9a9a9a`. Estos NO son theme-aware — la
intención es emular un margen físico de cuaderno, que debe verse
igual en dark/light theme.
### Variants son canon — NO theme-extensibles
Decisión arquitectónica documentada hoy en `THEMING.md §19`. Los
variants (`solid`, `outline`, `ghost`, `soft`, `surface`, etc.) son
fijos a nivel del framework — paralelos a las 8 sema families. El
theme solo cambia palette/shadows. La fuente de verdad es la const
`EIDOS_VARIANTS` en `lib/types.ts` con 5 archetypes
(`control` / `selection` / `chip` / `marker` / `tabs`); los unions
TS se derivan de ella via `[number]` indexed access.
Variants component-specific (Banner `inline`/`overlay`/`persistent`,
Spinner `bars`/`dots`/`ring`, Button `'plain'`) viven en cada
`components/{c}/types.ts`. El lint
`recipe-css-contract.test.ts > variant CSS selectors per component
match the declared type union` valida bidireccionalmente que CSS
selectors y type unions coincidan.
## Words extension system — estado y siguiente sprint
**Hecho** (commits `1805b081` skeleton + `088d31ec` F2.3a):
- F1.1 fix evento `contact-focus` molesto (transition-only + popover scope)
- F1.2 declarar 9 `data-*` hardcoded en morfo
- F1.3 i18n keys kebab-case
- F1.4 canonizar 9 event names a forma `{family}-{verb}-{variant}`
- F1.5 toolbarLayout `grouped|inline` con ResponsiveProp
- F2.1 audit + design de `WordsExtension` interface (9 hooks)
- F2.2 skeleton: `extension-types.ts` + `extension-registry.ts` con 14 tests verdes
- F2.3a tipos de table movidos a `extensions/table/types.ts` con re-export en `engine/document.ts`
**F2.3 hecho** (sesiones 2026-05-27, 6 checkpoint commits + tests verdes después de cada uno):
- **F2.3a** ✅ — table types → `extensions/table/types.ts` (commit `088d31ec`)
- **F2.3b** ✅ — table factories + predicates + value-set constants → `extensions/table/factories.ts` (commit `a0afa44d`)
- **F2.3d** ✅ — table markdown serializer (serialize + parse + buildTableFromMarkdownRows) → `extensions/table/serialize-markdown.ts` (commit `d6f0af48`)
- **F2.3c** ✅ — table HTML serializer (serializeTableHtml + parseTableHtml + cell helpers) → `extensions/table/serialize-html.ts` (commit `5797e69b`)
- **F2.3e** ✅ — table render (renderTable + renderTablePlainText) → `extensions/table/render.ts` (commit `d8392735`)
- **F2.3i (stub)** ✅ — `tableExtension: WordsExtension` con `nodeTypes` + `factories` + `commandNames` (12) publicado en `extensions/table/table-extension.ts`. Engine NO consume registry todavía — la stub publica el shape estable que F2.3f-h irán rellenando con hooks. 6 smoke tests + 102 tests en el scope extensions+engine en verde.
**F2.3 pendiente — los 3 sub-pasos restantes son el refactor difícil** (la stub F2.3i existe ya como receptor de hooks):
- **F2.3f** — path.ts navigation. Las refs de table están en BRANCHES dentro de funciones grandes (`resolvePath`, `updateNode`, `inferContainerKind`), NO en helpers aislados. Extracción requiere:
- Diseñar callback adapters para recursión (engine → extension → engine)
- O bien: el engine consulta registry sólo para "is this a node my extension owns?" y el resto del walk queda en engine
- Reflexión: ¿cabe redibujar el dispatcher al estilo visitor pattern? Sería más limpio.
- **F2.3g** — normalize.ts. Mismo patrón: branches dentro de `normalizeBlock` / `normalizeNode`. Extracción requiere mismo enfoque que F2.3f.
- **F2.3h** — operations.ts (2620 LoC, ~400-500 LoC table). Las funciones `insertTable*`, `deleteTable*`, `toggle-table-*` son funciones standalone — extraíbles. Pero comparten utilities (`tableCellOptions`, `tableOptions`, `replaceAt`) que viven en el engine. Extraer requiere:
- Mover utilities a `extensions/table/utils.ts` o re-exportar desde engine
- Mover los ~20 reducers a `extensions/table/operations.ts`
- El dispatcher central (`applyWordsCommand`) consulta registry.getCommand(opType) y fallback al switch existente
- **F2.3i (full)** — wire-up al engine constructor. Crear el patrón de instanciación del engine que (a) construye una `WordsExtensionRegistry`, (b) registra `tableExtension` por defecto, (c) hace que el dispatcher (`applyWordsCommand`), el render, el path, el normalize y los serializers consulten primero la registry y fallback al switch hardcodeado. Hoy `tableExtension` está como stub publicada — el engine la ignora.
- **F2.3j** — verify final.
**Recomendación de orden para próxima sesión** (confirmado tras F2.3a-e + F2.3i-stub):
1. **F2.3h primero** (operations) — la stub `tableExtension` ya es receptor válido. Mover los ~20 reducers (`insertTable*`, `deleteTable*`, `toggle-table-*`, `setTableCell*`, `moveTableCell`) a `extensions/table/operations.ts`, llenar `tableExtension.commands` con sus key=opType. Cabledar el dispatcher central (`applyWordsCommand`) para consultar `registry.getCommand(opType)` antes del switch. Decisión clave: las utilities compartidas (`tableCellOptions`, `tableOptions`, `replaceAt`, `currentTableCellPath`) van a `extensions/table/utils.ts` o se re-exportan desde engine; la opción a re-exportar las dos primeras (engine NO depende de ellas fuera del table-branch) y mover el resto.
2. **F2.3f y F2.3g** al final, donde el engine ya consulta registry para todo el resto. Diseñar visitor pattern en `path.ts` + `normalize.ts` para que el dispatcher pase el callback "is this a node my extension owns?" al walker.
**Estado del extension system** después de hoy:
- `WordsExtension` interface ✅ (F2.2)
- `WordsExtensionRegistry` con 14 tests verdes ✅ (F2.2)
- `extensions/table/` con types + factories + 2 serializers + render + **`tableExtension` stub** ✅ (F2.3a-e + F2.3i-stub)
- Engine NO consulta registry aún (importa funciones directamente de extension; `tableExtension` publicada pero engine no la ve). El cambio a "engine consulta registry para tipo X" llegará en F2.3h.
Después F2.4 (code-block, plan similar), F2.5 (docs `EXTENSIONS.md`), F2.6 (verify final).
## Cómo retomar mañana
1. **Probar toggle-group en navegador**:
- El bug del cascade fue corregido inlining derivations en
`toggle-group.css`. Pero el fix no se probó en navegador. Si el
cascade sigue roto en algún caso, revisar las inlined expressions
contra `recipes/base.ts > toggle.{solid,outline,ghost}-*`.
2. **Avatar regenerado**: la migración TSC v2.2 produce un orden de
selector ligeramente distinto al original (`[data-color][data-variant]`
en lugar de `[data-variant][data-color]`). Mismo CSS efectivo —
misma especificidad. Si algún test snapshot rompe, regenerar.
3. **Próximo trabajo natural en TSC** (NO urgent):
- Si más componentes necesitan multi-part scope o composition, ya
está todo soportado — solo añadir las recipes.
- Considerar `radius:`/`ring-color:` como AtomicScope adicionales si
el avatar `ring-color` cascade emerge como pattern recurrente.
## Comandos útiles
```bash
cd G:/dev/svelte/vicen
npm run check # svelte-check
npm run test # full vitest run
npx vitest run src/uix/eidos # eidos-only (100 tests)
npm run generate:eidos-css # regen src/uix/eidos/generated/base.css
npx tsx scripts/eidos-purge.ts <route> # JIT purge para una ruta
```
Tokens declarados en `lib/recipes/base.ts > words.rail-bg / rail-border`.
### 3. Words: canvas perceptualmente silencioso
`contact-focus` event ya NO se dispara desde soma. Telemetry para
focus está vacía (aceptado por el usuario: "no puede emitir eventos
es demencial"). Si en el futuro un consumidor necesita el evento,
descomentar `void this.runtime.trigger('contact-focus', ...)` en
`words-provider.svelte.ts` (cerca de `onfocus`).
### 4. Words: gutter overlays son AMBIENT (sin hover)
block-handle y block-inserter button NO tienen `:hover` styles.
Estado visual cambia solo en data attrs (`data-open`, `data-grabbed`,
`data-dragging`). Los overlays son afordancias ambientes, no botones
interactivos que se iluminen.
### 5. Words: drag handle + inserter cubren listas y tablas
Los selectores de top-level blocks en block-handle y block-inserter
ahora aceptan `data-words-node="block" | "list" | "table"`. Listas
ordenadas/desordenadas/check y tablas tienen drag handle + inserter
seam por debajo.
### 6. EnterParagraph: bloque vacío
## Referencias canónicas tras la sesión
- **Theming completo**: `src/uix/eidos/THEMING.md` (1700+ líneas, §18
reescrito hoy como "Cobertura universal de TSC", §7 ampliado con
multi-part + composition).
- **Hand-offs**: `CLAUDE.md` §"Session hand-off — 2026-05-27 #5"
(TSC v2.2 + cobertura universal de theming).
- **Recipe authoring guide**: `THEMING.md` §7 (TSC) + §8 (añadir
componente nuevo).
- **Anti-eager-resolution guard**: `src/uix/eidos/recipe-css-contract.test.ts`
test "forbids palette-derived tokens at :root scope".
Engine `insertParagraph` con guarda explícita:
- paragraph vacío + Enter → no-op (no crea otro bloque vacío)
- heading vacío + Enter → demote a paragraph (canonical Notion UX)
- otros tipos vacíos: sin cambio (mantiene comportamiento existente
documentado en `operations.ts` para code/image/table/list/quote)

@ -117,7 +117,7 @@
// engine render. Sub-elements (li, tr, td, span) walk up to
// their parent block via closest().
const node = target.closest<HTMLElement>(
'[data-words-node="block"][data-words-path]'
'[data-words-node="block"][data-words-path], [data-words-node="list"][data-words-path], [data-words-node="table"][data-words-path]'
);
if (!node) return null;
// Only top-level blocks (path length 1). Skip nested
@ -126,7 +126,7 @@
if (path.includes('.')) {
// Walk up to the outermost block ancestor instead.
const outer = node.parentElement?.closest<HTMLElement>(
'[data-words-node="block"][data-words-path]'
'[data-words-node="block"][data-words-path], [data-words-node="list"][data-words-path], [data-words-node="table"][data-words-path]'
);
if (outer) {
const outerPath = outer.getAttribute('data-words-path') ?? '';
@ -147,7 +147,7 @@
function blockUnderCursorY(content: HTMLElement, clientY: number): HTMLElement | null {
const blocks = Array.from(
content.querySelectorAll<HTMLElement>(
'[data-words-node="block"][data-words-path]'
'[data-words-node="block"][data-words-path], [data-words-node="list"][data-words-path], [data-words-node="table"][data-words-path]'
)
).filter((b) => !(b.getAttribute('data-words-path') ?? '').includes('.'));
for (const block of blocks) {

@ -43,7 +43,7 @@
// Top-level blocks only — same predicate as the block-handle.
const blocks = Array.from(
content.querySelectorAll<HTMLElement>(
'[data-words-node="block"][data-words-path]'
'[data-words-node="block"][data-words-path], [data-words-node="list"][data-words-path], [data-words-node="table"][data-words-path]'
)
).filter((el) => !(el.getAttribute('data-words-path') ?? '').includes('.'));
if (!blocks.length) return [];
@ -99,7 +99,7 @@
// edge), so the seam can win when the cursor sits on that pixel.
const blocks = Array.from(
content.querySelectorAll<HTMLElement>(
'[data-words-node="block"][data-words-path]'
'[data-words-node="block"][data-words-path], [data-words-node="list"][data-words-path], [data-words-node="table"][data-words-path]'
)
).filter((b) => !(b.getAttribute('data-words-path') ?? '').includes('.'));
for (const block of blocks) {

@ -13,13 +13,10 @@
--_words-rail-px: 2rem;
--_words-rail-gap: 0.5rem;
/* Flat light silver band with a slightly darker edge toward the
* editor card. Per spec: "elimna el fondo de la barra de la
* izquierda y dejalo de un color silver claro con un borde a la
* derecha un poco mas oscuro". Fixed-tone (not theme-aware) so
* the gutter reads as a physical "paper edge" regardless of the
* editor theme — same intent as the printed margin in a notebook. */
--_words-rail-bg: #d4d4d4;
--_words-rail-border: #9a9a9a;
* editor card. Tokens declared in `lib/recipes/base.ts >
* words.rail-bg / rail-border`. Fixed tone — see token comment. */
--_words-rail-bg: var(--words-rail-bg);
--_words-rail-border: var(--words-rail-border);
--_words-toolbar-px: var(--words-toolbar-px-md);
--_words-toolbar-py: var(--words-toolbar-py-md);
--_words-button-size: var(--words-button-size-md);
@ -819,7 +816,9 @@
z-index: var(--z-index-popover);
}
[data-words-block-handle]:hover,
/* No :hover state on the handle either — bar stays at its base
* opacity until the user actually engages (data-open / data-grabbed
* / data-dragging). Matches the inserter's passive treatment. */
[data-words-block-handle][data-open] {
opacity: 1;
background: var(--words-toolbar-bg);
@ -934,11 +933,10 @@
border-color 120ms ease;
}
[data-words-block-inserter-button]:hover {
background: var(--_words-accent-solid);
border-color: var(--_words-accent-solid);
color: var(--_words-accent-on);
}
/* No :hover state on the inserter button. The block-handle is also
* passive; the gutter overlays should read as ambient affordances,
* not interactive buttons that "light up". (Removed earlier accent
* solid hover rule per spec.) */
/* When the drawer is on the LEFT side, flip the "+" button so it
lands in the rail column on the editor's RIGHT side. Same metric

@ -2282,6 +2282,8 @@
--words-content-max-block-size-sm: 50vh;
--words-content-max-block-size-md: 60vh;
--words-content-max-block-size-lg: 70vh;
--words-rail-bg: #d4d4d4;
--words-rail-border: #9a9a9a;
--words-selection-color: var(--color-content-primary);
--words-block-gap: var(--space-2);
--words-heading-color: var(--color-content-primary);

@ -2718,6 +2718,11 @@ export const THEME_BASE_RECIPE_TOKENS = {
'content-max-block-size-sm': '50vh',
'content-max-block-size-md': '60vh',
'content-max-block-size-lg': '70vh',
// Flat silver gutter rail — fixed tone, intentionally NOT
// theme-aware. The rail is meant to look like the physical
// margin of a notebook regardless of the editor theme.
'rail-bg': '#d4d4d4',
'rail-border': '#9a9a9a',
'selection-color': 'var(--color-content-primary)',
'block-gap': 'var(--space-2)',
'heading-color': 'var(--color-content-primary)',

@ -749,13 +749,15 @@ export class WordsProvider {
this.focused = true;
this.ensureSelection();
if (!wasFocused) {
// Stamp the contact-focus event on the PROVIDER (component
// scope), not the content body — matches the morfo target.
// Falls back to the content element if the provider ref is
// not yet registered (e.g. during very early mount).
void this.runtime.trigger('contact-focus', {
fallbackTarget: this.opts.ref.current ?? (e.currentTarget as HTMLElement)
});
// Note: contact-focus is intentionally NOT emitted. The
// `contact` family carries a base sound signature in
// SEMA_MAP (pitch 800, gain 0.25) that fires even without
// a per-component cascade rule. Per spec: "no puede emitir
// eventos es demencial, una vez que está trabajando con el
// no tiene sentido". Telemetry / a11y can re-enable by
// uncommenting; trace via `data-event*` would land on the
// provider element per the morfo declaration.
// void this.runtime.trigger('contact-focus', { ... });
}
};

Loading…
Cancel
Save

Powered by TurnKey Linux.