You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/continue.md

21 KiB

Continue tomorrow

Fecha de corte: 2026-06-01. Rama: active-uix.

Sesión 2026-06-01 — Words inspector UX (sprint en curso)

Sprint de pulido del inspector del editor Words (sidebar derecha) por feedback iterativo del usuario. Todo verificado en navegador real (localhost:5173/uix/components/words, Chrome tab). npm run check: 1 error pre-existente (engine/operations/visual.ts:147, baseline, NO mío). Words soma 452/452. Contract test keeps raw color values out of component CSS pasa.

Lo cerrado y verificado este sprint

Ronda 1 — 7 ítems (todos DONE + verificados):

  1. Quitado el dropdown "Add block" del panel de BLOQUE Columns (vive solo en el inspector específico de columna). dropdownsInColumnsBlockPanel: 0.
  2. Validación de ancho de columna (isValidColumnWidth + commitColumnWidth en words-block-panel.svelte): nº flex / longitud CSS / fr / keywords; garbage → [data-invalid], no commit.
  3. Validación de URL de imagen vía sanitizeWordsUrl (export añadido al barrel soma/components/words/exports.ts): javascript: rechazado.
  4. Shift+Enter soft break: ya funcionaba (\n + white-space: pre-wrap); verificado con keystrokes reales MARKA⇧⏎MARKB → mismo bloque. 5+7. Rediseño cabecera inspector: mark de acento por profundidad (primary/affirm/loss/risk) + wash + divisor + breadcrumb estilado ([data-words-inspector-breadcrumb], single-row scroll).
  5. Botones add/delete normalizados a icon-only (Trash2 delete, flechas Arrow{Up,Down,Left,Right} para table-ops, Plus para add).

Ronda 2 — chrome systémico (DONE + verificado):

  • Inputs unificados: un solo frame bordeado (text inputs + NumberFields), un solo focus-ring de acento (NO más outline inset sobre el texto, NO más mezcla ghost/borde-negro). Specificity [data-number-field][data-size] (0,3,0) gana al recipe sin tocar el componente.
  • NumberField rowhead ensanchado a 6.75rem (decimales visibles); inline rows pinneados a 6.75rem (Ancho/Alto misma anchura).
  • Clamp de formato: WordsNumRow reenvía max → line-height clampa a 2.5, font-size 12–40. Adiós "Interlineado 191".
  • Secciones separadas: divisor hairline + headers uppercase en el acordeón ([data-accordion-item] + [data-accordion-item]).
  • Spacing link sincroniza al vincular (adopta el mayor de los dos lados).
  • Colores raw → tokens --color-*-solid (theme-aware, sobreviven al portal del popover).

Ronda 3 — defectos de controles (DONE + verificado):

  • ToggleGroups con valor por defecto seleccionado (uno siempre activo): Fuente=default, Grosor=400/Normal, Alinear=left, Borde=none. El default se almacena como undefined en el modelo (limpio). Verificado: Predeterminada/Normal/Izquierda/Ninguno seleccionados al activar bloque.
  • Borde gana opción none (chip que limpia border: undefined). Nuevo type BorderChipValue = 'none' | BorderStyleValue. Langs BORDER_NONE
    • bundle border.none ({en:'None', es:'Ninguno'}).
  • Image "Ancho total" → Switch (era Toggle con texto on/off).
  • Bug del radio de esquina arreglado: el ring de selección era un outline/box-shadow que HEREDA el border-radius del bloque, así que redondear el bloque a 40px redondeaba el ring. Ahora el ring es un pseudo-elemento ::after con radio FIJO (4px), inset: -3px, pointer-events: none. Verificado: blockRadius=40px, ringRadius=4px, decoupled=true.
  • Slider-only para "Grosor del borde" + "Radio de esquina": nuevo prop sliderOnly en WordsNumRow que oculta el NumberField y muestra un chip de valor read-only (el slider + input era redundante). Verificado: hasNumberField:false, hasValueChip:true, hasSlider:true.

