@noxlovette/material 0.8.3 → 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 (38) 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/components/containers/list/theme.js +1 -1
  4. package/dist/components/containers/pane/theme.js +1 -1
  5. package/dist/components/date/theme.js +1 -1
  6. package/dist/components/forms/command/Command.mdx +90 -0
  7. package/dist/components/forms/command/Command.stories.svelte +69 -10
  8. package/dist/components/forms/command/Command.stories.svelte.d.ts +2 -17
  9. package/dist/components/forms/command/CommandDialog.svelte +58 -0
  10. package/dist/components/forms/command/CommandDialog.svelte.d.ts +11 -0
  11. package/dist/components/forms/command/CommandItem.svelte +48 -5
  12. package/dist/components/forms/command/CommandItem.svelte.d.ts +4 -0
  13. package/dist/components/forms/command/index.d.ts +1 -0
  14. package/dist/components/forms/command/index.js +1 -0
  15. package/dist/components/forms/command/theme.d.ts +371 -6
  16. package/dist/components/forms/command/theme.js +32 -1
  17. package/dist/components/forms/command/types.d.ts +27 -1
  18. package/dist/components/forms/search/Search.mdx +9 -0
  19. package/dist/components/forms/search/Search.stories.svelte +14 -2
  20. package/dist/components/forms/search/Search.svelte +41 -9
  21. package/dist/components/forms/search/Search.svelte.d.ts +3 -0
  22. package/dist/components/forms/search/SearchView.svelte +7 -2
  23. package/dist/components/forms/search/SearchView.svelte.d.ts +2 -1
  24. package/dist/components/forms/search/theme.js +4 -2
  25. package/dist/components/forms/search/types.d.ts +14 -1
  26. package/dist/components/forms/slider/theme.js +1 -1
  27. package/dist/components/nav/appbar/AppBar.stories.svelte +30 -2
  28. package/dist/components/nav/appbar/AppBar.svelte +40 -8
  29. package/dist/components/nav/appbar/types.d.ts +14 -0
  30. package/dist/components/nav/rail/Rail.mdx +6 -0
  31. package/dist/components/nav/rail/Rail.svelte +55 -1
  32. package/dist/components/nav/rail/Rail.svelte.d.ts +3 -0
  33. package/dist/components/nav/rail/types.d.ts +7 -0
  34. package/dist/utils/index.d.ts +1 -0
  35. package/dist/utils/index.js +1 -0
  36. package/dist/utils/shortcut.d.ts +26 -0
  37. package/dist/utils/shortcut.js +84 -0
  38. package/package.json +1 -1
