Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< script lang = "ts" >
import {
Words,
type WordsColor,
type WordsSize,
type WordsToolbarConfig,
type WordsToolbarPreset,
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
type WordsVariant
} from '$uix/eidos/components/words';
import type {
WordsDocument,
WordsInline,
WordsSelection,
WordsToolbarOrientation
} from '$soma/components/words';
import { compileMorfo } from '$uix/morfo';
import { wordsMorfo } from '@/uix/morfo/components/words';
import { getActiveUix } from '$active-uix';
import type { Intent } from '$uix/intent';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type DocumentPreset = 'sample' | 'empty' | 'long';
type ToolbarChoice = 'none' | WordsToolbarPreset;
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
type RawValue = { kind? : string ; value? : unknown ; prop? : string ; part? : string } ;
type RawPart = {
kebab: string;
name: string;
defaultElement: string;
role?: string;
archetype?: string;
states?: readonly string[];
optional?: boolean;
aria?: ReadonlyArray< {
attr: string;
value: RawValue;
condition?: { when : string ; prop? : string ; part? : string } ;
severity?: string;
}>;
keyboard?: ReadonlyArray< { key : string ; action : string } >;
};
type RawEvent = {
name: string;
semantic: {
family: string;
verb?: string;
sequence?: string;
intent?: string | RawValue;
target?: RawValue;
};
prewrite?: unknown;
commit?: unknown;
};
const uix = getActiveUix();
const compiled = compileMorfo(wordsMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const sizes: WordsSize[] = ['sm', 'md', 'lg'];
const variants: WordsVariant[] = ['surface', 'outline', 'ghost'];
const colors: WordsColor[] = [
'primary',
'secondary',
'neutral',
'affirm',
'fulfill',
'risk',
'threat',
'loss'
];
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']
]
};
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let tab = $state< Tab > ('live');
let preset = $state< DocumentPreset > ('sample');
let value = $state< WordsDocument > (sampleDocument());
let selection = $state< WordsSelection | null > (null);
let placeholder = $state('Write the product note...');
let disabled = $state(false);
let readonly = $state(false);
let required = $state(false);
let invalid = $state(false);
let commitOnBlur = $state(true);
let toolbarChoice = $state< ToolbarChoice > ('full');
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let showStatus = $state(true);
let orientation = $state< WordsToolbarOrientation > ('horizontal');
let size = $state< WordsSize > ('md');
let variant = $state< WordsVariant > ('surface');
let color = $state< WordsColor > ('primary');
let lastCommit = $state('none');
let documentRevision = $state(0);
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
const plainText = $derived(readPlainText(value));
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]
);
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const somaSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import * as Words from '$soma/components/words';",
" import type { WordsDocument } from '$soma/components/words';",
` let value = $state< WordsDocument > (${ documentJson } );`,
'< /' + 'script>',
'',
'< Words.Provider ' ,
' bind:value',
` placeholder={ $ { quote ( placeholder )}} `,
' name="body"',
disabled & & ' disabled',
readonly & & ' readonly',
required & & ' required',
invalid & & ' invalid',
!commitOnBlur && ' commitOnBlur={ false } ',
'>',
...somaToolbarSnippet(toolbarSnippetGroups, orientation),
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
' < Words.Placeholder / > ',
' < Words.Content / > ',
' < Words.HiddenInput / > ',
showStatus & & ' < Words.Status / > ',
'< / Words.Provider > '
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Words } from '$uix/eidos/components/words';",
" import type { WordsDocument } from '$soma/components/words';",
` let value = $state< WordsDocument > (${ documentJson } );`,
'< /' + 'script>',
'',
'< Words ' ,
' bind:value',
` placeholder={ $ { quote ( placeholder )}} `,
size !== 'md' && ` size="${ size } "`,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
toolbarChoice !== 'none' && ` toolbar="${ toolbarChoice } "`,
toolbarChoice === 'none' && ' toolbar={ false } ',
orientation !== 'horizontal' && ` toolbarOrientation="${ orientation } "`,
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
disabled & & ' disabled',
readonly & & ' readonly',
required & & ' required',
invalid & & ' invalid',
!commitOnBlur && ' commitOnBlur={ false } ',
'>',
' < Words.Placeholder / > ',
' < Words.Content / > ',
' < Words.HiddenInput / > ',
showStatus & & ' < Words.Status / > ',
'< / Words > '
]
.filter(Boolean)
.join('\n')
);
const recipeSelectors = [
{
selector: '[data-words]',
source: 'morfo + eidos',
kind: 'eidos',
note: 'root, size, variant, color'
},
{ selector : '[data-words][data-focused]' , source : 'morfo' , kind : 'soma' , note : 'focus ring' } ,
{ selector : '[data-words][data-invalid]' , source : 'morfo' , kind : 'soma' , note : 'risk border' } ,
{ selector : '[data-words-toolbar]' , source : 'morfo' , kind : 'soma' , note : 'toolbar shell' } ,
{
selector: '[data-words-toolbar][data-orientation]',
source: 'morfo',
kind: 'soma',
note: 'toolbar axis'
},
{
selector: '[data-words-command-button][data-state]',
source: 'morfo',
kind: 'soma',
note: 'active command'
},
{
selector: '[data-words-command-button][data-command]',
source: 'morfo',
kind: 'soma',
note: 'command-specific treatment'
},
{
selector: '[data-words-placeholder][hidden]',
source: 'morfo',
kind: 'soma',
note: 'empty document hint'
},
{
selector: '[data-words-content]',
source: 'morfo',
kind: 'soma',
note: 'contenteditable area'
},
{
selector: '[data-words-content] [data-words-marks]',
source: 'soma engine',
kind: 'soma',
note: 'rendered marks'
},
{
selector: '[data-words-status][data-status]',
source: 'morfo',
kind: 'soma',
note: 'idle/error status'
}
];
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '-',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes : true , subtree : true , attributeFilter : [ 'data-event' ] } );
return () => obs.disconnect();
});
function sampleDocument(): WordsDocument {
return {
version: 1,
children: [
{
type: 'heading',
level: 1,
children: [{ type : 'text' , text : 'ActiveUIX Words' , marks : [ 'bold' ] } ]
},
{
type: 'paragraph',
children: [
{ type : 'text' , text : 'A rich text surface with ' } ,
{ type : 'text' , text : 'canonical JSON' , marks : [ 'bold' ] } ,
{ type : 'text' , text : ', selection-aware commands and UIX semantic events.' }
]
},
{
type: 'quote',
children: [
{ type : 'text' , text : 'The visual recipe reads Morfo data attrs; Soma owns editing.' }
]
},
{
type: 'list',
kind: 'unordered',
children: [
{
type: 'list-item',
children: [{ type : 'text' , text : 'Toolbar commands update the real document.' } ]
},
{
type: 'list-item',
children: [{ type : 'text' , text : 'HiddenInput serializes the JSON payload.' } ]
},
{
type: 'list-item',
children: [
{
type: 'text',
text: 'Sema receives focus, commit, format, history and clear events.'
}
]
}
]
}
]
};
}
function emptyDocument(): WordsDocument {
return {
version: 1,
children: [{ type : 'paragraph' , children : [{ type : 'text' , text : '' }] } ]
};
}
function longDocument(): WordsDocument {
return {
version: 1,
children: [
{
type: 'heading',
level: 2,
children: [{ type : 'text' , text : 'Editor surface stress case' } ]
},
{
type: 'paragraph',
children: [
{
type: 'text',
text: 'Longer copy keeps the editor stable while the toolbar wraps, the placeholder disappears and the content area keeps its rhythm. '
},
{ type : 'text' , text : 'Inline code' , marks : [ 'code' ] } ,
{ type : 'text' , text : ' and ' } ,
{ type : 'text' , text : 'emphasis' , marks : [ 'italic' ] } ,
{
type: 'text',
text: ' are rendered from the engine metadata, not hand-written demo markup.'
}
]
},
{
type: 'paragraph',
children: [
{
type: 'text',
text: 'This document is intentionally verbose so size, variant and color controls have enough surface to show their actual effect across shell, toolbar, command buttons and text.'
}
]
},
{
type: 'list',
kind: 'ordered',
children: [
{
type: 'list-item',
children: [{ type : 'text' , text : 'Inspect the Morfo tab for parts and attributes.' } ]
},
{
type: 'list-item',
children: [{ type : 'text' , text : 'Use Sema play buttons against the mounted stage.' } ]
},
{
type: 'list-item',
children: [
{ type : 'text' , text : 'Compare Soma and Eidos snippets after changing controls.' }
]
}
]
}
]
};
}
function setPreset(next: DocumentPreset) {
preset = next;
value =
next === 'empty' ? emptyDocument() : next === 'long' ? longDocument() : sampleDocument();
selection = null;
lastCommit = `preset:${ next } `;
documentRevision += 1;
}
function readPlainText(document: WordsDocument): string {
return document.children
.map((block) => {
if (block.type === 'list') {
return block.children
.map((item) => item.children.map(readInlineText).join(''))
.join('\n');
}
return block.children.map(readInlineText).join('');
})
.join('\n');
}
function readInlineText(inline: WordsInline): string {
if (inline.type === 'text') return inline.text;
return inline.children.map(readInlineText).join('');
}
function countWords(text: string): number {
const trimmed = text.trim();
return trimmed ? trimmed.split(/\s+/).length : 0;
}
function selectionLabel(current: WordsSelection | null): string {
if (!current) return 'none';
const samePath =
current.anchor.path.length === current.focus.path.length & &
current.anchor.path.every((part, index) => part === current.focus.path[index]);
return samePath & & current.anchor.offset === current.focus.offset ? 'collapsed' : 'range';
}
function fmtTime(at: number): string {
const d = new Date(at);
return `${ String ( d . getSeconds ()). padStart ( 2 , '0' )} .${ String ( d . getMilliseconds ()). padStart ( 3 , '0' )} `;
}
function quote(value: string): string {
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(' ');
}
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
function describeValue(value: unknown): string {
if (!value || typeof value !== 'object') return '-';
const rec = value as RawValue;
if (rec.kind === 'part-ref') return `part:${ String ( rec . part ?? '-' )} `;
if (rec.kind === 'prop-ref') return `prop:${ String ( rec . prop ?? '-' )} `;
if (rec.kind === 'state-ref') return `state:${ String ( rec . value ?? rec . prop ?? '-' )} `;
if (rec.kind === 'literal') return `literal:${ String ( rec . value ?? '-' )} `;
if (rec.kind === 'translation-ref') return 'translation';
return rec.kind ?? '-';
}
function describeCondition(condition: unknown): string {
if (!condition || typeof condition !== 'object') return '-';
const rec = condition as { when? : string ; prop? : string ; part? : string } ;
return [rec.when, rec.prop, rec.part].filter(Boolean).join(':') || '-';
}
function describeTarget(target: unknown): string {
const text = describeValue(target);
return text === '-' ? 'provider' : text;
}
function intentLiteral(value: unknown): Intent | undefined {
return typeof value === 'string' ? (value as Intent) : undefined;
}
function targetForAction(actionTarget: unknown): HTMLElement | null {
const part = String(actionTarget || 'provider');
const selector = part === 'provider' ? '[data-words]' : `[data-words-${ part } ]`;
return (
(stageRef?.querySelector(selector) as HTMLElement | null) ??
(stageRef?.querySelector('[data-words]') as HTMLElement | null) ??
stageRef
);
}
< / script >
< svelte:head >
< meta name = "perm-axes" content = "disabled,readonly,invalid,required" / >
< / svelte:head >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Editors · Words< / div >
< h1 data-uix-page-title > Words< / h1 >
< p data-uix-page-lede >
Rich-text editor primitive with canonical JSON, contenteditable bridge, command toolbar, form
serialization and semantic events. Morfo declares eight parts and six events; Soma owns state,
selection, input and history; Eidos only paints the emitted data contract.
< / p >
< div data-uix-page-meta >
< span data-uix-meta-pill
>< span data-uix-meta-key > parts</ span > { compiled . parts . order . length } < /span
>
< span data-uix-meta-pill >< span data-uix-meta-key > events</ span > { events . length } </ span >
< span data-uix-meta-pill >< span data-uix-meta-key > sizes</ span > { sizes . length } </ span >
< span data-uix-meta-pill >< span data-uix-meta-key > colors</ span > { colors . length } </ span >
< span data-uix-meta-pill >
< span data-uix-meta-key > apg< / span >
< a href = { wordsMorfo . apg } target="_blank" rel = "noreferrer" > textbox ↗</ a >
< / span >
< / div >
< / header >
< div data-uix-stage >
< div
data-uix-stage-area
bind:this={ stageRef }
style="display: flex; align-items: stretch; justify-content: center; padding: var(--uix-space-5);"
>
{ #key documentRevision }
< Words
bind:value
bind:selection
{ placeholder }
name="body"
{ disabled }
{ readonly }
{ required }
{ invalid }
{ commitOnBlur }
{ size }
{ variant }
{ color }
toolbar={ toolbarConfig }
toolbarOrientation={ orientation }
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
onValueCommit={( _ , reason ) => ( lastCommit = reason )}
>
< Words.Placeholder > { placeholder } </ Words.Placeholder >
< Words.Content
data-perm-step={ ! disabled && ! readonly ? '0' : undefined }
data-perm-label={ ! disabled && ! readonly ? 'edit words content' : undefined }
/>
< Words.HiddenInput / >
{ #if showStatus }
< Words.Status >
{ invalid
? 'Invalid content'
: `${ required ? 'Required editor' : 'Optional editor' } · ${ wordCount } words · ${ lastCommit } `}
< / Words.Status >
{ /if }
< / Words >
{ /key }
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > focus the editor or use toolbar commands to see events< / span >
{ : else }
{ #each trace . slice ( 0 , 3 ) as entry }
< span >
< span data-uix-stage-trace-event > { entry . event } </ span >
· { entry . family }{ entry . intent ? ' · ' + entry . intent : '' }
< / span >
< span style = "color: var(--uix-text-faint)" > { fmtTime ( entry . at )} </ span >
{ /each }
{ /if }
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > selection< / span >
{ selectionState }
< span data-uix-stage-trace-key > words< / span >
{ wordCount }
< / span >
< / div >
< / div >
< 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 > 17< / span >
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / button >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< span data-uix-layer-badge = "morfo" > morfo< / span >
< span data-uix-tab-count > { partsList . length } p · { events . length } e</ span >
< / button >
< button data-uix-tab data-active = { tab === 'sema' } onclick= {() => ( tab = 'sema' )} >
< span data-uix-layer-badge = "sema" > sema< / span >
< span data-uix-tab-count > { events . length } </ span >
< / button >
< button data-uix-tab data-active = { tab === 'recipe' } onclick= {() => ( tab = 'recipe' )}
>Recipe< /button
>
< button data-uix-tab data-active = { tab === 'a11y' } onclick= {() => ( tab = 'a11y' )} > A11y</ button >
< / div >
{ #if tab === 'live' }
< section data-uix-section >
< h2 data-uix-section-title > Controls< / h2 >
< p data-uix-section-desc >
< span data-uix-layer-badge = "soma" > soma< / span > owns document, selection and input.
< span data-uix-layer-badge = "eidos" > eidos< / span > owns size, variant and color.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > props · editor state
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > preset< / span >
< span data-uix-chips role = "radiogroup" >
{ #each presets as item }
< button data-uix-chip data-active = { preset === item } onclick= {() => setPreset ( item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > placeholder< / span >
< input bind:value = { placeholder } / >
< / label >
< label data-uix-control >
< span data-uix-control-label > toolbar< / 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 }
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > toolbar orientation< / span >
< span data-uix-chips role = "radiogroup" >
{ #each orientations as item }
< button
data-uix-chip
data-active={ orientation === item }
onclick={() => ( orientation = item )}
>
{ item }
< / button >
{ /each }
< / span >
< / label >
{ #each [ 'disabled' , 'readonly' , 'required' , 'invalid' , 'commitOnBlur' , 'status' ] as key }
< label data-uix-control >
< span data-uix-control-label > { key } </ span >
< span data-uix-switch >
{ #if key === 'disabled' }
< input type = "checkbox" bind:checked = { disabled } / >
< span data-uix-switch-label > { disabled ? 'on' : 'off' } </ span >
{ :else if key === 'readonly' }
< input type = "checkbox" bind:checked = { readonly } / >
< span data-uix-switch-label > { readonly ? 'on' : 'off' } </ span >
{ :else if key === 'required' }
< input type = "checkbox" bind:checked = { required } / >
< span data-uix-switch-label > { required ? 'on' : 'off' } </ span >
{ :else if key === 'invalid' }
< input type = "checkbox" bind:checked = { invalid } / >
< span data-uix-switch-label > { invalid ? 'on' : 'off' } </ span >
{ :else if key === 'commitOnBlur' }
< input type = "checkbox" bind:checked = { commitOnBlur } / >
< span data-uix-switch-label > { commitOnBlur ? 'on' : 'off' } </ span >
{ : else }
< input type = "checkbox" bind:checked = { showStatus } / >
< span data-uix-switch-label > { showStatus ? 'shown' : 'hidden' } </ span >
{ /if }
< / span >
< / label >
{ /each }
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > props · visual treatment
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > size< / span >
< span data-uix-chips role = "radiogroup" >
{ #each sizes as item }
< button data-uix-chip data-active = { size === item } onclick= {() => ( size = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each variants as item }
< button data-uix-chip data-active = { variant === item } onclick= {() => ( variant = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color< / span >
< span data-uix-chips role = "radiogroup" >
{ #each colors as item }
< button data-uix-chip data-active = { color === item } onclick= {() => ( color = item )} >
{ item }
< / button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-subsection-head > Live document< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< tbody >
< tr >< td class = "name" > blocks</ td >< td > { value . children . length } </ td ></ tr >
< tr >< td class = "name" > words</ td >< td > { wordCount } </ td ></ tr >
< tr >< td class = "name" > selection</ td >< td > { selectionState } </ td ></ tr >
< tr >< td class = "name" > last commit</ td >< td > { lastCommit } </ td ></ tr >
< / tbody >
< / table >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "soma" > soma< / span >
< span > headless · document and commands< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { somaSnippet } </ code ></ pre >
< / div >
< div data-uix-code style = "margin-top: var(--uix-space-3);" >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · compound editor< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { eidosSnippet } </ code ></ pre >
< / div >
< / section >
{ /if }
{ #if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title > API surface< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop / part< / th > < th > Owner< / th > < th > Default< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr
>< td class = "name" > value< / td > < td > soma< / td > < td class = "default" > empty document< / td > < td
>Canonical JSON document, bindable.< /td
>< /tr
>
< tr
>< td class = "name" > selection< / td > < td > soma< / td > < td class = "default" > null< / td > < td
>Document selection, bindable.< /td
>< /tr
>
< tr
>< td class = "name" > placeholder< / td > < td > soma< / td > < td class = "default empty" > -< / td > < td
>Used by Placeholder while empty.< /td
>< /tr
>
< tr
>< td class = "name" > name< / td > < td > soma< / td > < td class = "default empty" > -< / td > < td
>Enables HiddenInput JSON serialization.< /td
>< /tr
>
< tr
>< td class = "name" > disabled / readonly< / td > < td > soma< / td > < td class = "default" > false< /td
>< td > Disables editing or keeps read-only focus semantics.< / td > < /tr
>
< tr
>< td class = "name" > required / invalid< / td > < td > soma< / td > < td class = "default" > false< /td
>< td > ARIA and recipe state, Field-compatible.< / td > < /tr
>
< tr
>< td class = "name" > commitOnBlur< / td > < td > soma< / td > < td class = "default" > true< / td > < td
>Fires commit-content on blur.< /td
>< /tr
>
< tr
>< td class = "name" > onValueCommit< / td > < td > soma< / td > < td class = "default empty" > -< / td > < td
>Receives document and reason.< /td
>< /tr
>
< tr
>< td class = "name" > Content< / td > < td > soma< / td > < td class = "default" > required< / td > < td
>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
>
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< tr
>< td class = "name" > Toolbar / ToolbarGroup< / td > < td > soma< / td > < td class = "default"
>optional< /td
>< td > Manual composition stays available for fully custom command layouts.< / td > < /tr
Build TextArea component + fix locale propagation bug across soma providers
Major work in this session:
1. NumberField demo refactored to expose Intl.NumberFormatOptions live
- 9 canonical presets (currency/unit/percent/compact/plain)
- Locale/currency/unit lists derived from $libs/langs, $libs/currency,
$format/units (no more hardcoded arrays)
- Default locale from uix.format.numbers.getLocale()
2. Bug fix: locale prop ignored when soma.format service is active
- soma/NumberField: 5 bypass points via new `hasOwnLocale` getter
(decimalSeparator, groupSeparator, formatValue, formatEditValue, parseValue)
- soma/Calendar: dateOrder respects locale prop (matches DateField precedence)
- soma/RangeCalendar: same dateOrder fix
- When the consumer passes `locale` prop, soma.nums/dates is bypassed
and Intl is used directly with the prop locale
3. TextArea — new component, full 4-layer stack
- morfo: 3 parts (provider/input/count), 3 events
(commit-input/commit-submit/shift-count-overflow), APG textbox
- soma: TextAreaProvider with autosize (scrollHeight measurement),
submitOn modes (false|enter|shift+enter|mod+enter), maxLength soft clamp,
Field integration (OR-merge), counter overflow tracking
- eidos: 5 sizes × 3 variants × 8 colors, autosize suppresses CSS resize
- sema: commit-submit + shift-count-overflow perceptual cues
- demo: canonical 6-tab depth, all props exposed as live controls
- PrimitiveTextareaAttributes added to soma/types/html.ts
4. Also includes accumulated unreleased work from prior sessions:
- Eidos ports: announce, clipboard, drag-drop, feed, table,
tree-grid, tree-view, virtual-grid, virtual-list, words
- Their canonical demos under web/routes/uix/components/
- Semantic events defined for utility morfos
(announce/clipboard/virtual-list/virtual-grid)
- Tree indent guides fixed (TreeView ::before, TreeGrid stacked gradients)
- Sidebar reorganized with Data + Utilities groups
- Eidos words content layer and primitives
Reference library survey performed before TextArea API design (Mantine,
Radix Themes, Chakra v3, React Aria, Ark UI, Bits UI, shadcn, MUI, Prime).
Decision documented: no separate CurrencyField/UnitField — single
NumberField with formatOptions is the canon (matches all surveyed
headless libs except PrimeReact's mode enum, which still uses 1 component).
svelte-check: 0 errors in modified/created files.
See CONTINUE.md for full session notes and next steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
>
< tr
>< td class = "name" > CommandButton< / td > < td > soma< / td > < td class = "default" > optional< / td > < td
>Runs command names and exposes active state.< /td
>< /tr
>
< tr
>< td class = "name" > HiddenInput / Status< / td > < td > soma< / td > < td class = "default"
>optional< /td
>< td > Form serialization and polite status.< / td > < /tr
>
< tr
>< td class = "name" > size< / td > < td > eidos< / td > < td class = "default" > md< / td > < td
>{ sizes . join ( ' | ' )} < /td
>< /tr
>
< tr
>< td class = "name" > variant< / td > < td > eidos< / td > < td class = "default" > surface< / td > < td
>{ variants . join ( ' | ' )} < /td
>< /tr
>
< tr
>< td class = "name" > color< / td > < td > eidos< / td > < td class = "default" > primary< / td > < td
>{ colors . join ( ' | ' )} < /td
>< /tr
>
< tr
>< td class = "name" > reference row< / td > < td > audit< / td > < td class = "default" > V1< / td > < td
>React Aria/WAI-ARIA inform textbox + toolbar; Lexical informs editor model;
Edra/Tiptap stays a benchmark, not a dependency.< /td
>< /tr
>
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title > Morfo contract< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< tbody >
< tr >< td class = "name" > name</ td >< td > { wordsMorfo . name } </ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td > { wordsMorfo . kebab } </ td ></ tr >
< tr >< td class = "name" > scope</ td >< td > { wordsMorfo . scope . join ( ' + ' )} </ td ></ tr >
< tr
>< td class = "name" > apg< / td > < td
>< a href = { wordsMorfo . apg } target="_blank" rel = "noreferrer" > textbox</ a > < /td
>< /tr
>
< tr >< td class = "name" > parts</ td >< td > { wordsMorfo . parts . length } </ td ></ tr >
< tr >< td class = "name" > events</ td >< td > { wordsMorfo . events . length } </ td ></ tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Parts overview< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead
>< tr
>< th > Part< / th > < th > Marker< / th > < th > Element< / th > < th > Role< / th > < th > Archetype< / th > < th
>States< /th
>< th > Optional< / th > < /tr
>< /thead
>
< tbody >
{ #each partsList as part }
< tr >
< td class = "name" > { part . kebab } </ td >
< td >< code data-uix-part-marker > [{ part . marker } ]</ code ></ td >
< td class = "type" > < { part . defaultElement } > </ td >
< td class = "type" > { part . role ?? '-' } </ td >
< td > { part . archetype ?? '-' } </ td >
< td > { part . states ? . join ( ' | ' ) ?? '-' } </ td >
< td class = "default" > { part . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ #each wordsMorfo . parts as rawPart }
{ @const partAny = rawPart as unknown as RawPart }
{ @const dataAttrs = compiled . contracts . dataAttrsByPart . get ( partAny . kebab ) ?? []}
{ @const ariaAttrs = partAny . aria ?? []}
{ @const keyboard = partAny . keyboard ?? []}
{ #if dataAttrs . length || ariaAttrs . length || keyboard . length }
< div data-uix-subsection-head > { partAny . kebab } </ div >
{ #if dataAttrs . length }
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Data attr< / th > < th > Values< / th > < th > Source< / th > < / tr > < / thead >
< tbody >
{ #each dataAttrs as attr }
< tr >
< td class = "name" > { attr . attr } </ td >
< td class = "type" > { attr . values ? attr . values . join ( ' | ' ) : '-' } </ td >
< td
>{ 'value' in attr
? describeValue((attr as { value? : unknown } ).value)
: '-'}< /td
>
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ #if ariaAttrs . length }
< div data-uix-table-wrap >
< table data-uix-table >
< thead
>< tr > < th > ARIA attr< / th > < th > Source< / th > < th > Condition< / th > < th > Severity< / th > < /tr
>< /thead
>
< tbody >
{ #each ariaAttrs as attr }
< tr >
< td class = "name" > { attr . attr } </ td >
< td > { describeValue ( attr . value )} </ td >
< td > { describeCondition ( attr . condition )} </ td >
< td > { attr . severity ?? '-' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ #if keyboard . length }
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Key< / th > < th > Action< / th > < / tr > < / thead >
< tbody >
{ #each keyboard as key }
< tr >< td >< span data-uix-kbd > { key . key } </ span ></ td >< td > { key . action } </ td ></ tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
{ /if }
{ /each }
< div data-uix-subsection-head > Events declaration< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead
>< tr
>< th > Name< / th > < th > Family< / th > < th > Verb< / th > < th > Sequence< / th > < th > Intent< / th > < th
>Target< /th
>< th > Prewrite< / th > < th > Commit< / th > < /tr
>< /thead
>
< tbody >
{ #each wordsMorfo . events as event }
{ @const eventAny = event as unknown as RawEvent }
< tr >
< td class = "name" > { eventAny . name } </ td >
< td class = "type" > { eventAny . semantic . family } </ td >
< td > { eventAny . semantic . verb ?? '-' } </ td >
< td > { eventAny . semantic . sequence ?? '-' } </ td >
< td
>{ typeof eventAny . semantic . intent === 'string'
? eventAny.semantic.intent
: describeValue(eventAny.semantic.intent)}< /td
>
< td > { describeTarget ( eventAny . semantic . target )} </ td >
< td > { eventAny . prewrite ? 'yes' : '-' } </ td >
< td > { eventAny . commit ? 'yes' : '-' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'sema' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "sema" > sema< / span > · events + playback
< / h2 >
< p data-uix-section-desc >
Typing stays local until it commits; discrete focus, format, history, clear and invalid
input actions emit semantic events.
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead
>< tr
>< th > Name< / th > < th > Family< / th > < th > Verb< / th > < th > Sequence< / th > < th > Intent< / th > < th
>Target< /th
>< th > Play< / th > < /tr
>< /thead
>
< tbody >
{ #each events as action }
< tr >
< td class = "name" > { action . name } </ td >
< td class = "type" > { action . semantic . family } </ td >
< td > { action . semantic . verb ?? '-' } </ td >
< td > { action . semantic . sequence ?? '-' } </ td >
< td > { action . semantic . intent ?? '-' } </ td >
< td > { action . target } </ td >
< td >
< button
data-uix-play
onclick={() => {
const target = targetForAction(action.target);
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
intent: intentLiteral(action.semantic.intent),
target
});
}}
>
Play
< / button >
< / td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'recipe' }
< section data-uix-section >
< h2 data-uix-section-title > Eidos recipe< / h2 >
< p data-uix-section-desc >
Selectors live in < code > src/uix/eidos/components/words/words.css< / code > ; public tokens are
declared under < code > THEME_BASE_RECIPE_TOKENS.words< / code > .
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Source< / th > < th > Role< / th > < / tr > < / thead >
< tbody >
{ #each recipeSelectors as row }
< tr >
< td class = "name" > { row . selector } </ td >
< td >< span data-uix-tag data-kind = { row . kind } > { row . source } </span ></ td >
< td > { row . note } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Concern< / th > < th > Contract< / th > < / tr > < / thead >
< tbody >
< tr
>< td class = "name" > Content< / td > < td
>< code > role="textbox"< / code > , < code > aria-multiline="true"< / code > and contenteditable managed
by Soma.< /td
>< /tr
>
< tr
>< td class = "name" > Toolbar< / td > < td
>< code > role="toolbar"< / code > with orientation propagated to ARIA.< /td
>< /tr
>
< tr
>< td class = "name" > Buttons< / td > < td
>Command buttons are native buttons; toggle commands expose < code > aria-pressed< /code
>.< /td
>< /tr
>
< tr
>< td class = "name" > Readonly< / td > < td
>Readonly keeps content focusable and sets < code > aria-readonly< / code > .< /td
>< /tr
>
< tr
>< td class = "name" > Disabled< / td > < td
>Disabled removes editing and prevents command execution.< /td
>< /tr
>
< tr
>< td class = "name" > Status< / td > < td
>Status uses < code > role="status"< / code > and < code > aria-live="polite"< / code > .< /td
>< /tr
>
< tr
>< td class = "name" > Form< / td > < td
>HiddenInput serializes the full JSON document when < code > name< / code > is present.< /td
>< /tr
>
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Keyboard< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Key< / th > < th > Action< / th > < / tr > < / thead >
< tbody >
{ #each ( wordsMorfo . parts . find (( part ) => part . kebab === 'content' ) as unknown as RawPart | undefined ) ? . keyboard ?? [] as key }
< tr >< td >< span data-uix-kbd > { key . key } </ span ></ td >< td > { key . action } </ td ></ tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >