From 065cc6a3693b0d3b5db5114ea179426620dd5f12 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 18:20:23 +0200 Subject: [PATCH] feat(eidos): port GridList (selectable list w/ interactive row children) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #133. Eidos wrapper + types + recipe + canonical-depth demo. 4 parts: Provider, Row, Cell, SelectionCheckbox. Soma owns selection (none/single/multiple), 2D keyboard nav (Up/Down rows + Left/Right cells), Shift/Ctrl range/toggle, Ctrl+A select-all, Escape-clear, typeahead, hidden form input, Field merge. Eidos adds variant + size + color + defaultValue (uncontrolled init). Reference parity (no air baseline): | Feature | soma | react-aria GridList | this port | |---|---|---|---| | selectionMode none/single/multiple | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | | defaultValue (uncontrolled init) | ✗ | ✓ | ✓ added (parity with Combobox/Select) | | Item with interactive descendants | ✓ | ✓ | ✓ | | 2D keyboard nav (Up/Down rows, L/R cells) | ✓ | ✓ | ✓ | | Shift/Ctrl range/toggle | ✓ | ✓ | ✓ | | Ctrl+A select-all (multiple) | ✓ | ✓ | ✓ | | Escape-clear | ✓ | ✓ | ✓ | | Typeahead | ✓ | ✓ | ✓ | | ARIA grid/row/gridcell | ✓ | ✓ | ✓ | | Field merge (disabled/invalid/required/readonly) | ✓ | ✗ | ✓ | | Form participation (hidden input) | ✓ | ✗ | ✓ | | Drag-to-reorder | ✗ | ✓ | ⚠ deferred (compose w/ future DragDrop) | | onAction (row click → navigate) | ✗ | ✓ | ⚠ deferred (use button/link inside Cell) | | selectionBehavior toggle/replace | ✗ implicit | ✓ | ⚠ deferred | | disabledBehavior 'selection'/'all' | ✗ skip-only | ✓ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ⚠ deferred | | Empty state slot | ✗ | ✓ | ⚠ document `data-empty` styling | Naming: keeps soma's part names (Provider, Row, Cell, SelectionCheckbox). react-aria's "GridListItem" maps to `Row`; soma's vocabulary is more precise and consistent with future Table / TreeGrid in the same family. Variant set: - variant: ControlVariant (surface/outline/ghost), default 'surface' - size: xs..xl (5-step, mirrors Combobox/Listbox) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Disambiguation (in index.ts comment): use GridList for rows with interactive children; use Listbox for atomic text options (ARIA forbids interactive descendants inside role="listbox"); use Select/Combobox for closed-picker patterns; use Table for tabular data with columns + sort. Visual reuse: - SelectionCheckbox uses the same `mask-image: url(svg-check)` pattern as the shared menu-indicator partial (commit a2709476). Filled accent box → white ✓ on `[data-state='checked']`. - Row hover/highlight tokens mirror Combobox.Item / Listbox.Item so the form-control surface family stays coherent. - Same `max-block-size: min(60vh, 22rem) + overflow-y: auto` scroll contract as Listbox (commit 830025b4). Sidebar entry under Forms (between Listbox and Calendar). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../grid-list/grid-list-cell.svelte | 7 + .../components/grid-list/grid-list-row.svelte | 11 + .../grid-list-selection-checkbox.svelte | 7 + .../eidos/components/grid-list/grid-list.css | 235 ++++++++ .../components/grid-list/grid-list.svelte | 56 ++ src/uix/eidos/components/grid-list/index.ts | 56 ++ src/uix/eidos/components/grid-list/types.ts | 46 ++ src/uix/eidos/index.css | 1 + web/routes/uix/+layout@.svelte | 1 + .../uix/components/grid-list/+page.svelte | 522 ++++++++++++++++++ 10 files changed, 942 insertions(+) create mode 100644 src/uix/eidos/components/grid-list/grid-list-cell.svelte create mode 100644 src/uix/eidos/components/grid-list/grid-list-row.svelte create mode 100644 src/uix/eidos/components/grid-list/grid-list-selection-checkbox.svelte create mode 100644 src/uix/eidos/components/grid-list/grid-list.css create mode 100644 src/uix/eidos/components/grid-list/grid-list.svelte create mode 100644 src/uix/eidos/components/grid-list/index.ts create mode 100644 src/uix/eidos/components/grid-list/types.ts create mode 100644 web/routes/uix/components/grid-list/+page.svelte diff --git a/src/uix/eidos/components/grid-list/grid-list-cell.svelte b/src/uix/eidos/components/grid-list/grid-list-cell.svelte new file mode 100644 index 000000000..26f1cbc3a --- /dev/null +++ b/src/uix/eidos/components/grid-list/grid-list-cell.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/grid-list/grid-list-row.svelte b/src/uix/eidos/components/grid-list/grid-list-row.svelte new file mode 100644 index 000000000..ab994768e --- /dev/null +++ b/src/uix/eidos/components/grid-list/grid-list-row.svelte @@ -0,0 +1,11 @@ + + + + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/grid-list/grid-list-selection-checkbox.svelte b/src/uix/eidos/components/grid-list/grid-list-selection-checkbox.svelte new file mode 100644 index 000000000..e4d65ec3c --- /dev/null +++ b/src/uix/eidos/components/grid-list/grid-list-selection-checkbox.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/grid-list/grid-list.css b/src/uix/eidos/components/grid-list/grid-list.css new file mode 100644 index 000000000..bc31e228e --- /dev/null +++ b/src/uix/eidos/components/grid-list/grid-list.css @@ -0,0 +1,235 @@ +/* + * GridList recipe — role="grid" list with interactive row children. + * + * [data-grid-list] → root container + * [data-grid-list-row] → row, focusable, [data-selected] + * [data-grid-list-cell] → cell slot + * [data-grid-list-selection-checkbox] → per-row selection box + * + * Sizes — xs..xl drive row height + font-size. + * Variant `'surface' | 'outline' | 'ghost'` (ControlVariant). + * Color cascades the accent for selected rows + focus ring. + * + * Cross-component reuse: SelectionCheckbox uses the menu-indicator + * recipe family (see `lib/menu-indicator.css`) — the indicator inside + * the soma part will pick up that visual when the row is selected. + * Row hover/highlight tokens mirror Combobox.Item + Listbox.Item. + * + * Disambiguation: use GridList for rows with interactive children + * (buttons / links / checkbox). Use Listbox for atomic text options + * (ARIA forbids interactive descendants inside role="listbox"). + */ + +[data-grid-list] { + --_grid-list-row-height: var(--control-height-md); + --_grid-list-row-px: var(--space-3); + --_grid-list-font-size: var(--font-size-sm); + --_grid-list-max-height: min(60vh, 22rem); + --_grid-list-accent: var(--color-primary-solid); + --_grid-list-accent-soft: var(--color-primary-element); + + display: flex; + flex-direction: column; + gap: 1px; + padding: var(--space-1); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + background: var(--color-surface-default); + color: var(--color-content-primary); + font-family: var(--font-ui); + font-size: var(--_grid-list-font-size); + line-height: var(--leading-ui); + outline: none; + max-block-size: var(--_grid-list-max-height); + overflow-y: auto; + overscroll-behavior: contain; +} + +[data-grid-list][data-block] { + inline-size: 100%; +} + +/* ── Variant ─ */ +[data-grid-list][data-variant='outline'] { + background: transparent; +} + +[data-grid-list][data-variant='ghost'] { + background: transparent; + border-color: transparent; +} + +/* ── Sizes ─ */ +[data-grid-list][data-size='xs'] { + --_grid-list-row-height: var(--control-height-xs); + --_grid-list-row-px: var(--space-2); + --_grid-list-font-size: var(--font-size-xs); +} + +[data-grid-list][data-size='sm'] { + --_grid-list-row-height: var(--control-height-sm); + --_grid-list-row-px: var(--space-2); + --_grid-list-font-size: var(--font-size-sm); +} + +[data-grid-list][data-size='lg'] { + --_grid-list-row-height: var(--control-height-lg); + --_grid-list-row-px: var(--space-4); + --_grid-list-font-size: var(--font-size-md); +} + +[data-grid-list][data-size='xl'] { + --_grid-list-row-height: var(--control-height-xl); + --_grid-list-row-px: var(--space-5); + --_grid-list-font-size: var(--font-size-lg); +} + +/* ── Color cascades ─ */ +[data-grid-list][data-color='secondary'] { + --_grid-list-accent: var(--color-secondary-solid); + --_grid-list-accent-soft: var(--color-secondary-element); +} +[data-grid-list][data-color='affirm'] { + --_grid-list-accent: var(--color-affirm-solid); + --_grid-list-accent-soft: var(--color-affirm-element); +} +[data-grid-list][data-color='risk'] { + --_grid-list-accent: var(--color-risk-solid); + --_grid-list-accent-soft: var(--color-risk-element); +} +[data-grid-list][data-color='threat'] { + --_grid-list-accent: var(--color-threat-solid); + --_grid-list-accent-soft: var(--color-threat-element); +} +[data-grid-list][data-color='fulfill'] { + --_grid-list-accent: var(--color-fulfill-solid); + --_grid-list-accent-soft: var(--color-fulfill-element); +} +[data-grid-list][data-color='loss'] { + --_grid-list-accent: var(--color-loss-solid); + --_grid-list-accent-soft: var(--color-loss-element); +} +[data-grid-list][data-color='neutral'] { + --_grid-list-accent: var(--color-neutral-solid); + --_grid-list-accent-soft: var(--color-neutral-element); +} + +[data-grid-list][data-disabled] { + opacity: 0.55; + cursor: not-allowed; +} + +[data-grid-list][data-invalid] { + border-color: var(--color-threat-element); +} + +[data-grid-list]:focus-visible, +[data-grid-list][data-focused] { + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); +} + +/* ── Row ─ */ +[data-grid-list-row] { + display: flex; + align-items: center; + gap: var(--space-2); + min-block-size: var(--_grid-list-row-height); + padding-block: var(--space-1); + padding-inline: var(--_grid-list-row-px); + border-radius: var(--radius-sm); + color: var(--color-content-primary); + cursor: pointer; + user-select: none; + transition: + background var(--duration-fast) var(--ease-default), + color var(--duration-fast) var(--ease-default); +} + +[data-grid-list-row]:hover:not([data-disabled]), +[data-grid-list-row][data-highlighted]:not([data-disabled]) { + background: var(--color-surface-overlay); +} + +[data-grid-list-row][data-selected]:not([data-disabled]) { + background: var(--_grid-list-accent-soft); +} + +[data-grid-list-row][data-disabled] { + color: var(--color-content-muted); + cursor: default; + opacity: 0.6; +} + +[data-grid-list-row]:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: -1px; +} + +/* ── Cell ─ */ +[data-grid-list-cell] { + display: inline-flex; + align-items: center; +} + +[data-grid-list-cell]:first-of-type { + flex: 0 0 auto; +} + +[data-grid-list-cell]:nth-of-type(2) { + flex: 1 1 auto; + min-inline-size: 0; +} + +[data-grid-list-cell]:last-of-type:not(:first-of-type):not(:nth-of-type(2)) { + flex: 0 0 auto; + margin-inline-start: auto; +} + +/* ── SelectionCheckbox ─ + * + * Soma renders a `[data-grid-list-selection-checkbox]` element with + * `data-state="checked"` / "unchecked". Reuse the menu-indicator visual + * pattern: empty outlined square → filled accent + white ✓. + */ +[data-grid-list-selection-checkbox] { + --_grid-list-checkbox-size: 0.875rem; + position: relative; + display: inline-flex; + flex: 0 0 auto; + inline-size: var(--_grid-list-checkbox-size); + block-size: var(--_grid-list-checkbox-size); + border: 1px solid var(--color-border-default); + border-radius: 0.2rem; + background: transparent; + cursor: pointer; + outline: none; + transition: + background var(--duration-fast) var(--ease-default), + border-color var(--duration-fast) var(--ease-default); +} + +[data-grid-list-selection-checkbox][data-state='checked'] { + border-color: var(--_grid-list-accent); + background: var(--_grid-list-accent); +} + +[data-grid-list-selection-checkbox][data-state='checked']::after { + content: ''; + position: absolute; + inset: 0; + background-color: var(--color-content-on-solid, #fff); + mask-image: url("data:image/svg+xml;utf8,"); + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: contain; +} + +[data-grid-list-selection-checkbox]:focus-visible { + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: 1px; +} diff --git a/src/uix/eidos/components/grid-list/grid-list.svelte b/src/uix/eidos/components/grid-list/grid-list.svelte new file mode 100644 index 000000000..3dd0acbff --- /dev/null +++ b/src/uix/eidos/components/grid-list/grid-list.svelte @@ -0,0 +1,56 @@ + + + + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/grid-list/index.ts b/src/uix/eidos/components/grid-list/index.ts new file mode 100644 index 000000000..39e8e3ee7 --- /dev/null +++ b/src/uix/eidos/components/grid-list/index.ts @@ -0,0 +1,56 @@ +// GridList — eidos compound API. +// +// import { GridList } from '$uix/eidos/components/grid-list'; +// +// +// +// +// Row label +// +// +// +// +// Disambiguation: +// - Use GridList for rows with interactive children (button / link / +// checkbox inside the row). `role="grid"` + 2D nav (Up/Down rows, +// Left/Right inner controls) lets AT navigate those properly. +// - Use Listbox for atomic options (text rows only). ARIA forbids +// interactive descendants inside `role="listbox"`. +// - Use Select / Combobox for closed-picker patterns. +// - Use Table for tabular data with columns + sort. +// +// Soma owns selection (single/multiple), 2D keyboard nav, +// Shift/Ctrl/Cmd+A range and toggle helpers, Escape-clear, typeahead, +// hidden form input, Field merge. Eidos adds variant + size + color + +// defaultValue (uncontrolled init, parity with Combobox/Select). +import GridListRoot from './grid-list.svelte'; +import Row from './grid-list-row.svelte'; +import Cell from './grid-list-cell.svelte'; +import SelectionCheckbox from './grid-list-selection-checkbox.svelte'; + +type GridListNamespace = typeof GridListRoot & { + Row: typeof Row; + Cell: typeof Cell; + SelectionCheckbox: typeof SelectionCheckbox; +}; + +const GridList = GridListRoot as GridListNamespace; +GridList.Row = Row; +GridList.Cell = Cell; +GridList.SelectionCheckbox = SelectionCheckbox; + +export { GridList }; +export default GridList; + +export type { + GridListProps, + GridListRowProps as RowProps, + GridListCellProps as CellProps, + GridListSelectionCheckboxProps as SelectionCheckboxProps, + GridListSize, + GridListVariant, + GridListColor, + GridListSelectionMode, + GridListProviderSnippetProps, + GridListRowSnippetProps +} from './types'; diff --git a/src/uix/eidos/components/grid-list/types.ts b/src/uix/eidos/components/grid-list/types.ts new file mode 100644 index 000000000..5ce1a9f11 --- /dev/null +++ b/src/uix/eidos/components/grid-list/types.ts @@ -0,0 +1,46 @@ +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'; +import type { + ProviderProps, + RowProps, + CellProps, + SelectionCheckboxProps, + ProviderSnippetProps, + RowSnippetProps, + GridListSelectionMode +} from '$soma/components/grid-list'; + +/** + * GridList visual size — five-step subset matching the form-control + * family (Combobox / Listbox / Select). Row height + font-size cascade + * from this token. + */ +export type GridListSize = Extract; + +export type GridListVariant = ControlVariant; +export type GridListColor = ColorRole; + +export type GridListProps = ProviderProps & { + /** Visual size — row height + font-size. @default 'md' */ + size?: ResponsiveProp; + /** Container chrome variant. @default 'surface' */ + variant?: GridListVariant; + /** Accent palette for selected rows + focus ring. @default 'primary' */ + color?: GridListColor; + /** Stretch to fill the container's inline-size. @default false */ + block?: boolean; + /** + * Uncontrolled initial selection. Mirrors Combobox/Select pattern; + * ignored when `value` is bound by the consumer. + */ + defaultValue?: string[]; +}; + +export type GridListRowProps = RowProps; +export type GridListCellProps = CellProps; +export type GridListSelectionCheckboxProps = SelectionCheckboxProps; + +export type { + ProviderSnippetProps as GridListProviderSnippetProps, + RowSnippetProps as GridListRowSnippetProps, + GridListSelectionMode +}; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index e3cb640e3..3858cf54e 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -98,6 +98,7 @@ @import './components/command/command.css'; @import './components/listbox/listbox.css'; @import './components/navigation-menu/navigation-menu.css'; +@import './components/grid-list/grid-list.css'; /* Shared menu-item indicator partial — must load AFTER the menu recipes so its `padding-inline-start` calc wins over the row rule's `padding-inline` shorthand at equal specificity. */ diff --git a/web/routes/uix/+layout@.svelte b/web/routes/uix/+layout@.svelte index 2fc42527a..736e95c2c 100644 --- a/web/routes/uix/+layout@.svelte +++ b/web/routes/uix/+layout@.svelte @@ -149,6 +149,7 @@ { slug: '/uix/components/select', label: 'Select' }, { slug: '/uix/components/combobox', label: 'Combobox' }, { slug: '/uix/components/listbox', label: 'Listbox' }, + { slug: '/uix/components/grid-list', label: 'Grid list' }, { slug: '/uix/components/calendar', label: 'Calendar' }, { slug: '/uix/components/month-grid', label: 'Month grid' }, { slug: '/uix/components/year-grid', label: 'Year grid' }, diff --git a/web/routes/uix/components/grid-list/+page.svelte b/web/routes/uix/components/grid-list/+page.svelte new file mode 100644 index 000000000..c6206a748 --- /dev/null +++ b/web/routes/uix/components/grid-list/+page.svelte @@ -0,0 +1,522 @@ + + +
+
+
Forms · GridList
+

