diff --git a/continue.md b/continue.md index 7e03e8043..1a9649de1 100644 --- a/continue.md +++ b/continue.md @@ -14,6 +14,7 @@ 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` @@ -23,13 +24,14 @@ component CSS` pasa. 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 + 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). -6. Botones add/delete normalizados a icon-only (Trash2 delete, flechas +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]` @@ -45,20 +47,21 @@ component CSS` pasa. 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'}). + - 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`. +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: @@ -108,8 +111,8 @@ Fecha de corte: **2026-05-29**. Rama: `active-uix`. **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 +(`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. @@ -173,28 +176,35 @@ inverso fiel). El import sólo acepta **JSON** (`JSON.parse` + - **Tabla**: `{ type:'table', rows, headerRow?, headerCol? }`; fila `{ cells, visual? }`; celda `{ children, align?, verticalAlign?, - colspan?, rowspan?, visual? }`. **Celda usa `align`**, no `textAlign`. +colspan?, rowspan?, visual? }`. **Celda usa `align`**, no `textAlign`. - **Lista**: `{ type:'list', kind, items }`; item `{ children, checked?, - indent? }`. **`items`**, no `children`. Sin `type:'list-item'`. +indent? }`. **`items`**, no `children`. Sin `type:'list-item'`. - **Bloques con alineación**: paragraph/heading/quote llevan `textAlign?` (a nivel bloque). -## ⚠️ Decisión consecuente NO firmada — export HTML +## ✅ Decisión FIRMADA (2026-06-02) — export-only (no roundtrip) -`serializeHtmlV2` emite **HTML semántico limpio**, no un formato -roundtrip-lossless. Como V2 no tiene parser HTML, HTML y Markdown son -export-only. Si el usuario quería export HTML roundtrip-friendly (re- -importable), es un encargo aparte. **Pendiente de confirmar con él.** +`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 | +| 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 @@ -231,7 +241,7 @@ Borrados los selectores CSS muertos de `words.css` los `[data-words-cell-header]` huérfanos — los headers se renderizan como ``). 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. +intacto: documenta la _decisión_ de eliminarlos, no features stale. Verificado en browser (tabla 2×2 renderiza, `` conserva su tinte vía `:where(th)`). Commit `4a0661a5`. @@ -330,6 +340,7 @@ 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 @@ -338,6 +349,7 @@ imperativo del inserter (focus + insert + selección). - 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. @@ -347,7 +359,7 @@ imperativo del inserter (focus + insert + selección). - `soma/components/words/types.ts` — `restoreCaret` añadido al snippet API type. - `words-column-inserter.svelte` — flow `close → tick → focus → - applyCommand → setTimeout(setSelection(intended))`. +applyCommand → setTimeout(setSelection(intended))`. ### Lo que SÍ quedó cabledado en columns (sesión 2026-05-31) @@ -355,7 +367,7 @@ imperativo del inserter (focus + insert + selección). 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. +.insertContent().run()` adaptada a nuestro stack. - **Command `insertBlockInColumn`** + dispatcher en `engine/operations/commands.ts`. - **`applyCommandWithOptions` ahora dispara `restoreDomSelection`** diff --git a/src/uix/soma/components/words/README.md b/src/uix/soma/components/words/README.md index 51a1882f2..40cfe5787 100644 --- a/src/uix/soma/components/words/README.md +++ b/src/uix/soma/components/words/README.md @@ -30,6 +30,46 @@ empezar hasta cerrar contrato Morfo y alcance V1. - Serializacion JSON/HTML/Markdown. - Integracion con logger, clipboard, format y events de ActiveUIX. +## Serializacion e intercambio — doctrina V2 (FIRMADA 2026-06-02) + +**JSON es la verdad. HTML y Markdown son proyecciones de export-only.** + +| Formato | Export | Import | Lossless | +| ------------ | ---------------------------------- | ----------------------------------------- | ----------------- | +| **JSON** | `exportContent('json')` | `importContent` + `validateWordsDocument` | si (es el modelo) | +| Texto plano | si | `parseWordsPlainText` | no (solo texto) | +| **HTML** | `serializeHtml` (semantico limpio) | — | n/a | +| **Markdown** | `serializeMarkdown` (GFM) | — | n/a | + +### Por que export-only (no roundtrip) + +Es la misma postura que TipTap/ProseMirror, Lexical, Slate y Notion: **el modelo +JSON es el formato canonico de persistencia (lossless); HTML/MD son +proyecciones**. Hacer del HTML _limpio_ un formato de persistencia lossless es la +trampa que todos evitan — el HTML semantico no puede cargar cada visual prop / +metadato de bloque sin ensuciarlo de `data-*` (el approach "state-complete" de +TipTap, donde cada nodo declara `toDOM` + `parseDOM` y serializa TODOS sus attrs +al HTML). JSON esquiva el problema entero; por eso es canonico en los cuatro. + +Consecuencias deliberadas: + +- `serializeHtml` emite HTML semantico limpio (sin `data-words-*`), pensado para + lectura / copia / preview, **no** para re-importar. +- `serializeMarkdown` es lo mas lossy: sin color ni visual props, y las celdas + GFM son inline-only — desde P5m las celdas llevan bloques, que markdown no + puede representar (se aplanan con `
`). MD es proyeccion, no roundtrip. +- El import solo acepta **JSON** (via `validateWordsDocument`, que protege los + `$derived` de un doc invalido) y **texto plano** (`parseWordsPlainText`). + +### Si algun dia hace falta _pegar_ HTML externo + +No se construye "roundtrip de persistencia". Se anade un `parseHtml` +**por-bloque, simetrico al `toHtml` de cada spec** (el patron `parseDOM` / +`importDOM` de ProseMirror/Lexical), declarado como **interop best-effort de +portapapeles**, explicitamente lossy fuera del schema. La persistencia sigue +siendo JSON; el HTML solo entra por el paste. Markdown import: no merece la pena +(es lo mas lossy). + ## Submodulos propuestos ```text