Archivos tocados este sprint

  • src/uix/eidos/components/words/words-inspector.svelte — header markup, 4 ToggleGroups con default + border none, sliderOnly en 2 numrows.
  • src/uix/eidos/components/words/words-block-panel.svelte — validaciones (width/gap/src), table-ops icon-only, image full-width → Switch.
  • src/uix/eidos/components/words/words-num-row.svelte — max forward + prop sliderOnly.
  • src/uix/eidos/components/words/words-spacing-row.svelte — link sincroniza.
  • src/uix/eidos/components/words/words.css — chrome unificado, breadcrumb, ring pseudo-elemento, tokens de color, anchuras inline.
  • src/uix/eidos/components/words/langs-inspector.ts — BORDER_NONE + bundle.
  • src/uix/soma/components/words/exports.ts — export sanitizeWordsUrl.
  • (sesión previa, ya en el árbol) words-activate.svelte, engine/block-tree.ts (active-block por id materializado).

Pendiente / próximos pasos (donde retomar)

  • El usuario sigue iterando sobre el diseño del inspector — esperar su siguiente ronda de feedback visual. El patrón establecido: leer el estado EXACTO del control antes de editar (los Edit fallan por reformateo de prettier entre llamadas; usar substrings únicos cortos).
  • Defecto conocido NO abordado: nada pendiente reportado al cierre.
  • npm run check baseline error en visual.ts:147 (cast WordsNode → Record) sigue ahí — no es de este sprint.

Nota de proceso

El harness de tools devolvió output vacío/corrupto varias veces esta sesión (batches paralelos grandes se cancelan). Mitigación: editar de uno en uno, verificar con npm run check (verdad de tipos) + navegador (verdad de runtime) cuando un Read/Bash dé resultado dudoso.


Sesión 2026-05-29 (anterior)

Fecha de corte: 2026-05-29. Rama: active-uix.

TL;DR de la sesión

V1 del editor Words ELIMINADO. V2 es ahora el único motor.

La sesión arrancó con una auditoría del componente Words (codex-full-audit.md en la raíz) y la pregunta del usuario: "elimina v1 y continua v2, ¿qué sentido tiene v1?". Decisión firmada y ejecutada de cabo a rabo (F1 → F6). Ya no hay coexistencia V1/V2 ni migrador: el modelo de documento público es WordsDocumentV2 y punto.

Decisiones firmadas (vinculantes)

  • D1 = "V2 puro, sin legacy": borrados engine/document.ts + engine/migrate-v2.ts. SIN importador de retrocompatibilidad. La API pública pasa de WordsDocument → WordsDocumentV2. Los docs persistidos en V1 dejan de cargar (ruptura aceptada por el usuario).
  • D2 = "Aceptar el drop": eliminados por completo los controles / props / commands / atributos morfo de tabla striped, compact y cell-tone. NO se re-mapean ni se re-portan.

Qué se borró (V1) y a dónde se movió (V2)

