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 {
DragDrop,
type DragDropSize,
type DragDropColor,
type DropEvent
} from '$uix/eidos/components/drag-drop';
import { compileMorfo } from '$uix/morfo';
import { dragDropMorfo } from '@/uix/morfo/components/drag-drop';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state< Tab > ('live');
type Item = { id : string ; label : string } ;
let columns = $state< Record < string , Item [] > >({
todo: [
{ id : 't1' , label : 'Write specs' } ,
{ id : 't2' , label : 'Review PR #42' } ,
{ id : 't3' , label : 'Sync with design' }
],
doing: [{ id : 'd1' , label : 'Ship TreeGrid' } ],
done: [
{ id : 'x1' , label : 'Refactor Table' } ,
{ id : 'x2' , label : 'Carousel autoplay' }
]
});
let size = $state< DragDropSize > ('md');
let color = $state< DragDropColor > ('primary');
let announceEnabled = $state(true);
let moveBuffer = $state(5);
function handleDrop(e: DropEvent< { from : string } >) {
const fromColumn = e.data.from;
const toColumn = e.target.dataset.column!;
if (fromColumn === toColumn) return;
const item = columns[fromColumn].find((i) => i.id === e.value);
if (!item) return;
columns = {
...columns,
[fromColumn]: columns[fromColumn].filter((i) => i.id !== e.value),
[toColumn]: [...columns[toColumn], item]
};
}
const COL_LABELS: Record< string , string > = {
todo: 'Todo',
doing: 'In progress',
done: 'Done'
};
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | 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) 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();
});
const compiled = compileMorfo(dragDropMorfo);
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 totalItems = $derived(
Object.values(columns).reduce((sum, list) => sum + list.length, 0)
);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { DragDrop } from '$uix/eidos/components/drag-drop';",
'< /' + 'script>',
'',
'< DragDrop ' ,
' onDrop={ handleDrop } ',
!announceEnabled && ' announceEnabled={ false } ',
size !== 'md' && ` size="${ size } "`,
color !== 'primary' && ` color="${ color } "`,
'>',
' { # snippet children ({ active })} ',
' < DragDrop.Droppable accept = {( d ) => d . kind === "task" } > ',
' { # snippet children ({ over , accepting })} ',
' < div data-column = "todo" > …< / div > ',
' { /snippet } ',
' < / DragDrop.Droppable > ',
' < DragDrop.Draggable ' ,
' value="t1"',
' data={{ kind : "task" , from : "todo" }} ',
moveBuffer !== 5 && ` moveBuffer={ $ { moveBuffer }} `,
' >',
' < div > Card< / div > ',
' < / DragDrop.Draggable > ',
' { #if active } ',
' < DragDrop.Preview > { active . label } </ DragDrop.Preview > ',
' { /if } ',
' { /snippet } ',
'< / DragDrop > '
]
.filter(Boolean)
.join('\n')
);
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Utility · DragDrop< / div >
< h1 data-uix-page-title > DragDrop< / h1 >
< p data-uix-page-lede >
Pointer + keyboard drag and drop. Soma owns the gesture coordinator and
ARIA announcements via the global < code > Announce< / code > service. Eidos
paints the visual states (< code > data-dragging< / code > ghosts the source,
< code > data-accepting< / code > highlights compatible targets,
< code > data-dragover< / code > tints the hover target with the accent color).
< / 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 > cards</ span > { totalItems }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > columns</ span > { Object . keys ( columns ). length }
< / span >
< / div >
< / header >
< DragDrop
{ size }
{ color }
{ announceEnabled }
onDrop={( e ) => handleDrop ( e as DropEvent < { from : string } > )}
>
{ # snippet children ( snippetProps )}
{ @const { active } = snippetProps }
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } style="padding: var ( --uix-space-3 ); display: flex ; gap: var ( --uix-space-3 ); flex-wrap: wrap ; align-items: flex-start ;" >
{ #each Object . keys ( columns ) as colKey ( colKey )}
< DragDrop.Droppable
textValue={ COL_LABELS [ colKey ]}
data-column={ colKey }
style="inline-size: 12rem; min-block-size: 14rem; padding: var(--uix-space-2); border: 1px dashed var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-subtle); display: block;"
>
{ # snippet children ( _dropSnippet )}
< div style = "font-weight: var(--font-weight-semibold, 600); margin-block-end: 0.5rem;" >
{ COL_LABELS [ colKey ]}
< span style = "color: var(--color-content-secondary); font-weight: normal; font-size: var(--font-size-xs);" > · { columns [ colKey ]. length } </ span >
< / div >
< div style = "display: flex; flex-direction: column; gap: var(--uix-space-1);" >
{ #each columns [ colKey ] as item ( item . id )}
< DragDrop.Draggable value = { item . id } data= {{ from : colKey }} textValue = { item . label } { moveBuffer } >
< div style = "padding: var(--uix-space-2) var(--uix-space-3); border-radius: var(--radius-sm); background: var(--color-surface-default); border: 1px solid var(--color-border-default); font-size: var(--font-size-sm);" >
⋮⋮ { item . label }
< / div >
< / DragDrop.Draggable >
{ /each }
< / div >
{ /snippet }
< / DragDrop.Droppable >
{ /each }
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if active }
< span >< strong > { active . label } </ strong > · from {( active . data as { from ?: string }). from } </ span >
{ :else if trace . length === 0 }
< span > drag cards across columns · keyboard: Tab → Space → arrows → Enter / Escape< / 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 trace . 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 >< 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 }
< / div >
< / div >
{ #if active }
< DragDrop.Preview > ⋮⋮ { active . label } </ DragDrop.Preview >
{ /if }
{ /snippet }
< / DragDrop >
< 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 > 4</ 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 >
Soma coordinates the gesture (pointer + keyboard), filters drop targets by < code > accept< / code > , and announces via the global Announce service. Eidos paints the visual states.
< / 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 > moveBuffer< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 0 , 5 , 12 , 24 ] as n ( n )}
< button data-uix-chip data-active = { moveBuffer === n } onclick= {() => ( moveBuffer = n )} > { n } px</ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > announceEnabled< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { announceEnabled } / >
< span data-uix-switch-label > { announceEnabled ? 'SR announcements on' : 'silent' } </ span >
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > visual props
< / 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 DragDropSize )} > { s } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color< / 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 DragDropColor )} > { c } </ button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · DragDrop + Draggable + Droppable + Preview< / 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 > 4 parts: < code > Provider · Draggable · Droppable · Preview< / code > . The Provider does NOT own the data — consumer receives < code > onDrop< / code > and mutates state.< / p >
< div data-uix-subsection-head > Provider (DragDrop)< / 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" > onDragStart · onDragEnd · onDrop< / td > < td class = "type" > callbacks< / td > < td class = "default empty" > —< / td > < td > Gesture lifecycle hooks.< / td > < / tr >
< tr > < td class = "name" > announceEnabled< / td > < td class = "type" > boolean< / td > < td class = "default" > true< / td > < td > Emit SR announcements via the global Announce service.< / 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 visual size.< / 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" > 'primary'< / td > < td > Accent for drop targets + preview border + shadow.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Draggable< / 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 > Unique identifier returned in onDrop.< / td > < / tr >
< tr >< td class = "name" > data</ td >< td class = "type" > DragData</ td >< td class = "default" > { '{}' } </ td >< td > Arbitrary payload received in onDrop.</ td ></ tr >
< tr > < td class = "name" > disabled< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Skip from drag gesture.< / td > < / tr >
< tr > < td class = "name" > moveBuffer< / td > < td class = "type" > number< / td > < td class = "default" > 5< / td > < td > Pixels the pointer must move before drag is recognised.< / td > < / tr >
< tr > < td class = "name" > textValue< / td > < td class = "type" > string< / td > < td class = "default empty" > aria-label/textContent< / td > < td > Label for SR announcements.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Droppable< / 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" > accept< / td > < td class = "type" > (data, value) => boolean< / td > < td class = "default" > () => true< / td > < td > Filter — return false to skip this target.< / td > < / tr >
< tr > < td class = "name" > disabled< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Disable drops.< / td > < / tr >
< tr > < td class = "name" > textValue< / td > < td class = "type" > string< / td > < td class = "default empty" > —< / td > < td > Label for SR announcements.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Snippet props< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Snippet< / th > < th > Key< / th > < th > Type< / th > < th > Description< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > Provider.children< / td > < td > active< / td > < td class = "type" > ActiveDrag | null< / td > < td > Info about the in-flight drag (value, data, label, pointer).< / td > < / tr >
< tr > < td class = "name" > Provider.children< / td > < td > cancel< / td > < td class = "type" > () => void< / td > < td > Imperative cancel.< / td > < / tr >
< tr > < td class = "name" > Draggable.children< / td > < td > dragging< / td > < td class = "type" > boolean< / td > < td > True while THIS draggable is the source.< / td > < / tr >
< tr > < td class = "name" > Draggable.children< / td > < td > handleProps< / td > < td class = "type" > object< / td > < td > Spread on a child to restrict the drag handle.< / td > < / tr >
< tr > < td class = "name" > Droppable.children< / td > < td > over< / td > < td class = "type" > boolean< / td > < td > Pointer/focus is over THIS target.< / td > < / tr >
< tr > < td class = "name" > Droppable.children< / td > < td > accepting< / td > < td class = "type" > boolean< / td > < td > True for compatible targets while ANY drag is active.< / 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/drag-drop.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" > < { part . defaultElement } > </ 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 >
{ #if events . length === 0 }
< p data-uix-section-desc > DragDrop's SR feedback is delivered via the global Announce service rather than sema events — the perception is verbal, not audio cues.< / p >
{ : else }
< 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 > < / tr > < / thead >
< tbody >
{ #each events as action ( action . name )}
{ @const intentDecl = 'intent' in action . semantic ? action.semantic.intent : 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" > { typeof intentDecl === 'string' ? intentDecl : '—' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
{ /if }
< / 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/drag-drop/drag-drop.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-drag-drop-draggable]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > < code > cursor: grab< / code > , < code > touch-action: none< / code > .< / td > < / tr >
< tr > < td class = "name" > [data-drag-drop-draggable][data-dragging]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Source ghosted (< code > opacity: 0.4< / code > ).< / td > < / tr >
< tr > < td class = "name" > [data-drag-drop-droppable][data-accepting]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Compatible target outline (soft accent).< / td > < / tr >
< tr > < td class = "name" > [data-drag-drop-droppable][data-dragover]< / td > < td > < span data-uix-tag data-kind = "soma" > morfo< / span > < / td > < td > Hover target accent fill + inset border.< / td > < / tr >
< tr > < td class = "name" > [data-drag-drop-preview]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Floating pointer follower — card chrome with accent border + shadow.< / 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 >
Full keyboard parity with pointer — and SR announcements via the global
< code > Announce< / code > service when < code > announceEnabled< / code > is on.
< / p >
< 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 >
< tr > < td > < span data-uix-kbd > Tab< / span > < / td > < td > Focus the next Draggable.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Space< / span > < / td > < td > Lift the focused Draggable.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Arrow< / span > / < span data-uix-kbd > Tab< / span > < / td > < td > Navigate compatible Droppables.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Enter< / span > < / td > < td > Drop on the current target.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Escape< / span > < / td > < td > Cancel the lift.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Announcements (via global Announce)< / div >
< ul style = "margin: 0; padding-inline-start: 1.25rem; color: var(--color-content-secondary); font-size: var(--font-size-sm);" >
< li > Drag start → "Started dragging { `{item}` } "</ li >
< li > Drag over Droppable → "Over { `{target}` } "</ li >
< li > Drop → "Dropped { `{item}` } on { `{target}` } "</ li >
< li > Cancel → "Cancelled"< / li >
< / ul >
< / section >
{ /if }
< / div >