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 {
Table,
type TableSize,
type TableVariant,
type TableColor
} from '$uix/eidos/components/table';
import { Button } from '$uix/eidos/components/button';
import { Pagination } from '$uix/eidos/components/pagination';
import {
createTable,
type ColumnDef,
type TableInstance,
type TableHeader,
type TableRow,
type TableCell
} from '$libs/datagrid';
import { compileMorfo } from '$uix/morfo';
import { tableMorfo } from '@/uix/morfo/components/table';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state< Tab > ('live');
// ── Sample data ──────────────────────────────────────────────────────
type Person = {
id: string;
name: string;
role: string;
team: 'engineering' | 'design' | 'sales' | 'ops';
score: number;
joined: string;
status: 'active' | 'invited' | 'paused';
bio: string;
};
const PEOPLE: Person[] = [
{ id : '1' , name : 'Ada Lovelace' , role : 'Founder' , team : 'engineering' , score : 96 , joined : '2018-03-12' , status : 'active' , bio : 'Pioneer of analytical engines. Believes in legible code over clever tricks.' } ,
{ id : '2' , name : 'Ben Eich' , role : 'Architect' , team : 'engineering' , score : 89 , joined : '2019-07-08' , status : 'active' , bio : 'Standards veteran. Writes interpreters at night.' } ,
{ id : '3' , name : 'Cora Ng' , role : 'Designer' , team : 'design' , score : 91 , joined : '2020-02-19' , status : 'active' , bio : 'Type-driven systems thinker. Aspiring origami master.' } ,
{ id : '4' , name : 'Dio Park' , role : 'Engineer' , team : 'engineering' , score : 78 , joined : '2021-09-04' , status : 'paused' , bio : 'Reactive runtime obsessive. Maintains 3 mechanical keyboards.' } ,
{ id : '5' , name : 'Elin Sato' , role : 'Account exec' , team : 'sales' , score : 84 , joined : '2020-11-23' , status : 'active' , bio : 'Closes deals with empathy. Hikes on Sundays.' } ,
{ id : '6' , name : 'Fae Olu' , role : 'Researcher' , team : 'design' , score : 88 , joined : '2019-04-14' , status : 'invited' , bio : 'Diary interviewer. Believes in slow research.' } ,
{ id : '7' , name : 'Gus Lin' , role : 'Operations' , team : 'ops' , score : 72 , joined : '2022-01-30' , status : 'active' , bio : 'Logistics minded. Plans every commute.' } ,
{ id : '8' , name : 'Hari Sen' , role : 'Engineer' , team : 'engineering' , score : 81 , joined : '2021-06-11' , status : 'active' , bio : 'CRDTs by day, contra-dance by night.' } ,
{ id : '9' , name : 'Ines Vega' , role : 'Sales lead' , team : 'sales' , score : 93 , joined : '2018-09-27' , status : 'active' , bio : 'Pipeline poet. Drinks espresso, distrusts dashboards.' } ,
{ id : '10' , name : 'Joon Han' , role : 'Engineer' , team : 'engineering' , score : 87 , joined : '2020-08-05' , status : 'paused' , bio : 'WASM tinkerer. Hosts board-game nights.' } ,
{ id : '11' , name : 'Kai Reyes' , role : 'Designer' , team : 'design' , score : 79 , joined : '2022-03-17' , status : 'invited' , bio : 'Motion-system curator. Sketches every meeting.' } ,
{ id : '12' , name : 'Lin Park' , role : 'Operations' , team : 'ops' , score : 85 , joined : '2019-12-01' , status : 'active' , bio : 'Process auditor. Knows where every key is kept.' }
];
const COLUMNS: ColumnDef< Person > [] = [
{ accessorKey : 'name' , header : 'Name' , enableSorting : true , enableFiltering : true , enablePinning : true , size : 180 } ,
{ accessorKey : 'role' , header : 'Role' , enableSorting : true , enableFiltering : true , size : 140 } ,
{ accessorKey : 'team' , header : 'Team' , enableSorting : true , enableFiltering : true , size : 130 } ,
{ accessorKey : 'score' , header : 'Score' , enableSorting : true , sortDescFirst : true , size : 90 } ,
{ accessorKey : 'joined' , header : 'Joined' , enableSorting : true , size : 130 } ,
{ accessorKey : 'status' , header : 'Status' , enableSorting : true , enableFiltering : true , enablePinning : true , size : 120 }
];
// ── Visual props ─────────────────────────────────────────────────────
let size = $state< TableSize > ('md');
let variant = $state< TableVariant > ('surface');
let color = $state< TableColor > ('primary');
let block = $state(true);
let striped = $state(true);
let hoverable = $state(true);
let stickyHeader = $state(false);
let useScrollRegion = $state(false);
let stableRowsOn = $state(true);
// ── i18n controls (canonical pattern) ────────────────────────────────
let locale = $state('en-US');
let dir = $state< 'ltr' | 'rtl'>('ltr');
// ── Soma feature controls ────────────────────────────────────────────
// Pagination + filter + pinning are mutable via the imperative API.
// `selectionMode` + `sorting.multi` are captured by soma in a const at
// construction time, so changing them live requires recreating the
// table instance (see `rebuildTable` below). All other state is
// re-applied so the demo doesn't lose filter / sort / pinning.
let selectionMode = $state< 'none' | 'single' | 'multiple'>('single');
let multiSort = $state(true);
let pageSize = $state(5);
let globalFilter = $state('');
let pinnedName = $state(false);
let pinnedStatus = $state(false);
// ── Table instance ───────────────────────────────────────────────────
function buildTable(mode: typeof selectionMode, multi: boolean) {
return createTable< Person > ({
data: () => PEOPLE,
columns: COLUMNS,
getRowId: (row) => row.id,
sorting: { enabled : true , multi } ,
filtering: { enabled : true } ,
selection: { mode } ,
pagination: { enabled : true , pageSize }
});
}
let table = $state(buildTable(selectionMode, multiSort));
function rebuildTable() {
const next = buildTable(selectionMode, multiSort);
// Carry over the user's other settings so the UI doesn't reset.
if (globalFilter) next.setGlobalFilter(globalFilter);
if (pageSize !== 10) next.setPageSize(pageSize);
if (pinnedName) next.pinColumn('name', 'left');
if (pinnedStatus) next.pinColumn('status', 'right');
table = next;
}
function setSelectionMode(m: typeof selectionMode) {
if (m === selectionMode) return;
selectionMode = m;
rebuildTable();
}
function setMultiSort(v: boolean) {
if (v === multiSort) return;
multiSort = v;
rebuildTable();
}
function setGlobalFilter(v: string) {
globalFilter = v;
table.setGlobalFilter(v);
}
function togglePinName() {
pinnedName = !pinnedName;
if (pinnedName) table.pinColumn('name', 'left');
else table.unpinColumn('name');
}
function togglePinStatus() {
pinnedStatus = !pinnedStatus;
if (pinnedStatus) table.pinColumn('status', 'right');
else table.unpinColumn('status');
}
function setPageSize(n: number) {
pageSize = n;
table.setPageSize(n);
}
// ── Trace / event monitor ────────────────────────────────────────────
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(tableMorfo);
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 selectedCount = $derived(
Object.values(table.selection).filter(Boolean).length
);
const pageIndex = $derived(table.pagination.pageIndex);
const pageCount = $derived(table.pageCount);
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { Table } from '$uix/eidos/components/table';",
" import { createTable } from '$libs/datagrid';",
' const table = createTable({ data : () => people , columns } );',
'< /' + 'script>',
'',
'< Table ' ,
' { table } ',
" label=\"Team members\"",
locale !== 'en-US' && ` locale="${ locale } "`,
dir !== 'ltr' && ` dir="${ dir } "`,
size !== 'md' && ` size="${ size } "`,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
!block && ' block={ false } ',
striped & & ' striped',
!hoverable && ' hoverable={ false } ',
stickyHeader & & ' stickyHeader',
useScrollRegion & & ' maxHeight="20rem"',
stableRowsOn && !useScrollRegion && ` stableRows={ $ { pageSize }} `,
'>',
' < Table.Header > ',
' < tr > ',
' { #each table . headers as header } ',
' < Table.ColumnHeader { header } > ',
' < Table.SortButton { header } > { header . label } </ Table.SortButton > ',
' < / Table.ColumnHeader > ',
' { /each } ',
' < / tr > ',
' < / Table.Header > ',
' < Table.Body > ',
' { #each table . rows as row ( row . id )} ',
' < Table.Row { row } > ',
' { #each row . cells as cell , i } ',
' < Table.Cell { cell } colIndex = { i } > { cell . value } </Table.Cell > ',
' { /each } ',
' < / Table.Row > ',
' { /each } ',
' < / Table.Body > ',
'< / Table > '
]
.filter(Boolean)
.join('\n')
);
function fmtStatus(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function fmtDate(s: string) {
try {
return new Intl.DateTimeFormat(locale, { year : 'numeric' , month : 'short' , day : '2-digit' } ).format(new Date(s));
} catch {
return s;
}
}
< / script >
< div data-uix-canvas-inner { dir } >
< header >
< div data-uix-eyebrow > Data · Table< / div >
< h1 data-uix-page-title > Table< / h1 >
< p data-uix-page-lede >
Headless tabular data with sorting, filtering, selection,
pagination, column pinning, sizing and row-detail disclosure
panels — wrapped by an eidos visual recipe (variant / size /
color / striped / sticky header). Use < code > Table< / code > for
flat data; reach for < code > TreeGrid< / code > when rows are
hierarchical, or < code > GridList< / code > when rows carry inline
interactive children.
< / 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 > apg< / span >
< a href = "https://www.w3.org/WAI/ARIA/apg/patterns/table/" target = "_blank" rel = "noreferrer" > table ↗< / a >
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > rows</ span > { table . filteredRowCount } / { PEOPLE . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > selected</ span > { selectedCount }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > locale</ span > { locale }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > dir</ span > { dir }
< / span >
< / div >
< / header >
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } style="padding: var ( --uix-space-3 );" >
< div style = "display: flex; flex-direction: column; gap: var(--uix-space-3);" >
< div style = "display: flex; gap: var(--uix-space-2); align-items: center; flex-wrap: wrap;" >
< input
type="search"
placeholder="Filter people…"
value={ globalFilter }
oninput={( e ) => setGlobalFilter (( e . currentTarget as HTMLInputElement ). value )}
style="padding: 0.375rem 0.625rem; border: 1px solid var(--color-border-default); border-radius: var(--radius-md); inline-size: 18rem; background: var(--color-surface-default); color: var(--color-content-primary); font: inherit;"
/>
< Button
size="sm"
variant="ghost"
onclick={() => { setGlobalFilter ( '' ); table . clearFilters (); }}
disabled={ ! globalFilter && table . columnFilters . length === 0 }
>
Clear filters
< / Button >
< Button
size="sm"
variant="ghost"
onclick={() => table . clearSorting ()}
disabled={ table . sorting . length === 0 }
>
Clear sort
< / Button >
< Button
size="sm"
variant="ghost"
onclick={() => table . toggleAllSelection ()}
disabled={ selectionMode !== 'multiple' }
>
{ table . getIsAllSelected () ? 'Deselect all' : 'Select all' }
< / Button >
< Button
size="sm"
variant="ghost"
onclick={() => table . closeAllRowDetails ()}
>
Close all details
< / Button >
< / div >
< Table
table={ table as unknown as TableInstance }
label="Team members"
{ dir }
{ size }
{ variant }
{ color }
{ block }
{ striped }
{ hoverable }
{ stickyHeader }
maxHeight={ useScrollRegion ? '20rem' : undefined }
stableRows={ stableRowsOn && ! useScrollRegion ? pageSize : undefined }
>
< Table.Header >
< tr >
< th data-table-column-header = "" style = "inline-size: 2.5rem;" aria-label = "Row detail toggle" > < / th >
{ #each table . headers as header ( header . id )}
{ @const h = header as unknown as TableHeader }
< Table.ColumnHeader header = { h } >
< Table.SortButton header = { h } > { header . label } </Table.SortButton >
< / Table.ColumnHeader >
{ /each }
< / tr >
< / Table.Header >
< Table.Body >
{ #each table . rows as row ( row . id )}
{ @const r = row as unknown as TableRow }
< Table.Row row = { r } >
< td data-table-cell = "" style = "inline-size: 2.5rem;" >
< Table.RowDetailTrigger row = { r } aria-label="Toggle bio " />
< / td >
{ #each row . cells as cell , i ( cell . id )}
< Table.Cell cell = { cell as unknown as TableCell } colIndex= { i } >
{ #if cell . column . id === 'status' }
< span data-uix-meta-pill data-status = { cell . value } > { fmtStatus ( String ( cell . value ))} </span >
{ :else if cell . column . id === 'score' }
< strong > { cell . value } </ strong >
{ :else if cell . column . id === 'joined' }
{ fmtDate ( String ( cell . value ))}
{ : else }
{ cell . value }
{ /if }
< / Table.Cell >
{ /each }
< / Table.Row >
< Table.RowDetail row = { r } >
< div style = "display: flex; gap: var(--uix-space-3); align-items: flex-start; flex-wrap: wrap;" >
< div style = "flex: 1 1 18rem; min-inline-size: 0;" >
< div style = "font-weight: var(--font-weight-semibold, 600); margin-block-end: 0.25rem;" > { row . original . name } </ div >
< div style = "color: var(--color-content-secondary);" > { row . original . bio } </ div >
< / div >
< div style = "display: flex; gap: var(--uix-space-2); flex-wrap: wrap;" >
< span data-uix-meta-pill >< span data-uix-meta-key > id</ span > { row . original . id } </ span >
< span data-uix-meta-pill >< span data-uix-meta-key > team</ span > { row . original . team } </ span >
< span data-uix-meta-pill >< span data-uix-meta-key > joined</ span > { fmtDate ( row . original . joined )} </ span >
< / div >
< / div >
< / Table.RowDetail >
{ /each }
< / Table.Body >
< / Table >
< div style = "display: flex; justify-content: space-between; align-items: center; gap: var(--uix-space-3); flex-wrap: wrap;" >
< span style = "color: var(--color-content-secondary); font-size: var(--font-size-xs);" >
Page { pageIndex + 1 } of { pageCount } ·
showing { table . rows . length } of { table . filteredRowCount }
< / span >
< Pagination
page={ pageIndex + 1 }
count={ table . filteredRowCount }
{ pageSize }
size="sm"
onPageChange={( p ) => table . setPageIndex ( p - 1 )}
>
{ # snippet children ({ pages })}
< Pagination.FirstTrigger > «< / Pagination.FirstTrigger >
< Pagination.PrevTrigger > Prev< / Pagination.PrevTrigger >
{ #each pages as item , i ( i )}
{ #if item . type === 'page' }
< Pagination.Item value = { item . value } > { item . value } </Pagination.Item >
{ : else }
< Pagination.Ellipsis position = { item . position } > …</Pagination.Ellipsis >
{ /if }
{ /each }
< Pagination.NextTrigger > Next< / Pagination.NextTrigger >
< Pagination.LastTrigger > »< / Pagination.LastTrigger >
{ /snippet }
< / Pagination >
< / div >
< / div >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > click sort, select a row, expand a detail panel…< / 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 }
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > sort< / span >
{ table . sorting . length === 0 ? '—' : table . sorting . map (( s ) => `$ { s . id } : $ { s . desc ? '↓' : '↑' } `).join(' · ') }
< / 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 > 9< / 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 owns sort / filter / select / paginate / pin / row-detail state and ARIA wiring.
Eidos adds visual chrome (variant / size / color), striping, hover feedback and the optional sticky header + scroll region.
`dir` and `locale` are demo-driven for verification — Table forwards `dir` to the soma Provider and `locale` is used by the demo for date formatting.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > i18n
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > locale< / span >
< select bind:value = { locale } >
< option value = "en-US" > en-US< / option >
< option value = "es-ES" > es-ES< / option >
< option value = "fr-FR" > fr-FR< / option >
< / select >
< / label >
< label data-uix-control >
< span data-uix-control-label > direction< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'ltr' , 'rtl' ] as value ( value )}
< button data-uix-chip data-active = { dir === value } onclick= {() => ( dir = value as 'ltr' | 'rtl' )} > { value } </ button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > feature controls
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > selectionMode< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'none' , 'single' , 'multiple' ] as m ( m )}
< button
data-uix-chip
data-active={ selectionMode === m }
onclick={() => setSelectionMode ( m as typeof selectionMode )}
>{ m } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > multiSort< / span >
< span data-uix-switch >
< input type = "checkbox" checked = { multiSort } onchange= {( e ) => setMultiSort (( e . currentTarget as HTMLInputElement ). checked )} />
< span data-uix-switch-label > { multiSort ? 'shift-click adds' : 'replace' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > pageSize< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 3 , 5 , 8 , 12 ] as n ( n )}
< button data-uix-chip data-active = { pageSize === n } onclick= {() => setPageSize ( n )} > { n } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > pin name (left)< / span >
< button data-uix-chip data-active = { pinnedName } onclick= { togglePinName } > { pinnedName ? 'sticky' : 'off' } </ button >
< / label >
< label data-uix-control >
< span data-uix-control-label > pin status (right)< / span >
< button data-uix-chip data-active = { pinnedStatus } onclick= { togglePinStatus } > { pinnedStatus ? 'sticky' : 'off' } </ button >
< / 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 > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'surface' , 'outline' , 'ghost' ] as v ( v )}
< button data-uix-chip data-active = { variant === v } onclick= {() => ( variant = v as TableVariant )} > { v } </ button >
{ /each }
< / span >
< / label >
< 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 TableSize )} > { 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 TableColor )} > { c } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > block< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { block } / >
< span data-uix-switch-label > { block ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > striped< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { striped } / >
< span data-uix-switch-label > { striped ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > hoverable< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { hoverable } / >
< span data-uix-switch-label > { hoverable ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > scrollable< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { useScrollRegion } / >
< span data-uix-switch-label > { useScrollRegion ? 'maxHeight=20rem' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > stickyHeader< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { stickyHeader } disabled= { ! useScrollRegion } />
< span data-uix-switch-label > { stickyHeader && useScrollRegion ? 'pinned' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > stableRows< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { stableRowsOn } disabled= { useScrollRegion } />
< span data-uix-switch-label > { stableRowsOn && ! useScrollRegion ? `reserve $ { pageSize } rows ` : 'off' } </ span >
< / span >
< / label >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > visual · createTable + Table compound< / 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 >
9 soma parts (< code > Provider · Header · Body · Footer · ColumnHeader · Row · Cell · RowDetail · RowDetailTrigger< / code > ) plus the eidos-only < code > SortButton< / code > ergonomic helper. The < code > table< / code > instance carries every imperative method (< code > toggleSort< / code > , < code > setGlobalFilter< / code > , < code > toggleRowSelection< / code > , < code > pinColumn< / code > , < code > setPageIndex< / code > , < code > toggleRowDetail< / code > …).
< / p >
< div data-uix-subsection-head > Provider (Table)< / 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" > table< / td > < td class = "type" > TableInstance< / td > < td class = "default empty" > required< / td > < td > Returned by < code > createTable< / code > .< / td > < / tr >
< tr > < td class = "name" > label< / td > < td class = "type" > string< / td > < td class = "default" > 'Data table'< / td > < td > Accessible name. Translated.< / td > < / tr >
< tr > < td class = "name" > dir < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td class = "type" > 'ltr' | 'rtl'< / td > < td class = "default" > from Soma prefs< / td > < td > Text direction.< / td > < / tr >
< tr > < td class = "name" > variant < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'surface' | 'outline' | 'ghost'< / td > < td class = "default" > 'surface'< / td > < td > Container chrome.< / 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 > Row height + cell padding + font.< / 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 selected rows + sort indicator + focus.< / td > < / tr >
< tr > < td class = "name" > block < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > boolean< / td > < td class = "default" > true< / td > < td > Stretch root to container inline-size.< / td > < / tr >
< tr > < td class = "name" > striped < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Alternate-row tint (skipped in ghost variant).< / td > < / tr >
< tr > < td class = "name" > hoverable < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > boolean< / td > < td class = "default" > true< / td > < td > Highlight rows on hover.< / td > < / tr >
< tr > < td class = "name" > stickyHeader < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Pins < code > < thead> < / code > to the top of the scroll region.< / td > < / tr >
< tr > < td class = "name" > maxHeight < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > string< / td > < td class = "default empty" > —< / td > < td > Wraps the table in a scrolling region with the given CSS block-size.< / td > < / tr >
< tr > < td class = "name" > stableRows < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > number< / td > < td class = "default empty" > —< / td > < td > Reserve vertical space for N rows + header. Keeps the wrapper height steady so pagination/footer don't jump when filters change row count. Ignored when < code > maxHeight< / code > is set.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > SortButton (eidos helper)< / 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" > header< / td > < td class = "type" > TableHeader< / td > < td class = "default empty" > required< / td > < td > From < code > table.headers< / code > .< / td > < / tr >
< tr > < td class = "name" > children< / td > < td class = "type" > Snippet< / td > < td class = "default" > header.label< / td > < td > Visible label.< / td > < / tr >
< tr > < td class = "name" > hideIndicator< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Hide the tri-state chevron.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > RowDetailTrigger< / 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" > row< / td > < td class = "type" > TableRow< / td > < td class = "default empty" > required< / td > < td > From < code > table.rows< / code > .< / td > < / tr >
< tr > < td class = "name" > aria-label< / td > < td class = "type" > string< / td > < td class = "default" > 'Show details' / 'Hide details'< / td > < td > Translated swap.< / td > < / tr >
< tr > < td class = "name" > variant · size · rounded · intent · color · iconOnly · icon< / td > < td class = "type" > Button props< / td > < td class = "default" > ghost · md · md< / td > < td > Forwarded to underlying < code > < Button> < / code > .< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > ColumnDef opt-in flags< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Flag< / th > < th > Default< / th > < th > Effect< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > enableSorting< / td > < td class = "default" > false< / td > < td > Column shows up in < code > getCanSort< / code > + < code > aria-sort< / code > .< / td > < / tr >
< tr > < td class = "name" > enableFiltering< / td > < td class = "default" > false< / td > < td > Column participates in global filter + per-column filters.< / td > < / tr >
< tr > < td class = "name" > enableHiding< / td > < td class = "default" > true< / td > < td > Consumer can call < code > toggleColumnVisibility< / code > .< / td > < / tr >
< tr > < td class = "name" > enableResizing< / td > < td class = "default" > false< / td > < td > Consumer can call < code > setColumnSize< / code > with constraints.< / td > < / tr >
< tr > < td class = "name" > enablePinning< / td > < td class = "default" > false< / td > < td > Consumer can call < code > pinColumn(id, 'left' | 'right')< / code > .< / td > < / tr >
< tr > < td class = "name" > size · minSize · maxSize< / td > < td class = "default empty" > —< / td > < td > Initial width in px + bounds.< / td > < / tr >
< tr > < td class = "name" > sortDescFirst< / td > < td class = "default" > false< / td > < td > First click sorts descending (numeric columns).< / 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/table.ts< / code > .< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Field< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > name</ td >< td class = "type" > "{ tableMorfo . name } "</ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td class = "type" > "{ tableMorfo . kebab } "</ td ></ tr >
< tr >< td class = "name" > parts.length</ td >< td class = "default" > { tableMorfo . parts . length } </ td ></ tr >
< tr >< td class = "name" > events.length</ td >< td class = "default" > { events . length } </ td ></ tr >
< / tbody >
< / table >
< / div >
< 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 >
Soma raises < code > shift-sort< / code > on column-header click,
< code > commit-select< / code > on row toggle and
< code > shift-expand-row< / code > when the detail panel toggles.
Use Play to replay each event against the first match in the stage.
< / 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 selector =
action.target === 'row'
? '[data-table-row]'
: action.target === 'column-header'
? '[data-table-column-header]'
: '[data-table]';
const target = (stageRef?.querySelector(selector) ?? 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/table/table.css< / code > . The visual data-attrs (< code > data-size< / code > , < code > data-variant< / code > , < code > data-color< / code > , < code > data-block< / code > , < code > data-striped< / code > , < code > data-hoverable< / code > , < code > data-sticky-header< / code > , < code > data-scrollable< / code > ) live on the < code > < div data-table-root> < / code > wrapper so the semantic < code > < table> < / code > stays clean.< / 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-table-root]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Wrapper — radius, chrome, scroll cap.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-variant]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Border + background per variant.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-size]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Row block-size, cell padding, font-size scale.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-color]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Accent token cascade.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-striped]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Alternate-row tint.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-hoverable]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Row hover highlight.< / td > < / tr >
< tr > < td class = "name" > [data-table-root][data-sticky-header] [data-table-column-header]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Sticky header inside scroll region.< / td > < / tr >
< tr > < td class = "name" > [data-table-column-header][data-sortable]< / td > < td > < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td > Cursor + sort indicator hook.< / td > < / tr >
< tr > < td class = "name" > [data-table-column-header][data-sorted]< / td > < td > < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td > Active sort accent colour.< / td > < / tr >
< tr > < td class = "name" > [data-table-row][data-selected]< / td > < td > < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td > Selected-row accent-soft fill.< / td > < / tr >
< tr > < td class = "name" > [data-table-cell][data-pinned]< / td > < td > < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td > Pinned-cell sticky offset + background.< / td > < / tr >
< tr > < td class = "name" > [data-table-row-detail][data-state]< / td > < td > < span data-uix-tag data-kind = "soma" > soma< / span > < / td > < td > Disclosure panel show/hide.< / td > < / tr >
< tr > < td class = "name" > [data-table-sort-button]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Header sort affordance — bare button + chevron.< / 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 >
Emits a native < code > < table> < / code > with < code > aria-label< / code > and per-column < code > aria-sort< / code > . Rows expose < code > aria-rowindex< / code > + < code > aria-selected< / code > (when selection is enabled). Detail panels are wired with < code > aria-expanded< / code > + < code > aria-controls< / code > . Use < code > TreeGrid< / code > for true hierarchical rows (arrow-key navigation) — Table is for flat tabular data only.
< / 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 > Move focus through sort buttons, selectable rows, detail triggers, pagination.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Enter< / span > / < span data-uix-kbd > Space< / span > on row< / td > < td > Toggle selection (when selectionMode ≠ 'none').< / td > < / tr >
< tr > < td > < span data-uix-kbd > Enter< / span > / < span data-uix-kbd > Space< / span > on RowDetailTrigger< / td > < td > Open / close the row's detail panel.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Click< / span > on sort button< / td > < td > Cycle asc → desc → none.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Shift+Click< / span > on sort button< / td > < td > Add column to multi-sort (when < code > sorting.multi: true< / code > ).< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > ARIA contract< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Part< / th > < th > Attribute< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > Provider< / td > < td > aria-label< / td > < td class = "type" > translated 'Data table'< / td > < / tr >
< tr > < td class = "name" > ColumnHeader< / td > < td > role< / td > < td class = "type" > "columnheader"< / td > < / tr >
< tr > < td class = "name" > ColumnHeader< / td > < td > aria-sort< / td > < td class = "type" > "ascending" | "descending" | "none"< / td > < / tr >
< tr > < td class = "name" > Row< / td > < td > role / aria-rowindex< / td > < td class = "type" > "row" / 1-based< / td > < / tr >
< tr > < td class = "name" > Row< / td > < td > aria-selected< / td > < td class = "type" > "true" | "false" (selection enabled)< / td > < / tr >
< tr > < td class = "name" > Cell< / td > < td > role / aria-colindex< / td > < td class = "type" > "gridcell" / 1-based< / td > < / tr >
< tr > < td class = "name" > RowDetailTrigger< / td > < td > aria-expanded / aria-controls< / td > < td class = "type" > boolean / id of sibling RowDetail< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >
< style >
[data-status='active'] {
color: var(--color-affirm-solid);
}
[data-status='paused'] {
color: var(--color-loss-solid);
}
[data-status='invited'] {
color: var(--color-fulfill-solid);
}
< / style >