diff --git a/src/uix/eidos/components/words/langs-menu.ts b/src/uix/eidos/components/words/langs-menu.ts new file mode 100644 index 000000000..01ce9177b --- /dev/null +++ b/src/uix/eidos/components/words/langs-menu.ts @@ -0,0 +1,30 @@ +/** + * Translation refs for the block-handle action menu (`WordsBlockGutter`). + * + * Same `idlangref` pattern as `langs-inspector.ts`: each entry is + * `#?components.words.menu.{key}|fallback`. `eidos.langs.ts(ref)` returns + * the localized string when `WORDS_MENU_BUNDLE` is registered (the eidos + * `` wrapper extends it on mount), else the English fallback. + */ + +export const WORDS_MENU_LANGS = { + ACTIONS: '#?components.words.menu.actions|Block actions', + INSPECT: '#?components.words.menu.inspect|Inspect', + MOVE_UP: '#?components.words.menu.move-up|Move up', + MOVE_DOWN: '#?components.words.menu.move-down|Move down', + DUPLICATE: '#?components.words.menu.duplicate|Duplicate', + INSERT_BELOW: '#?components.words.menu.insert-below|Insert below', + PASTE_BELOW: '#?components.words.menu.paste-below|Paste below', + DELETE: '#?components.words.menu.delete|Delete' +} as const; + +export const WORDS_MENU_BUNDLE = { + actions: { en: 'Block actions', es: 'Acciones del bloque' }, + inspect: { en: 'Inspect', es: 'Inspeccionar' }, + 'move-up': { en: 'Move up', es: 'Subir' }, + 'move-down': { en: 'Move down', es: 'Bajar' }, + duplicate: { en: 'Duplicate', es: 'Duplicar' }, + 'insert-below': { en: 'Insert below', es: 'Insertar debajo' }, + 'paste-below': { en: 'Paste below', es: 'Pegar debajo' }, + delete: { en: 'Delete', es: 'Eliminar' } +}; diff --git a/src/uix/eidos/components/words/words-block-gutter.svelte b/src/uix/eidos/components/words/words-block-gutter.svelte index 3d9aeb439..43bdda88d 100644 --- a/src/uix/eidos/components/words/words-block-gutter.svelte +++ b/src/uix/eidos/components/words/words-block-gutter.svelte @@ -19,6 +19,7 @@ * blur / commit / refocus cycle. */ import type { ActiveDom } from '$adom'; + import { ActiveEidos } from '$uix/eidos'; import { DropdownMenu } from '$uix/eidos/components/dropdown-menu'; import { GripVertical } from '$uix/eidos/components/icon'; import { @@ -27,6 +28,7 @@ type WordsBlockMenuEntry } from '$soma/components/words'; import { pickImageFile } from './words-image-file'; + import { WORDS_MENU_LANGS as L } from './langs-menu'; let { api, @@ -38,6 +40,11 @@ dom: ActiveDom; } = $props(); + // Localized labels for the block-action menu (the bundle is registered by + // the eidos `` wrapper on mount). + const eidos = ActiveEidos.require(); + const t = (ref: string) => eidos.langs.ts(ref); + const frame = $derived(content.closest('[data-words]') as HTMLElement | null); let index = $state(-1); @@ -330,26 +337,26 @@ {/snippet} - Block actions + {t(L.ACTIONS)} - Inspect + {t(L.INSPECT)} - Move up - Move down - Duplicate + {t(L.MOVE_UP)} + {t(L.MOVE_DOWN)} + {t(L.DUPLICATE)} - Insert below + {t(L.INSERT_BELOW)} {#each inserts as ins (ins.id)} handleInsert(ins)}>{ins.label} {/each} - Paste below + {t(L.PASTE_BELOW)} - Delete + {t(L.DELETE)} diff --git a/src/uix/eidos/components/words/words.svelte b/src/uix/eidos/components/words/words.svelte index 59af48adb..23d687dca 100644 --- a/src/uix/eidos/components/words/words.svelte +++ b/src/uix/eidos/components/words/words.svelte @@ -23,6 +23,7 @@ import WordsBubble from './words-bubble.svelte'; import WordsInspector from './words-inspector.svelte'; import { WORDS_INSPECTOR_BUNDLE } from './langs-inspector'; + import { WORDS_MENU_BUNDLE } from './langs-menu'; import type { WordsProps } from './types'; let { @@ -52,6 +53,7 @@ // applied AFTER mount still win (the engine warns on leaf // overwrites in DEV). eidos.langs.extend('components.words.inspector', WORDS_INSPECTOR_BUNDLE); + eidos.langs.extend('components.words.menu', WORDS_MENU_BUNDLE); }); let contentEl = $state(null); diff --git a/src/uix/soma/components/words/README.md b/src/uix/soma/components/words/README.md index 40cfe5787..725216734 100644 --- a/src/uix/soma/components/words/README.md +++ b/src/uix/soma/components/words/README.md @@ -38,7 +38,7 @@ empezar hasta cerrar contrato Morfo y alcance V1. | ------------ | ---------------------------------- | ----------------------------------------- | ----------------- | | **JSON** | `exportContent('json')` | `importContent` + `validateWordsDocument` | si (es el modelo) | | Texto plano | si | `parseWordsPlainText` | no (solo texto) | -| **HTML** | `serializeHtml` (semantico limpio) | — | n/a | +| **HTML** | `serializeHtml` (semantico limpio) | **paste** (`parseWordsHtml`, best-effort) | n/a | | **Markdown** | `serializeMarkdown` (GFM) | — | n/a | ### Por que export-only (no roundtrip) @@ -58,17 +58,26 @@ Consecuencias deliberadas: - `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). +- `importContent` solo acepta **JSON** (via `validateWordsDocument`, que protege + los `$derived` de un doc invalido) y **texto plano** (`parseWordsPlainText`). + El **paste** del portapapeles SI acepta HTML (camino aparte — ver abajo). + +### Pegar HTML externo — IMPLEMENTADO (2026-06-02) + +Lo anticipado aqui ya existe. `parseWordsHtml` (`engine/operations/parse-html.ts`) +es el inverso de `serializeHtml`: un parser **best-effort de portapapeles**, +explicitamente lossy fuera del schema (el patron `parseDOM` / `importDOM` de +ProseMirror/Lexical). Mapea `p` / `h1-6` / `blockquote` / `pre` / listas / `hr` / +`img` / `table` + marcas (negrita/cursiva/subrayado/tachado/codigo, color y +fondo) y enlaces; URLs saneadas. `insertBlocks` +(`engine/operations/insert-blocks.ts`) los inserta en el caret — o, con indice +explicito, debajo de un bloque (el "Paste below" del menu del handle). El +provider `onpaste` parsea el HTML en un documento **INERTE** +(`implementation.createHTMLDocument` del active-dom: no ejecuta scripts ni carga +recursos). + +La persistencia sigue siendo JSON; el HTML SOLO entra por el paste — **NO** por +`importContent`. Markdown import: sigue sin merecer la pena (es lo mas lossy). ## Submodulos propuestos @@ -230,8 +239,9 @@ type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code'; - `FindReplace` mantiene query, matches, regex validada, navegacion, find-in-selection, replace-current, replace-all y `replacePreserveCase` para adaptar el replacement al casing del match (`word`, `Word`, `WORD`). -- Paste HTML sin texto plano se rechaza y se reporta por el logger UIX como - `soma.words`. +- Paste HTML se parsea a bloques (`parseWordsHtml`); solo el HTML sin contenido + extraible Y sin texto plano se reporta como `unsupported-html` por el logger + UIX `soma.words`. - IME/composition queda conectado al Provider: `compositionend` aplica el comando final. - `deleteWord*` y `deleteLine*` tienen comandos propios; los `beforeinput`