@noxlovette/material 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/claude-skill/material-design/SKILL.md +1 -1
  2. package/claude-skill/material-design/references/component-patterns.md +36 -1
  3. package/dist/animation/containerTransform.js +21 -3
  4. package/dist/components/containers/list/theme.js +1 -1
  5. package/dist/components/containers/pane/theme.js +1 -1
  6. package/dist/components/date/theme.js +1 -1
  7. package/dist/components/forms/command/Command.mdx +90 -0
  8. package/dist/components/forms/command/Command.stories.svelte +69 -10
  9. package/dist/components/forms/command/Command.stories.svelte.d.ts +2 -17
  10. package/dist/components/forms/command/CommandDialog.svelte +58 -0
  11. package/dist/components/forms/command/CommandDialog.svelte.d.ts +11 -0
  12. package/dist/components/forms/command/CommandItem.svelte +48 -5
  13. package/dist/components/forms/command/CommandItem.svelte.d.ts +4 -0
  14. package/dist/components/forms/command/index.d.ts +1 -0
  15. package/dist/components/forms/command/index.js +1 -0
  16. package/dist/components/forms/command/theme.d.ts +371 -6
  17. package/dist/components/forms/command/theme.js +32 -1
  18. package/dist/components/forms/command/types.d.ts +27 -1
  19. package/dist/components/forms/search/Search.mdx +9 -0
  20. package/dist/components/forms/search/Search.stories.svelte +14 -2
  21. package/dist/components/forms/search/Search.svelte +41 -9
  22. package/dist/components/forms/search/Search.svelte.d.ts +3 -0
  23. package/dist/components/forms/search/SearchView.svelte +7 -2
  24. package/dist/components/forms/search/SearchView.svelte.d.ts +2 -1
  25. package/dist/components/forms/search/theme.js +4 -2
  26. package/dist/components/forms/search/types.d.ts +14 -1
  27. package/dist/components/forms/slider/theme.js +1 -1
  28. package/dist/components/nav/appbar/AppBar.stories.svelte +30 -2
  29. package/dist/components/nav/appbar/AppBar.svelte +40 -8
  30. package/dist/components/nav/appbar/types.d.ts +14 -0
  31. package/dist/components/nav/rail/Rail.mdx +6 -0
  32. package/dist/components/nav/rail/Rail.svelte +55 -1
  33. package/dist/components/nav/rail/Rail.svelte.d.ts +3 -0
  34. package/dist/components/nav/rail/types.d.ts +7 -0
  35. package/dist/styles/motion.css +6 -1
  36. package/dist/utils/index.d.ts +1 -0
  37. package/dist/utils/index.js +1 -0
  38. package/dist/utils/shortcut.d.ts +26 -0
  39. package/dist/utils/shortcut.js +84 -0
  40. package/package.json +1 -1
@@ -66,6 +66,6 @@ This repo is a single, already-opinionated M3 component library, not a blank can
66
66
  ## References
67
67
 
68
68
  - [`references/tokens-and-styles.md`](references/tokens-and-styles.md) — full token/utility inventory
69
- - [`references/component-patterns.md`](references/component-patterns.md) — `tv()` slots/variants/compoundVariants pattern + variant decision tree
69
+ - [`references/component-patterns.md`](references/component-patterns.md) — `tv()` slots/variants/compoundVariants pattern + variant decision tree; search vs command palette; page-wide keyboard shortcuts
70
70
  - [`references/motion-guide.md`](references/motion-guide.md) — applying transitions (qualities of a good transition, choosing a pattern), which animation primitive and spring to reach for, reduced-motion target behavior
71
71
  - [`references/accessibility-checklist.md`](references/accessibility-checklist.md) — pre-delivery checklist
@@ -26,7 +26,7 @@ Only fall back to `bare` (no color/background at all) when the component supplie
26
26
 
27
27
  ## Reuse before you build
28
28
 
