@ -3,6 +3,8 @@
Words,
type WordsColor,
type WordsSize,
type WordsToolbarConfig,
type WordsToolbarPreset,
type WordsVariant
} from '$uix/eidos/components/words';
import type {
@ -11,22 +13,6 @@
WordsSelection,
WordsToolbarOrientation
} from '$soma/components/words';
import {
Bold,
Code,
Eraser,
Heading1,
Italic,
List,
ListOrdered,
Pilcrow,
Quote,
Redo2,
Save,
Strikethrough,
Underline,
Undo2
} from '$uix/eidos/components/icon';
import { compileMorfo } from '$uix/morfo';
import { wordsMorfo } from '@/uix/morfo/components/words';
import { getActiveUix } from '$active-uix';
@ -34,6 +20,7 @@
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type DocumentPreset = 'sample' | 'empty' | 'long';
type ToolbarChoice = 'none' | WordsToolbarPreset;
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
type RawValue = { kind? : string ; value? : unknown ; prop? : string ; part? : string } ;
@ -85,6 +72,20 @@
];
const orientations: WordsToolbarOrientation[] = ['horizontal', 'vertical'];
const presets: DocumentPreset[] = ['sample', 'empty', 'long'];
const toolbarChoices: ToolbarChoice[] = ['none', 'minimal', 'formatting', 'full'];
const toolbarPresetGroups: Record< WordsToolbarPreset , string [][] > = {
minimal: [['bold', 'italic', 'underline']],
formatting: [
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', 'heading', 'quote']
],
full: [
['undo', 'redo'],
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', 'heading', 'quote', 'unordered-list', 'ordered-list'],
['clear-formatting', 'clear-document', 'commit']
]
};
let tab = $state< Tab > ('live');
let preset = $state< DocumentPreset > ('sample');
@ -96,7 +97,7 @@
let required = $state(false);
let invalid = $state(false);
let commitOnBlur = $state(true);
let showToolbar = $state(true );
let toolbarChoice = $state< ToolbarChoice > ('full' );
let showStatus = $state(true);
let orientation = $state< WordsToolbarOrientation > ('horizontal');
let size = $state< WordsSize > ('md');
@ -111,6 +112,12 @@
const wordCount = $derived(countWords(plainText));
const selectionState = $derived(selectionLabel(selection));
const documentJson = $derived(JSON.stringify(value, null, 2));
const toolbarConfig = $derived< WordsToolbarConfig > (
toolbarChoice === 'none' ? false : toolbarChoice
);
const toolbarSnippetGroups = $derived(
toolbarChoice === 'none' ? [] : toolbarPresetGroups[toolbarChoice]
);
const somaSnippet = $derived(
[
@ -130,16 +137,7 @@
invalid & & ' invalid',
!commitOnBlur && ' commitOnBlur={ false } ',
'>',
showToolbar && ` < Words.Toolbar orientation = "$ { orientation } " > `,
showToolbar & & ' < Words.ToolbarGroup > ',
showToolbar & & ' < Words.CommandButton command = "bold" aria-label = "Bold" / > ',
showToolbar & & ' < Words.CommandButton command = "italic" aria-label = "Italic" / > ',
showToolbar & &
' < Words.CommandButton command = "heading" level = { 1 } aria-label="Heading" /> ',
showToolbar & & ' < Words.CommandButton command = "unordered-list" aria-label = "List" / > ',
showToolbar & & ' < Words.CommandButton command = "clear-document" aria-label = "Clear" / > ',
showToolbar & & ' < / Words.ToolbarGroup > ',
showToolbar & & ' < / Words.Toolbar > ',
...somaToolbarSnippet(toolbarSnippetGroups, orientation),
' < Words.Placeholder / > ',
' < Words.Content / > ',
' < Words.HiddenInput / > ',
@ -154,7 +152,6 @@
[
"< script lang = 'ts' > " ,
" import { Words } from '$uix/eidos/components/words';",
" import { Bold , Heading1 , Italic , List , Save } from '$uix/eidos/components/icon';",
" import type { WordsDocument } from '$soma/components/words';",
` let value = $state< WordsDocument > (${ documentJson } );`,
'< /' + 'script>',
@ -165,26 +162,15 @@
size !== 'md' && ` size="${ size } "`,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
toolbarChoice !== 'none' && ` toolbar="${ toolbarChoice } "`,
toolbarChoice === 'none' && ' toolbar={ false } ',
orientation !== 'horizontal' && ` toolbarOrientation="${ orientation } "`,
disabled & & ' disabled',
readonly & & ' readonly',
required & & ' required',
invalid & & ' invalid',
!commitOnBlur && ' commitOnBlur={ false } ',
'>',
showToolbar && ` < Words.Toolbar orientation = "$ { orientation } " > `,
showToolbar & & ' < Words.ToolbarGroup > ',
showToolbar & &
' < Words.CommandButton command = "bold" aria-label = "Bold" > < Bold size = "sm" decorative / > < / Words.CommandButton > ',
showToolbar & &
' < Words.CommandButton command = "italic" aria-label = "Italic" > < Italic size = "sm" decorative / > < / Words.CommandButton > ',
showToolbar & &
' < Words.CommandButton command = "heading" level = { 1 } aria-label="Heading" >< Heading1 size = "sm" decorative /></ Words.CommandButton > ',
showToolbar & &
' < Words.CommandButton command = "unordered-list" aria-label = "List" > < List size = "sm" decorative / > < / Words.CommandButton > ',
showToolbar & &
' < Words.CommandButton command = "commit" aria-label = "Commit" > < Save size = "sm" decorative / > < / Words.CommandButton > ',
showToolbar & & ' < / Words.ToolbarGroup > ',
showToolbar & & ' < / Words.Toolbar > ',
' < Words.Placeholder / > ',
' < Words.Content / > ',
' < Words.HiddenInput / > ',
@ -437,6 +423,34 @@
return JSON.stringify(value);
}
function somaToolbarSnippet(
groups: string[][],
toolbarOrientation: WordsToolbarOrientation
): string[] {
if (groups.length === 0) return [];
return [
` < Words.Toolbar orientation = "$ { toolbarOrientation } " > `,
...groups.flatMap((group) => [
' < Words.ToolbarGroup > ',
...group.map((command) => somaCommandSnippet(command)),
' < / Words.ToolbarGroup > '
]),
' < / Words.Toolbar > '
];
}
function somaCommandSnippet(command: string): string {
const level = command === 'heading' ? ' level={ 1 } ' : '';
return ` < Words.CommandButton command = "$ { command } " $ { level } aria-label = "$ { commandLabel ( command )} " /> `;
}
function commandLabel(command: string): string {
return command
.split('-')
.map((part) => part[0]?.toUpperCase() + part.slice(1))
.join(' ');
}
function describeValue(value: unknown): string {
if (!value || typeof value !== 'object') return '-';
const rec = value as RawValue;
@ -521,65 +535,10 @@
{ size }
{ variant }
{ color }
toolbar={ toolbarConfig }
toolbarOrientation={ orientation }
onValueCommit={( _ , reason ) => ( lastCommit = reason )}
>
{ #if showToolbar }
< Words.Toolbar { orientation } >
< Words.ToolbarGroup { orientation } >
< Words.CommandButton command = "undo" aria-label = "Undo"
>< Undo2 size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "redo" aria-label = "Redo"
>< Redo2 size = "sm" decorative / > < /Words.CommandButton
>
< / Words.ToolbarGroup >
< Words.ToolbarGroup { orientation } >
< Words.CommandButton command = "bold" aria-label = "Bold"
>< Bold size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "italic" aria-label = "Italic"
>< Italic size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "underline" aria-label = "Underline"
>< Underline size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "strike" aria-label = "Strike"
>< Strikethrough size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "code" aria-label = "Code"
>< Code size = "sm" decorative / > < /Words.CommandButton
>
< / Words.ToolbarGroup >
< Words.ToolbarGroup { orientation } >
< Words.CommandButton command = "paragraph" aria-label = "Paragraph"
>< Pilcrow size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "heading" level = { 1 } aria-label="Heading"
>< Heading1 size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "quote" aria-label = "Quote"
>< Quote size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "unordered-list" aria-label = "Unordered list"
>< List size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "ordered-list" aria-label = "Ordered list"
>< ListOrdered size = "sm" decorative / > < /Words.CommandButton
>
< / Words.ToolbarGroup >
< Words.ToolbarGroup { orientation } >
< Words.CommandButton command = "clear-formatting" aria-label = "Clear formatting"
>< Eraser size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "clear-document" aria-label = "Clear document"
>< Eraser size = "sm" decorative / > < /Words.CommandButton
>
< Words.CommandButton command = "commit" aria-label = "Commit document"
>< Save size = "sm" decorative / > < /Words.CommandButton
>
< / Words.ToolbarGroup >
< / Words.Toolbar >
{ /if }
< Words.Placeholder > { placeholder } </ Words.Placeholder >
< Words.Content
data-perm-step={ ! disabled && ! readonly ? '0' : undefined }
@ -621,7 +580,7 @@
< div data-uix-tabs role = "tablist" >
< button data-uix-tab data-active = { tab === 'live' } onclick= {() => ( tab = 'live' )} > Live</ button >
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )} >
API < span data-uix-tab-count > 16 < / span >
API < span data-uix-tab-count > 17 < / span >
< / button >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< span data-uix-layer-badge = "morfo" > morfo< / span >
@ -665,9 +624,16 @@
< / label >
< label data-uix-control >
< span data-uix-control-label > toolbar< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { showToolbar } / >
< span data-uix-switch-label > { showToolbar ? 'shown' : 'hidden' } </ span >
< span data-uix-chips role = "radiogroup" >
{ #each toolbarChoices as item }
< button
data-uix-chip
data-active={ toolbarChoice === item }
onclick={() => ( toolbarChoice = item )}
>
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
@ -829,10 +795,15 @@
>contenteditable textbox surface.< /td
>< /tr
>
< tr
>< td class = "name" > toolbar< / td > < td > eidos< / td > < td class = "default" > false< / td > < td
>Optional preset/config property: none, minimal, formatting or full.< /td
>< /tr
>
< tr
>< td class = "name" > Toolbar / ToolbarGroup< / td > < td > soma< / td > < td class = "default"
>optional< /td
>< td > Composition controls command affordances.< / td > < /tr
>< td > Manual composition stays available for fully custom command layout s.< / td > < /tr
>
< tr
>< td class = "name" > CommandButton< / td > < td > soma< / td > < td class = "default" > optional< / td > < td