@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.
- package/claude-skill/material-design/SKILL.md +1 -1
- package/claude-skill/material-design/references/component-patterns.md +36 -1
- package/dist/animation/containerTransform.js +21 -3
- package/dist/components/containers/list/theme.js +1 -1
- package/dist/components/containers/pane/theme.js +1 -1
- package/dist/components/date/theme.js +1 -1
- package/dist/components/forms/command/Command.mdx +90 -0
- package/dist/components/forms/command/Command.stories.svelte +69 -10
- package/dist/components/forms/command/Command.stories.svelte.d.ts +2 -17
- package/dist/components/forms/command/CommandDialog.svelte +58 -0
- package/dist/components/forms/command/CommandDialog.svelte.d.ts +11 -0
- package/dist/components/forms/command/CommandItem.svelte +48 -5
- package/dist/components/forms/command/CommandItem.svelte.d.ts +4 -0
- package/dist/components/forms/command/index.d.ts +1 -0
- package/dist/components/forms/command/index.js +1 -0
- package/dist/components/forms/command/theme.d.ts +371 -6
- package/dist/components/forms/command/theme.js +32 -1
- package/dist/components/forms/command/types.d.ts +27 -1
- package/dist/components/forms/search/Search.mdx +9 -0
- package/dist/components/forms/search/Search.stories.svelte +14 -2
- package/dist/components/forms/search/Search.svelte +41 -9
- package/dist/components/forms/search/Search.svelte.d.ts +3 -0
- package/dist/components/forms/search/SearchView.svelte +7 -2
- package/dist/components/forms/search/SearchView.svelte.d.ts +2 -1
- package/dist/components/forms/search/theme.js +4 -2
- package/dist/components/forms/search/types.d.ts +14 -1
- package/dist/components/forms/slider/theme.js +1 -1
- package/dist/components/nav/appbar/AppBar.stories.svelte +30 -2
- package/dist/components/nav/appbar/AppBar.svelte +40 -8
- package/dist/components/nav/appbar/types.d.ts +14 -0
- package/dist/components/nav/rail/Rail.mdx +6 -0
- package/dist/components/nav/rail/Rail.svelte +55 -1
- package/dist/components/nav/rail/Rail.svelte.d.ts +3 -0
- package/dist/components/nav/rail/types.d.ts +7 -0
- package/dist/styles/motion.css +6 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/shortcut.d.ts +26 -0
- package/dist/utils/shortcut.js +84 -0
- 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
|
|
54
|
-
//
|
|
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
|
-
|
|
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"
|
|
39
|
+
{#snippet leading()}<Icon name="calendar_today" />{/snippet}
|
|
31
40
|
Calendar
|
|
32
41
|
</CommandItem>
|
|
33
42
|
<CommandItem value="search-emoji">
|
|
34
|
-
<Icon name="mood"
|
|
43
|
+
{#snippet leading()}<Icon name="mood" />{/snippet}
|
|
35
44
|
Search Emoji
|
|
36
45
|
</CommandItem>
|
|
37
46
|
<CommandItem value="calculator">
|
|
38
|
-
<Icon name="calculate"
|
|
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"
|
|
54
|
+
{#snippet leading()}<Icon name="person" />{/snippet}
|
|
46
55
|
Profile
|
|
47
56
|
</CommandItem>
|
|
48
57
|
<CommandItem value="billing">
|
|
49
|
-
<Icon name="credit_card"
|
|
58
|
+
{#snippet leading()}<Icon name="credit_card" />{/snippet}
|
|
50
59
|
Billing
|
|
51
60
|
</CommandItem>
|
|
52
61
|
<CommandItem value="settings">
|
|
53
|
-
<Icon name="settings"
|
|
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
|
-
|
|
3
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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}
|
|
13
|
-
{
|
|
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';
|