29
- - **State layer / ripple** — wrap the interactive element with `Layer.svelte` (`src/lib/utils/Layer.svelte`) rather than writing hover/press opacity by hand. It listens for `.m3-layer` on its parent, already respects `prefers-reduced-motion` for the ripple, and its tint (hover 0.08, focus and pressed 0.10) matches the M3 state-layer tokens — don't retune those numbers per component.
29
+ - **State layer / ripple** — wrap the interactive element with `Layer.svelte` (`src/lib/utils/Layer.svelte`) rather than writing hover/press opacity by hand. It listens for `.m3-layer` on its parent, already respects `prefers-reduced-motion` for the ripple, and its tint (hover 0.08, focus and pressed 0.10) matches the M3 state-layer tokens — don't retune those numbers per component. Its tint only reacts to real `:hover`/`:focus-visible`/`:active`, and no Tailwind class can drive it (its CSS is unlayered and beats `@layer utilities`). For a highlight that follows an attribute while focus stays elsewhere (a combobox's `data-selected`/`data-highlighted` option), fill the item: `data-selected:bg-md-sys-color-on-surface/10`, as menus and `commandItem` do.
30
30
  - **Icons** — always `Icon.svelte`, never a raw `<span class="material-symbols-...">` or an inline SVG for a Material Symbol.
31
31
  - `name` is typed (`MaterialSymbolName`), so a typo is a type error. A `string[]` of names needs `as const` or `satisfies MaterialSymbolName[]`.
32
32
  - Show state with `fill` (0 → 1 on the selected item; it animates), not a weight change.
@@ -71,6 +71,41 @@ See `/docs/pane` in the showcase site for the full prop reference and worked exa
71
71
 
72
72
  A viewport-anchored `Rail` publishes `--md-rail-inset` on `<html>`: 0 below `md`, the collapsed 96dp on medium windows (the expanded rail is modal there and overlays), and its live width from `lg` (it pushes content, in step with its spring). `App`'s shell pads by it and `AppBar` starts at it. Never add `md:ml-24` or similar to a page, a `PaneGrid` or an app bar. A custom shell uses `ps-(--md-rail-inset)`; another fixed surface spanning the window uses `left-(--md-rail-inset)`. A rail with `anchor="parent"` renders a spacer beside itself instead, so it and its content go in a flex row.
73
73
 
74
+ ## Search, command palette and keyboard shortcuts
75
+
76
+ Two components take a typed query over a list. Pick by what the user is looking for, not by
77
+ looks:
78
+
79
+ | The user wants to… | Component | Why |
80
+ | ------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
81
+ | Find **content** (records, pages, products, people) | `Search` with `results` (or search `AppBar`) | M3 search. The app filters (you render `results`), Enter with nothing highlighted calls `onsearch(query)`, the query stays in the bar |
82
+ | **Do** something or **go** somewhere (commands, destinations) | `CommandDialog` + `CommandItem`s | Not M3; built from M3 parts. bits-ui filters and ranks, the best match is always highlighted so Enter always runs one, the query is discarded on close |
83
+ | A central search entry point in a page body, not the top bar | `Search` placed in the page | The view docks where the bar sits (medium+) or goes full-screen (compact); it doesn't need an `AppBar` |
84
+
85
+ Don't make the palette the app's search: it can't search for exactly what was typed, and it only
86
+ filters items already rendered. Don't fill a search view with actions. Having both is normal; a
87
+ palette may end with a "Search for “…”" item that hands the query to search.
88
+
89
+ Page-wide keys, all built on `src/lib/utils/shortcut.ts` (`triggersShortcut`, `matchesShortcut`,
90
+ `ariaKeyShortcut`, `shortcutLabel`), each on by default and off with `null`:
91
+
92
+ | Key | Component | Prop |
93
+ | ---------------- | ------------------------- | ----------------------------- |
94
+ | `/` | `Search`, search `AppBar` | `shortcut` / `searchShortcut` |
95
+ | ⌘K / Ctrl+K | `CommandDialog` | `shortcut` |
96
+ | ⌘1–⌘9 / Ctrl+1–9 | `Rail` (Nth destination) | `shortcutModifier` |
97
+
98
+ - Write shortcuts like `aria-keyshortcuts`, with `Mod` for ⌘ on Apple and Ctrl elsewhere
99
+ (`'Mod+K'`). Never bind the Windows/Super key: the OS owns it.
100
+ - A new page-wide shortcut goes through `triggersShortcut(e, shortcut, ownerEl)` on
101
+ `<svelte:window onkeydown>`. It already skips bare keys while typing in a field, IME
102
+ composition, repeats, and any open modal the owner isn't inside. Set `aria-keyshortcuts` on the
103
+ control it activates, resolving `Mod` on the client (`isApplePlatform()` in an `$effect`) so SSR
104
+ and hydration agree.
105
+ - One owner per key per page: with two `Search` bars, pass `shortcut={null}` to all but one.
106
+ - Show a shortcut with `shortcutLabel()` (`⌘K` / `Ctrl+K`), e.g. in a `Kbd` or `CommandItem`'s
107
+ `shortcut`.
108
+
74
109
  ## Before exporting a new component
