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
|
|
|
<script lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* Eidos `<Command.Dialog>` — modal palette built on top of the eidos
|
|
|
|
|
* `<Dialog>` wrapper (so panel chrome + overlay + sizing stay
|
|
|
|
|
* consistent with the rest of the system). Mirrors soma's
|
|
|
|
|
* CommandDialog API but routes through the eidos layer.
|
|
|
|
|
*
|
|
|
|
|
* <Command.Dialog bind:open shortcut="mod+k">
|
|
|
|
|
* <Command.Input placeholder="Search…" />
|
|
|
|
|
* <Command.List>…</Command.List>
|
|
|
|
|
* </Command.Dialog>
|
|
|
|
|
*
|
|
|
|
|
* Default `shortcut='mod+k'` opens the dialog on the canonical
|
|
|
|
|
* keyboard binding. Set `shortcut={null}` to disable.
|
|
|
|
|
*/
|
|
|
|
|
import { Dialog } from '$uix/eidos/components/dialog';
|
|
|
|
|
import CommandRoot from './command.svelte';
|
|
|
|
|
import type { CommandDialogProps } from './types';
|
|
|
|
|
import { bindCommandShortcut } from './command-shortcut.svelte';
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
|
open = $bindable(false),
|
|
|
|
|
onOpenChange,
|
|
|
|
|
modal = true,
|
|
|
|
|
shortcut = 'mod+k',
|
|
|
|
|
size = 'md',
|
|
|
|
|
dialogSize = 'md',
|
|
|
|
|
// Command pass-through
|
|
|
|
|
label,
|
|
|
|
|
value = $bindable(''),
|
|
|
|
|
onValueChange,
|
|
|
|
|
search = $bindable(''),
|
|
|
|
|
onSearchChange,
|
|
|
|
|
shouldFilter = true,
|
|
|
|
|
filter,
|
|
|
|
|
loop = false,
|
|
|
|
|
vimBindings = true,
|
|
|
|
|
columns = 1,
|
|
|
|
|
disablePointerSelection = false,
|
|
|
|
|
overlay,
|
|
|
|
|
// Rename to avoid shadowing the inner snippet
|
|
|
|
|
children: bodyContent
|
|
|
|
|
}: CommandDialogProps = $props();
|
|
|
|
|
|
fix(uix audit): apply Kim audit remediations across morfo/soma/eidos/recipe layers
Resolves 34 of 40 failures from src/uix/audit-uix-kimi-1.md. The remaining
6 are all Words component (excluded per project owner — actively developed
on a separate track) or unrelated infra (cookie-adapter-client jsdom timeout).
Key changes:
- soma/provider/context.ts: swallow Svelte's `lifecycle_outside_component`
in `context.set()` so unit tests can instantiate providers without a
component tree (unblocked 18 picker tests in one line).
- soma/textarea: route inline style writes through `dom.apply({attrs})`
instead of direct `el.style.X = Y` (contract regex compliance).
- soma/picker-shell: add minimal Provider component + morfo so the shared
handle utility passes the morfo/component contract.
- eidos/_layout: lift `shared.ts` to `eidos/lib/layout-helpers.ts` (the
`_layout` directory wasn't a real component) and update 8 layout
primitive imports.
- eidos/command: replace `onMount` with `$effect` (visual layer can't use
Svelte 4 lifecycle hooks per component-api contract); restructure
`Item.Icon`/`Item.Shortcut` namespace so contract regex matches.
- eidos/color-picker: extract hue spectrum hsl literals to sidecar
`color-picker-spectrum.css`; keep main recipe color-literal-free.
- eidos/lib/recipes/base.ts: declare 196 layout-primitive + chrome CSS
vars (box, flex, grid, float, group, banner, container, section,
separator, toggle-group, pin-input, scroll-area, splitter,
navigation-menu, dropdown-menu, context-menu, date-range-field,
time-range-field, plus per-component additions to date-field,
color-picker, button, select). 197 missing → 0 non-Words.
- morfo: add `data-dir` to carousel, `data-kind` to date-picker, add
StrengthMeterLabel sub-part to password-field, register picker-shell.
- 5 pickers (date/color/date-range/time/time-range): add `internals.ts`
indirection so `exports.ts` doesn't re-export `*provider.svelte.ts`
directly (contract D); rewrite virtual root types to plain object
literals (no `WithChild`/`PrimitiveDivAttributes`).
- 4 new READMEs (button, textarea, password-field, picker-shell)
following the search-field canonical template.
Test result: 2391/2397 passing (99.75%). Remaining 6 failures are all
Words-scope or jsdom infra timeout — explicitly out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
|
|
|
// Uses `$effect` (see command.svelte — visual layer can't import
|
|
|
|
|
// Svelte 4 lifecycle hooks).
|
|
|
|
|
$effect(() => {
|
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
|
|
|
if (!shortcut) return undefined;
|
|
|
|
|
return bindCommandShortcut(shortcut, () => {
|
|
|
|
|
const next = !open;
|
|
|
|
|
open = next;
|
|
|
|
|
onOpenChange?.(next);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<Dialog bind:open {modal} onOpenChange={(v) => onOpenChange?.(v)}>
|
|
|
|
|
<Dialog.Portal>
|
|
|
|
|
{#if overlay}{@render overlay()}{:else}<Dialog.Overlay />{/if}
|
|
|
|
|
<Dialog.Content size={dialogSize}>
|
|
|
|
|
<CommandRoot
|
|
|
|
|
{label}
|
|
|
|
|
bind:value
|
|
|
|
|
onValueChange={(v) => onValueChange?.(v)}
|
|
|
|
|
bind:search
|
|
|
|
|
onSearchChange={(v) => onSearchChange?.(v)}
|
|
|
|
|
{shouldFilter}
|
|
|
|
|
{filter}
|
|
|
|
|
{loop}
|
|
|
|
|
{vimBindings}
|
|
|
|
|
{columns}
|
|
|
|
|
{disablePointerSelection}
|
|
|
|
|
{size}
|
|
|
|
|
>
|
|
|
|
|
{#snippet children(snippetProps)}
|
|
|
|
|
{@render bodyContent?.(snippetProps)}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</CommandRoot>
|
|
|
|
|
</Dialog.Content>
|
|
|
|
|
</Dialog.Portal>
|
|
|
|
|
</Dialog>
|