Borrado — motor V1 (engine/*.ts): commands.ts, document.ts, dom-selection.ts, engine.test.ts, find-replace.ts, history.ts, index.ts (barrel V1), input.ts, migrate-v2.ts (+test), operations.ts, render.ts, serialize-html.ts, serialize-json.ts, serialize-markdown.ts, serialize-text.ts.

Borrado — árbol extensions/ entero: extension-registry (+test), extension-types, image/*, table/*. El modelo V2 no usa el sistema de extensiones; tabla/imagen/divider/callout son bloques nativos del union WordsBlockV2.

El motor V2 vive SOLO en engine/operations-v2/ + un puñado de .ts en la raíz de engine/ (types-v2, validate-v2, normalize, render-v2, serialize-html-v2, serialize-markdown-v2, sema-parity, code-highlight, path, selection, dom). Esta tanda portó al namespace V2 lo que faltaba de V1: commands, history, input, find-replace, render-dom, serialize-text, extra-ops, insert-block-types, markdown (input-rule shortcuts), factories, dom-helpers, dom-selection — además de cerrar R4A.2→R4A.8 (text / delete / marks-ops / paragraph-ops / delete-ops / block-format / list-ops / link-ops / table-ops / selection-walkers / inline-split).

NO existe parser HTML / Markdown / JSON-schema en V2. Las serializaciones HTML y Markdown son export-only (lossy → sin inverso fiel). El import sólo acepta JSON (JSON.parse + validateWordsDocument) y texto plano (parseWordsPlainText).

Superficie pública V2 (lo que consume cualquier app)

  • Tipos: WordsDocumentV2, WordsBlockV2, WordsInlineV2, WordsSelection, WordsExportFormat, WordsImportFormat — todos re-exportados desde $soma/components/words.
  • WORDS_DOCUMENT_VERSION_V2 = '2.0.0' desde engine/types-v2.
  • Provider (words-provider.svelte.ts): importContent (text + json), exportContent (json / text / html / markdown), plainText derivado vía renderWordsPlainText.
  • Serializers: serializeHtmlV2(doc), serializeMarkdownV2(doc), renderWordsPlainText(doc), renderWordsDocumentV2, renderWordsDomHtml / Pretty.
  • Comando central: applyWordsCommand(state, WordsCommandV2) en operations-v2/commands.ts.

Forma de los contenedores V2 (recordatorio — NO hay discriminador type en sub-nodos)

  • Tabla: { type:'table', rows, headerRow?, headerCol? }; fila { cells, visual? }; celda { children, align?, verticalAlign?, colspan?, rowspan?, visual? }. Celda usa align, no textAlign.
  • Lista: { type:'list', kind, items }; item { children, checked?, indent? }. items, no children. Sin type:'list-item'.
  • Bloques con alineación: paragraph/heading/quote llevan textAlign? (a nivel bloque).

✅ Decisión FIRMADA (2026-06-02) — export-only (no roundtrip)

serializeHtml / serializeMarkdown emiten proyecciones de export (HTML semántico limpio / GFM), no un formato re-importable, y así se queda: JSON es el formato canónico lossless; HTML/MD son export-only. Misma postura que TipTap/ProseMirror, Lexical, Slate y Notion — el modelo JSON persiste, HTML/MD son proyecciones. El import acepta sólo JSON (vía validateWordsDocument) + texto plano (parseWordsPlainText). Si en el futuro se necesita pegar HTML externo, se añade un parseHtml por-bloque (patrón parseDOM/importDOM de ProseMirror/Lexical) como interop best-effort de portapapeles — nunca como persistencia; markdown import no merece la pena (es lo más lossy). Doctrina completa + tabla de formatos en src/uix/soma/components/words/README.md → "Serializacion e intercambio".

Verificación al cierre (todo verde)

Gate Status Notas
npm run check 0 errors / 5168 files 26 warnings pre-existentes, ninguno en archivos tocados
npx vitest run src/uix/soma/components/words 455/456 el único fallo es el flake de sema-parity por timeout 5s bajo carga paralela
sema-parity aislado 6/6 en 362ms confirma que el flake es de infra, no regresión
npm run morfo:vocabulary exit 0 sólo WARN suaves (vocabularios noveles legítimos)
Browser smoke /uix/components/words OK mount limpio, 0 console errors

Smoke detallado: el doc sample V2 renderiza (tabla 2×2 con headerRow, 4 list-items incl. el indentado); export markdown emite tabla GFM válida; import texto round-trip ("imported text"); import JSON válido round-trip a través del gate ("imported json"); import JSON inválido (versión mala) → "invalid json" y deja el doc intacto (la razón de añadir el gate validateWordsDocument: un doc malo asignado al value bindeado rompería los $derived).

Consumidores re-cableados a V2

  • words-provider.svelte.ts + words-provider.svelte.test.ts
  • exports.ts + types.ts (superficie pública)
  • morfo/components/words.ts (drop de atributos striped/compact/tone)
  • eidos: words.svelte, words-drawer.svelte, words-block-handle, words-block-inserter, words-command-icon, words-heading-picker, words-toolbar-family-menu, words.css, types.ts
  • soma components/words.svelte
  • test/words-content-harness.svelte (importaba el barrel V1 borrado; reescrito a operations-v2 + types-v2 + WordsDocumentV2)
  • web/routes/uix/components/words/+page.svelte (último consumidor; imports V2, importFormats → ['json','text'], docs sample/empty/long a forma V2, walkers readPlainText/blockToPlainText/readInlineText sobre el union V2, exportWordsDocument + importDraftDocument reescritos, prosa de la API-tab limpiada de striped/tone)

Cerrado en la continuación (2026-05-29)

✅ Limpieza de huérfanos del drop D2

Borrados los selectores CSS muertos de words.css ([data-command='table-cell-tone-*'], table[data-words-table-striped], [data-words-table-compact], [data-words-cell-tone] + focus-within, y los [data-words-cell-header] huérfanos — los headers se renderizan como <th>). README actualizado a la realidad V2 (tabla lista solo formato vivo; HTML/MD export-only; import JSON + texto). ARCHITECTURE_PROPOSAL.md intacto: documenta la decisión de eliminarlos, no features stale. Verificado en browser (tabla 2×2 renderiza, <th> conserva su tinte vía :where(th)). Commit 4a0661a5.

✅ R5 — POLISH-1b (image visuals)

El panel de imagen del drawer (eidos/components/words/words-drawer.svelte > imagePanel) ya tenía radius slider + shadow/border toggles. Añadido lo que faltaba: shadow blur slider, shadow color picker, border color picker y background color picker, vía un snippet reutilizable imageColorField (presets + <input type=color> + clear opcional), mismo patrón que el color de texto del panel format. Todos despachan setBlockVisual → block.visual.* → visualToStyle → inline style en el <figure>. Verificado en browser: el panel renderiza los 4 controles nuevos; click en swatch de Background actualiza el figure (background-color:#22c55e) preservando radius/border/shadow (shallow-merge correcto). npm run check 0 errors.

✅ P1 — Drag handle sobre <code> (NO reproduce)

Investigado: NO reproduce en el código actual. El "rail" al que se refería el bug fue ELIMINADO en el rediseño de gutter (commit 5450d5f7, "rail out"), posterior al sprint EV donde se reportó. measureFromEl (words-block-handle.svelte) ancla el handle a contentRect.left + 8 — block-type-agnostic. Verificado en browser: sobre code y sobre paragraph el gutter cae en left: 350.333 idéntico (solo difiere top). No hay nada que arreglar; bug obsoleto.

✅ P1 — Heading inline level change (h1 ↔ h2 ↔ h3)

Añadido al Block panel del drawer (eidos/components/words/words-drawer.svelte > blockPanel): cuando currentBlock === 'heading', un ToggleGroup H1/H2/H3 (value = currentHeadingLevel) despacha applyCommand({ type: 'setBlock', block: 'heading', level }). runCommand / WordsCommandName no tiene id por-nivel (heading-1/2/3 son ids de slash / insert-menu), por eso se usa el comando estructurado setBlock con el nivel numérico (as const mantiene cada level como literal 1|2|3). Verificado en browser: caret en heading → el panel muestra "Heading level"; el toggle refleja el nivel actual; el click convierte bidireccional (H1→H3→H2) y el toggle se actualiza. npm run check 0 errors.

Rework del editor Words (2026-05-29 #2 — redirect del usuario)

El usuario redirigió a un nuevo bloque de trabajo sobre el editor. Plan en 3 pasos (acordado: overlays a eidos POR FASES, con revisión entre cada una):

  • Paso 1 — gutter izquierdo (✅ hecho): quitado el botón "+" del gutter; "insertar bloque" integrado en el menú del drag handle como "Insert below" (vía act('insertBelow')). Gutter convertido en banda silver estrecha: --_words-gutter-size 3.5rem→2.5rem, fondo --words-rail-bg #ededed (muy suave) + borde derecho 1px --words-rail-border #cfcfcf (un poco más fuerte), pintado como linear-gradient en [data-words-content] (solo background, no toca el contenteditable). Tokens del recipe actualizados + generate:eidos-css. El inserter de SEAM (words-block-inserter.svelte, el "+" entre bloques) NO se tocó — es otro overlay; preguntar si también debe irse.
  • Paso 2 — props de bloque (#3, pendiente): añadir color (texto) a BlockVisual + per-type subsets + visualToStyle + validador; y sección común (margen/espaciado/fondo/color-texto) en el block panel. Construir esos color controls con eidos ColorPicker y migrar los <input type=color> nativos existentes (foreground/background marks + panel de imagen) a ColorPicker. (Cubre #3 + la parte "color pickers → eidos" de #4-fase1.)
  • Paso 3 — menú del drag → eidos (#4-fase1 resto) + #4-fase2: migrar el menú del block-handle a eidos Menu/Popover; luego link editor / find / status a Popover+Tooltip.

Pendiente para mañana

P0 — RESUELTO 2026-05-31 (column inserter end-to-end + path encoding)

Lo que ayer documenté como "selection sync con paths nested" resultó ser TRES bugs distintos, todos cerrados:

  1. Path 5-segmentos en insertBlockInColumn — mi op pasaba [colsIdx, colIdx, innerIdx, 0] (= el text node) a pointFromInlineTextOffset, que espera la ruta al CONTENEDOR. La función añade su propio inline-index al final → producía [12, 0, 0, 0, 0] (5 segmentos) en vez del canónico [12, 0, 0, 0]. Fix: pasar el container path ([12, 0, 0] para heading, [12, 0, 0, 0] para list-item) — la función ya añade el , inlineIdx al final.
  2. Selectionchange echo sobrescribía el modelo — api.focus() movía el caret DOM a inicio del doc (default browser behavior), disparaba selectionchange → syncSelectionFromDom → updateSelection sobrescribía la selección que el op había puesto. Fix: capturar intended = api.selection inmediatamente después del op y re-aplicarla vía setTimeout(0) AFTER del echo handler.
  3. DropdownMenu focus trap interceptaba api.focus() — Fix: trapFocus={false} en DropdownMenu.Content desactiva el focusin capture-listener.

Edición DOM↔model con paths nested SÍ funciona correctamente — el typing dentro de columnas, callouts, listas en columnas, etc., todo ok desde el primer click. El problema era exclusivamente el flow imperativo del inserter (focus + insert + selección).

Lo que se verificó end-to-end en navegador (Chrome):

  • Click + en columna vacía → dropdown abre, primer-item auto-focus.
  • Pick "Heading" → "Title" stub aparece + se selecciona.
  • Tipear inmediatamente → "Title" se reemplaza con el texto tecleado (Notion-style replace-stub-on-first-keystroke).
  • Inspector cambia automáticamente al panel correcto del nuevo bloque.
  • Doc title y resto del doc intactos — typing va al lugar correcto.

Pieces en disco:

  • engine/operations/insert-block-types.ts:381 — insertBlockInColumn op con container-path correcto.
  • engine/operations/commands.ts — command + dispatcher.
  • words-provider.svelte.ts:700-733 — applyCommandWithOptions dispara restoreDomSelection tras cambio de selección.
  • words-provider.svelte.ts:1274 — api.restoreCaret() expuesto.
  • soma/components/words/types.ts — restoreCaret añadido al snippet API type.
  • words-column-inserter.svelte — flow close → tick → focus → applyCommand → setTimeout(setSelection(intended)).

Lo que SÍ quedó cabledado en columns (sesión 2026-05-31)

  • insertBlockInColumn engine op (insert-block-types.ts) — inserta un bloque en una columna desde un trigger sin caret (el + overlay). Devuelve {document, selection, activeMarks} en una transacción. Verbatim de la doctrina Tiptap chain().focus() .insertContent().run() adaptada a nuestro stack.
  • Command insertBlockInColumn + dispatcher en engine/operations/commands.ts.
  • applyCommandWithOptions ahora dispara restoreDomSelection cuando la selección cambia (excepto batches de typing). Era el hueco: comandos invocados desde fuera del editor (overlay buttons, drag-drop, column inserter) actualizaban el modelo pero el caret DOM se quedaba donde el usuario clickeó por última vez. Fix en words-provider.svelte.ts:700-733.
  • WordsProviderSnippetProps sincronizado con la implementación: selectedBlockPath, selectAtomicBlock(idx, blockPath?), setSelection. Antes faltaban y rompían el typecheck del eidos.
  • words-column-inserter.svelte simplificado — patrón: applyCommand + close dropdown + onCloseAutoFocus preventDefault (bloquea el return-to-trigger que producía Space-repeat). Safety timeout 250ms para el busy lock — el botón nunca se queda muerto por una excepción intermedia.
  • web/routes/uix/components/words/+page.svelte — Columns block añadido al doc inicial para que el test fixture esté siempre a mano.

Lo que NO consigué cerrar: el typing dentro de columnas. Ese es el P0 de arriba — el inserter está listo para cuando el path encoding nested funcione.

P2 — Heredados

  • POLISH-3: topbar del demo (breadcrumb, Edit/Preview, Saved).
  • WORDS-DRAGDROP-SOMA: migrar drag-drop a soma DragDrop.Provider en vez de HTML5 nativo (intento previo falló — requiere reestructurar el render del engine).

Notas de infra

  • sema-parity.test.ts flakea por timeout de 5s sólo bajo carga paralela (full-suite). Aislado pasa 6/6 en <500ms. Si molesta, subir el testTimeout de ese archivo; no es regresión.
  • .claude/scheduled_tasks.lock es un artefacto transitorio — NO commitear.

Powered by TurnKey Linux.