GridList

+

+ Selectable list with role="grid" semantics — rows + that can contain interactive children (buttons, links, per-row + checkboxes). 2D keyboard nav (Up/Down between rows + Left/Right + between cells / inner controls) lets assistive tech reach the + row-level actions properly. Use over Listbox when + rows aren't atomic options — ARIA forbids interactive descendants + inside role="listbox". +

+
+ + parts{compiled.parts.order.length} + + + events{events.length} + + + sizes5 + + + apg + grid ↗ + + + selected {value.join(', ') || '—'} + + {#if lastAction} + + last {lastAction} + + {/if} +
+
+ +
+
+
+ + {#each rows as row (row.value)} + + + + + +
+ {row.label} + {row.meta} +
+
+ + + +
+ {/each} +
+
+
+
+ trace + {#if trace.length === 0} + click row body to select; Tab into row to reach inner button + {:else} + {#each trace.slice(0, 3) as entry (entry.at)} + {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + value [{value.join(', ')}] + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Soma owns selection + 2D nav + Shift/Ctrl range/toggle + Ctrl+A select-all + Escape-clear + typeahead. + Eidos adds variant/size/color chrome. +

+ +
+ soma Provider props +
+
+ + + + +
+ +
+ eidos visual props +
+
+ + + +
+ +
+
+ eidos + visual · variant + size + color + defaultValue + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

4 parts: Provider, Row, Cell, SelectionCheckbox.

+ +
Provider (GridList)
+
+ + + + + + + + + + + + + + +
PropTypeDefaultDescription
value bindablestring[][]Selected row values.
defaultValue eidosstring[]—Uncontrolled initial selection. Ignored when value is bound.
selectionMode'none' | 'single' | 'multiple''single'—
loopbooleanfalseWrap arrow nav at edges.
typeaheadbooleantrueLetter typing jumps to matching row.
disabled · readonly · required · invalidbooleanfalseOR-merged with Field.Provider.
namestring—Form name → hidden input.
variant eidosControlVariant: 'surface' | 'outline' | 'ghost''surface'Container chrome.
size eidos'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Row height + font.
color eidosColorRole'primary'Selected-row + focus accent.
+
+ +
Row
+
+ + + + + + + +
PropTypeDefaultDescription
valuestringrequiredUnique row identifier.
textValuestringtextContentOverride for typeahead search.
disabledbooleanfalseSkip from selection + 2D nav.
+
+ +
Cell · SelectionCheckbox
+

+ Cell renders role="gridcell" as a content slot — wrap any interactive children inside. SelectionCheckbox is a per-row toggle that participates in selectionMode='multiple'; click toggles the row in value. Visual matches the shared menu-indicator pattern (filled accent + white ✓). +

+
+ {/if} + + {#if tab === 'morfo'} +
+

+ morfo + · declarative contract +

+

Source: src/uix/morfo/components/grid-list.ts.

+ +
+ + + + + + + + + +
FieldValue
name"{gridListMorfo.name}"
kebab"{gridListMorfo.kebab}"
apg{gridListMorfo.apg}
parts.length{gridListMorfo.parts.length}
events.length{events.length}
+
+ +
Parts
+
+ + + + {#each partsList as part (part.kebab)} + + + + + + + + + + {/each} + +
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.archetype ?? '—'}{part.states.length ? part.states.join(' | ') : '—'}{part.optional ? 'yes' : 'no'}
+
+ +
Events declaration
+
+ + + + {#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 ?? '?'}` : '—'} + + + + + + + + + {/each} + +
namefamilyverbsequenceintenttarget
{action.name}{sem.family}{sem.verb ?? '—'}{sem.sequence ?? 'pre'}{intentStr}{action.target}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema + · events +

+
+ + + + {#each events as action (action.name)} + {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined} + {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined} + + + + + + + + + {/each} + +
NameFamilyVerbSequenceIntentPlay
{action.name}{action.semantic.family}{action.semantic.verb ?? '—'}{action.semantic.sequence ?? 'pre'}{effectiveIntent ?? '—'} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

Selectors at src/uix/eidos/components/grid-list/grid-list.css.

+
+ + + + + + + + + + + + +
SelectorSourceWhat it paints
[data-grid-list]eidosContainer — flex column, border, padding, scroll cap.
[data-grid-list][data-size]eidosRow height + font-size scale.
[data-grid-list][data-variant]eidosContainer chrome (surface / outline / ghost).
[data-grid-list][data-color]eidosAccent palette for selected rows + focus ring.
[data-grid-list-row]morfoRow — flex layout, padding, hover/highlight states.
[data-grid-list-row][data-selected]morfoSelected-row accent-soft fill.
[data-grid-list-cell]morfoCell slot — first/last child get special flex treatment.
[data-grid-list-selection-checkbox][data-state]eidos0.875rem outlined box → filled accent + white ✓.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+

+ Follows the APG grid pattern. + Container is role="grid"; each Row is + role="row"; each Cell is role="gridcell". + Roving tabindex on rows; arrow nav handles cell-level focus too + so inner buttons / links are reachable. +

+ +
Keyboard
+
+ + + + + + + + + + + + +
KeyAction
ArrowDown / ArrowUpMove row focus ±1.
ArrowRight / ArrowLeftMove cell focus within the row (lets you reach inner buttons / links).
Home / EndFirst / last row.
Enter / SpaceToggle row selection.
Shift+Click / Shift+ArrowUp/DownExtend range (multiple mode).
Ctrl/Cmd+ASelect all (multiple mode).
EscapeClear selection.
Letter / digitTypeahead — jump to next row whose textValue starts with that character.
+
+ +
ARIA contract
+
+ + + + + + + + + + + +
PartAttributeValue
grid-listrole"grid"
grid-listaria-multiselectable"true" (multiple mode)
rowrole"row"
rowaria-selected"true" | "false"
rowaria-disabled"true" (disabled)
cellrole"gridcell"
selection-checkboxrole / aria-checked"checkbox" / "true|false"
+
+
+ {/if} +