diff --git a/CLAUDE.md b/CLAUDE.md index 8121e68ed..e3966a72c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `` (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 diff --git a/continue.md b/continue.md index cc4128f34..cb89bd052 100644 --- a/continue.md +++ b/continue.md @@ -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 `` (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 `` + +El usuario reporta que el grip se va fuera de la rail cuando hovers +un code block. Posibles causas: + +- `
` tiene `border: var(--words-border-width)` + padding propio
+  (1.25rem) + `overflow-x: auto`. El `rect.left` debería seguir
+  siendo el outer edge del `
` (igual que cualquier `

`), 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 `

`
+   vs `

`. 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 # 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) diff --git a/src/uix/eidos/components/words/words-block-handle.svelte b/src/uix/eidos/components/words/words-block-handle.svelte index d4b2516ad..6cb1e7f6e 100644 --- a/src/uix/eidos/components/words/words-block-handle.svelte +++ b/src/uix/eidos/components/words/words-block-handle.svelte @@ -117,7 +117,7 @@ // engine render. Sub-elements (li, tr, td, span) walk up to // their parent block via closest(). const node = target.closest( - '[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( - '[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( - '[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) { diff --git a/src/uix/eidos/components/words/words-block-inserter.svelte b/src/uix/eidos/components/words/words-block-inserter.svelte index 1e0d7136c..9e11a519d 100644 --- a/src/uix/eidos/components/words/words-block-inserter.svelte +++ b/src/uix/eidos/components/words/words-block-inserter.svelte @@ -43,7 +43,7 @@ // Top-level blocks only — same predicate as the block-handle. const blocks = Array.from( content.querySelectorAll( - '[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( - '[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) { diff --git a/src/uix/eidos/components/words/words.css b/src/uix/eidos/components/words/words.css index 1f5cf5a7a..4370e9c9d 100644 --- a/src/uix/eidos/components/words/words.css +++ b/src/uix/eidos/components/words/words.css @@ -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 diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 88ac7d1bc..efac12a87 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index ca63ea0f9..9a29be02e 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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)', diff --git a/src/uix/soma/components/words/words-provider.svelte.ts b/src/uix/soma/components/words/words-provider.svelte.ts index 8ede5c0df..62594cef5 100644 --- a/src/uix/soma/components/words/words-provider.svelte.ts +++ b/src/uix/soma/components/words/words-provider.svelte.ts @@ -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', { ... }); } };