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

493 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Continue tomorrow
Fecha de corte: **2026-06-03**. Rama: `active-uix`.
## Sesión 2026-06-03 — Words chrome + crash tabla-en-columna
Sprint de UX del editor (gutter / menubar / active-block / drag-drop)
por feedback iterativo, cerrado con un crash diagnosticado en Chrome
real. Todo verificado en navegador (`localhost:5173/uix/components/words`).
### Chrome del editor (commits locales del sprint, verificados)
- **Drag-drop de bloques (HTML5 nativo)** vía el grip del gutter
(`words-block-drag.svelte` + `words-block-drag-type.ts`).
`dropAt(clientY)` calcula el seam; `to = from < n ? n-1 : n` →
`applyCommand({ type:'moveBlockTo' })`.
- **Menubar** (`words-menubar.svelte`): undo/redo directos, block-style
DropdownMenu, marcas (B/I/U/S/code), listas, alineación, menú Insert,
toggle inspector, ToggleGroup Edit|Preview. El editor es un richtext
gestionado SIEMPRE por un form (como un `<input>`); en preview el
Provider es `readonly`.
- **Active block por id materializado + rail del gutter**
(`words-active-rail.svelte`, `words-activate.svelte`): el activo se
fija SOLO en pointerdown sobre un bloque; auto-seed al primer bloque
al cargar; el rail (barra fija en la gutter) abarca el bloque
TOP-LEVEL (una tabla entera, no la celda clicada). Frame-relative, un
color, sin desplazamiento. En preview se apaga (limpia el stamp).
- **Borrar bloque** (gutter + inspector): captura el id del bloque
ANTERIOR *antes* del delete (leer `document.children` después daba el
bloque equivocado) → `setActiveBlock(prevId)`.
- **Modelo confirmado = rich text plano** (`children[]`), NO
page-builder (`sections→rows→columns→blocks`). Decisión del usuario.
### Crash arreglado — tabla en columna (3 capas)
Reportado en Chrome real: _"no puede seleccionar una columna para
insertar un bloque, no puedo editar ninguna celda"_. Stack:
`insertBlockInColumn → getActiveMarksForSelection → collectBlockText →
TypeError: Cannot read properties of undefined (reading 'type')`.
**Causa raíz**: el inserter de bloque-en-columna del inspector
(`words-inspector.svelte > insertIntoActiveColumn`) crea el bloque con
`spec.menu[0].create()`. La factory del slash-menu de TABLA
(`engine/blocks/built-ins.ts > tableSpec`) sembraba las celdas con
`emptyText()` = nodos **inline** `{type:'text'}`. Desde P5m las celdas
llevan **bloques**, así que `normalizeBlock` (sin `default` para
`'text'`) devolvía `undefined` → `cell.children = [undefined]` → el
mark-walk petaba al leer `.type`, abortaba el comando sin capturar y
dejaba el editor sin poder editar. (El toolbar/slash "Table" NO usan
esa factory — delegan a `insertTable` → `createEmptyTable`, que ya
sembraba celdas válidas; por eso solo petaba por el inserter de
columna.)
**3 capas** (commits `76b0d1c8`, `4a8f6331` + normalize sin commitear al
abrir este hand-off):
1. `built-ins.ts` — `tableSpec.create()` envuelve cada celda en un
párrafo (`{children:[{type:'paragraph',children:emptyText()}]}`),
igual que el doc demo y la factory de callout.
2. `normalize.ts > normalizeTableCells` — self-heal: filtra slots
`null`/`undefined` ANTES del map + descarta lo que `normalizeBlock`
no normaliza + re-siembra un párrafo si la celda queda vacía. Repara
docs ya corruptos.
3. `selection-walkers.ts > collectBlockText` — guard `if (!block) return`
al inicio (defensa del walk).
**Verificación (navegador, motor cargado vía HMR)**: 4 formas de celda
corrupta (`[undefined]`, mixto undefined+inline, `[]`, inline) →
normalize NO peta + se reparan a `[paragraph]` + el mark-walk devuelve
limpio. Insertar tabla por el menú + tipear en una celda → "CELL OK"
persiste, consola limpia. Engine tests 430/430. `npm run check`: solo el
error pre-existente de grafito (`+page.svelte:361`, NO mío).
### Pendiente para mañana (feedback del usuario al cierre)
1. **El menú Insert de la barra debe ser como el de insertar-bloque de
columna** — mismo set de bloques + misma UX. Hoy el toolbar Insert
(`words-menubar.svelte`) y el inserter de columna divergen.
2. **El menú `+` (inserter de columna) solo muestra 3 bloques**
(Image/Table/Divider). `columnInsertableSpecs` en
`words-inspector.svelte` está demasiado filtrado; debe ofrecer
también párrafo/heading/quote/code/lista/callout.
3. **La imagen sale con el `prompt()` HTML nativo** — el toolbar
(`words-provider > insertBlockOfType('image')`) usa `window.prompt`
para la URL. Debe insertar el **placeholder inline** (como el slash
menu: `insertImage({ src:'' })`) y dejar que el inspector pida la URL.
---
## 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.