16 KiB
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 paravariant+size.eidos/.../toggle-group.svelte: setea el contexto en el root.eidos/.../toggle-group-item.svelte: lee el contexto y escribedata-variant+data-sizeen 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
orientationdrift ensoma/components/words-toolbar*(deuda mía del sprint anterior). - 12 errores en el sprint Words activo:
onUploadImagepropagado en somaWords.Provider.'insert-image'añadido a 2 RecordsWordsToolbarButtonCommandName.leafItemsnippet hoisted FUERA de<SomaWords.Provider>(en Svelte 5, snippets dentro de un component element se pasan como props del componente — el snippet era helper local, debe estar a nivel root de template).- 5 arrays inline
{id: ...}enwords-drawer.svelteextraídos a constantes typedas 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):
- Cascade sema de
contact-focusREMOVIDA → canvas mudo en focus. - 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). - Toolbar:
undo+redoDIRECTOS (no popover);insert-menueliminado. Resultado: 5 items (undo, redo, Links, Tools, Find/replace). - Family-panel base 12.5 → 16rem · tools 17rem · link 20rem.
- Link popover scroll: consecuencia del #4, ya no aparece.
- Engine
insertParagraphcon 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:
contactfamily tiene BASE signature en SEMA_MAP que suena aunque no haya cascade per-component. Solución: comentar elruntime.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.intentusaSemaIntentcanónico (nowarning/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 conid+visualopcionales, BlockVisual RAW (números px + hex), per-typePick<>(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),WordsEvalIntentdeclarado localmente para evitar dependency a$uix/sema(con plan de runtime parity assertion).engine/validate-v2.ts(~500 líneas): validador purovalidateWordsDocument(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. |
| R3 — Render + HTML + MD serializers V2 | ✅ Cerrado | render-v2.ts (pure WordsDocumentV2 → WordsRenderNodeV2; emite inline styles desde block.visual; nuevos tags hr/div para divider/callout). serialize-html-v2.ts (HTML escapado correctamente + roundtrip-friendly via inline styles + pretty mode). serialize-markdown-v2.ts (LOSSY: drop visual + color/bg marks; preserva estructura; INTENT_TO_GFM_ADMONITION mapping risk→WARNING/etc.). 56 tests. POLISH-1b deferida a R5 porque requiere provider V2 (R4 antes). Total engine: 226/226 pass. |
| R4A.1 — Foundation V2 ops | ✅ Cerrado | operations-v2/{types,normalize,visual,block,index}.ts (~600 líneas). normalizeDocumentV2 (autogen ids + seed empty containers + coalesce text + strip code marks). setBlockVisual (operación EXCLUSIVA V2, P8 whitelist runtime, base directa para POLISH-1b). 5 block ops: updateBlockAt, deleteBlockAt, insertBlockAt, moveBlockAt, duplicateBlockAt, moveBlockToAt. 31 tests. Engine total: 257/257 pass. |
| R4A.2 — Text + selection ops | ⏳ Pendiente | Operaciones de texto (insertText, deleteRange, insertParagraph, deleteBackward, deleteForward, toggleMark, clearFormatting, applyMarkdownShortcut). ~2500 líneas de V1 ops a mecánicamente traducir. Sprint dedicado. |
| R4A.3 — Container ops | ⏳ Pendiente | List ops (toggleList, increaseIndent, decreaseIndent, toggleCheckItem), Table ops (insertTable, insertTableRow, insertTableColumn, deleteTableRow, deleteTableColumn, moveTableCell, toggleTableHeaderRow, toggleTableHeaderColumn — NOTA: striped/compact NO en V2), link ops (insertLink, unlink), code ops (exitCodeBlock, outdentCodeLine, setCodeLanguage). |
| R4B — Provider migration | ⏳ Pendiente | WordsProvider re-cabledado para operar sobre V2 internamente. Mount: migrateV1ToV2 al cargar value. Runtime sidecar imageStatus poblado. API pública mantiene shape V1 vía adapter. |
| R4C — Eidos opt-in | ⏳ Pendiente | Wrapper eidos puede pasar V2 doc directamente. Render eidos usa render-v2 cuando recibe doc V2. Compat para consumers V1 vía migrate-on-read. |
| R5 — POLISH-1b (image visuals) | ⏳ Pendiente | Drawer image panel: sliders (radius, shadow blur), color pickers (shadow color, borderColor, background), toggles (shadow, border). Provider operation setImageVisual(blockId, patch). Requiere R4. |
| R6 — V1 removal | ⏳ Pendiente | Una vez R4+R5 estables: borrar V1 types/operations. Audit script: 0 referencias a V1 en engine/extensions. |
| 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 <code>
El usuario reporta que el grip se va fuera de la rail cuando hovers un code block. Posibles causas:
<pre>tieneborder: var(--words-border-width)+ padding propio (1.25rem) +overflow-x: auto. Elrect.leftdeberí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:
- Ir a
/uix/components/words, hover sobre el code block, inspector → log deblock.getBoundingClientRect()para<pre>vs<p>. Compararrect.left. - Si difieren, hay un shift en CSS — buscar margen/padding/
transform en
[data-words-content] :where(pre)(línea 1970 dewords.css). - Si NO difieren pero el handle igual sale mal, el bug es en la
resta de
cssToPxo enrememberRailMetricscuando 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
WordsImageBlockconradius+ 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.tspara code/image/table/list/quote)