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 {
Clipboard,
type ClipboardSize,
type ClipboardColor
} from '$uix/eidos/components/clipboard';
import { Button } from '$uix/eidos/components/button';
import { compileMorfo } from '$uix/morfo';
import { clipboardMorfo } from '@/uix/morfo/components/clipboard';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state< Tab > ('live');
// ── User-controlled value ────────────────────────────────────────────
let userValue = $state('https://anthropic.com/claude');
let textareaValue = $state(`{
"name": "@anthropic-ai/sdk",
"version": "1.0.0",
"description": "Claude SDK"
}`);
// ── Visual props ─────────────────────────────────────────────────────
let size = $state< ClipboardSize > ('md');
let color = $state< ClipboardColor > ('affirm');
let timeout = $state(2000);
let triggerVariant = $state< 'surface' | 'outline' | 'ghost'>('ghost');
let iconOnly = $state(false);
// ── Audit log ────────────────────────────────────────────────────────
type LogEntry = { kind : 'ok' | 'err' ; msg : string ; at : number } ;
let log = $state< LogEntry [ ] > ([]);
function pushLog(kind: 'ok' | 'err', msg: string) {
log = [{ kind , msg , at : Date.now () } , ...log].slice(0, 8);
}
// ── Read-back ("last copied" memory — eidos echoes what we sent) ────
let lastCopied = $state< string | null > (null);
function recordCopy(v: string) {
lastCopied = v;
pushLog('ok', `copied ${ v . length } chars: "${ v . slice ( 0 , 40 )} ${ v . length > 40 ? '…' : '' } "`);
}
function recordError(e: unknown) {
const msg = String(e instanceof Error ? e.message : e);
const friendlier = msg.includes('not focused')
? 'Browser blocked clipboard (iframe sin foco). En navegador real funciona — click humano da foco al documento.'
: msg;
pushLog('err', friendlier);
}
// ── Trace from MutationObserver ─────────────────────────────────────
let stageRef = $state< HTMLElement | null > (null);
let lastEvent = $state< string | null > (null);
$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) lastEvent = ev;
}
});
obs.observe(el, { attributes : true , subtree : true , attributeFilter : [ 'data-event' ] } );
return () => obs.disconnect();
});
const compiled = compileMorfo(clipboardMorfo);
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' )} `;
}
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Clipboard } from '$uix/eidos/components/clipboard';",
'< /' + 'script>',
'',
'< Clipboard ' ,
` value="${ userValue . length > 40 ? userValue . slice ( 0 , 40 ) + '…' : userValue } "`,
size !== 'md' && ` size="${ size } "`,
color !== 'affirm' && ` color="${ color } "`,
timeout !== 2000 && ` timeout={ $ { timeout }} `,
' onCopy={( v ) => log ( "copied" , v )} ',
' onError={( e ) => log ( "error" , e )} ',
'>',
` < Clipboard.Trigger $ { triggerVariant !== 'ghost' ? ` variant="$ { triggerVariant } " ` : '' } $ { iconOnly ? ' iconOnly' : '' } > ${ iconOnly ? '' : 'Copy' } </ Clipboard.Trigger > `,
' < Clipboard.Indicator / > ',
'< / Clipboard > '
]
.filter(Boolean)
.join('\n')
);
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Utility · Clipboard< / div >
< h1 data-uix-page-title > Clipboard< / h1 >
< p data-uix-page-lede >
Copy text to the clipboard with an auto-resetting < code > copied< / code >
state. Soma drives < code > navigator.clipboard.writeText< / code > + the
< code > timeout< / code > reset; eidos forwards the Trigger to
< code > < Button> < / code > via the consumer pattern and animates
the Indicator chip with the cascaded color accent.
< / 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 > 5
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > timeout</ span > { timeout } ms
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > color</ span > { color }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > last event</ span > { lastEvent ?? '—' }
< / span >
< / div >
< / header >
< 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 );" >
<!-- Single - line value (inline trigger + indicator) -->
< div >
< div style = "font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);" >
Inline value · Trigger sits next to the content to copy
< / div >
< div style = "display: flex; align-items: stretch; gap: var(--uix-space-2);" >
< input
type="text"
bind:value={ userValue }
placeholder="Type or paste anything to copy…"
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; font-family: monospace;"
/>
< div style = "display: inline-flex; align-items: center; gap: var(--uix-space-1); flex-shrink: 0;" >
< Clipboard
value={ userValue }
{ size }
{ color }
{ timeout }
onCopy={ recordCopy }
onError={ recordError }
>
< Clipboard.Trigger variant = { triggerVariant } { iconOnly } >
{ iconOnly ? '📋' : 'Copy' }
< / Clipboard.Trigger >
< Clipboard.Indicator / >
< / Clipboard >
< / div >
< / div >
< / div >
<!-- Multi - line block (separate trigger) -->
< div >
< div style = "font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);" >
Block content · Trigger above a multi-line snippet
< / div >
< div style = "display: flex; justify-content: flex-end; margin-block-end: 0.25rem;" >
< Clipboard
value={ textareaValue }
{ size }
{ color }
{ timeout }
onCopy={ recordCopy }
onError={ recordError }
>
< Clipboard.Trigger variant = { triggerVariant } { iconOnly } >
{ iconOnly ? '📋' : 'Copy snippet' }
< / Clipboard.Trigger >
< Clipboard.Indicator / >
< / Clipboard >
< / div >
< textarea
bind:value={ textareaValue }
rows="5"
style="inline-size: 100%; box-sizing: border-box; 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); color: var(--color-content-primary); font: inherit; font-family: monospace; font-size: var(--font-size-xs); resize: vertical;"
>< / textarea >
< / div >
<!-- Read - back: what was last copied -->
< div >
< div style = "font-size: var(--font-size-xs); color: var(--color-content-secondary); margin-block-end: var(--uix-space-2);" >
Last copied · what the consumer's < code > onCopy< / code > recorded
< / div >
< 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); min-block-size: 2.5rem; white-space: pre-wrap; word-break: break-all;" > { lastCopied ?? '— nothing copied yet —' } </ div >
< / div >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > log< / span >
{ #if log . length === 0 }
< span > type a value above, click Copy — the Indicator chip pops for { timeout } ms; < code > onCopy</ code > / < code > onError</ code > entries land here.</ span >
{ : else }
fix(demos): eliminate each_key_duplicate crash in 22 component demos
The event-trace inspector in each demo keyed its `{#each}` by a
`Date.now()` timestamp (`entry.at` / `h.at`). A single interaction can
stamp `data-event` on several elements within the same millisecond, so
two trace entries share the key and Svelte throws `each_key_duplicate`
(reported on dropdown-menu).
The trace is an ephemeral, 3-item, text-only log with no transitions or
stateful children, so positional reconciliation is correct: drop the
timestamp key (the each becomes unkeyed). The `fmtTime(...at)` display is
left intact.
Swept all 22 demos carrying the pattern (21 keyed by `entry.at` + announce
by `h.at`): dropdown-menu, context-menu, menubar, navigation-menu, listbox,
grid-list, table, tree-view, tree-grid, feed, command, carousel, drag-drop,
clipboard, announce, alert-dialog, button, color-field, link-preview,
range-calendar, time-field, time-range-field.
Verified: `as \w+ (\w+.at)` -> 0 occurrences site-wide; `npm run check`
0 errors; browser repro on dropdown-menu + table (burst of events, no
each_key_duplicate, console clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
{ #each log . slice ( 0 , 3 ) as entry }
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 style = "color: { entry . kind === 'err' ? 'var(--color-threat-solid)' : 'inherit' } " >< strong > { entry . kind } </ strong > · { entry . msg } </ span >
< span style = "color: var(--uix-text-faint)" > { fmtTime ( entry . at )} </ span >
{ /each }
{ /if }
< / 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 > 3</ 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 Trigger renders via the Button consumer pattern — every Button visual prop
(variant, iconOnly, rounded, size, color, intent) is forwarded. The Indicator
is conditionally mounted while < code > copied=true< / code > .
< / 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 > timeout< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 500 , 2000 , 5000 ] as n ( n )}
< button data-uix-chip data-active = { timeout === n } onclick= {() => ( timeout = n )} > { n } ms</ button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > Clipboard cascade
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > size< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'xs' , 'sm' , 'md' , 'lg' , 'xl' ] as s ( s )}
< button data-uix-chip data-active = { size === s } onclick= {() => ( size = s as ClipboardSize )} > { s } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color (Indicator chip)< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'primary' , 'secondary' , 'neutral' , 'affirm' , 'fulfill' , 'risk' , 'threat' , 'loss' ] as c ( c )}
< button data-uix-chip data-active = { color === c } onclick= {() => ( color = c as ClipboardColor )} > { c } </ button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > Trigger Button
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'surface' , 'outline' , 'ghost' ] as v ( v )}
< button data-uix-chip data-active = { triggerVariant === v } onclick= {() => ( triggerVariant = v as 'surface' | 'outline' | 'ghost' )} > { v } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > iconOnly< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { iconOnly } / >
< span data-uix-switch-label > { iconOnly ? '📋 only' : 'with label' } </ span >
< / span >
< / label >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · Clipboard + Trigger (Button consumer) + Indicator< / 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 >
3 parts: < code > Provider · Trigger · Indicator< / code > . Trigger reuses the eidos
< code > < Button> < / code > via the consumer pattern; pass < code > variant< / code > ,
< code > iconOnly< / code > , < code > icon< / code > , etc just like any Button.
< / p >
< div data-uix-subsection-head > Provider (Clipboard)< / 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" > value< / td > < td class = "type" > string< / td > < td class = "default empty" > required< / td > < td > String to copy on Trigger click.< / td > < / tr >
< tr > < td class = "name" > timeout< / td > < td class = "type" > number< / td > < td class = "default" > 2000< / td > < td > ms before < code > copied< / code > resets to false.< / td > < / tr >
< tr > < td class = "name" > onCopy< / td > < td class = "type" > (value) => void< / td > < td class = "default empty" > —< / td > < td > Fires on successful copy.< / td > < / tr >
< tr > < td class = "name" > onError< / td > < td class = "type" > (err) => void< / td > < td class = "default empty" > —< / td > < td > Fires when the clipboard API throws (permission denied, document not focused, etc).< / td > < / tr >
< tr > < td class = "name" > size < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'xs' | 'sm' | 'md' | 'lg' | 'xl'< / td > < td class = "default" > 'md'< / td > < td > Cascaded to the Trigger Button.< / td > < / tr >
< tr > < td class = "name" > color < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > ColorRole< / td > < td class = "default" > 'affirm'< / td > < td > Accent for the Indicator chip background + foreground.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Trigger (Button consumer)< / 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" > aria-label< / td > < td class = "type" > string< / td > < td class = "default" > 'Copy to clipboard'< / td > < td > Translated default.< / td > < / tr >
< tr > < td class = "name" > variant · rounded · iconOnly · icon · intent · color< / td > < td class = "type" > Button props< / td > < td class = "default" > ghost · md · false< / td > < td > Forwarded to < code > < Button> < / code > via the < code > child< / code > snippet.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Indicator< / 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" > forceMount< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Keep in DOM while not copied — useful for CSS exit transitions.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Snippet props (Provider children)< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Key< / th > < th > Type< / th > < th > Description< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > value< / td > < td class = "type" > string< / td > < td > Current value that gets copied.< / td > < / tr >
< tr > < td class = "name" > copied< / td > < td class = "type" > boolean< / td > < td > < code > true< / code > for < code > timeout< / code > ms after a successful copy.< / td > < / tr >
< tr > < td class = "name" > copy< / td > < td class = "type" > () => Promise< void> < / td > < td > Imperative copy — bypass the Trigger button (e.g. trigger from a keyboard shortcut).< / 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/clipboard.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 > Archetype< / 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" > < { part . defaultElement } > </ td >
< td class = "type" > { part . role ?? '—' } </ td >
< td class = "default" > { part . archetype ?? '—' } </ 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 > Clipboard raises < code > commit-copy< / code > (intent < code > affirm< / code > ) on success, < code > commit-copy-error< / code > (intent < code > threat< / code > ) on failure, and < code > shift-reset< / code > when the auto-reset timer fires.< / 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 > 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-clipboard-trigger]') ?? 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/clipboard/clipboard.css< / 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-clipboard]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > < code > display: contents< / code > host — color cascade only.< / td > < / tr >
< tr > < td class = "name" > [data-clipboard-trigger]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Marker on the underlying Button (Button consumer pattern).< / td > < / tr >
< tr > < td class = "name" > [data-clipboard-indicator]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Pill chip with accent fill + pop-in animation.< / td > < / tr >
< tr > < td class = "name" > @keyframes clipboard-pop< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Indicator mount animation. Disabled under < code > prefers-reduced-motion< / code > .< / 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 >
The Trigger is a native < code > < button> < / code > with a translated
< code > aria-label< / code > ("Copy to clipboard" by default). The Indicator is
decorative — the announcement that "the copy succeeded" should be delivered
via < code > Announce< / code > if the consumer wants screen-reader feedback.
< / p >
< div data-uix-subsection-head > Suggested compose with Announce< / div >
< pre style = "margin: 0; padding: var(--uix-space-3); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-subtle); font-size: var(--font-size-xs); overflow-x: auto;" >< code > { `<Announce>
{ # snippet children ({ announce })}
< Clipboard
value={ src }
onCopy={() => announce ( "Copied to clipboard" )}
>
< Clipboard.Trigger > Copy< / Clipboard.Trigger >
< Clipboard.Indicator / >
< / Clipboard >
{ /snippet }
< / Announce > `}< / code > < / pre >
< div data-uix-subsection-head > Limitations< / div >
< ul style = "margin: 0; padding-inline-start: 1.25rem; color: var(--color-content-secondary); font-size: var(--font-size-sm);" >
< li > < code > navigator.clipboard.writeText< / code > requires a focused document — fails with < em > "Document is not focused"< / em > in unfocused iframes / dev previews. Works normally with real user clicks.< / li >
< li > For SSR / tests, override the default writer via < code > createActiveUix({ '{' } clipboard { '}' } )</ code > or < code > defineUixServices({ '{' } clipboard { '}' } )</ code > .</ li >
< li > No < code > execCommand('copy')< / code > fallback — the modern Clipboard API is the only path.< / li >
< / ul >
< / section >
{ /if }
< / div >