75
110
 
76
111
  Follow CLAUDE.md's "Adding a New Component" steps as the mechanical checklist (create `.svelte` + `types.ts`, define `theme.ts` with `tv()`, export from the category `index.ts`, run `bun scripts/generate-components-index.ts`, add a showcase route and a docs page) — this skill governs the _design_ decisions (which variant/color/motion) that should be made before or while writing that `theme.ts`, not the export mechanics themselves.
@@ -30,6 +30,7 @@ import { springTokens, springTransition } from './spring.js';
30
30
  * ```
31
31
  */
32
32
  const FILL = '--md-container-transform-color';
33
+ const SHADOW = '--md-container-transform-shadow';
33
34
  const resolve = (target) => typeof target === 'string'
34
35
  ? document.querySelector(target)
35
36
  : target instanceof Element
@@ -46,16 +47,33 @@ const fillOf = (target) => {
46
47
  const colour = target ? getComputedStyle(target).backgroundColor : '';
47
48
  return colour && !TRANSPARENT.has(colour) ? colour : 'transparent';
48
49
  };
50
+ /*
51
+ The container's elevation while it morphs. Motion clips the group (`overflow: clip`) whenever
52
+ the aspect ratio changes, which cuts the shadow out of both snapshots, so an elevated
53
+ destination (a floating pane) would only get its shadow when the live DOM takes over at the
54
+ end. The group's own `box-shadow` isn't clipped by its overflow, so it carries the shadow
55
+ instead: the destination's when it has one (opening into an elevated pane), otherwise the
56
+ source's (closing it back into a flat card), so the container stays elevated the whole way.
57
+ */
58
+ const shadowOf = (target) => {
59
+ const shadow = target ? getComputedStyle(target).boxShadow : '';
60
+ return shadow && shadow !== 'none' ? shadow : undefined;
61
+ };
49
62
  export const containerTransform = (update, { from, to, spring = springTokens.spatial }) => {
50
63
  // The incoming snapshot is opaque wherever the destination is, and the outgoing one fades off
51
64
  // it (motion.css layers them), so nothing behind shows through. The fill (motion.css) covers
52
65
  // the rest of a destination with its own background, e.g. a full-screen view's area below the
53
- // incoming snapshot's top as the bar grows. Only the transition's group reads the property,
54
- // and it's rewritten before each new snapshot, so it's left in place afterwards.
66
+ // incoming snapshot's top as the bar grows, and the shadow keeps its elevation (`shadowOf`).
67
+ // Only the transition's group reads the properties, and they're rewritten before each new
68
+ // snapshot, so they're left in place afterwards.
55
69
  const root = document.documentElement;
56
70
  const updateAndFill = async () => {
71
+ // Read before `update`, which may unmount it.
72
+ const fromShadow = shadowOf(resolve(from));
57
73
  await update();
58
- root.style.setProperty(FILL, fillOf(resolve(to)));
74
+ const target = resolve(to);
75
+ root.style.setProperty(FILL, fillOf(target));
76
+ root.style.setProperty(SHADOW, shadowOf(target) ?? fromShadow ?? 'none');
59
77
  };
60
78
  // The class lets motion.css keep both snapshots at their width, clipped by the container.
61
79
  const builder = animateView(updateAndFill, springTransition(spring))
@@ -71,7 +71,7 @@ export const listItem = tv({
71
71
  'cursor-pointer outline-none',
72
72
  'hover:[--li-shape:0.75rem]',
73
73
  'focus-visible:[--li-shape:1rem]',
74
- 'focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-md-sys-color-secondary',
74
+ 'focus-visible:outline-solid focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-md-sys-color-secondary',
75
75
  'active:[--li-shape:1rem]'
76
76
  ]
77
77
  },
@@ -218,7 +218,7 @@ export const pane = tv({
218
218
  */
219
219
  export const paneHandle = tv({
220
220
  slots: {
221
- base: 'group relative flex shrink-0 w-spacing-150 self-stretch cursor-col-resize touch-none items-center justify-center rounded-full bg-transparent outline-none hover:bg-md-sys-color-outline/20 focus-visible:outline-2 focus-visible:outline-md-sys-color-secondary',
221
+ base: 'group relative flex shrink-0 w-spacing-150 self-stretch cursor-col-resize touch-none items-center justify-center rounded-full bg-transparent outline-none hover:bg-md-sys-color-outline/20 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-md-sys-color-secondary',
222
222
  // Size is sprung inline by PaneHandle.svelte; only color transitions here.
223
223
  grip: 'rounded-full transition-colors md-sys-motion-effects'
224
224
  },
@@ -286,7 +286,7 @@ export const dateCalendar = tv({
286
286
  cursor-pointer outline-none
287
287
  transition-colors md-sys-motion-fast-effects
288
288
 
289
- focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-md-sys-color-secondary
289
+ focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-md-sys-color-secondary
290
290
 
291
291
  data-today:border data-today:border-md-sys-color-primary data-today:text-md-sys-color-primary
292
292
 
@@ -0,0 +1,90 @@
1
+ import { Meta, Title, Subtitle, Canvas, Controls, ArgTypes } from '@storybook/addon-docs/blocks';
2
+ import * as CommandStories from './Command.stories.svelte';
3
+ import CommandItem from './CommandItem.svelte';
4
+ import CommandDialog from './CommandDialog.svelte';
5
+
6
+ <Meta of={CommandStories} />
7
+
8
+ <Title />
9
+
10
+ <Subtitle>
11
+ A command palette: type to filter a known set of actions and destinations, then run one.
12
+ </Subtitle>
13
+
14
+ `import { Command, CommandDialog, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, CommandSeparator } from '@noxlovette/material';`
15
+
16
+ Not an M3 component. M3 has no command palette, so this one is assembled from M3 parts: the
17
+ items are M3 list items, and `CommandDialog` takes the docked search view's width (360–720dp), a
18
+ modal dialog's scrim, extra-large shape and enter/exit motion. Filtering, ranking and keyboard
19
+ navigation come from bits-ui's `Command`.
20
+
21
+ <Canvas of={CommandStories.Playground} />
22
+
23
+ <Controls of={CommandStories.Playground} />
24
+
25
+ ## Search or command palette?
26
+
27
+ They look alike, a field over a list, but they answer different questions. Use both in one app
28
+ if it has both kinds of need.
29
+
30
+ | | `Search` (bar + search view) | `CommandDialog` (palette) |
31
+ | --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
32
+ | Answers | "Where is the thing that matches this?" | "What do I want to do, or where do I want to go?" |
33
+ | Items | App content: open-ended, often fetched, rich rows | A finite, known set of actions and destinations |
34
+ | Filtering | Yours: you render the matches in `results` | The component's: bits-ui scores and sorts the items |
35
+ | Enter, nothing picked | Searches for the query as typed (`onsearch`) | Can't happen: the best match is always highlighted, Enter runs it |
36
+ | The query | Stays in the bar; it's part of the page's state | Thrown away on close |
37
+ | Where it lives | On the page or in the search `AppBar`, visible | Nowhere until summoned; one per app |
38
+ | Page-wide key | `/` | ⌘K (Ctrl+K off Apple platforms) |
39
+ | M3 | [Search](https://m3.material.io/components/search/overview), spec'd with motion | None; built from M3 parts |
40
+
41
+ Rules of thumb:
42
+
43
+ - **Content goes in search, commands go in the palette.** "Invoices from March" is a search.
44
+ "New invoice", "Go to settings", "Toggle dark theme" are commands.
45
+ - **Don't use the palette as the app's search.** Its first match is always highlighted, so it
46
+ can't search for exactly what was typed, and its filter only sees items already rendered.
47
+ - **Don't use search as a launcher.** A search view's suggestions are content, and a bar
48
+ on every page for actions is the wrong emphasis.
49
+ - **Both at once is fine.** A palette can end with a "Search for “…”" item that hands the query
50
+ to search.
51
+
52
+ ## Items
53
+
54
+ Each `CommandItem` is an M3 list item: its children are the headline, with an optional `leading`
55
+ icon, `supporting` second line, and `trailingText`. `shortcut` shows a keyboard shortcut at the end,
56
+ labelled for the platform (`⌘,` on a Mac, `Ctrl+,` elsewhere). It's only shown; bind the key
57
+ yourself.
58
+
59
+ ```svelte
60
+ <CommandItem value="settings" shortcut="Mod+," onSelect={openSettings}>
61
+ {#snippet leading()}<Icon name="settings" />{/snippet}
62
+ Settings
63
+ </CommandItem>
64
+ ```
65
+
66
+ The highlighted item, from the arrow keys or the pointer, takes a focused item's look: a 10%
67
+ on-surface fill and the 12dp shape. Focus itself stays in the input.
68
+
69
+ <ArgTypes of={CommandItem} />
70
+
71
+ ## Dialog
72
+
73
+ `CommandDialog` wraps `Command` in a modal overlay opened with ⌘K from anywhere, including from
74
+ inside a text field. ⌘K again, Esc or a click on the scrim closes it. Close it yourself from an
75
+ item's `onSelect` (`bind:open`). `shortcut` changes the keys; `null` turns them off.
76
+
77
+ <Canvas of={CommandStories.Dialog} />
78
+
79
+ <ArgTypes of={CommandDialog} />
80
+
81
+ ## Accessibility
82
+
83
+ - **Combobox.** The input is a `combobox` over the `listbox`, with `aria-activedescendant` on the
84
+ highlighted item, so screen readers follow the arrow keys while focus stays in the field.
85
+ - **Dialog.** `CommandDialog` is a modal dialog named by `label` (default "Command palette") and
86
+ keeps focus inside while open.
87
+ - **Shortcut.** ⌘K doesn't fire while another modal dialog is open, so it never opens on top of
88
+ a search view or a confirmation dialog.
89
+ - **Keys.** ↑/↓ move through the items, Home/End jump to the first and last, Enter runs the
90
+ highlighted one.
@@ -7,18 +7,27 @@
7
7
  import CommandGroup from './CommandGroup.svelte';
8
8
  import CommandItem from './CommandItem.svelte';
9
9
  import CommandSeparator from './CommandSeparator.svelte';
10
+ import CommandDialog from './CommandDialog.svelte';
11
+ import Kbd from '../../typography/kbd/Kbd.svelte';
12
+ import Button from '../../buttons/Button.svelte';
13
+ import { shortcutLabel } from '../../../utils/shortcut.js';
10
14
  import Icon from '../../../utils/icon/Icon.svelte';
11
- import { command } from './theme.js';
12
-
13
- const { itemIcon } = command();
14
15
 
15
16
  const { Story } = defineMeta({
16
17
  title: 'Forms/Command',
17
- tags: ['autodocs'],
18
18
  component: Command
19
19
  });
20
20
  </script>
21
21
 
22
+ <script lang="ts">
23
+ let paletteOpen = $state(false);
24
+ let ran = $state('');
25
+ const run = (name: string) => {
26
+ ran = name;
27
+ paletteOpen = false;
28
+ };
29
+ </script>
30
+
22
31
  <Story name="Playground" asChild>
23
32
  <div class="w-96">
24
33
  <Command>
@@ -27,30 +36,30 @@
27
36
  <CommandEmpty>No results found.</CommandEmpty>
28
37
  <CommandGroup heading="Suggestions">
29
38
  <CommandItem value="calendar">
30
- <Icon name="calendar_today" class={itemIcon()} />
39
+ {#snippet leading()}<Icon name="calendar_today" />{/snippet}
31
40
  Calendar
32
41
  </CommandItem>
33
42
  <CommandItem value="search-emoji">
34
- <Icon name="mood" class={itemIcon()} />
43
+ {#snippet leading()}<Icon name="mood" />{/snippet}
35
44
  Search Emoji
36
45
  </CommandItem>
37
46
  <CommandItem value="calculator">
38
- <Icon name="calculate" class={itemIcon()} />
47
+ {#snippet leading()}<Icon name="calculate" />{/snippet}
39
48
  Calculator
40
49
  </CommandItem>
41
50
  </CommandGroup>
42
51
  <CommandSeparator />
43
52
  <CommandGroup heading="Settings">
44
53
  <CommandItem value="profile">
45
- <Icon name="person" class={itemIcon()} />
54
+ {#snippet leading()}<Icon name="person" />{/snippet}
46
55
  Profile
47
56
  </CommandItem>
48
57
  <CommandItem value="billing">
49
- <Icon name="credit_card" class={itemIcon()} />
58
+ {#snippet leading()}<Icon name="credit_card" />{/snippet}
50
59
  Billing
51
60
  </CommandItem>
52
61
  <CommandItem value="settings">
53
- <Icon name="settings" class={itemIcon()} />
62
+ {#snippet leading()}<Icon name="settings" />{/snippet}
54
63
  Settings
55
64
  </CommandItem>
56
65
  </CommandGroup>
@@ -59,6 +68,56 @@
59
68
  </div>
60
69
  </Story>
61
70
 
71
+ <!--
72
+ Press ⌘K (Ctrl+K off Apple platforms) anywhere in the story, or the button. Arrow keys move the
73
+ highlight, Enter runs the command, Esc or ⌘K again closes it.
74
+ -->
75
+ <Story
76
+ name="Dialog"
77
+ asChild
78
+ parameters={{ layout: 'fullscreen', docs: { story: { inline: false, height: '520px' } } }}
79
+ >
80
+ <div
81
+ class="gap-spacing-200 p-spacing-300 md-sys-typescale-body-medium text-md-sys-color-on-surface flex min-h-dvh flex-col items-start"
82
+ >
83
+ <Button variant="tonal" onclick={() => (paletteOpen = true)}>
84
+ Open the palette <Kbd position="relative">{shortcutLabel('Mod+K')}</Kbd>
85
+ </Button>
86
+ <p class="text-md-sys-color-on-surface-variant">{ran ? `Ran: ${ran}` : 'Nothing run yet'}</p>
87
+ <CommandDialog bind:open={paletteOpen}>
88
+ <CommandInput placeholder="Type a command or search..." />
89
+ <CommandList>
90
+ <CommandEmpty>No results found.</CommandEmpty>
91
+ <CommandGroup heading="Suggestions">
92
+ <CommandItem value="calendar" onSelect={() => run('Calendar')}>
93
+ {#snippet leading()}<Icon name="calendar_today" />{/snippet}
94
+ Calendar
95
+ </CommandItem>
96
+ <CommandItem value="calculator" onSelect={() => run('Calculator')}>
97
+ {#snippet leading()}<Icon name="calculate" />{/snippet}
98
+ Calculator
99
+ </CommandItem>
100
+ </CommandGroup>
101
+ <CommandSeparator />
102
+ <CommandGroup heading="Settings">
103
+ <CommandItem
104
+ value="profile"
105
+ supporting="Name, photo and email"
106
+ onSelect={() => run('Profile')}
107
+ >
108
+ {#snippet leading()}<Icon name="person" />{/snippet}
109
+ Profile
110
+ </CommandItem>
111
+ <CommandItem value="settings" shortcut="Mod+," onSelect={() => run('Settings')}>
112
+ {#snippet leading()}<Icon name="settings" />{/snippet}
113
+ Settings
114
+ </CommandItem>
115
+ </CommandGroup>
116
+ </CommandList>
117
+ </CommandDialog>
118
+ </div>
119
+ </Story>
120
+
62
121
  <Story name="Empty State" asChild>
63
122
  <div class="w-96">
64
123
  <Command>
@@ -1,19 +1,4 @@
1
1
  import Command from './Command.svelte';
2
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
- $$bindings?: Bindings;
5
- } & Exports;
6
- (internal: unknown, props: {
7
- $$events?: Events;
8
- $$slots?: Slots;
9
- }): Exports & {
10
- $set?: any;
11
- $on?: any;
12
- };
13
- z_$$bindings?: Bindings;
14
- }
15
- declare const Command: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
- [evt: string]: CustomEvent<any>;
17
- }, {}, {}, string>;
18
- type Command = InstanceType<typeof Command>;
2
+ declare const Command: import("svelte").Component<Record<string, never>, {}, "">;
3
+ type Command = ReturnType<typeof Command>;
19
4
  export default Command;
@@ -0,0 +1,58 @@
1
+ <!--
2
+ @component
3
+ A command palette: a `Command` in a modal overlay, opened from anywhere with ⌘K (Ctrl+K off Apple
4
+ platforms). The same keys close it again, as do Esc and a click on the scrim.
5
+
6
+ Put `CommandInput`, `CommandList` and the rest inside it, as with `Command`, and close it from an
7
+ item's `onSelect` (`bind:open`).
8
+ -->
9
+ <script lang="ts">
10
+ import { Dialog } from 'bits-ui';
11
+ import clsx from 'clsx';
12
+ import { enterExit, presence } from '../../../animation/index.js';
13
+ import { triggersShortcut } from '../../../utils/index.js';
14
+ import Command from './Command.svelte';
15
+ import { commandDialog } from './theme.js';
16
+ import type { CommandDialogProps } from './types.js';
17
+
18
+ let {
19
+ open = $bindable(false),
20
+ value = $bindable(''),
21
+ shortcut = 'Mod+K',
22
+ label = 'Command palette',
23
+ children,
24
+ class: className,
25
+ ...restProps
26
+ }: CommandDialogProps = $props();
27
+
28
+ const s = commandDialog();
29
+ let content = $state<HTMLElement | null>(null);
30
+
31
+ function onShortcut(e: KeyboardEvent) {
32
+ if (!triggersShortcut(e, shortcut, open ? content : null)) return;
33
+ e.preventDefault();
34
+ open = !open;
35
+ }
36
+ </script>
37
+
38
+ <svelte:window onkeydown={onShortcut} />
39
+
40
+ <Dialog.Root bind:open>
41
+ <Dialog.Portal>
42
+ <Dialog.Overlay>
43
+ {#snippet child({ props, open: isOpen })}
44
+ <div {...props} class={s.scrim()} {@attach presence(() => isOpen, enterExit.fade)}></div>
45
+ {/snippet}
46
+ </Dialog.Overlay>
47
+ <Dialog.Content bind:ref={content}>
48
+ {#snippet child({ props, open: isOpen })}
49
+ <div {...props} class={s.content()} {@attach presence(() => isOpen, enterExit.dialog)}>
50
+ <Dialog.Title class="sr-only">{label}</Dialog.Title>
51
+ <Command {...restProps} bind:value class={s.command({ class: clsx(className) })}>
52
+ {@render children()}
53
+ </Command>
54
+ </div>
55
+ {/snippet}
56
+ </Dialog.Content>
57
+ </Dialog.Portal>
58
+ </Dialog.Root>
@@ -0,0 +1,11 @@
1
+ import type { CommandDialogProps } from './types.js';
2
+ /**
3
+ * A command palette: a `Command` in a modal overlay, opened from anywhere with ⌘K (Ctrl+K off Apple
4
+ * platforms). The same keys close it again, as do Esc and a click on the scrim.
5
+ *
6
+ * Put `CommandInput`, `CommandList` and the rest inside it, as with `Command`, and close it from an
7
+ * item's `onSelect` (`bind:open`).
8
+ */
9
+ declare const CommandDialog: import("svelte").Component<CommandDialogProps, {}, "value" | "open">;
10
+ type CommandDialog = ReturnType<typeof CommandDialog>;
11
+ export default CommandDialog;
@@ -1,14 +1,57 @@
1
+ <!--
2
+ @component
3
+ A command in a `Command` or `CommandDialog`: an M3 list item. The children are its headline; add a
4
+ `leading` icon, `supporting` text, and a `shortcut` (shown, not bound) or other `trailingText`.
5
+ -->
1
6
  <script lang="ts">
2
7
  import { Command } from 'bits-ui';
3
8
  import clsx from 'clsx';
4
- import { command } from './theme.js';
9
+ import { Layer, isApplePlatform, shortcutLabel } from '../../../utils/index.js';
10
+ import { commandItem } from './theme.js';
5
11
  import type { CommandItemProps } from './types.js';
6
12
 
7
- let { children, class: className, value, ...restProps }: CommandItemProps = $props();
13
+ let {
14
+ children,
15
+ leading,
16
+ supporting,
17
+ trailingText,
18
+ shortcut,
19
+ disabled = false,
20
+ class: className,
21
+ value,
22
+ ...restProps
23
+ }: CommandItemProps = $props();
8
24
 
9
- const { item } = command();
25
+ // `Mod` resolves on the client, so SSR and the first render agree.
26
+ let apple = $state(false);
27
+ $effect(() => {
28
+ apple = isApplePlatform();
29
+ });
30
+
31
+ const trailing = $derived(shortcut ? shortcutLabel(shortcut, apple) : trailingText);
32
+ const cls = $derived(commandItem({ lines: supporting ? 2 : 1, disabled }));
10
33
  </script>
11
34
 
12
- <Command.Item {...restProps} {value} class={item({ class: clsx(className) })}>
13
- {@render children()}
35
+ <Command.Item {...restProps} {value} {disabled}>
36
+ {#snippet child({ props })}
37
+ <div {...props} class={cls.base({ class: clsx(className) })}>
38
+ {#if !disabled}
39
+ <Layer />
40
+ {/if}
41
+ {#if leading}
42
+ <div class={cls.leading()} aria-hidden="true">{@render leading()}</div>
43
+ {/if}
44
+ <div class={cls.body()}>
45
+ <span class={cls.headline()}>{@render children()}</span>
46
+ {#if supporting}
47
+ <span class={cls.supporting()}>{supporting}</span>
48
+ {/if}
49
+ </div>
50
+ {#if trailing}
51
+ <div class={cls.trailing()}>
52
+ <span class={cls.trailingText()}>{trailing}</span>
53
+ </div>
54
+ {/if}
55
+ </div>
56
+ {/snippet}
14
57
  </Command.Item>
@@ -1,4 +1,8 @@
1
1
  import type { CommandItemProps } from './types.js';
2
+ /**
3
+ * A command in a `Command` or `CommandDialog`: an M3 list item. The children are its headline; add a
4
+ * `leading` icon, `supporting` text, and a `shortcut` (shown, not bound) or other `trailingText`.
5
+ */
2
6
  declare const CommandItem: import("svelte").Component<CommandItemProps, {}, "">;
3
7
  type CommandItem = ReturnType<typeof CommandItem>;
4
8
  export default CommandItem;
@@ -1,6 +1,7 @@
1
1
  export * from './theme.js';
2
2
  export * from './types.js';
3
3
  export { default as Command } from './Command.svelte';
4
+ export { default as CommandDialog } from './CommandDialog.svelte';
4
5
  export { default as CommandEmpty } from './CommandEmpty.svelte';
5
6
  export { default as CommandGroup } from './CommandGroup.svelte';
6
7
  export { default as CommandInput } from './CommandInput.svelte';
@@ -4,6 +4,7 @@ export * from './theme.js';
4
4
  export * from './types.js';
5
5
  // default exports for components in this folder
6
6
  export { default as Command } from './Command.svelte';
7
+ export { default as CommandDialog } from './CommandDialog.svelte';
7
8
  export { default as CommandEmpty } from './CommandEmpty.svelte';
8
9
  export { default as CommandGroup } from './CommandGroup.svelte';
9
10
  export { default as CommandInput } from './CommandInput.svelte';