You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/announce/+page.svelte

536 lines
24 KiB

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 {
Announce,
type AnnounceApi,
type AnnouncePriority,
type AnnounceRole
} from '$uix/eidos/components/announce';
import { Button } from '$uix/eidos/components/button';
import { compileMorfo } from '$uix/morfo';
import { announceMorfo } from '@/uix/morfo/components/announce';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
// ── Imperative path: api.announce(msg, priority) ─────────────────────
let api: AnnounceApi | null = $state(null);
let customMessage = $state('Profile saved');
let priority = $state<AnnouncePriority>('polite');
let defaultTimeout = $state(1000);
type HistoryEntry = { msg: string; priority: AnnouncePriority; at: number };
let history = $state<HistoryEntry[]>([]);
function send(msg: string, p: AnnouncePriority = priority) {
if (!msg.trim() || !api) return;
api.announce(msg, p);
history = [{ msg, priority: p, at: Date.now() }, ...history].slice(0, 12);
}
function sendCustom() {
send(customMessage);
}
// ── Quick scenarios (canonical uses, not hardcoded fluff) ────────────
const SCENARIOS: { label: string; msg: string; priority: AnnouncePriority }[] = [
{ label: 'Save success', msg: 'Profile saved', priority: 'polite' },
{ label: 'Validation error', msg: 'Email is required', priority: 'assertive' },
{ label: 'Network failure', msg: 'Connection lost — retrying', priority: 'assertive' },
{ label: 'Pagination', msg: 'Showing page 3 of 12', priority: 'polite' },
{ label: 'Item added', msg: 'Item added to cart (4 items)', priority: 'polite' }
];
// ── Declarative path: Region with bound `message` prop ───────────────
let regionRole = $state<AnnounceRole>('status');
let regionMessage = $state('');
let regionVisible = $state(false);
let regionAtomic = $state(true);
// ── Log-mode demo: bounded chat-like history ─────────────────────────
let logEntries = $state<string[]>([]);
let logInput = $state('');
function pushLog() {
if (!logInput.trim()) return;
logEntries = [...logEntries.slice(-9), logInput];
logInput = '';
}
// ── Live region inspector (read back what the AT would receive) ──────
let politeRegionText = $state('');
let assertiveRegionText = $state('');
$effect(() => {
// Soma's internal regions live somewhere in the DOM — find them after mount.
const tick = () => {
const polite = document.querySelector('[aria-live="polite"][data-announce-internal-region]');
const assertive = document.querySelector('[aria-live="assertive"][data-announce-internal-region]');
politeRegionText = polite?.textContent ?? '';
assertiveRegionText = assertive?.textContent ?? '';
};
const id = setInterval(tick, 100);
return () => clearInterval(id);
});
const compiled = compileMorfo(announceMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
let stageRef = $state<HTMLElement | null>(null);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Announce } from '$uix/eidos/components/announce';",
' let api: AnnounceApi | null = $state(null);',
'</' + 'script>',
'',
'<Announce',
defaultTimeout !== 1000 && ` defaultTimeout={${defaultTimeout}}`,
'>',
' {#snippet children(snippetProps)}',
' {(api = snippetProps, "")}',
'',
' <!-- Imperative: post on demand -->',
` <button onclick={() => api.announce("${customMessage}", "${priority}")}>Send</button>`,
'',
' <!-- Declarative: addressable region -->',
' <Announce.Region',
regionRole !== 'status' && ` role="${regionRole}"`,
' message={someBoundState}',
!regionVisible ? ' visuallyHidden' : ' visuallyHidden={false}',
!regionAtomic && ' aria-atomic={false}',
' />',
' {/snippet}',
'</Announce>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Utility · Announce</div>
<h1 data-uix-page-title>Announce</h1>
<p data-uix-page-lede>
ARIA live-region service for screen-reader announcements. The component
itself paints nothing — its job is to write text into hidden live regions
so assistive tech reads it aloud. Two usage shapes: <strong>imperative</strong>
(<code>api.announce(msg, priority)</code> from any descendant) and
<strong>declarative</strong> (<code>&lt;Announce.Region message&gt;</code>
bound to reactive state). Compose with <code>Clipboard</code>,
<code>DragDrop</code>, form validation, chat logs.
</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>history</span> {history.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>region role</span> {regionRole}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>timeout</span> {defaultTimeout}ms
</span>
</div>
</header>
<Announce {defaultTimeout}>
{#snippet children(snippetProps)}
{(api = snippetProps, '')}
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="padding: var(--uix-space-4); display: flex; flex-direction: column; gap: var(--uix-space-4);">
<!-- Imperative path -->
<div>
<div style="font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);">
Imperative path · <code>api.announce(msg, priority)</code>
</div>
<div style="display: flex; gap: var(--uix-space-2); align-items: stretch; flex-wrap: wrap;">
<input
type="text"
bind:value={customMessage}
onkeydown={(e) => { if (e.key === 'Enter') sendCustom(); }}
placeholder="Type a message to announce…"
style="flex: 1 1 18rem; padding: 0.375rem 0.625rem; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-content-primary); font: inherit;"
/>
<span data-uix-chips role="radiogroup" style="align-self: stretch;">
{#each ['polite', 'assertive'] as p (p)}
<button data-uix-chip data-active={priority === p} onclick={() => (priority = p as AnnouncePriority)}>{p}</button>
{/each}
</span>
<Button size="sm" variant="surface" onclick={sendCustom} disabled={!customMessage.trim()}>
Announce
</Button>
</div>
<div style="margin-block-start: var(--uix-space-2); display: flex; flex-wrap: wrap; gap: var(--uix-space-1);">
{#each SCENARIOS as s (s.label)}
<button
data-uix-chip
onclick={() => { customMessage = s.msg; priority = s.priority; send(s.msg, s.priority); }}
>{s.label}</button>
{/each}
<button data-uix-chip onclick={() => api?.clear()}>Clear regions</button>
</div>
</div>
<!-- Live region inspector — show what the AT actually sees -->
<div>
<div style="font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);">
Live region inspector · what the screen reader currently sees
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--uix-space-2);">
<div style="padding: var(--uix-space-2) var(--uix-space-3); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-subtle); font-family: monospace; font-size: var(--font-size-xs);">
<div style="color: var(--color-content-secondary); margin-block-end: 0.25rem;">polite</div>
<div style="min-block-size: 1.5rem;">{politeRegionText || '—'}</div>
</div>
<div style="padding: var(--uix-space-2) var(--uix-space-3); border: 1px solid var(--color-threat-element); border-radius: var(--radius-md); background: var(--color-threat-surface, var(--color-surface-subtle)); font-family: monospace; font-size: var(--font-size-xs);">
<div style="color: var(--color-content-secondary); margin-block-end: 0.25rem;">assertive</div>
<div style="min-block-size: 1.5rem;">{assertiveRegionText || '—'}</div>
</div>
</div>
</div>
<!-- Declarative Region path -->
<div>
<div style="font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);">
Declarative path · <code>&lt;Announce.Region role message&gt;</code>
</div>
<div style="display: flex; gap: var(--uix-space-2); align-items: center; flex-wrap: wrap; margin-block-end: var(--uix-space-2);">
<input
type="text"
bind:value={regionMessage}
placeholder="Bound message — AT announces on every change…"
style="flex: 1 1 16rem; padding: 0.375rem 0.625rem; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-content-primary); font: inherit;"
/>
<label style="display: inline-flex; align-items: center; gap: 0.375rem; font-size: var(--font-size-xs);">
<input type="checkbox" bind:checked={regionVisible} />
visible
</label>
<label style="display: inline-flex; align-items: center; gap: 0.375rem; font-size: var(--font-size-xs);">
<input type="checkbox" bind:checked={regionAtomic} />
aria-atomic
</label>
</div>
<Announce.Region role={regionRole} message={regionMessage} visuallyHidden={!regionVisible} aria-atomic={regionAtomic} />
</div>
<!-- Log-mode showcase -->
<div>
<div style="font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);">
Log mode · <code>role="log"</code> — running history (chat / activity)
</div>
<div style="display: flex; gap: var(--uix-space-2); align-items: stretch; margin-block-end: var(--uix-space-2);">
<input
type="text"
bind:value={logInput}
onkeydown={(e) => { if (e.key === 'Enter') pushLog(); }}
placeholder='Append a "chat" line…'
style="flex: 1 1 auto; padding: 0.375rem 0.625rem; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-default); color: var(--color-content-primary); font: inherit;"
/>
<Button size="sm" variant="surface" onclick={pushLog} disabled={!logInput.trim()}>Append</Button>
<Button size="sm" variant="ghost" onclick={() => (logEntries = [])}>Clear</Button>
</div>
<Announce.Region role="log" visuallyHidden={false} aria-atomic={false}>
{#if logEntries.length === 0}
<div style="color: var(--color-content-secondary); font-size: var(--font-size-xs); font-style: italic;">No entries yet — type and Enter to append.</div>
{:else}
<ul style="margin: 0; padding-inline-start: 1rem; font-size: var(--font-size-sm);">
{#each logEntries as line, i (i + line)}
<li>{line}</li>
{/each}
</ul>
{/if}
</Announce.Region>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>history</span>
{#if history.length === 0}
<span>type a message and Enter, or pick a scenario — the live region inspector above shows what the AT receives</span>
{:else}
{#each history.slice(0, 3) as h}
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><strong>{h.priority}</strong> · "{h.msg}"</span>
<span style="color: var(--uix-text-faint)">{fmtTime(h.at)}</span>
{/each}
{/if}
</div>
</div>
{/snippet}
</Announce>
<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>2</span></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>
The 4 ARIA region roles imply different semantics. <code>status</code> = polite info,
<code>alert</code> = assertive (interrupts), <code>log</code> = running history,
<code>timer</code> = time-sensitive (don't announce every tick).
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> behaviour
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>defaultTimeout</span>
<span data-uix-chips role="radiogroup">
{#each [500, 1000, 3000] as n (n)}
<button data-uix-chip data-active={defaultTimeout === n} onclick={() => (defaultTimeout = n)}>{n}ms</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>Region role</span>
<span data-uix-chips role="radiogroup">
{#each ['status', 'alert', 'log', 'timer'] as r (r)}
<button data-uix-chip data-active={regionRole === r} onclick={() => (regionRole = r as AnnounceRole)}>{r}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>default priority</span>
<span data-uix-chips role="radiogroup">
{#each ['polite', 'assertive'] as p (p)}
<button data-uix-chip data-active={priority === p} onclick={() => (priority = p as AnnouncePriority)}>{p}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · Announce + Region (imperative + declarative)</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 reference</h2>
<p data-uix-section-desc>
2 parts: <code>Provider · Region</code>. Provider mounts 2 internal regions
(polite + assertive) and exposes an imperative <code>api</code> via snippet
props + via context. Region is for declarative purpose-specific live regions.
</p>
<div data-uix-subsection-head>Provider (Announce)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">defaultTimeout</td><td class="type">number</td><td class="default">1000</td><td>ms before a posted message clears so the same text can be re-announced.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Region (declarative)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">role</td><td class="type">'status' | 'alert' | 'log' | 'timer'</td><td class="default">'status'</td><td>Implies polite/assertive + AT-specific handling semantics.</td></tr>
<tr><td class="name">aria-live</td><td class="type">'polite' | 'assertive'</td><td class="default empty">from role</td><td>Override the implicit urgency.</td></tr>
<tr><td class="name">aria-atomic</td><td class="type">boolean</td><td class="default">true</td><td>Whether AT reads the entire region on change (vs the diff only).</td></tr>
<tr><td class="name">aria-relevant</td><td class="type">string</td><td class="default">'additions text'</td><td>Which mutations trigger an announcement.</td></tr>
<tr><td class="name">message</td><td class="type">string</td><td class="default empty">—</td><td>Bound reactive text — AT reads on change.</td></tr>
<tr><td class="name">visuallyHidden</td><td class="type">boolean</td><td class="default">true</td><td>Standard sr-only technique. Set false for visible+announced banners.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Imperative API (Provider snippet props)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Method</th><th>Signature</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">announce</td><td class="type">(message, priority?, timeout?) => void</td><td>Post to the polite (default) or assertive internal region.</td></tr>
<tr><td class="name">clear</td><td class="type">() => void</td><td>Clear all internal live regions immediately.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span>
· declarative contract
</h2>
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/announce.ts</code>.</p>
<div data-uix-subsection-head>Parts</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>States</th><th>Optional</th></tr></thead>
<tbody>
{#each partsList as part (part.kebab)}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '—'}</td>
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<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></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
<td>{sem.verb ?? '—'}</td>
<td>{sem.sequence ?? 'pre'}</td>
<td class="default">{intentStr}</td>
<td>{action.target}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section id="sema">
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span>
· events
</h2>
<p data-uix-section-desc>Announce raises <code>commit-announce-polite</code> / <code>commit-announce-assertive</code> on every message post, and <code>commit-reset</code> when regions are wiped. Use Play to replay an event against the Provider.</p>
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
<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>Play</th></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
<tr>
<td class="name">{action.name}</td>
<td class="type">{action.semantic.family}</td>
<td>{action.semantic.verb ?? '—'}</td>
<td>{action.semantic.sequence ?? 'pre'}</td>
<td class="default">{effectiveIntent ?? '—'}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = (stageRef?.querySelector('[data-announce-region]') ?? stageRef) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ 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 at <code>src/uix/eidos/components/announce/announce.css</code>. The Provider is <code>display: contents</code> — no visual surface. Regions get the standard sr-only technique unless <code>visuallyHidden={false}</code>.</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>What it paints</th></tr></thead>
<tbody>
<tr><td class="name">[data-announce]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td><code>display: contents</code> — invisible host.</td></tr>
<tr><td class="name">[data-announce-region]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Live region — sr-only when <code>visuallyHidden</code>.</td></tr>
<tr><td class="name">[data-announce-region]:not([data-visually-hidden])</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Card chrome when the Region opts into visible UI.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Announce IS the perception channel for the screen reader audience —
the component's whole purpose is to write text into <code>aria-live</code>
regions. Without an active screen reader, the regions are functionally
invisible (sr-only). The live region inspector above mirrors the region
contents so you can verify behaviour without launching a real AT.
</p>
<div data-uix-subsection-head>Role implications</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>role</th><th>Implicit aria-live</th><th>Use for</th><th>Example message</th></tr></thead>
<tbody>
<tr><td class="name">status</td><td class="type">polite</td><td>Non-critical updates.</td><td>"Profile saved"</td></tr>
<tr><td class="name">alert</td><td class="type">assertive</td><td>Errors, urgent notices.</td><td>"Connection lost"</td></tr>
<tr><td class="name">log</td><td class="type">polite</td><td>Running history (chat, activity).</td><td>New chat lines appended.</td></tr>
<tr><td class="name">timer</td><td class="type">polite</td><td>Time-sensitive info (don't announce every tick).</td><td>"5 minutes remaining"</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Best practices</div>
<ul style="margin: 0; padding-inline-start: 1.25rem; color: var(--color-content-secondary); font-size: var(--font-size-sm);">
<li>Mount <code>&lt;Announce&gt;</code> ONCE near the app root — multiple providers create duplicate regions and AT may read messages twice.</li>
<li>Posts are cleared after <code>defaultTimeout</code> ms so identical consecutive strings still trigger an announcement (AT dedupes within a region).</li>
<li>Use <code>polite</code> by default. Reserve <code>assertive</code> for errors and urgent interruptions only — abusing it makes the AT exhausting.</li>
<li>For visually-shown status banners, set <code>visuallyHidden={false}</code> — the eidos recipe paints outlined card chrome.</li>
<li>Verify with a real screen reader (VoiceOver / NVDA / JAWS) — the inspector above is a debugging aid, not a substitute.</li>
</ul>
</section>
{/if}
</div>

Powered by TurnKey Linux.