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

212 lines
5.9 KiB

feat(eidos): port Listbox (embedded selection list) #131. Eidos wrapper + types + recipe + canonical-depth demo. Soma owns single/multi selection (value as string[]), roving-tabindex keyboard nav (Arrow + Home/End + typeahead + Ctrl+A), Field-merged flags (disabled/readonly/required/invalid), hidden form inputs. Eidos adds the visual surface and exposes variant + size + color. Reference parity (no air baseline — port directly from soma): | Feature | soma | react-aria | Headless UI | bits-ui | this port | |---|---|---|---|---|---| | Single + multiple selection | ✓ | ✓ | ✓ | ✓ | ✓ | | bind:value (controlled) | ✓ | ✓ | ✓ | ✓ | ✓ | | Typeahead + Arrow / Home / End | ✓ | ✓ | ✓ | ✓ | ✓ | | Loop nav | ✓ | ✗ | ✗ | ✗ | ✓ | | Section / Group + label | ✓ | ✓ | ✗ | ✓ | ✓ | | Ctrl+A select-all (multi) | ✓ | ✓ | ✗ | ✗ | ✓ | | Form participation (hidden inputs) | ✓ | ✗ | ✓ | ✗ | ✓ | | Field.Provider merge (disabled/invalid/…) | ✓ | ✗ | ✗ | ✗ | ✓ | | Horizontal orientation | ✓ | ✓ | ✓ | ✓ | ✓ | | Item indicator (✓ glyph) | ✓ | ✓ | ✗ | ✓ | ✓ shared partial | | Drag-to-reorder | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | Async / infinite loading | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | | selectionBehavior (toggle/replace) | ⚠ implicit | ✓ | ✗ | ✓ | ⚠ deferred | | PageUp/Down nav | ✗ | ✓ | ✗ | ✗ | ⚠ deferred | Parts (5): Listbox (Root, replaces soma's Provider name), Item, ItemIndicator, Group, GroupLabel. Variant set: - variant: ControlVariant (`'surface' | 'outline' | 'ghost'`), default 'surface' - size: xs..xl (5-step canonical form-control scale) - color: ColorRole (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) - block: stretch to container width Cross-component reuse: - `ItemIndicator` shares the menu-indicator visual (`a2709476`) — ✓ glyph identical to standalone Checkbox and Dropdown/Context Menu CheckboxItems. CSS-only difference: shown only inside `[data-listbox-item][data-selected]`. - Item row hover / highlight tokens mirror Combobox.Item / Command.Item — the form-control surface family stays coherent. Disambiguation note (in index.ts comment): - Listbox: atomic options, text rows, no inner interactive elements (ARIA forbids interactive descendants inside role="listbox") - GridList: rows with buttons / links / checkboxes inside (2D nav) - Select / Combobox: closed picker surface (Listbox-like dropdown) Gotcha: listbox.svelte and listbox-item.svelte rename `children` to `bodyContent` on destructure to avoid shadowing the inner `{#snippet children}` block (same recursion trap documented in `feedback_button_consumer_pattern.md` Rule 3). Demo: 6 tabs at canonical depth — Live wires all soma + eidos props, all 3 ControlVariant chips, 5 size chips, 8 ColorRole chips, single + multi mode toggle, RTL/horizontal toggle. Sidebar entry added under Forms (between Combobox and Calendar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
/*
* Listbox recipe.
*
* [data-listbox] → root container
* [data-listbox-item] → option row
* [data-listbox-item-indicator] → ✓ glyph on selected items
* [data-listbox-group] → group container
* [data-listbox-group-label] → muted group label
*
* Sizes — xs..xl on the root; cascades to item height + font-size.
* Variant `'surface' | 'outline' | 'ghost'` (ControlVariant).
* Color cascades the accent for selected-item highlight + focus ring.
*
* Cross-component reuse:
* - The ItemIndicator visual is the canonical check glyph — same as
* [data-checkbox-indicator] / menu-item indicators. Painted here as
* a filled accent square with white ✓ on selected items.
*/
[data-listbox] {
--_listbox-item-height: var(--control-height-md);
--_listbox-item-px: var(--space-3);
--_listbox-item-font-size: var(--font-size-sm);
--_listbox-accent: var(--color-primary-solid);
--_listbox-accent-soft: var(--color-primary-element);
--_listbox-accent-text: var(--color-content-on-solid);
display: flex;
flex-direction: column;
gap: var(--space-1);
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(--_listbox-item-font-size);
line-height: var(--leading-ui);
outline: none;
}
[data-listbox][data-block] {
display: flex;
inline-size: 100%;
}
[data-listbox][data-orientation='horizontal'] {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
/* ── Variant — surface (default) ─ */
[data-listbox][data-variant='surface'] {
background: var(--color-surface-default);
border-color: var(--color-border-default);
}
[data-listbox][data-variant='outline'] {
background: transparent;
border-color: var(--color-border-default);
}
[data-listbox][data-variant='ghost'] {
background: transparent;
border-color: transparent;
}
/* ── Sizes ─ */
[data-listbox][data-size='xs'] {
--_listbox-item-height: var(--control-height-xs);
--_listbox-item-px: var(--space-2);
--_listbox-item-font-size: var(--font-size-xs);
}
[data-listbox][data-size='sm'] {
--_listbox-item-height: var(--control-height-sm);
--_listbox-item-px: var(--space-2);
--_listbox-item-font-size: var(--font-size-sm);
}
[data-listbox][data-size='lg'] {
--_listbox-item-height: var(--control-height-lg);
--_listbox-item-px: var(--space-4);
--_listbox-item-font-size: var(--font-size-md);
}
[data-listbox][data-size='xl'] {
--_listbox-item-height: var(--control-height-xl);
--_listbox-item-px: var(--space-5);
--_listbox-item-font-size: var(--font-size-lg);
}
/* ── Color cascades ─ */
[data-listbox][data-color='secondary'] {
--_listbox-accent: var(--color-secondary-solid);
--_listbox-accent-soft: var(--color-secondary-element);
}
[data-listbox][data-color='affirm'] {
--_listbox-accent: var(--color-affirm-solid);
--_listbox-accent-soft: var(--color-affirm-element);
}
[data-listbox][data-color='risk'] {
--_listbox-accent: var(--color-risk-solid);
--_listbox-accent-soft: var(--color-risk-element);
}
[data-listbox][data-color='threat'] {
--_listbox-accent: var(--color-threat-solid);
--_listbox-accent-soft: var(--color-threat-element);
}
[data-listbox][data-color='fulfill'] {
--_listbox-accent: var(--color-fulfill-solid);
--_listbox-accent-soft: var(--color-fulfill-element);
}
[data-listbox][data-color='loss'] {
--_listbox-accent: var(--color-loss-solid);
--_listbox-accent-soft: var(--color-loss-element);
}
[data-listbox][data-color='neutral'] {
--_listbox-accent: var(--color-neutral-solid);
--_listbox-accent-soft: var(--color-neutral-element);
}
[data-listbox][data-disabled] {
opacity: 0.55;
cursor: not-allowed;
}
[data-listbox][data-invalid] {
border-color: var(--color-threat-element);
}
[data-listbox]:focus-visible,
[data-listbox][data-focused] {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
/* ── Item ─ */
[data-listbox-item] {
display: flex;
align-items: center;
gap: var(--space-2);
min-block-size: var(--_listbox-item-height);
padding-block: var(--space-1);
padding-inline: var(--_listbox-item-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-listbox-item][data-highlighted]:not([data-disabled]) {
background: var(--color-surface-overlay);
}
[data-listbox-item][data-selected]:not([data-disabled]) {
background: var(--_listbox-accent-soft);
color: var(--color-content-primary);
}
[data-listbox-item][data-selected][data-highlighted]:not([data-disabled]) {
background: var(--_listbox-accent-soft);
}
[data-listbox-item][data-disabled] {
color: var(--color-content-muted);
cursor: default;
opacity: 0.6;
}
/* ── ItemIndicator (✓ glyph on selected items) ─
*
* Painted as a 0.875rem filled-accent rounded square with white check —
* same shape as the menu-indicator partial used by Dropdown/Context
* Menu CheckboxItems. The ItemIndicator is rendered by the consumer
* (soma emits the data-attr; visibility is gated to selected).
*/
[data-listbox-item-indicator] {
display: none;
inline-size: 0.875rem;
block-size: 0.875rem;
flex: 0 0 auto;
color: var(--_listbox-accent);
}
[data-listbox-item][data-selected] [data-listbox-item-indicator] {
display: inline-flex;
align-items: center;
justify-content: center;
}
/* ── Group / GroupLabel ─ */
[data-listbox-group] {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
[data-listbox-group-label] {
padding-block: var(--space-1);
padding-inline: var(--_listbox-item-px);
color: var(--color-content-muted);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
}

Powered by TurnKey Linux.