diff --git a/src/uix/eidos/components/command/command-dialog.svelte b/src/uix/eidos/components/command/command-dialog.svelte new file mode 100644 index 000000000..a3f92ba14 --- /dev/null +++ b/src/uix/eidos/components/command/command-dialog.svelte @@ -0,0 +1,80 @@ + + + onOpenChange?.(v)}> + + {#if overlay}{@render overlay()}{:else}{/if} + + onValueChange?.(v)} + bind:search + onSearchChange={(v) => onSearchChange?.(v)} + {shouldFilter} + {filter} + {loop} + {vimBindings} + {columns} + {disablePointerSelection} + {size} + > + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + + + + diff --git a/src/uix/eidos/components/command/command-empty.svelte b/src/uix/eidos/components/command/command-empty.svelte new file mode 100644 index 000000000..044cc3e98 --- /dev/null +++ b/src/uix/eidos/components/command/command-empty.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-group-heading.svelte b/src/uix/eidos/components/command/command-group-heading.svelte new file mode 100644 index 000000000..64ff02edb --- /dev/null +++ b/src/uix/eidos/components/command/command-group-heading.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-group-items.svelte b/src/uix/eidos/components/command/command-group-items.svelte new file mode 100644 index 000000000..ca10d8123 --- /dev/null +++ b/src/uix/eidos/components/command/command-group-items.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-group.svelte b/src/uix/eidos/components/command/command-group.svelte new file mode 100644 index 000000000..d940164c5 --- /dev/null +++ b/src/uix/eidos/components/command/command-group.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-input.svelte b/src/uix/eidos/components/command/command-input.svelte new file mode 100644 index 000000000..db9d6cac6 --- /dev/null +++ b/src/uix/eidos/components/command/command-input.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-item-icon.svelte b/src/uix/eidos/components/command/command-item-icon.svelte new file mode 100644 index 000000000..cda8a0672 --- /dev/null +++ b/src/uix/eidos/components/command/command-item-icon.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/uix/eidos/components/command/command-item-shortcut.svelte b/src/uix/eidos/components/command/command-item-shortcut.svelte new file mode 100644 index 000000000..3369d9902 --- /dev/null +++ b/src/uix/eidos/components/command/command-item-shortcut.svelte @@ -0,0 +1,14 @@ + + + + {@render children?.()} + diff --git a/src/uix/eidos/components/command/command-item.svelte b/src/uix/eidos/components/command/command-item.svelte new file mode 100644 index 000000000..ad0ccb1cd --- /dev/null +++ b/src/uix/eidos/components/command/command-item.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-link-item.svelte b/src/uix/eidos/components/command/command-link-item.svelte new file mode 100644 index 000000000..5ce697ceb --- /dev/null +++ b/src/uix/eidos/components/command/command-link-item.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-list.svelte b/src/uix/eidos/components/command/command-list.svelte new file mode 100644 index 000000000..d5a5ea4db --- /dev/null +++ b/src/uix/eidos/components/command/command-list.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-loading.svelte b/src/uix/eidos/components/command/command-loading.svelte new file mode 100644 index 000000000..a34248830 --- /dev/null +++ b/src/uix/eidos/components/command/command-loading.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-separator.svelte b/src/uix/eidos/components/command/command-separator.svelte new file mode 100644 index 000000000..a28eb4961 --- /dev/null +++ b/src/uix/eidos/components/command/command-separator.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command-shortcut.svelte.ts b/src/uix/eidos/components/command/command-shortcut.svelte.ts new file mode 100644 index 000000000..acfc957d6 --- /dev/null +++ b/src/uix/eidos/components/command/command-shortcut.svelte.ts @@ -0,0 +1,74 @@ +/** + * Cross-platform keyboard shortcut listener for ``. + * Mantine Spotlight-style convenience: bind once on mount, parse `mod` → + * Cmd on macOS / Ctrl elsewhere. + * + * Returns a cleanup function. Caller owns the open-state binding. + */ +import type { CommandShortcut } from './types'; + +function isMac(): boolean { + if (typeof navigator === 'undefined') return false; + return /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent); +} + +type ParsedKey = { + key: string; + meta?: boolean; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; +}; + +function parseShortcut(spec: string): ParsedKey { + const parts = spec + .toLowerCase() + .split('+') + .map((p) => p.trim()); + const out: ParsedKey = { key: '' }; + const mac = isMac(); + for (const p of parts) { + if (p === 'mod') { + if (mac) out.meta = true; + else out.ctrl = true; + } else if (p === 'meta' || p === 'cmd' || p === 'command') out.meta = true; + else if (p === 'ctrl' || p === 'control') out.ctrl = true; + else if (p === 'shift') out.shift = true; + else if (p === 'alt' || p === 'option') out.alt = true; + else out.key = p; + } + return out; +} + +function matches(e: KeyboardEvent, parsed: ParsedKey): boolean { + if (e.key.toLowerCase() !== parsed.key) return false; + if (!!e.metaKey !== !!parsed.meta) return false; + if (!!e.ctrlKey !== !!parsed.ctrl) return false; + if (!!e.shiftKey !== !!parsed.shift) return false; + if (!!e.altKey !== !!parsed.alt) return false; + return true; +} + +/** + * Bind a window-level keydown listener that calls `onTrigger` when any of + * the configured shortcuts fires. Pass `null`/`undefined` shortcut to no-op. + */ +export function bindCommandShortcut( + shortcut: CommandShortcut | null | undefined, + onTrigger: (e: KeyboardEvent) => void +): () => void { + if (!shortcut || typeof window === 'undefined') return () => {}; + const specs = Array.isArray(shortcut) ? shortcut : [shortcut]; + const parsed = specs.map(parseShortcut); + const handler = (e: KeyboardEvent) => { + for (const p of parsed) { + if (matches(e, p)) { + e.preventDefault(); + onTrigger(e); + return; + } + } + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); +} diff --git a/src/uix/eidos/components/command/command-viewport.svelte b/src/uix/eidos/components/command/command-viewport.svelte new file mode 100644 index 000000000..c49d92a89 --- /dev/null +++ b/src/uix/eidos/components/command/command-viewport.svelte @@ -0,0 +1,7 @@ + + +{@render children?.()} diff --git a/src/uix/eidos/components/command/command.css b/src/uix/eidos/components/command/command.css new file mode 100644 index 000000000..8402da2d6 --- /dev/null +++ b/src/uix/eidos/components/command/command.css @@ -0,0 +1,225 @@ +/* + * 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 `` 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-floating); + 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); +} diff --git a/src/uix/eidos/components/command/command.svelte b/src/uix/eidos/components/command/command.svelte new file mode 100644 index 000000000..0553029c0 --- /dev/null +++ b/src/uix/eidos/components/command/command.svelte @@ -0,0 +1,69 @@ + + + + {#snippet children(snippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/command/index.ts b/src/uix/eidos/components/command/index.ts new file mode 100644 index 000000000..3f071ddbd --- /dev/null +++ b/src/uix/eidos/components/command/index.ts @@ -0,0 +1,115 @@ +// Command — eidos compound API (⌘K palette). +// +// import { Command } from '$uix/eidos/components/command'; +// +// Inline (non-modal) usage: +// +// +// +// +// No results. +// +// Files +// +// +// 📁 +// Open file +// ⌘O +// +// +// +// +// +// +// Modal usage with built-in ⌘K binding: +// +// +// +// … +// +// +// Soma owns scoring (computeFuzzyScore), keyboard nav (Arrow / vim +// bindings / typeahead / loop / grid columns / RTL), item visibility, +// and the imperative `CommandApi` snippet props. Eidos paints the +// palette chrome + size + Item sub-parts (Icon / Shortcut) + the +// Dialog composition. +import CommandRoot from './command.svelte'; +import CommandDialog from './command-dialog.svelte'; +import Input from './command-input.svelte'; +import List from './command-list.svelte'; +import Viewport from './command-viewport.svelte'; +import Item from './command-item.svelte'; +import LinkItem from './command-link-item.svelte'; +import Group from './command-group.svelte'; +import GroupHeading from './command-group-heading.svelte'; +import GroupItems from './command-group-items.svelte'; +import Empty from './command-empty.svelte'; +import Loading from './command-loading.svelte'; +import Separator from './command-separator.svelte'; +import ItemIcon from './command-item-icon.svelte'; +import ItemShortcut from './command-item-shortcut.svelte'; + +// Attach Icon / Shortcut as nested sub-parts of Item so the usage +// reads `` / `` — cmdk-style. +type ItemNamespace = typeof Item & { + Icon: typeof ItemIcon; + Shortcut: typeof ItemShortcut; +}; +const ItemWithSubparts = Item as ItemNamespace; +ItemWithSubparts.Icon = ItemIcon; +ItemWithSubparts.Shortcut = ItemShortcut; + +type CommandNamespace = typeof CommandRoot & { + Dialog: typeof CommandDialog; + Input: typeof Input; + List: typeof List; + Viewport: typeof Viewport; + Item: ItemNamespace; + LinkItem: typeof LinkItem; + Group: typeof Group; + GroupHeading: typeof GroupHeading; + GroupItems: typeof GroupItems; + Empty: typeof Empty; + Loading: typeof Loading; + Separator: typeof Separator; +}; + +const Command = CommandRoot as CommandNamespace; +Command.Dialog = CommandDialog; +Command.Input = Input; +Command.List = List; +Command.Viewport = Viewport; +Command.Item = ItemWithSubparts; +Command.LinkItem = LinkItem; +Command.Group = Group; +Command.GroupHeading = GroupHeading; +Command.GroupItems = GroupItems; +Command.Empty = Empty; +Command.Loading = Loading; +Command.Separator = Separator; + +export { Command }; +export default Command; + +export type { + CommandProps, + CommandInputProps as InputProps, + CommandListProps as ListProps, + CommandViewportProps as ViewportProps, + CommandItemProps as ItemProps, + CommandLinkItemProps as LinkItemProps, + CommandGroupProps as GroupProps, + CommandGroupHeadingProps as GroupHeadingProps, + CommandGroupItemsProps as GroupItemsProps, + CommandEmptyProps as EmptyProps, + CommandLoadingProps as LoadingProps, + CommandSeparatorProps as SeparatorProps, + CommandItemIconProps as ItemIconProps, + CommandItemShortcutProps as ItemShortcutProps, + CommandDialogProps as DialogProps, + CommandSize, + CommandShortcut, + CommandFilterFn, + CommandApi, + CommandSnippetProps +} from './types'; diff --git a/src/uix/eidos/components/command/types.ts b/src/uix/eidos/components/command/types.ts new file mode 100644 index 000000000..ecd0e3a82 --- /dev/null +++ b/src/uix/eidos/components/command/types.ts @@ -0,0 +1,123 @@ +import type { Snippet } from 'svelte'; +import type { ResponsiveProp, Size } from '$uix/eidos/lib/types'; +import type { DialogSize } from '$uix/eidos/components/dialog/types'; +import type { + CommandFilterFn, + CommandApi, + CommandSnippetProps, + ProviderProps, + InputProps, + ListProps, + ViewportProps, + ItemProps, + LinkItemProps, + GroupProps, + GroupHeadingProps, + GroupItemsProps, + EmptyProps, + LoadingProps, + SeparatorProps +} from '$soma/components/command'; + +/** + * Command visual size — three-step subset matching the air baseline. + * `xs` would crowd item rows and `xl` is unnecessary for a palette + * UX; comfortable density is sm/md/lg. + */ +export type CommandSize = Extract; + +/** Single shortcut spec — `'mod+k'`, `'ctrl+shift+p'`, etc. */ +export type CommandShortcut = string | string[]; + +export type CommandProps = ProviderProps & { + /** + * Visual size preset — `'sm' | 'md' | 'lg'`. Resolved reactively via + * `Eidos.resolve(size)`. Maps to input height, item height, font-size. + * + * @default 'md' + */ + size?: ResponsiveProp; + /** + * Optional keyboard shortcut(s) that toggle the palette open. Uses + * `mod` as a cross-platform meta/ctrl alias (cmd on macOS, ctrl elsewhere). + * Pass a single string (`'mod+k'`) or an array for multiple bindings. + * Requires `open` to be bindable. + * + * Mantine Spotlight-style convenience — pass undefined to skip and + * bind your own keydown handler. + */ + shortcut?: CommandShortcut; + /** Bindable open flag — required when `shortcut` is set. */ + open?: boolean; + /** Callback when open toggles (via shortcut or other source). */ + onOpenChange?: (open: boolean) => void; +}; + +export type CommandInputProps = InputProps; +export type CommandListProps = ListProps; +export type CommandViewportProps = ViewportProps; +export type CommandItemProps = ItemProps; +export type CommandLinkItemProps = LinkItemProps; +export type CommandGroupProps = GroupProps; +export type CommandGroupHeadingProps = GroupHeadingProps; +export type CommandGroupItemsProps = GroupItemsProps; +export type CommandEmptyProps = EmptyProps; +export type CommandLoadingProps = LoadingProps; +export type CommandSeparatorProps = SeparatorProps; + +/** + * Eidos-only sub-parts that go INSIDE `` to layout the + * three slots (leading icon / label / trailing shortcut hint) without + * the consumer having to handcraft the flex container. Pure markers — + * the recipe handles flex/gap. + */ +export type CommandItemIconProps = { + id?: string; + children?: Snippet; +}; + +export type CommandItemShortcutProps = { + id?: string; + children?: Snippet; +}; + +/** + * `` — modal palette built on top of eidos Dialog. + * Mirrors soma's CommandDialog but uses the eidos Dialog wrappers so + * the panel chrome stays consistent with the rest of the system. + */ +export type CommandDialogProps = { + /** Bindable open flag. @default false */ + open?: boolean; + onOpenChange?: (open: boolean) => void; + /** Block outside interaction. @default true */ + modal?: boolean; + /** + * Optional keyboard shortcut(s) that toggle the dialog open. Same + * format as `Command.Root.shortcut`. @default 'mod+k' (set to null/undefined to disable) + */ + shortcut?: CommandShortcut | null; + /** Visual size for the inner Command palette. @default 'md' */ + size?: ResponsiveProp; + /** Visual size of the Dialog panel itself. @default 'md' */ + dialogSize?: ResponsiveProp; + + // Pass-through to inner Command + label?: string; + value?: string; + onValueChange?: (value: string) => void; + search?: string; + onSearchChange?: (value: string) => void; + shouldFilter?: boolean; + filter?: CommandFilterFn; + loop?: boolean; + vimBindings?: boolean; + columns?: number; + disablePointerSelection?: boolean; + + /** Custom overlay — defaults to eidos Dialog.Overlay. */ + overlay?: Snippet; + children?: Snippet<[CommandSnippetProps]>; +}; + +export type { CommandFilterFn, CommandApi, CommandSnippetProps }; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 4321b2112..2299d9ea1 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -95,6 +95,7 @@ @import './components/dropdown-menu/dropdown-menu.css'; @import './components/context-menu/context-menu.css'; @import './components/menubar/menubar.css'; +@import './components/command/command.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/components/command/+page.svelte b/web/routes/uix/components/command/+page.svelte new file mode 100644 index 000000000..a9859baf2 --- /dev/null +++ b/web/routes/uix/components/command/+page.svelte @@ -0,0 +1,708 @@ + + +
+
+
Overlays · Command
+

Command

+

+ ⌘K command palette. Soma owns fuzzy scoring + (computeFuzzyScore), keyboard nav (arrow + vim + bindings + typeahead + loop + grid columns + RTL), item + visibility and the imperative CommandApi. Eidos + paints the palette chrome, exposes size + (sm/md/lg), adds Item.Icon / Item.Shortcut + sub-parts, and provides a <Command.Dialog> + composition wired to the eidos Dialog with an optional + mod+k shortcut binding. +

+
+ + parts{compiled.parts.order.length} + + + events{events.length} + + + sizes3 + + + apg + combobox ↗ + + {#if lastInvoked} + + last{lastInvoked} + + {/if} +
+
+ +
+
+
+ + + + No commands match. + + Files + + + 📄 + New file + ⌘N + + + 📁 + Open file… + ⌘O + + + 💾 + Save file + ⌘S + + + + + + Edit + + + ↶ + Undo + ⌘Z + + + ↷ + Redo + ⇧⌘Z + + + 🔍 + Find in file + ⌘F + + + + + + Help + + + 📚 + Open documentation + ↗ + + + ℹ︎ + About (disabled) + + + + + +
+
+
+ trace + {#if trace.length === 0} + type or arrow-nav to highlight, Enter to invoke + {: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 || '—'} + +
+
+ + +
+
+ +
+
+ + + + + No matches. + + Quick actions + + + + + New project + ⌘N + + + 🔍 + Global search + ⌘P + + + ⚙︎ + Open settings + ⌘, + + + + + + +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Soma owns scoring + keyboard nav. Eidos adds size on the root and the Item.Icon / Item.Shortcut layout sub-parts. +

+ +
+ soma Provider props +
+
+ + + + + + + +
+ +
+ eidos visual size +
+
+ +
+ +
+ eidos Dialog mode + + · built on top of <Dialog> with optional shortcut binding + +
+
+ + +
+ +
+
+ soma + headless · inline palette + svelte +
+
{somaSnippet}
+
+ +
+
+ eidos + visual · size + Item sub-parts + svelte +
+
{eidosSnippet}
+
+ +
+
+ eidos + modal · Command.Dialog with mod+k binding + svelte +
+
{dialogSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

+ 13 soma parts plus 3 eidos additions: Command.Dialog + (wraps eidos Dialog), Command.Item.Icon and + Command.Item.Shortcut (Item layout sub-parts). +

+ +
Provider (Command)
+
+ + + + + + + + + + + + + + + +
PropTypeDefaultDescription
value bindablestring''Currently highlighted item value.
search bindablestring''Current search string.
shouldFilterbooleantrueWhether to run the scoring filter over items.
filterCommandFilterFncomputeFuzzyScoreCustom scoring function. Return 0 to exclude.
loopbooleanfalseLoop keyboard navigation at the edges.
vimBindingsbooleantrueCtrl+N/J = next, Ctrl+P/K = previous.
columnsnumber1Grid columns — arrows move ±1 (Right/Left) or ±columns (Down/Up).
disablePointerSelectionbooleanfalseHovering items doesn't change the highlight.
dir'ltr' | 'rtl'from soma configGrid arrow inversion in RTL.
size eidosResponsiveProp<'sm' | 'md' | 'lg'>'md'Visual size — input height + item height + font.
shortcut eidosstring | string[]—Optional keyboard shortcut (e.g. 'mod+k') that toggles open. Requires bindable open.
+
+ +
Item
+
+ + + + + + + + + +
PropTypeDefaultDescription
valuestringrequiredUnique value used by the scorer.
keywordsreadonly string[][]Text aliases the scorer also matches.
disabledbooleanfalseDisable activation. Still focusable.
forceMountbooleanfalseRender even when filtered out.
onSelect() => void—Fired on Enter or click.
+
+ +
Item.Icon / Item.Shortcut eidos
+

+ Sub-parts of Item. Both accept children; the recipe handles layout — Icon is a leading flex slot, Shortcut is pushed to row's end via margin-inline-start: auto. Pair with <Kbd> for richer shortcut styling. +

+ +
Command.Dialog eidos
+
+ + + + + + + + + +
PropTypeDefaultDescription
open bindablebooleanfalseDialog open state.
shortcutstring | string[] | null'mod+k'Keyboard binding(s) to toggle. null disables.
size'sm' | 'md' | 'lg''md'Inner Command palette size.
dialogSizeDialogSize'md'Eidos Dialog panel size (sm | md | lg | xl | full).
modal · label · value · search · shouldFilter · filter · loop · …pass-through—Forwarded to Dialog + Command.
+
+
+ {/if} + + {#if tab === 'morfo'} +
+

+ morfo + · declarative contract +

+

Source: src/uix/morfo/components/command.ts.

+ +
+ + + + + + + + + +
FieldValue
name"{commandMorfo.name}"
kebab"{commandMorfo.kebab}"
scope[{commandMorfo.scope.map((s) => `"${s}"`).join(', ')}]
parts.length{commandMorfo.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 + perceptual signature +

+

+ One event: commit-invoke fires when the user + activates a highlighted Item (Enter or click). Verb submit, + intent fulfill. +

+ +
+ + + + {#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/command/command.css. + Inside <Command.Dialog> the dialog panel chrome + comes from dialog.css and the inner palette uses these + selectors. +

+
+ + + + + + + + + + + + + +
SelectorSourceWhat it paints
[data-command]eidosPalette container — flex column, border, surface bg.
[data-command][data-size]eidosSize variant — input + item heights, font-size.
[data-command-input]morfoSearch input row + divider.
[data-command-list], [data-command-viewport]morfoScrolling list.
[data-command-item], [data-command-link-item]morfoRow — flex, padding, gap.
[data-command-item][data-selected]morfoHighlighted row tint.
[data-command-item-icon], [data-command-item-shortcut]eidosItem sub-part layout slots — icon fixed, shortcut auto-end.
[data-command-group-heading]morfoMuted uppercase group label.
[data-command-separator]morfo1px divider between groups.
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+

+ Follows the APG combobox pattern with a listbox popup. Input has role="combobox"; List is role="listbox"; each Item is role="option". +

+ +
Keyboard
+
+ + + + + + + + + + + + +
KeyAction
ArrowDown / ArrowUpMove highlight ±1 (or ±columns if columns > 1).
ArrowRight / ArrowLeftIn grid mode, move ±1 column (RTL inverts).
Ctrl+N / Ctrl+JNext (vim binding, if vimBindings=true).
Ctrl+P / Ctrl+KPrevious (vim binding).
Home / EndFirst / last visible item.
EnterActivate highlighted item — fires onSelect + commit-invoke.
EscapeClear search (or close dialog if inside Command.Dialog).
mod+kToggle dialog open/close (if shortcut is configured).
+
+ +
ARIA contract
+
+ + + + + + + + + + + + +
PartAttributeValue
inputrole"combobox"
inputaria-expanded"true" (always while palette is mounted)
inputaria-controls{``}
inputaria-activedescendant{``}
listrole"listbox"
itemrole"option"
itemaria-selected"true" (when highlighted)
itemaria-disabled"true" (when disabled)
+
+
+ {/if} +