From c1c3cd9404528b76296fd13a17f3ec0fab00d000 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 27 May 2026 21:47:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(words):=20parametric=20color=20marks=20?= =?UTF-8?q?=E2=80=94=20foreground=20+=20background=20pickers=20(COLOR-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks now support parametric values via colon syntax: - color:#hex — foreground text color - bgcolor:#hex — highlight / background color Existing boolean marks (bold/italic/underline/strike/code) are unchanged. Empty value (color:, bgcolor:) clears that slot. Engine - WordsMark union extended with `color:\${string}` and `bgcolor:\${string}` template literal variants. - isWordsMark accepts the parametric form (validates against #hex 3/4/6/8 digits). - normalizeMarks de-duplicates by prefix for parametric marks (only one color, one bgcolor per inline survives). - toggleMarkSet REPLACES same-prefix entries instead of toggling them; empty value strips the slot. - New helpers markValue() and markPrefix() for reading parametric data. Serializers - HTML out: wrapMarks collects color/bgcolor into a single `` instead of nested spans, then wraps with the boolean-mark tags. - HTML in: not extended this pass — round-trip via style attr is a follow-up. The marks survive in the document model + render OK. - Markdown out: silently drops parametric marks (no MD syntax for inline color; HTML round-trip is the supported path). DOM render - createWordsTextAttrs splits marks into booleans (→ data-words-marks) and parametrics (→ inline `style` attr) so the contenteditable paints the colors directly. Drawer Format panel - New "Foreground" + "Background" sections with: - 10-swatch palette (red/orange/amber/green/cyan/blue/violet/pink/ white/grey) — hand-picked editorial set - Native for free hex pick (no preset constraint) - "×" clear button (only when a value is set for that slot) - Active swatch outline when the current value matches the preset - runCommand passes the `color:#hex` / `bgcolor:#hex` string straight through commandFromName → toggleMark. WordsCommandName extended with `color:\${string}` and `bgcolor:\${string}` for type-safe consumer calls. 152/152 tests pass. Verified visually: clicking the red swatch on selected "ActiveUI" text in the demo produces `ActiveUI` in the rendered HTML, text paints red in the editor, the swatch shows active outline + clear (×) button appears. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/words/words-drawer.svelte | 98 +++++++++++++++++++ src/uix/eidos/components/words/words.css | 66 +++++++++++++ .../soma/components/words/engine/document.ts | 71 +++++++++++++- src/uix/soma/components/words/engine/dom.ts | 25 ++++- .../components/words/engine/operations.ts | 9 ++ .../components/words/engine/serialize-html.ts | 25 ++++- .../words/engine/serialize-markdown.ts | 6 +- src/uix/soma/components/words/types.ts | 6 +- .../components/words/words-provider.svelte.ts | 7 ++ 9 files changed, 303 insertions(+), 10 deletions(-) diff --git a/src/uix/eidos/components/words/words-drawer.svelte b/src/uix/eidos/components/words/words-drawer.svelte index d3464dabd..cc4a303f0 100644 --- a/src/uix/eidos/components/words/words-drawer.svelte +++ b/src/uix/eidos/components/words/words-drawer.svelte @@ -94,6 +94,32 @@ { id: 'bash', label: 'Bash' }, { id: 'python', label: 'Py' } ] as const; + + /** Hand-picked palette — keeps the picker compact while covering + * most editorial intents. Free hex still available via the input + * next to the chips. */ + const COLOR_PRESETS = [ + '#ef4444', // red + '#f97316', // orange + '#eab308', // amber + '#22c55e', // green + '#06b6d4', // cyan + '#3b82f6', // blue + '#8b5cf6', // violet + '#ec4899', // pink + '#ffffff', // white + '#a1a1aa' // grey + ] as const; + + /** Read the parametric mark value for a given prefix from the + * active marks. Returns '' when not set. */ + function currentParametricMark( + marks: readonly string[], + prefix: 'color' | 'bgcolor' + ): string { + const found = marks.find((m) => m.startsWith(`${prefix}:`)); + return found ? found.slice(prefix.length + 1) : ''; + } @@ -268,6 +294,78 @@ {/each} +
Foreground
+
+ {#each COLOR_PRESETS as preset (preset)} + + {/each} + { + const v = (e.currentTarget as HTMLInputElement).value; + s.runCommand(`color:${v}` as never); + }} + /> + {#if currentParametricMark(s.activeMarks, 'color')} + + {/if} +
+ +
Background
+
+ {#each COLOR_PRESETS as preset (preset)} + + {/each} + { + const v = (e.currentTarget as HTMLInputElement).value; + s.runCommand(`bgcolor:${v}` as never); + }} + /> + {#if currentParametricMark(s.activeMarks, 'bgcolor')} + + {/if} +
+