@@ -3,7 +3,6 @@ export type CommandVariants = VariantProps<typeof command>;
3
3
  export declare const command: import("tailwind-variants").TVReturnType<{
4
4
  [key: string]: {
5
5
  [key: string]: import("tailwind-variants").ClassValue | {
6
- item?: import("tailwind-variants").ClassValue;
7
6
  base?: import("tailwind-variants").ClassValue;
8
7
  input?: import("tailwind-variants").ClassValue;
9
8
  group?: import("tailwind-variants").ClassValue;
@@ -18,7 +17,6 @@ export declare const command: import("tailwind-variants").TVReturnType<{
18
17
  } | {
19
18
  [x: string]: {
20
19
  [x: string]: import("tailwind-variants").ClassValue | {
21
- item?: import("tailwind-variants").ClassValue;
22
20
  base?: import("tailwind-variants").ClassValue;
23
21
  input?: import("tailwind-variants").ClassValue;
24
22
  group?: import("tailwind-variants").ClassValue;
@@ -39,12 +37,10 @@ export declare const command: import("tailwind-variants").TVReturnType<{
39
37
  empty: string;
40
38
  group: string;
41
39
  separator: string;
42
- item: string;
43
40
  itemIcon: string;
44
41
  }, undefined, {
45
42
  [key: string]: {
46
43
  [key: string]: import("tailwind-variants").ClassValue | {
47
- item?: import("tailwind-variants").ClassValue;
48
44
  base?: import("tailwind-variants").ClassValue;
49
45
  input?: import("tailwind-variants").ClassValue;
50
46
  group?: import("tailwind-variants").ClassValue;
@@ -65,7 +61,6 @@ export declare const command: import("tailwind-variants").TVReturnType<{
65
61
  empty: string;
66
62
  group: string;
67
63
  separator: string;
68
- item: string;
69
64
  itemIcon: string;
70
65
  }, import("tailwind-variants").TVReturnTypeLike<unknown, {
71
66
  base: string;
@@ -76,6 +71,376 @@ export declare const command: import("tailwind-variants").TVReturnType<{
76
71
  empty: string;
77
72
  group: string;
78
73
  separator: string;
79
- item: string;
80
74
  itemIcon: string;
81
75
  }>>;
76
+ export declare const commandItem: import("tailwind-variants").TVReturnType<{
77
+ variant: {
78
+ standard: {
79
+ base: string;
80
+ };
81
+ };
82
+ }, {
83
+ base: string[];
84
+ }, undefined, {
85
+ lines: {
86
+ 1: {
87
+ base: string;
88
+ };
89
+ 2: {
90
+ base: string;
91
+ };
92
+ 3: {
93
+ base: string;
94
+ leading: string;
95
+ trailing: string;
96
+ };
97
+ };
98
+ variant: {
99
+ standard: {
100
+ base: string;
101
+ nested: string;
102
+ };
103
+ segmented: {
104
+ base: string;
105
+ nested: string;
106
+ };
107
+ };
108
+ interactive: {
109
+ true: {
110
+ base: string[];
111
+ };
112
+ false: "";
113
+ };
114
+ selected: {
115
+ true: {
116
+ base: string;
117
+ leading: string;
118
+ overline: string;
119
+ headline: string;
120
+ supporting: string;
121
+ trailing: string;
122
+ trailingText: string;
123
+ };
124
+ false: "";
125
+ };
126
+ disabled: {
127
+ true: {
128
+ base: string;
129
+ leading: string;
130
+ overline: string;
131
+ headline: string;
132
+ supporting: string;
133
+ trailing: string;
134
+ trailingText: string;
135
+ };
136
+ false: "";
137
+ };
138
+ expanded: {
139
+ true: {
140
+ base: string;
141
+ expandIcon: string;
142
+ region: string;
143
+ nested: string;
144
+ };
145
+ false: {
146
+ expandIcon: string;
147
+ region: string;
148
+ };
149
+ };
150
+ }, {
151
+ base: string[];
152
+ selection: string;
153
+ leading: string;
154
+ body: string;
155
+ overline: string;
156
+ headline: string;
157
+ supporting: string;
158
+ trailing: string;
159
+ trailingText: string;
160
+ expandIcon: string;
161
+ group: string;
162
+ region: string;
163
+ regionInner: string;
164
+ nested: string;
165
+ }, import("tailwind-variants").TVReturnType<{
166
+ lines: {
167
+ 1: {
168
+ base: string;
169
+ };
170
+ 2: {
171
+ base: string;
172
+ };
173
+ 3: {
174
+ base: string;
175
+ leading: string;
176
+ trailing: string;
177
+ };
178
+ };
179
+ variant: {
180
+ standard: {
181
+ base: string;
182
+ nested: string;
183
+ };
184
+ segmented: {
185
+ base: string;
186
+ nested: string;
187
+ };
188
+ };
189
+ interactive: {
190
+ true: {
191
+ base: string[];
192
+ };
193
+ false: "";
194
+ };
195
+ selected: {
196
+ true: {
197
+ base: string;
198
+ leading: string;
199
+ overline: string;
200
+ headline: string;
201
+ supporting: string;
202
+ trailing: string;
203
+ trailingText: string;
204
+ };
205
+ false: "";
206
+ };
207
+ disabled: {
208
+ true: {
209
+ base: string;
210
+ leading: string;
211
+ overline: string;
212
+ headline: string;
213
+ supporting: string;
214
+ trailing: string;
215
+ trailingText: string;
216
+ };
217
+ false: "";
218
+ };
219
+ expanded: {
220
+ true: {
221
+ base: string;
222
+ expandIcon: string;
223
+ region: string;
224
+ nested: string;
225
+ };
226
+ false: {
227
+ expandIcon: string;
228
+ region: string;
229
+ };
230
+ };
231
+ }, {
232
+ base: string[];
233
+ selection: string;
234
+ leading: string;
235
+ body: string;
236
+ overline: string;
237
+ headline: string;
238
+ supporting: string;
239
+ trailing: string;
240
+ trailingText: string;
241
+ expandIcon: string;
242
+ group: string;
243
+ region: string;
244
+ regionInner: string;
245
+ nested: string;
246
+ }, undefined, {
247
+ lines: {
248
+ 1: {
249
+ base: string;
250
+ };
251
+ 2: {
252
+ base: string;
253
+ };
254
+ 3: {
255
+ base: string;
256
+ leading: string;
257
+ trailing: string;
258
+ };
259
+ };
260
+ variant: {
261
+ standard: {
262
+ base: string;
263
+ nested: string;
264
+ };
265
+ segmented: {
266
+ base: string;
267
+ nested: string;
268
+ };
269
+ };
270
+ interactive: {
271
+ true: {
272
+ base: string[];
273
+ };
274
+ false: "";
275
+ };
276
+ selected: {
277
+ true: {
278
+ base: string;
279
+ leading: string;
280
+ overline: string;
281
+ headline: string;
282
+ supporting: string;
283
+ trailing: string;
284
+ trailingText: string;
285
+ };
286
+ false: "";
287
+ };
288
+ disabled: {
289
+ true: {
290
+ base: string;
291
+ leading: string;
292
+ overline: string;
293
+ headline: string;
294
+ supporting: string;
295
+ trailing: string;
296
+ trailingText: string;
297
+ };
298
+ false: "";
299
+ };
300
+ expanded: {
301
+ true: {
302
+ base: string;
303
+ expandIcon: string;
304
+ region: string;
305
+ nested: string;
306
+ };
307
+ false: {
308
+ expandIcon: string;
309
+ region: string;
310
+ };
311
+ };
312
+ }, {
313
+ base: string[];
314
+ selection: string;
315
+ leading: string;
316
+ body: string;
317
+ overline: string;
318
+ headline: string;
319
+ supporting: string;
320
+ trailing: string;
321
+ trailingText: string;
322
+ expandIcon: string;
323
+ group: string;
324
+ region: string;
325
+ regionInner: string;
326
+ nested: string;
327
+ }, import("tailwind-variants").TVReturnTypeLike<{
328
+ lines: {
329
+ 1: {
330
+ base: string;
331
+ };
332
+ 2: {
333
+ base: string;
334
+ };
335
+ 3: {
336
+ base: string;
337
+ leading: string;
338
+ trailing: string;
339
+ };
340
+ };
341
+ variant: {
342
+ standard: {
343
+ base: string;
344
+ nested: string;
345
+ };
346
+ segmented: {
347
+ base: string;
348
+ nested: string;
349
+ };
350
+ };
351
+ interactive: {
352
+ true: {
353
+ base: string[];
354
+ };
355
+ false: "";
356
+ };
357
+ selected: {
358
+ true: {
359
+ base: string;
360
+ leading: string;
361
+ overline: string;
362
+ headline: string;
363
+ supporting: string;
364
+ trailing: string;
365
+ trailingText: string;
366
+ };
367
+ false: "";
368
+ };
369
+ disabled: {
370
+ true: {
371
+ base: string;
372
+ leading: string;
373
+ overline: string;
374
+ headline: string;
375
+ supporting: string;
376
+ trailing: string;
377
+ trailingText: string;
378
+ };
379
+ false: "";
380
+ };
381
+ expanded: {
382
+ true: {
383
+ base: string;
384
+ expandIcon: string;
385
+ region: string;
386
+ nested: string;
387
+ };
388
+ false: {
389
+ expandIcon: string;
390
+ region: string;
391
+ };
392
+ };
393
+ }, {
394
+ base: string[];
395
+ selection: string;
396
+ leading: string;
397
+ body: string;
398
+ overline: string;
399
+ headline: string;
400
+ supporting: string;
401
+ trailing: string;
402
+ trailingText: string;
403
+ expandIcon: string;
404
+ group: string;
405
+ region: string;
406
+ regionInner: string;
407
+ nested: string;
408
+ }>>>;
409
+ export type CommandItemVariants = VariantProps<typeof commandItem>;
410
+ export declare const commandDialog: import("tailwind-variants").TVReturnType<{
411
+ [key: string]: {
412
+ [key: string]: import("tailwind-variants").ClassValue | {
413
+ content?: import("tailwind-variants").ClassValue;
414
+ command?: import("tailwind-variants").ClassValue;
415
+ scrim?: import("tailwind-variants").ClassValue;
416
+ };
417
+ };
418
+ } | {
419
+ [x: string]: {
420
+ [x: string]: import("tailwind-variants").ClassValue | {
421
+ content?: import("tailwind-variants").ClassValue;
422
+ command?: import("tailwind-variants").ClassValue;
423
+ scrim?: import("tailwind-variants").ClassValue;
424
+ };
425
+ };
426
+ } | {}, {
427
+ scrim: string;
428
+ content: string;
429
+ command: string;
430
+ }, undefined, {
431
+ [key: string]: {
432
+ [key: string]: import("tailwind-variants").ClassValue | {
433
+ content?: import("tailwind-variants").ClassValue;
434
+ command?: import("tailwind-variants").ClassValue;
435
+ scrim?: import("tailwind-variants").ClassValue;
436
+ };
437
+ };
438
+ } | {}, {
439
+ scrim: string;
440
+ content: string;
441
+ command: string;
442
+ }, import("tailwind-variants").TVReturnTypeLike<unknown, {
443
+ scrim: string;
444
+ content: string;
445
+ command: string;
446
+ }>>;
@@ -1,4 +1,5 @@
1
1
  import { tv } from '../../../utils/tv.js';
2
+ import { listItem } from '../../containers/list/theme.js';
2
3
  export const command = tv({
3
4
  slots: {
4
5
  base: 'flex h-full w-full flex-col overflow-hidden rounded-2xl bg-md-sys-color-surface-container-high text-md-sys-color-on-surface shadow-elevation-3 ring-1 ring-md-sys-color-outline/20',
@@ -9,7 +10,37 @@ export const command = tv({
9
10
  empty: 'py-spacing-300 text-center md-sys-typescale-body-medium text-md-sys-color-on-surface-variant',
10
11
  group: 'overflow-hidden p-spacing-50 text-md-sys-color-on-surface-variant [&_[data-command-group-heading]]:px-spacing-150 [&_[data-command-group-heading]]:py-spacing-100 [&_[data-command-group-heading]]:md-sys-typescale-label-medium [&_[data-command-group-heading]]:text-md-sys-color-on-surface-variant',
11
12
  separator: '-mx-spacing-50 h-px bg-md-sys-color-outline-variant',
12
- item: 'relative flex cursor-default select-none items-center rounded-lg px-spacing-150 py-spacing-150 md-sys-typescale-body-medium outline-none data-[selected=true]:bg-md-sys-color-secondary-container data-[selected=true]:text-md-sys-color-on-secondary-container data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 transition-colors md-sys-motion-fast-effects',
13
13
  itemIcon: 'mr-spacing-150 h-spacing-250 w-spacing-250 shrink-0'
14
14
  }
15
15
  });
16
+ /*
17
+ Palette items are M3 list items (ListItem's anatomy, shape and type), sitting on the palette's
18
+ container colour. Focus stays in the input, so the item the arrow keys or pointer are on
19
+ (bits-ui's `data-selected`, an empty attribute) takes the focused item's state: a 10% on-surface
20
+ fill and the 12dp hover shape, the way menus show their highlighted item. A fill, not Layer's
21
+ tint: Layer's styles are unlayered component CSS, which beats any Tailwind utility.
22
+ */
23
+ export const commandItem = tv({
24
+ extend: listItem,
25
+ slots: {
26
+ base: [
27
+ 'cursor-default select-none outline-none',
28
+ 'data-selected:[--li-shape:0.75rem] data-selected:bg-md-sys-color-on-surface/10'
29
+ ]
30
+ },
31
+ // The list's own surface fill is a variant, so it's replaced here, not in the base.
32
+ variants: { variant: { standard: { base: 'bg-transparent' } } },
33
+ defaultVariants: { interactive: true, lines: 1 }
34
+ });
35
+ /*
36
+ The ⌘K palette. M3 has no command palette; it borrows the docked search view's width (360–720dp)
37
+ and a modal dialog's scrim, extra-large shape and motion, and sits in the top third of the
38
+ window so the list has room to grow downwards.
39
+ */
40
+ export const commandDialog = tv({
41
+ slots: {
42
+ scrim: 'fixed inset-spacing-0 z-layer-modal bg-md-sys-color-scrim/32',
43
+ content: 'fixed inset-x-spacing-200 top-spacing-200 md:top-spacing-900 z-layer-modal mx-auto max-w-(--md-comp-search-view-contained-docked-container-width-maximum) outline-none',
44
+ command: 'h-auto rounded-xl'
45
+ }
46
+ });
@@ -1,3 +1,4 @@
1
+ import type { Shortcut } from '../../../utils/index.js';
1
2
  import type { Command } from 'bits-ui';
2
3
  import type { Snippet } from 'svelte';
3
4
  import type { CommandVariants } from './theme.js';
@@ -13,9 +14,21 @@ export interface CommandListProps extends Command.ListProps {
13
14
  class?: string;
14
15
  children: Snippet;
15
16
  }
16
- export interface CommandItemProps extends Command.ItemProps {
17
+ export interface CommandItemProps extends Omit<Command.ItemProps, 'child' | 'children'> {
17
18
  class?: string;
19
+ /** The headline. */
18
20
  children: Snippet;
21
+ /** A leading icon, e.g. `<Icon name="settings" />`. */
22
+ leading?: Snippet;
23
+ /** A second line under the headline. */
24
+ supporting?: string;
25
+ /** Trailing label, e.g. a count. `shortcut` replaces it. */
26
+ trailingText?: string;
27
+ /**
28
+ * A keyboard shortcut to show at the end, written like `'Mod+,'` and labelled for the platform
29
+ * (`⌘,` on a Mac, `Ctrl+,` elsewhere). Only shown: bind the key yourself.
30
+ */
31
+ shortcut?: Shortcut;
19
32
  }
20
33
  export interface CommandGroupProps extends Command.GroupProps {
21
34
  class?: string;
@@ -29,3 +42,16 @@ export interface CommandEmptyProps extends Command.EmptyProps {
29
42
  export interface CommandSeparatorProps extends Command.SeparatorProps {
30
43
  class?: string;
31
44
  }
45
+ export interface CommandDialogProps extends CommandProps {
46
+ /** Whether the palette is open. */
47
+ open?: boolean;
48
+ /**
49
+ * Page-wide keys that open and close the palette. `Mod` is ⌘ on Apple platforms, Ctrl
50
+ * elsewhere. Fires inside text fields too, but not while another modal dialog is open. `null`
51
+ * turns it off.
52
+ * @default 'Mod+K'
53
+ */
54
+ shortcut?: Shortcut | null;
55
+ /** Accessible name of the dialog. */
56
+ label?: string;
57
+ }
@@ -104,3 +104,12 @@ the view on click or typing, not on focus: closing hands focus back to the bar.
104
104
  through the `role="option"` items (wrapping, skipping disabled ones) via
105
105
  `aria-activedescendant`, so focus stays in the field while typing; Enter picks the highlighted
106
106
  one. The highlighted item gets the M3 focus indicator.
107
+ - **Submitting.** Enter with no suggestion highlighted, or Enter in the bar itself, calls
108
+ `onsearch(query)` with the query as typed (never for an empty one). The view stays open; close
109
+ it yourself (`open = false`) when you navigate to results. This is where search differs from the
110
+ command palette (`CommandDialog`): a palette always highlights its best match and runs it, a
111
+ search can always search for exactly what was typed.
112
+ - **Shortcut.** `/` anywhere on the page, outside a text field, focuses the bar (or opens its view
113
+ when it has `results`), and the input advertises it with `aria-keyshortcuts`. `shortcut` changes
114
+ the key; `null` turns it off. With several bars on a page the first mounted takes it. It doesn't
115
+ fire while another modal dialog is open. ⌘K belongs to the command palette (`CommandDialog`).
@@ -47,6 +47,11 @@
47
47
  query ? topics.filter((t) => t.toLowerCase().includes(query.toLowerCase())) : recent
48
48
  );
49
49
 
50
+ const submit = (q: string) => {
51
+ picked = `search for “${q}”`;
52
+ open = false;
53
+ };
54
+
50
55
  const pick = (item: string) => {
51
56
  picked = item;
52
57
  query = item;
@@ -101,7 +106,8 @@
101
106
 
102
107
  <!--
103
108
  Click or type in the bar. The view is full-screen below the medium window class and docked from
104
- it up: resize the viewport to see both. Arrow keys move through the suggestions; Enter picks one.
109
+ it up: resize the viewport to see both. Arrow keys move through the suggestions; Enter picks one,
110
+ or with nothing highlighted searches for what was typed (onsearch). / from anywhere opens it.
105
111
  -->
106
112
  <Story
107
113
  name="Search view"
@@ -112,7 +118,13 @@
112
118
  }}
113
119
  >
114
120
  <div class="gap-spacing-200 p-spacing-300 flex min-h-dvh flex-col items-center">
115
- <Search placeholder="Search components" bind:value={query} bind:open results={suggestions}>
121
+ <Search
122
+ placeholder="Search components"
123
+ bind:value={query}
124
+ bind:open
125
+ results={suggestions}
126
+ onsearch={submit}
127
+ >
116
128
  {#snippet trailing()}
117
129
  <ButtonIcon variant="standard" iconProps={{ name: 'mic' }} aria-label="Voice search" />
118
130
  {/snippet}
@@ -7,13 +7,16 @@ Search bars allow users to enter a query to find specific information within an
7
7
  Give it `results` and the bar opens a search view (`SearchView`) when clicked or typed in:
8
8
  full-screen on compact windows, docked from medium up, with a container transform between them.
9
9
 
10
+ `/` anywhere on the page (outside a text field) jumps to the bar, or opens its view; `shortcut`
11
+ changes the key, `null` turns it off.
12
+
10
13
  @see https://m3.material.io/components/search/specs
11
14
  -->
12
15
  <script lang="ts">
13
16
  import { search } from './theme.js';
14
17
  import clsx from 'clsx';
15
18
  import type { SearchProps } from './types.js';
16
- import { Icon } from '../../../utils/index.js';
19
+ import { Icon, ariaKeyShortcut, isApplePlatform, triggersShortcut } from '../../../utils/index.js';
17
20
  import ButtonIcon from '../../buttons/ButtonIcon.svelte';
18
21
  import SearchView from './SearchView.svelte';
19
22
 
@@ -30,6 +33,8 @@ full-screen on compact windows, docked from medium up, with a container transfor
30
33
  clearLabel = 'Clear search',
31
34
  open = $bindable(false),
32
35
  results,
36
+ shortcut = '/',
37
+ onsearch,
33
38
  layout,
34
39
  backLabel,
35
40
  resultsLabel,
@@ -44,7 +49,7 @@ full-screen on compact windows, docked from medium up, with a container transfor
44
49
 
45
50
  let bar = $state<HTMLElement>();
46
51
 
47
- // With a search view, clicking the bar, typing in it or pressing ↓ opens the view. Not focus:
52
+ // With a search view, clicking the bar or typing in it opens the view. Not focus:
48
53
  // closing the view hands focus back here, which must not reopen it.
49
54
  const opener = $derived(
50
55
  results
@@ -58,18 +63,40 @@ full-screen on compact windows, docked from medium up, with a container transfor
58
63
  oninput: (e: Event & { currentTarget: HTMLInputElement }) => {
59
64
  restProps.oninput?.(e);
60
65
  if (!e.defaultPrevented) open = true;
61
- },
62
- onkeydown: (e: KeyboardEvent & { currentTarget: HTMLInputElement }) => {
63
- restProps.onkeydown?.(e);
64
- if (!e.defaultPrevented && e.key === 'ArrowDown') {
65
- e.preventDefault();
66
- open = true;
67
- }
68
66
  }
69
67
  }
70
68
  : {}
71
69
  );
72
70
 
71
+ // ↓ opens the view; Enter searches for the query as typed.
72
+ function onkeydown(e: KeyboardEvent & { currentTarget: HTMLInputElement }) {
73
+ restProps.onkeydown?.(e);
74
+ if (e.defaultPrevented) return;
75
+ if (e.key === 'ArrowDown' && results) {
76
+ e.preventDefault();
77
+ open = true;
78
+ } else if (e.key === 'Enter' && onsearch && value?.trim()) {
79
+ e.preventDefault();
80
+ onsearch(value);
81
+ }
82
+ }
83
+
84
+ // `Mod` resolves on the client, so SSR and the first render agree.
85
+ let apple = $state(false);
86
+ $effect(() => {
87
+ apple = isApplePlatform();
88
+ });
89
+
90
+ function onShortcut(e: KeyboardEvent) {
91
+ if (!triggersShortcut(e, shortcut, bar)) return;
92
+ e.preventDefault();
93
+ if (results) open = true;
94
+ else {
95
+ elementRef?.focus();
96
+ elementRef?.select();
97
+ }
98
+ }
99
+
73
100
  const showClear = $derived(!!trailingIconProps && !!value);
74
101
 
75
102
  const s = $derived(
@@ -80,6 +107,8 @@ full-screen on compact windows, docked from medium up, with a container transfor
80
107
  );
81
108
  </script>
82
109
 
110
+ <svelte:window onkeydown={onShortcut} />
111
+
83
112
  <label for={id} class={s.base({ class: clsx(className) })} bind:this={bar}>
84
113
  {#if leading}
85
114
  <span class={s.leading()}>{@render leading()}</span>
@@ -89,7 +118,9 @@ full-screen on compact windows, docked from medium up, with a container transfor
89
118
  <input
90
119
  {...restProps}
91
120
  {...opener}
121
+ {onkeydown}
92
122
  {id}
123
+ aria-keyshortcuts={shortcut ? ariaKeyShortcut(shortcut, apple) : undefined}
93
124
  placeholder={placeholder ?? undefined}
94
125
  bind:this={elementRef}
95
126
  bind:value
@@ -124,5 +155,6 @@ full-screen on compact windows, docked from medium up, with a container transfor
124
155
  {resultsLabel}
125
156
  {clearLabel}
126
157
  {trailing}
158
+ {onsearch}
127
159
  />
128
160
  {/if}
@@ -7,6 +7,9 @@ import type { SearchProps } from './types.js';
7
7
  * Give it `results` and the bar opens a search view (`SearchView`) when clicked or typed in:
8
8
  * full-screen on compact windows, docked from medium up, with a container transform between them.
9
9
  *
10
+ * `/` anywhere on the page (outside a text field) jumps to the bar, or opens its view; `shortcut`
11
+ * changes the key, `null` turns it off.
12
+ *
10
13
  * @see https://m3.material.io/components/search/specs
11
14
  */
12
15
  declare const Search: import("svelte").Component<SearchProps, {}, "value" | "open" | "elementRef">;
@@ -12,7 +12,8 @@ open a view from a bar of your own, passing that bar as `anchor`.
12
12
  the `spatial` spring; a crossfade under reduced motion.
13
13
  - **Behavior.** A bits-ui `Dialog`: focus stays in the view, the page doesn't scroll, and Esc, the
14
14
  back button or a click outside closes it. The field is a combobox over the `results` listbox:
15
- arrow keys move through the `role="option"` items and Enter picks one.
15
+ arrow keys move through the `role="option"` items and Enter picks one. Enter with no item
16
+ highlighted searches for the query as typed (`onsearch`).
16
17
 
17
18
  @see https://m3.material.io/components/search/specs
18
19
  -->
@@ -39,7 +40,8 @@ open a view from a bar of your own, passing that bar as `anchor`.
39
40
  clearLabel = 'Clear search',
40
41
  trailing,
41
42
  inputProps,
42
- inputRef = $bindable()
43
+ inputRef = $bindable(),
44
+ onsearch
43
45
  }: SearchViewProps = $props();
44
46
 
45
47
  const listboxId = `${uid}-listbox`;
@@ -167,6 +169,9 @@ open a view from a bar of your own, passing that bar as `anchor`.
167
169
  } else if (e.key === 'Enter' && active?.isConnected) {
168
170
  e.preventDefault();
169
171
  active.click();
172
+ } else if (e.key === 'Enter' && onsearch && value?.trim()) {
173
+ e.preventDefault();
174
+ onsearch(value);
170
175
  }
171
176
  }
172
177