# Continue tomorrow 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 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. ## Sprint Words V2 architecture — R1 cerrado Tras la discusión de arquitectura (separación contenido/diseño, P1-P8 + decisiones D-SEM-1/2/3 + Q1-Q6), R1 está cerrado. **Doctrina firmada** (full text en `src/uix/soma/components/words/ARCHITECTURE_PROPOSAL.md`): - P1-P8 principios firmados (incluido P6 Single Source of Truth + P7 Vocabulario evaluativo único + P8 reglas para qué entra al modelo) - D-SEM-2 → `CalloutBlock.intent` usa `SemaIntent` canónico (no `warning/info/success/danger`) - D-Q1-Q6 → id opcional autogenerado, MD lossy total, no presets en core, engine package en R5 (opcional) **Implementado en R1**: - `engine/types-v2.ts` (~300 líneas): WordsDocumentV2 v2.0.0, BlockBase con `id` + `visual` opcionales, BlockVisual RAW (números px + hex), per-type `Pick<>` (ParagraphVisual/HeadingVisual/QuoteVisual/CodeVisual/ListVisual/TableVisual/ImageVisual/DividerVisual/CalloutVisual), 9 bloques (paragraph/heading/quote/code/list/table/image + **divider** y **callout** nuevos), marks structured (`{type:'color'|'background', value:'#hex'}` en vez de template literal V1), `WordsEvalIntent` declarado localmente para evitar dependency a `$uix/sema` (con plan de runtime parity assertion). - `engine/validate-v2.ts` (~500 líneas): validador puro `validateWordsDocument(input) → ValidationResult`. 7 reglas duras + 11 códigos de error. Path JSON-pointer en cada error. - `engine/validate-v2.test.ts` (50 tests): cubre topo, todos los block types, marks (boolean + structured + rechazo del format V1 + rechazo de tokens), visual con whitelist per-type (P8), id uniqueness, callout SemaIntent + rechazo de "warning"/"info"/etc., divider, quote.cite, link nesting + URL parseability, table headerRow/headerCol/per-cell+per-row visual. **Estado tras R1**: 135/135 engine tests (85 V1 + 50 V2) · `npm run check` 0 errors. V1 sigue intacto, V2 en paralelo. ## Roadmap V2 pendiente | Sprint | Estado | Qué hace | |---|---|---| | **R1 — Audit + tipos + validador** | ✅ Cerrado | types-v2.ts + validate-v2.ts + 50 tests | | **R2 — Migrator V1→V2 + parity check** | ✅ Cerrado | migrate-v2.ts (P4: image.status → runtimeState.imageStatus; reshape marks template literal → structured; drop cell.tone / table.striped / table.compact con warnings; idempotente sobre V2; output siempre pasa validateWordsDocument). sema-parity.ts (assertSemaIntentParity contra $uix/intent.INTENTS). 35 tests. Total engine: 170/170 pass. | | **R3 — Visual sidecar implementation** | ⏳ Pendiente | Render emite inline styles desde block.visual. POLISH-1b (image radius/shadow/border) implementable bajo este modelo. Drawer image panel con controles. | | **R4 — Sweep tokens del contenido** | ⏳ Pendiente | Eliminar referencias `cell-tone` / `table-striped` / `table-compact` del rest del codebase (provider, render, eidos, serializers). Drawer panels actualizados. Audit script: 0 referencias a tokens en engine/extensions. | | **R5 — Engine as package (opcional)** | ⏳ Diferible | Mover engine/ a paquete propio. Eidos consume vía path normal. | ## 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) ``` 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) ``` ## Decisiones arquitectónicas (afectan al ecosistema) ### 1. Wrappers visuales declaran su DNA con el morfo del componente reutilizado **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. Ejemplo: el `Item` de toggle-group declara `data-toggle` porque ES un toggle estructuralmente. La receta de Toggle entonces lo pinta entero. Patrón aplicable si emerge: button-group, link-group, nav-menu, etc. Documentado en CLAUDE.md hand-off 2026-05-28. ### 2. Words: tokens de la rail son FIXED-TONE (no theme-aware) 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. 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 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)