feat(words): text + background colour in the bubble menu

The floating selection menu now carries text + background colour, applied
to the selection like bold/italic. A 'Color' (A) dropdown opens Text +
Background swatch grids; each swatch is a Words.CommandButton whose
color:#hex / bgcolor:#hex command the provider already parses into a
parametric toggleMark on the live selection — reusing the same
range-restoring path the mark buttons use, no bespoke selection wiring.
The 'Default' chip clears the mark (empty value).

- words-bubble.svelte: colour DropdownMenu (Baseline / Highlighter icons),
  9-hue palette + clear, marked data-words-bubble-color.
- words-provider: add [data-words-bubble-color] to the bubble's
  keep-selection-alive closed surface so interacting with the dropdown
  doesn't collapse the model selection.
- words.css: swatch-grid styling (hue from inline --words-swatch; clear
  chip is a diagonal strike).

Verified: selecting a word → A → blue swatch recolours it (rendered
rgb(0,145,255), model carries {type:'color',value:'#0091ff'}); bubble +
selection stay alive so multiple swatches can be applied in a row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent ed71ccf46b
commit 329f30cf30

@ -32,7 +32,9 @@
Link,
Unlink,
ChevronDown,
Check
Check,
Baseline,
Highlighter
} from '$uix/eidos/components/icon';
import type {
ProviderSnippetProps,
@ -44,10 +46,30 @@
let { api }: { api: ProviderSnippetProps } = $props();
let turnIntoOpen = $state(false);
let colorOpen = $state(false);
let linkOpen = $state(false);
let linkValue = $state('');
let linkInput = $state<HTMLInputElement | null>(null);
// Swatch palette for the bubble's text / background color marks — the
// SAME hues the inspector's color rows offer, minus pure white (invisible
// as a text color on a light surface; the "Default" chip clears instead).
// Each swatch applies via `Words.CommandButton` (command `color:#hex` /
// `bgcolor:#hex`) which the provider parses to a parametric `toggleMark`
// on the live selection — exactly how bold / italic apply, so a recolor
// rides the same range-restoring path with no bespoke wiring.
const COLOR_SWATCHES = [
'#e5484d',
'#f76b15',
'#ffc53d',
'#46a758',
'#0091ff',
'#8e4ec6',
'#e93d82',
'#646a73',
'#1a1a1a'
] as const;
// Blocks whose content the engine can reinterpret (setBlock / toggleList
// no-op on table / image / divider / callout), so turn-into is disabled
// elsewhere instead of offering inert options.
@ -263,6 +285,72 @@
<span data-words-bubble-divider></span>
<!-- Text + background colour. Each swatch is a `Words.CommandButton`
whose `color:#hex` / `bgcolor:#hex` command the provider parses
into a parametric `toggleMark` on the live selection — the same
range-restoring path bold / italic use. The "Default" chip clears
the mark (empty value). The dropdown carries `data-words-bubble-color`
so the bubble's focus scope keeps the selection alive. -->
<DropdownMenu bind:open={colorOpen}>
<DropdownMenu.Trigger
variant="ghost"
size="xs"
iconOnly
aria-label="Text and background colour"
data-words-bubble-trigger
>
{#snippet icon()}<Baseline />{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content side="bottom" align="start" data-words-bubble-color>
<div data-words-bubble-color-section>
<span data-words-bubble-color-label><Baseline size="sm" />Text</span>
<div data-words-bubble-swatches>
<Words.CommandButton command="color:" aria-label="Default text colour">
{#snippet child({ props })}
<button {...props} type="button" data-words-bubble-swatch data-clear></button>
{/snippet}
</Words.CommandButton>
{#each COLOR_SWATCHES as hex (hex)}
<Words.CommandButton command={`color:${hex}`} aria-label={`Text colour ${hex}`}>
{#snippet child({ props })}
<button
{...props}
type="button"
data-words-bubble-swatch
style="--words-swatch: {hex}"
></button>
{/snippet}
</Words.CommandButton>
{/each}
</div>
</div>
<div data-words-bubble-color-section>
<span data-words-bubble-color-label><Highlighter size="sm" />Background</span>
<div data-words-bubble-swatches>
<Words.CommandButton command="bgcolor:" aria-label="No background">
{#snippet child({ props })}
<button {...props} type="button" data-words-bubble-swatch data-clear></button>
{/snippet}
</Words.CommandButton>
{#each COLOR_SWATCHES as hex (hex)}
<Words.CommandButton command={`bgcolor:${hex}`} aria-label={`Background colour ${hex}`}>
{#snippet child({ props })}
<button
{...props}
type="button"
data-words-bubble-swatch
style="--words-swatch: {hex}"
></button>
{/snippet}
</Words.CommandButton>
{/each}
</div>
</div>
</DropdownMenu.Content>
</DropdownMenu>
<span data-words-bubble-divider></span>
<Button
variant="ghost"
size="xs"

@ -223,6 +223,61 @@
border-color: var(--words-border-hover);
}
/* Bubble colour menu — Text + Background swatch grids. The dropdown content
carries `data-words-bubble-color`; each `[data-words-bubble-swatch]` paints
its hue from the inline `--words-swatch` custom property. */
[data-words-bubble-color] {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-2);
}
[data-words-bubble-color-section] {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
[data-words-bubble-color-label] {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--words-font-size-sm);
color: var(--words-command-color);
--icon-size: 13px;
}
[data-words-bubble-swatches] {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 4px;
}
[data-words-bubble-swatch] {
inline-size: 1.15rem;
block-size: 1.15rem;
padding: 0;
border: 1px solid color-mix(in srgb, var(--words-content-color) 22%, transparent);
border-radius: 4px;
background: var(--words-swatch, var(--words-bg));
cursor: pointer;
transition: box-shadow 100ms var(--words-transition-ease, ease);
}
[data-words-bubble-swatch]:hover,
[data-words-bubble-swatch]:focus-visible {
outline: none;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--_words-accent) 45%, transparent);
}
/* "Default / none" chip — a diagonal strike over the surface tone. */
[data-words-bubble-swatch][data-clear] {
background:
linear-gradient(
to bottom right,
transparent calc(50% - 1px),
color-mix(in srgb, var(--words-content-color) 45%, transparent) calc(50% - 1px),
color-mix(in srgb, var(--words-content-color) 45%, transparent) calc(50% + 1px),
transparent calc(50% + 1px)
),
var(--words-bg);
}
/* Turn-into menu item — label + trailing active tick. */
[data-words-bubble-into-item] {
display: flex;

@ -1442,6 +1442,7 @@ export class WordsProvider {
'[data-words-block-handle-menu]',
'[data-words-block-inserter]',
'[data-words-block-inserter-button]',
'[data-words-bubble-color]',
'[data-words-bubble-menu]',
'[data-words-bubble-turn-into]',
'[data-words-code-language-panel]',

Loading…
Cancel
Save

Powered by TurnKey Linux.