| Link editor | `Words.LinkEditor` | default URL/title form + invalid feedback |
| Form | `Words.HiddenInput` | passthrough hidden JSON input |
| Status | `Words.Status` | idle/error status recipe |
| Text alignment | `Words.CommandButton` | align-left/center/right/justify via `setTextAlign` command |
## Decisiones
@ -96,17 +97,22 @@ rich-text ni wrapper visual reutilizable.
- `Words.LinkEditor` es una parte compuesta real, no markup de demo: Soma aplica
`insertLink/unlink`, Morfo declara `commit-link` y `commit-unlink`, y Eidos
ofrece una UI por defecto que puede reemplazarse con snippet.
- `Words.ToolPopover` centraliza el patron de herramientas flotantes de toolbar:
trigger iconico, popover intrinseco, boton de cierre/cancelacion y guardia de
cierre. `Words.LinkEditor` lo usa para validar URL al intentar cerrar por
outside/Escape sin confirmar.
- `Words.BubbleMenu` es una parte publica compuesta: solo aparece para
- `Words.BubbleMenu` es una parte publica compuesta y **opcional**: no se
incluye por defecto en `<Words>`; el consumidor debe agregarla explicitamente
como `<Words.BubbleMenu>` dentro del snippet `children`. Solo aparece para
seleccion de rango, toma su ancla del rectangulo real de `Selection API` y
preserva la seleccion en `pointerdown` para que los command buttons operen
sobre el texto seleccionado.
- El trigger del popover captura la seleccion DOM de `Words.Content` antes de
mover foco al formulario. Esto evita que aplicar un enlace dependa del timing
de `selectionchange` del navegador.
- En modo editable, los enlaces renderizados son contenido del editor: el click
no navega fuera de la demo y permite que `LinkEditor`/`unlink` operen sobre
la seleccion actual. En `readonly`, el enlace conserva comportamiento
navegable.
- El trigger de `Words.LinkEditor` se deshabilita junto con la parte cuando el
editor esta `disabled` o `readonly`; no abre una herramienta que luego no
puede aplicar cambios.
- `words.css` usa solo tokens publicos `--words-*` declarados en
`THEME_BASE_RECIPE_TOKENS.words`.
- Las marcas inline se pintan desde `data-words-marks`, emitido por el motor
@ -126,3 +132,30 @@ rich-text ni wrapper visual reutilizable.
| Images/files | diferir a ActiveApp storage/upload |
| Tables | diferir V2 |
| Collaboration | diferir; no core |
## Estado actual (2026-05-25)
Features implementadas en esta sesion:
- **Text alignment**: `setTextAlign` operacion de motor aplicable a `paragraph`, `heading` y `quote`. Renderiza `style="text-align: ..."` en DOM y HTML export. Se omite cuando es `left` (default). Comandos UI: `align-left`, `align-center`, `align-right`, `align-justify` con iconos `TextAlignStart`, `TextAlignCenter`, `TextAlignEnd`, `TextAlignJustify`. Presets `formatting` y `full` incluyen los 4 botones.
- **Heading levels h2/h3**: motor ya soportaba `level: 1|2|3`; se expusieron comandos y se creo `Words.HeadingPicker` (dropdown con `Popover` de Eidos) para seleccionar H1/H2/H3/Paragraph desde un solo boton en toolbar.
- **Eliminacion de `ToolPopover`**: componente borrado, `LinkEditor` migrado a `Popover` nativo de Eidos. Limpieza de types, CSS (~150 lineas) y README.
- **Word boundary robusto**: algoritmo naive reemplazado por `Intl.Segmenter` + fallback para puntuacion/CJK.
Tests:
- Engine: 42/42 pasan (incluye 5 nuevos de `setTextAlign`).
- Build: pasa (`npm run build` exitoso).
- Nota: `words-content.svelte.test.ts` tiene 2 fallos pre-existentes de montaje Svelte en entorno jsdom (no relacionados con alignment).
## Next steps (para continuar manana)
| Feature | Donde tocar | Complejidad | Bloqueos |
| **Atajos de teclado para alignment** (Ctrl/Cmd+L/R/E/J) | `words-provider.svelte.ts` (`onkeydown`), `words-content.svelte` (event forwarding) | Baja | Ninguno |
| **HeadingPicker keyboard navigation** (arrow keys + Enter en dropdown) | `words-heading-picker.svelte` | Baja | Ninguno |
| **Toolbar tooltips** (aria-label como tooltip nativo o title) | `words.svelte` + CSS | Baja | Ninguno |
| **Seleccion de bloque entero** (triple-click o margin-click) | `words-provider.svelte.ts` + `operations.ts` | Media | Ninguno |
| **Soft breaks** (Shift+Enter dentro de paragraph sin crear nuevo bloque) | `operations.ts` (nuevo `insertSoftBreak`), `input.ts` | Media | Disenar: como representar `<br>` en el modelo JSON |