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. **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) ## Session hand-off — 2026-05-09 (selector discipline + lint reframing)
- **Typed selector builder applied** in - **Typed selector builder applied** in

@ -1,295 +1,243 @@
# Continue tomorrow # Continue tomorrow
Fecha de corte: **2026-05-27**. Rama: `active-uix`. Working tree con cambios Fecha de corte: **2026-05-28**. Rama: `active-uix`. 7 commits por
sin commitear listos para push. delante de `gita/active-uix` (push pendiente al cerrar la sesión).
## TL;DR de la sesión ## TL;DR de la sesión
Sesión maratón del Token Scope Contract — del bug del toggle (ya conocido) Sesión doble — primera mitad **arquitectónica** (toggle-group
hasta cobertura universal sin excepciones arquitectónicas: estructural), segunda mitad **UX del editor Words** (silencio del
canvas, rail visual, scroll interno, toolbar mínima, popovers, drag
1. **Diagnóstico inicial**: usuario reporta "el toggle no funciona, el sobre listas/tablas).
color del intent no cambia". Eager-resolution de CSS custom properties
en el cascade del toggle. ### Parte A — toggle-group structural identity
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, Cierra la deuda del hand-off 2026-05-27 #5: el item de toggle-group
private tokens, multi-part + composition). ahora DECLARA `data-toggle` en su morfo y la receta de Toggle pinta
3. **Universal migration**: 18 componentes con `data-color` migrados a TSC end-to-end. Cero duplicación de derivaciones.
(15 con TSC v2.1, + 3 con extensiones v2.2 de hoy).
4. **Bug crítico post-composition**: la toggle-group composition - `morfo/components/toggle-group.ts`: item part añade
correctamente override `--toggle-palette-*` pero el cascade colapsaba `{ attr: 'data-toggle', value: v.literal(''), severity: 'required' }`.
downstream porque los derived tokens (`--toggle-solid-on-bg` etc.) - `eidos/components/toggle-group/context.ts` (nuevo): contexto
viven en scope `[data-toggle]` (sibling, no ancestor, de Svelte tipado con getters reactivos para `variant` + `size`.
`[data-toggle-group-item]`). Fix: inlined derivations en - `eidos/.../toggle-group.svelte`: setea el contexto en el root.
toggle-group.css con palette-direct refs. - `eidos/.../toggle-group-item.svelte`: lee el contexto y escribe
5. **Bundle JIT purge** (`scripts/eidos-purge.ts`): tool standalone que `data-variant` + `data-size` en el button del item.
tree-shakes el `generated/base.css` por componente, var() ref, - `eidos/.../toggle-group.css`: ~150 líneas borradas (derivaciones,
data-attr y component import. Reduce ~50% el bundle final. variant cascades, size cascade, focus/disabled/icon-only
6. **Docs**: THEMING.md (~1700 líneas) como referencia canónica del duplicados). Solo queda layout/orientation/attached/block/focus
theming. CLAUDE.md con 5 hand-offs del 2026-05-27. Eidos README z-index/group-disabled.
actualizado. - TSC v2.2 composition (palette overrides en `[data-toggle-group]
[data-color='X'] [data-toggle-group-item]`) sigue activa — palette
## Tareas completadas hoy (resumen) + toggle's derivation chain hace el trabajo.
Total: 87 tareas TaskList. Sprint TSC ocupa #50–#87. Highlights: Verificación: 12 combinaciones (4 colores × 3 variants × 2 estados)
con computed values bit-a-bit idénticos al baseline pre-refactor.
- **TSC v1** (#50–#56): primera versión — `scope`/`depends` explícitos,
generator agrupando por scope. ### Parte B — auditoría npm check verde
- **TSC v2** (#57–#62): álgebra de scope (ScopeSet covers), cross-axis
collision detection, var() auto-inferred deps, color:* rename. `npm run check` pasó de 16 errores a **0 errors** en esta sesión.
- **P0–P2 housekeeping** (#63–#68): rename `--{c}-color-{role}-{slot}` →
`--{c}-{role}-{slot}`, JIT purge script. - 4 errores `orientation` drift en `soma/components/words-toolbar*`
- **CONSOL + GAP** (#69–#73): consolidación de docs, universal anti-eager- (deuda mía del sprint anterior).
resolution guard test. - 12 errores en el sprint Words activo:
- **UNIV** (#75–#79): private tokens (`_` prefix) + migración universal - `onUploadImage` propagado en soma `Words.Provider`.
de 13 componentes. - `'insert-image'` añadido a 2 Records `WordsToolbarButtonCommandName`.
- **EXT** (#80–#86): TSC v2.2 — multi-part `parts` + cross-recipe - `leafItem` snippet hoisted FUERA de `<SomaWords.Provider>` (en
`composition` + 3 componentes restantes migrados (`select`, Svelte 5, snippets dentro de un component element se pasan como
`toggle-group`, `avatar`) + docs. props del componente — el snippet era helper local, debe estar
- **EXT-FIX** (#87): bug del toggle-group color cascade tras la a nivel root de template).
composition migration. - 5 arrays inline `{id: ...}` en `words-drawer.svelte` extraídos
a constantes typed `as const satisfies readonly { id:
## Patrones canon nuevos en esta sesión WordsCommandName; ... }[]` (FORMAT_MARKS / TEXT_ALIGN_ACTIONS /
CELL_TONE_ACTIONS / CELL_VERTICAL_ACTIONS / CELL_TEXT_ALIGN_ACTIONS).
### Token Scope Contract (TSC) — la fuente de verdad
### Parte C — Words editor: fixes UX en cascada (commits EV-*)
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 Orden en el que el usuario fue pidiendo:
álgebra de cobertura. **Imposible introducir el bug eager-resolution si
declaras tokens en TSC** — el validator lo rechaza antes de generar CSS. **EV-A — spam de eventos**: `isInsideWordsTool` extendido a drawer,
block-handle, block-handle-menu, block-inserter, image-float-bar.
Formas: `contact-focus` target movido de `content` a `provider` en morfo +
sema cascade. (Más tarde EV-F1 lo desactivó del todo).
```ts
recipes.toggle = { **EV-B — toolbar slim**: presets reducidos a `[history-menu,
'height-md': '32px', // shorthand → root insert-menu, link-menu, tools-menu, find-replace]` quitando
'solid-on-bg': { // single decl text/block/list/align/table que ya viven en el drawer.
value: 'var(--toggle-palette-solid)', scope: 'host'
}, **EV-C — drag handle UX**: borrado `setDragImage(hoverBlockEl)`
'palette-solid': { // multi-decl para que el ghost sea solo el grip (no el bloque entero). Nuevo
declarations: [ `data-dragging` + CSS fade del ancla estático.
{ value: 'var(--toggle-neutral-solid)', scope: 'host' },
{ value: 'var(--toggle-affirm-solid)', scope: 'color:affirm' } **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'] { ... }
``` ```
125c62cc fix(words): silence canvas, silver rail, direct undo/redo, popover widths, Enter empty
Único consumer hoy: `select` (Trigger + Content). Cualquier componente ce954732 feat(words): internal scroll for the content area + capture-phase scroll sync
que tenga atributos cascadeados per-part (no en root) usa este patrón. 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
### TSC v2.2 — `composition: { ... }` para cross-recipe override 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)
```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);
// }
``` ```
Único consumer hoy: `toggle-group`. Pattern reutilizable para futuros ## Decisiones arquitectónicas (afectan al ecosistema)
wrappers compositivos (button-group, nav-menu, etc.).
### 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 **Doctrina nueva** (toggle-group sprint): cuando un wrapper componente
underscore prefix). NO aparece en el public CSS contract. Solo accesible reusa visualmente otro (como toggle-group reusa Toggle), la respuesta
desde la propia CSS recipe del componente. Convención: usar para slots canónica NO es duplicar el cascade ni extender TSC con un tercer
intermedios de derivation que no quieres exponer como theming knobs. 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 Patrón aplicable si emerge: button-group, link-group, nav-menu, etc.
un elemento que NO es descendant del scope donde se declaró ese token.
Resultado: undefined → cascade colapsa.
**Pattern correcto**: inline la derivation expression localmente. Ejemplo Documentado en CLAUDE.md hand-off 2026-05-28.
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.
## Trabajo acumulado SIN commitear ### 2. Words: tokens de la rail son FIXED-TONE (no theme-aware)
``` Las decisiones de visual del editor para la gutter rail son: silver
Modificados (eidos + soma touched durante TSC sprint): plano `#d4d4d4` + borde `#9a9a9a`. Estos NO son theme-aware — la
M src/uix/eidos/components/avatar/avatar.css intención es emular un margen físico de cuaderno, que debe verse
M src/uix/eidos/components/badge/badge.css igual en dark/light theme.
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)
```
### Variants son canon — NO theme-extensibles Tokens declarados en `lib/recipes/base.ts > words.rail-bg / rail-border`.
Decisión arquitectónica documentada hoy en `THEMING.md §19`. Los ### 3. Words: canvas perceptualmente silencioso
variants (`solid`, `outline`, `ghost`, `soft`, `surface`, etc.) son
fijos a nivel del framework — paralelos a las 8 sema families. El `contact-focus` event ya NO se dispara desde soma. Telemetry para
theme solo cambia palette/shadows. La fuente de verdad es la const focus está vacía (aceptado por el usuario: "no puede emitir eventos
`EIDOS_VARIANTS` en `lib/types.ts` con 5 archetypes es demencial"). Si en el futuro un consumidor necesita el evento,
(`control` / `selection` / `chip` / `marker` / `tabs`); los unions descomentar `void this.runtime.trigger('contact-focus', ...)` en
TS se derivan de ella via `[number]` indexed access. `words-provider.svelte.ts` (cerca de `onfocus`).
Variants component-specific (Banner `inline`/`overlay`/`persistent`, ### 4. Words: gutter overlays son AMBIENT (sin hover)
Spinner `bars`/`dots`/`ring`, Button `'plain'`) viven en cada
`components/{c}/types.ts`. El lint block-handle y block-inserter button NO tienen `:hover` styles.
`recipe-css-contract.test.ts > variant CSS selectors per component Estado visual cambia solo en data attrs (`data-open`, `data-grabbed`,
match the declared type union` valida bidireccionalmente que CSS `data-dragging`). Los overlays son afordancias ambientes, no botones
selectors y type unions coincidan. interactivos que se iluminen.
## Words extension system — estado y siguiente sprint ### 5. Words: drag handle + inserter cubren listas y tablas
**Hecho** (commits `1805b081` skeleton + `088d31ec` F2.3a): Los selectores de top-level blocks en block-handle y block-inserter
- F1.1 fix evento `contact-focus` molesto (transition-only + popover scope) ahora aceptan `data-words-node="block" | "list" | "table"`. Listas
- F1.2 declarar 9 `data-*` hardcoded en morfo ordenadas/desordenadas/check y tablas tienen drag handle + inserter
- F1.3 i18n keys kebab-case seam por debajo.
- F1.4 canonizar 9 event names a forma `{family}-{verb}-{variant}`
- F1.5 toolbarLayout `grouped|inline` con ResponsiveProp ### 6. EnterParagraph: bloque vacío
- 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
```
## Referencias canónicas tras la sesión Engine `insertParagraph` con guarda explícita:
- paragraph vacío + Enter → no-op (no crea otro bloque vacío)
- **Theming completo**: `src/uix/eidos/THEMING.md` (1700+ líneas, §18 - heading vacío + Enter → demote a paragraph (canonical Notion UX)
reescrito hoy como "Cobertura universal de TSC", §7 ampliado con - otros tipos vacíos: sin cambio (mantiene comportamiento existente
multi-part + composition). documentado en `operations.ts` para code/image/table/list/quote)
- **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".

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

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

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

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