feat(eidos): port Command (⌘K palette)
Closes #130. Eidos wrapper + 13 part wrappers + 2 new sub-parts +
Dialog composition + recipe + canonical-depth demo. Soma owns scoring
(`computeFuzzyScore`), keyboard nav (Arrow + vim Ctrl+N/P/J/K +
typeahead + loop + grid columns + RTL), item visibility, and the
imperative `CommandApi`. Eidos adds size + Item.Icon / Item.Shortcut
slots + Dialog composition with optional shortcut binding.
Reference parity:
| Feature | cmdk | Mantine Spotlight | bits-ui Command | this port |
|---|---|---|---|---|
| Root provider w/ bind:value, bind:search | ✓ | ✓ | ✓ | ✓ |
| Input, List, Viewport, Empty | ✓ | ✓ | ✓ | ✓ |
| Group + GroupHeading + GroupItems | ✓ | ✓ | ✓ | ✓ |
| Item value, keywords, disabled, onSelect | ✓ | ✓ | ✓ | ✓ |
| LinkItem (anchor) | ✗ | ✗ | ✓ | ✓ |
| Separator, Loading (progress 0-100) | ✓ | partial | ✓ | ✓ |
| Custom filter / shouldFilter | ✓ | ✓ | ✓ | ✓ |
| Loop, vim bindings, grid columns | ✓ | partial | ✓ | ✓ |
| disablePointerSelection | ✓ | ✗ | ✓ | ✓ |
| Modal Dialog composition | ✓ | ✓ (Root is modal) | ✓ | ✓ via eidos Dialog |
| Built-in ⌘K shortcut binding | ✗ (BYO) | ✓ | ✗ (BYO) | ✓ optional |
| Item.Icon / Item.Shortcut slots | BYO | leftSection/rightSection | BYO | ✓ NEW |
Naming: `Command.Root` aliased as the default export (cmdk/bits-ui
parity, eidos convention). Inner parts as named sub-properties:
`Command.Input`, `Command.List`, `Command.Item`, `Command.Item.Icon`,
`Command.Item.Shortcut`, `Command.Dialog`, …
Variant set: only `size: 'sm' | 'md' | 'lg'` (drops xs/xl — palette UX
needs comfortable hit targets, matches air baseline).
Shortcut binding: cross-platform `mod` token (Cmd on macOS, Ctrl
elsewhere). Parses `'mod+k'`, `'ctrl+shift+p'`, etc. Window keydown
listener registered on mount, cleaned up on unmount. Pass `null`/
undefined to disable.
Recipe: `command.css` paints palette container, input row, scrolling
List/Viewport, item rows (with highlighted [data-selected] state),
icon + shortcut slot layout, group heading, separator, empty state.
Loading progress is a 2px bar under Input with `--_command-loading-
progress` for the fill. Inside Command.Dialog the panel chrome comes
from dialog.css.
Demo: 6 tabs (Live/API/Morfo/Sema/Recipe/A11y) — Live wires every
soma + eidos prop + Dialog mode with shortcut input. Two stages:
inline palette + dialog opener with shortcut hint. Three code
snippets (soma, eidos inline, eidos dialog).
Gotcha note: command.svelte renames the children prop to `bodyContent`
on destructure to avoid shadowing the inner `{#snippet children}` — same
infinite-recursion trap noted in `feedback_button_consumer_pattern.md`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
/*
|
|
|
|
|
* Command palette recipe — ⌘K command bar.
|
|
|
|
|
*
|
|
|
|
|
* [data-command] → root container (input + list)
|
|
|
|
|
* [data-command-input] → search input
|
|
|
|
|
* [data-command-list] → scrolling listbox
|
|
|
|
|
* [data-command-viewport] → optional inner scroller (if List wraps it)
|
|
|
|
|
* [data-command-empty] → "no results" message
|
|
|
|
|
* [data-command-loading] → progress bar slot
|
|
|
|
|
* [data-command-group] → group container
|
|
|
|
|
* [data-command-group-heading]→ group label
|
|
|
|
|
* [data-command-group-items] → group items wrapper
|
|
|
|
|
* [data-command-item] → row (highlighted via data-selected)
|
|
|
|
|
* [data-command-link-item] → anchor-style row
|
|
|
|
|
* [data-command-separator] → 1px divider
|
|
|
|
|
* [data-command-item-icon] → leading glyph slot (eidos sub-part)
|
|
|
|
|
* [data-command-item-shortcut]→ trailing kbd hint slot (eidos sub-part)
|
|
|
|
|
*
|
|
|
|
|
* Sizes — sm | md | lg via `data-size` on [data-command].
|
|
|
|
|
* Inside `<Command.Dialog>` the panel chrome comes from dialog.css; this
|
|
|
|
|
* recipe only paints the inner palette.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
[data-command] {
|
|
|
|
|
--_command-input-height: var(--control-height-md);
|
|
|
|
|
--_command-input-px: var(--space-3);
|
|
|
|
|
--_command-input-font-size: var(--font-size-sm);
|
|
|
|
|
--_command-item-height: var(--control-height-md);
|
|
|
|
|
--_command-item-px: var(--space-3);
|
|
|
|
|
--_command-item-font-size: var(--font-size-sm);
|
|
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
inline-size: 100%;
|
|
|
|
|
min-inline-size: 0;
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
background: var(--color-surface-overlay);
|
|
|
|
|
box-shadow: var(--shadow-overlay);
|
feat(eidos): port Command (⌘K palette)
Closes #130. Eidos wrapper + 13 part wrappers + 2 new sub-parts +
Dialog composition + recipe + canonical-depth demo. Soma owns scoring
(`computeFuzzyScore`), keyboard nav (Arrow + vim Ctrl+N/P/J/K +
typeahead + loop + grid columns + RTL), item visibility, and the
imperative `CommandApi`. Eidos adds size + Item.Icon / Item.Shortcut
slots + Dialog composition with optional shortcut binding.
Reference parity:
| Feature | cmdk | Mantine Spotlight | bits-ui Command | this port |
|---|---|---|---|---|
| Root provider w/ bind:value, bind:search | ✓ | ✓ | ✓ | ✓ |
| Input, List, Viewport, Empty | ✓ | ✓ | ✓ | ✓ |
| Group + GroupHeading + GroupItems | ✓ | ✓ | ✓ | ✓ |
| Item value, keywords, disabled, onSelect | ✓ | ✓ | ✓ | ✓ |
| LinkItem (anchor) | ✗ | ✗ | ✓ | ✓ |
| Separator, Loading (progress 0-100) | ✓ | partial | ✓ | ✓ |
| Custom filter / shouldFilter | ✓ | ✓ | ✓ | ✓ |
| Loop, vim bindings, grid columns | ✓ | partial | ✓ | ✓ |
| disablePointerSelection | ✓ | ✗ | ✓ | ✓ |
| Modal Dialog composition | ✓ | ✓ (Root is modal) | ✓ | ✓ via eidos Dialog |
| Built-in ⌘K shortcut binding | ✗ (BYO) | ✓ | ✗ (BYO) | ✓ optional |
| Item.Icon / Item.Shortcut slots | BYO | leftSection/rightSection | BYO | ✓ NEW |
Naming: `Command.Root` aliased as the default export (cmdk/bits-ui
parity, eidos convention). Inner parts as named sub-properties:
`Command.Input`, `Command.List`, `Command.Item`, `Command.Item.Icon`,
`Command.Item.Shortcut`, `Command.Dialog`, …
Variant set: only `size: 'sm' | 'md' | 'lg'` (drops xs/xl — palette UX
needs comfortable hit targets, matches air baseline).
Shortcut binding: cross-platform `mod` token (Cmd on macOS, Ctrl
elsewhere). Parses `'mod+k'`, `'ctrl+shift+p'`, etc. Window keydown
listener registered on mount, cleaned up on unmount. Pass `null`/
undefined to disable.
Recipe: `command.css` paints palette container, input row, scrolling
List/Viewport, item rows (with highlighted [data-selected] state),
icon + shortcut slot layout, group heading, separator, empty state.
Loading progress is a 2px bar under Input with `--_command-loading-
progress` for the fill. Inside Command.Dialog the panel chrome comes
from dialog.css.
Demo: 6 tabs (Live/API/Morfo/Sema/Recipe/A11y) — Live wires every
soma + eidos prop + Dialog mode with shortcut input. Two stages:
inline palette + dialog opener with shortcut hint. Three code
snippets (soma, eidos inline, eidos dialog).
Gotcha note: command.svelte renames the children prop to `bodyContent`
on destructure to avoid shadowing the inner `{#snippet children}` — same
infinite-recursion trap noted in `feedback_button_consumer_pattern.md`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
overflow: hidden;
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command][data-size='sm'] {
|
|
|
|
|
--_command-input-height: var(--control-height-sm);
|
|
|
|
|
--_command-input-px: var(--space-2);
|
|
|
|
|
--_command-input-font-size: var(--font-size-xs);
|
|
|
|
|
--_command-item-height: var(--control-height-sm);
|
|
|
|
|
--_command-item-px: var(--space-2);
|
|
|
|
|
--_command-item-font-size: var(--font-size-xs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command][data-size='lg'] {
|
|
|
|
|
--_command-input-height: var(--control-height-lg);
|
|
|
|
|
--_command-input-px: var(--space-4);
|
|
|
|
|
--_command-input-font-size: var(--font-size-md);
|
|
|
|
|
--_command-item-height: var(--control-height-lg);
|
|
|
|
|
--_command-item-px: var(--space-4);
|
|
|
|
|
--_command-item-font-size: var(--font-size-md);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Input ──────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-input] {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
inline-size: 100%;
|
|
|
|
|
block-size: var(--_command-input-height);
|
|
|
|
|
padding-inline: var(--_command-input-px);
|
|
|
|
|
border: 0;
|
|
|
|
|
border-block-end: 1px solid var(--color-border-subtle);
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
font-family: var(--font-ui);
|
|
|
|
|
font-size: var(--_command-input-font-size);
|
|
|
|
|
line-height: var(--leading-ui);
|
|
|
|
|
outline: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-input]::placeholder {
|
|
|
|
|
color: var(--color-content-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Loading progress (slim bar under Input) ───────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-loading] {
|
|
|
|
|
display: block;
|
|
|
|
|
block-size: 2px;
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
to right,
|
|
|
|
|
var(--color-primary-solid) 0%,
|
|
|
|
|
var(--color-primary-solid) var(--_command-loading-progress, 0%),
|
|
|
|
|
transparent var(--_command-loading-progress, 0%)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── List + Viewport ──────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-list] {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--space-1);
|
|
|
|
|
max-block-size: min(60vh, 24rem);
|
|
|
|
|
padding: var(--space-1);
|
|
|
|
|
overflow: auto;
|
|
|
|
|
overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-list]:has([data-command-viewport]) {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-viewport] {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--space-1);
|
|
|
|
|
max-block-size: min(60vh, 24rem);
|
|
|
|
|
padding: var(--space-1);
|
|
|
|
|
overflow: auto;
|
|
|
|
|
overscroll-behavior: contain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Group / GroupHeading / GroupItems ────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-group] {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--space-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-group-heading] {
|
|
|
|
|
padding-block: var(--space-1);
|
|
|
|
|
padding-inline: var(--_command-item-px);
|
|
|
|
|
color: var(--color-content-muted);
|
|
|
|
|
font-size: var(--font-size-xs);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-group-items] {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Item / LinkItem ──────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-item],
|
|
|
|
|
[data-command-link-item] {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--space-2);
|
|
|
|
|
min-block-size: var(--_command-item-height);
|
|
|
|
|
padding-block: var(--space-1);
|
|
|
|
|
padding-inline: var(--_command-item-px);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
font-size: var(--_command-item-font-size);
|
|
|
|
|
line-height: var(--leading-ui);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
user-select: none;
|
|
|
|
|
transition:
|
|
|
|
|
background var(--duration-fast) var(--ease-default),
|
|
|
|
|
color var(--duration-fast) var(--ease-default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-item][data-selected],
|
|
|
|
|
[data-command-link-item][data-selected] {
|
|
|
|
|
background: var(--color-primary-element);
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-item][data-disabled],
|
|
|
|
|
[data-command-link-item][data-disabled] {
|
|
|
|
|
cursor: default;
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Item sub-parts (eidos-only) ──────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-item-icon] {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
inline-size: 1.125em;
|
|
|
|
|
block-size: 1.125em;
|
|
|
|
|
color: var(--color-content-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-command-item-shortcut] {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--space-1);
|
|
|
|
|
margin-inline-start: auto;
|
|
|
|
|
color: var(--color-content-muted);
|
|
|
|
|
font-size: 0.875em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* When the row is highlighted, lift the muted sub-parts to primary. */
|
|
|
|
|
[data-command-item][data-selected] [data-command-item-icon],
|
|
|
|
|
[data-command-item][data-selected] [data-command-item-shortcut],
|
|
|
|
|
[data-command-link-item][data-selected] [data-command-item-icon],
|
|
|
|
|
[data-command-link-item][data-selected] [data-command-item-shortcut] {
|
|
|
|
|
color: var(--color-content-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Empty state ──────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-empty] {
|
|
|
|
|
padding-block: var(--space-4);
|
|
|
|
|
padding-inline: var(--_command-item-px);
|
|
|
|
|
color: var(--color-content-muted);
|
|
|
|
|
font-size: var(--_command-item-font-size);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Separator ────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
[data-command-separator] {
|
|
|
|
|
block-size: 1px;
|
|
|
|
|
margin-block: var(--space-1);
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
}
|