@noxlovette/material 0.7.1 → 0.8.1
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/references/motion-guide.md +9 -9
- package/dist/animation/containerTransform.d.ts +0 -27
- package/dist/animation/containerTransform.js +40 -9
- package/dist/components/containers/popover/theme.d.ts +3 -3
- package/dist/components/forms/checkbox/Checkbox.svelte +1 -1
- package/dist/components/forms/checkbox/theme.d.ts +0 -12
- package/dist/components/forms/checkbox/theme.js +2 -7
- package/dist/components/forms/search/Search.mdx +76 -8
- package/dist/components/forms/search/Search.stories.svelte +107 -0
- package/dist/components/forms/search/Search.stories.svelte.d.ts +2 -17
- package/dist/components/forms/search/Search.svelte +55 -2
- package/dist/components/forms/search/Search.svelte.d.ts +4 -1
- package/dist/components/forms/search/SearchView.svelte +269 -0
- package/dist/components/forms/search/SearchView.svelte.d.ts +21 -0
- package/dist/components/forms/search/index.d.ts +1 -0
- package/dist/components/forms/search/index.js +1 -0
- package/dist/components/forms/search/theme.d.ts +68 -1
- package/dist/components/forms/search/theme.js +91 -2
- package/dist/components/forms/search/types.d.ts +75 -2
- package/dist/components/nav/appbar/AppBar.mdx +5 -1
- package/dist/components/nav/appbar/AppBar.stories.svelte +38 -0
- package/dist/components/nav/appbar/AppBar.svelte +47 -2
- package/dist/components/nav/appbar/AppBar.svelte.d.ts +3 -2
- package/dist/components/nav/appbar/theme.js +1 -1
- package/dist/components/nav/appbar/types.d.ts +18 -1
- package/dist/components/table/theme.js +3 -1
- package/dist/components/time/TimeField.svelte +1 -4
- package/dist/styles/components.css +6 -0
- package/dist/styles/icon-font.css +17 -0
- package/dist/styles/motion.css +35 -0
- package/dist/utils/icon/MaterialSymbolsProvider.svelte +7 -1
- package/dist/utils/icon/MaterialSymbolsProvider.svelte.d.ts +3 -0
- package/dist/utils/icon/types.d.ts +11 -0
- package/package.json +2 -1
|
@@ -58,7 +58,7 @@ Spatial springs overshoot by design (fast spatial most, at damping ratio 0.6); e
|
|
|
58
58
|
| ----------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
59
59
|
| **Enter and exit** — a surface appears/leaves within the screen | `presence(() => open, enterExit.<preset>)` | Attachment. Presets: `fade`, `scale` (menus/popovers/tooltips — sets `transform-origin` to bits-ui's anchor side, so never add an `origin-*` class), `slideUp` (snackbar), `dialog`, `sideSheet`, `bottomSheet`. Interruptible: reopening mid-exit retargets from the current value with velocity. |
|
|
60
60
|
| Same, outside bits-ui (element must stay mounted during its exit) | `new Presence(() => open)` | `{#if p.mounted}<div {@attach p.attach(enterExit.x)}>`. Construct during component init. Used by Snackbar, SideSheet, BottomSheet. |
|
|
61
|
-
| **Container transform** — card → detail,
|
|
61
|
+
| **Container transform** — card → detail, search bar → view | `containerTransform(update, { from, to })` | Motion `animateView()` (View Transition API). `update` swaps the DOM (`async () => { open = true; await tick(); }`); `to` may be a selector for an element that only exists after the update. |
|
|
62
62
|
| **Forward and backward** — hierarchy levels, wizard steps | `sharedAxis(update, { target, axis, direction })` | `axis: 'x' \| 'y' \| 'z'`, `direction: 'forward' \| 'backward'`. `target` is the persistent region whose content changes; omit for the whole page. |
|
|
63
63
|
| **Lateral** — peer screens (tabs, carousels) | `lateral(update, { target, direction, axis })` | Edge-to-edge slide, no fade, along the axis the peers are laid out on: `axis: 'y'` for vertical tabs, a vertical carousel or a top-to-bottom stepper (`forward` = next, pushing up from below). Never on a vertical nav list: that's a drawer, so top level. |
|
|
64
64
|
| **Top level** — unrelated destinations (navigation bar) | `fadeThrough(update, { target })` | Old fades out, new fades in scaling from 92%. The `target` region swaps its box instantly (no slide or resize, so a scroll reset doesn't glide). In SvelteKit, call it from `onNavigate` in the root layout, skip hash-only changes, and fade only the region whose content changed: `main` between rail/navbar destinations, a section's content pane between pages of a nav that stays on screen (the showcase site's root layout does both). |
|
|
@@ -71,14 +71,14 @@ Spatial springs overshoot by design (fast spatial most, at damping ratio 0.6); e
|
|
|
71
71
|
|
|
72
72
|
A pattern belongs in a component only when that component owns both states of the change. When the content that changes lives outside the component (a route, an app screen), the app applies the pattern, not the library. Keep this table in sync when a component gains or loses one of these patterns; each primitive's JSDoc in `src/lib/animation/` repeats its own row.
|
|
73
73
|
|
|
74
|
-
| Pattern | Built into (must use it)
|
|
75
|
-
| ------------------------ |
|
|
76
|
-
| **Enter and exit** | Dialogue, BottomSheet, SideSheet, Menu, MenuSub, ContextMenu, Popover, Tooltip, LinkPreview, Select, FABMenu, SplitButton, Snackbar, DateField/DateRangeField/TimeField popups
|
|
77
|
-
| **Lateral** | TabHolder (switching content panels); DateField/DateRangeField (changing the visible month, via `date/calendarMotion.ts`)
|
|
78
|
-
| **Container transform** | FAB → its `surface` (`FAB.svelte`: a clip-path and colour morph on Motion, not `containerTransform`)
|
|
79
|
-
| **Forward and backward** | —
|
|
80
|
-
| **Top level** | —
|
|
81
|
-
| **Skeleton loaders** | —
|
|
74
|
+
| Pattern | Built into (must use it) | Left to the app |
|
|
75
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
76
|
+
| **Enter and exit** | Dialogue, BottomSheet, SideSheet, Menu, MenuSub, ContextMenu, Popover, Tooltip, LinkPreview, Select, FABMenu, SplitButton, Snackbar, DateField/DateRangeField/TimeField popups | — |
|
|
77
|
+
| **Lateral** | TabHolder (switching content panels); DateField/DateRangeField (changing the visible month, via `date/calendarMotion.ts`) | Navigation (`href`) tabs: the route change is the app's |
|
|
78
|
+
| **Container transform** | FAB → its `surface` (`FAB.svelte`: a clip-path and colour morph on Motion, not `containerTransform`); `Search` / search `AppBar` bar → `SearchView` (`containerTransform`, both ways) | Card → detail, Carousel item → detail |
|
|
79
|
+
| **Forward and backward** | — | Hierarchy levels, wizard steps |
|
|
80
|
+
| **Top level** | — | Page changes from `Navbar`/`Rail`: they don't own the content region |
|
|
81
|
+
| **Skeleton loaders** | — | Placeholders for data the app loads |
|
|
82
82
|
|
|
83
83
|
A new component that mounts a surface must use enter/exit; one that pages between peer views (a stepper of equal steps) must use lateral. `Carousel` is not a pager: its motion is the scroll itself, items resizing through keylines. Opening a tapped item into its detail is a container transform the app applies.
|
|
84
84
|
|
|
@@ -8,31 +8,4 @@ export interface ContainerTransformOptions {
|
|
|
8
8
|
/** Spring for the bounds/shape morph. `slowSpatial` suits full-screen expansions. */
|
|
9
9
|
spring?: SpringToken;
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
12
|
-
* M3 container transform: one container morphs its bounds, shape and color into another while
|
|
13
|
-
* the outgoing content fades out and the incoming content fades in on top.
|
|
14
|
-
* https://m3.material.io/styles/motion/transitions/transition-patterns#container-transform
|
|
15
|
-
*
|
|
16
|
-
* The most dramatic pattern (https://m3.material.io/styles/motion/transitions/applying-transitions).
|
|
17
|
-
* Use it for hero moments, shallow expand → collapse hierarchies and seamless element-to-element
|
|
18
|
-
* connections. Don't use it in deep hierarchies or utility-focused navigation, where it becomes
|
|
19
|
-
* excessive. Use `sharedAxis` there. Keep `spring` at `spatial`/`slowSpatial`, never the bouncy
|
|
20
|
-
* `fastSpatial`.
|
|
21
|
-
*
|
|
22
|
-
* Built on Motion's `animateView()` (View Transition API), so `from` and `to` never have to be in
|
|
23
|
-
* the DOM at the same time — `update` swaps one for the other. Browsers without the API just run
|
|
24
|
-
* `update`. A second call while one is running is queued, not interrupted.
|
|
25
|
-
*
|
|
26
|
-
* Not built into any component. FAB → sheet is the FAB's own morph (a clip-path on Motion, in
|
|
27
|
-
* `FAB.svelte`), because M3 keeps that container opaque and changes its colour, which a
|
|
28
|
-
* snapshot cross-fade can't. Card → detail is the app's own navigation, and `Search` is a plain
|
|
29
|
-
* field with no search view to expand into.
|
|
30
|
-
*
|
|
31
|
-
* ```ts
|
|
32
|
-
* containerTransform(
|
|
33
|
-
* async () => { expanded = true; await tick(); },
|
|
34
|
-
* { from: cardEl, to: '[data-detail]' }
|
|
35
|
-
* );
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
11
|
export declare const containerTransform: (update: () => void | Promise<void>, { from, to, spring }: ContainerTransformOptions) => import("motion-dom").ViewTransitionBuilder;
|
|
@@ -2,8 +2,9 @@ import { animateView } from 'motion';
|
|
|
2
2
|
import { prefersReducedMotion } from './reducedMotion.js';
|
|
3
3
|
import { springTokens, springTransition } from './spring.js';
|
|
4
4
|
/**
|
|
5
|
-
* M3 container transform: one container morphs its bounds, shape and color into another
|
|
6
|
-
*
|
|
5
|
+
* M3 container transform: one container morphs its bounds, shape and color into another. The
|
|
6
|
+
* incoming state is drawn underneath at full opacity from the start and the outgoing one fades
|
|
7
|
+
* out on top of it, so the morph ends exactly as the page looks and never shows through.
|
|
7
8
|
* https://m3.material.io/styles/motion/transitions/transition-patterns#container-transform
|
|
8
9
|
*
|
|
9
10
|
* The most dramatic pattern (https://m3.material.io/styles/motion/transitions/applying-transitions).
|
|
@@ -16,10 +17,10 @@ import { springTokens, springTransition } from './spring.js';
|
|
|
16
17
|
* the DOM at the same time — `update` swaps one for the other. Browsers without the API just run
|
|
17
18
|
* `update`. A second call while one is running is queued, not interrupted.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Built into `SearchView`: the search bar (`Search`, or a search `AppBar`) grows into the search
|
|
21
|
+
* view and back. FAB → sheet is the FAB's own morph (a clip-path on Motion, in `FAB.svelte`),
|
|
22
|
+
* because M3 keeps that container opaque and changes its colour, which a snapshot cross-fade
|
|
23
|
+
* can't. Card → detail is the app's own navigation.
|
|
23
24
|
*
|
|
24
25
|
* ```ts
|
|
25
26
|
* containerTransform(
|
|
@@ -28,12 +29,42 @@ import { springTokens, springTransition } from './spring.js';
|
|
|
28
29
|
* );
|
|
29
30
|
* ```
|
|
30
31
|
*/
|
|
32
|
+
const FILL = '--md-container-transform-color';
|
|
33
|
+
const resolve = (target) => typeof target === 'string'
|
|
34
|
+
? document.querySelector(target)
|
|
35
|
+
: target instanceof Element
|
|
36
|
+
? target
|
|
37
|
+
: null;
|
|
38
|
+
const TRANSPARENT = new Set(['transparent', 'rgba(0, 0, 0, 0)']);
|
|
39
|
+
/*
|
|
40
|
+
The destination's own background, if it has one. A container made of separate surfaces on a
|
|
41
|
+
transparent wrapper (the docked search view's bar and results) gets no fill: its opaque
|
|
42
|
+
incoming snapshot already covers what it should, and a flat fill would paper over the gaps
|
|
43
|
+
between its surfaces until the transition ends, then pop.
|
|
44
|
+
*/
|
|
45
|
+
const fillOf = (target) => {
|
|
46
|
+
const colour = target ? getComputedStyle(target).backgroundColor : '';
|
|
47
|
+
return colour && !TRANSPARENT.has(colour) ? colour : 'transparent';
|
|
48
|
+
};
|
|
31
49
|
export const containerTransform = (update, { from, to, spring = springTokens.spatial }) => {
|
|
32
|
-
|
|
50
|
+
// The incoming snapshot is opaque wherever the destination is, and the outgoing one fades off
|
|
51
|
+
// it (motion.css layers them), so nothing behind shows through. The fill (motion.css) covers
|
|
52
|
+
// 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.
|
|
55
|
+
const root = document.documentElement;
|
|
56
|
+
const updateAndFill = async () => {
|
|
57
|
+
await update();
|
|
58
|
+
root.style.setProperty(FILL, fillOf(resolve(to)));
|
|
59
|
+
};
|
|
60
|
+
// The class lets motion.css keep both snapshots at their width, clipped by the container.
|
|
61
|
+
const builder = animateView(updateAndFill, springTransition(spring))
|
|
62
|
+
.add(from, to)
|
|
63
|
+
.class('md-container-transform');
|
|
33
64
|
// Reduced motion: the container doesn't grow; its two states just crossfade in place.
|
|
34
65
|
if (prefersReducedMotion())
|
|
35
66
|
builder.layout({ duration: 0 });
|
|
36
67
|
return builder
|
|
37
|
-
.old({ opacity: [1, 0] }, springTransition(springTokens.
|
|
38
|
-
.new({ opacity: [
|
|
68
|
+
.old({ opacity: [1, 0] }, springTransition(springTokens.effects))
|
|
69
|
+
.new({ opacity: [1, 1] }, springTransition(springTokens.effects));
|
|
39
70
|
};
|
|
@@ -3,20 +3,20 @@ export type PopoverVariants = VariantProps<typeof popover>;
|
|
|
3
3
|
export declare const popover: import("tailwind-variants").TVReturnType<{
|
|
4
4
|
[key: string]: {
|
|
5
5
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
6
|
-
title?: import("tailwind-variants").ClassValue;
|
|
7
6
|
base?: import("tailwind-variants").ClassValue;
|
|
8
7
|
body?: import("tailwind-variants").ClassValue;
|
|
9
8
|
header?: import("tailwind-variants").ClassValue;
|
|
9
|
+
title?: import("tailwind-variants").ClassValue;
|
|
10
10
|
close?: import("tailwind-variants").ClassValue;
|
|
11
11
|
};
|
|
12
12
|
};
|
|
13
13
|
} | {
|
|
14
14
|
[x: string]: {
|
|
15
15
|
[x: string]: import("tailwind-variants").ClassValue | {
|
|
16
|
-
title?: import("tailwind-variants").ClassValue;
|
|
17
16
|
base?: import("tailwind-variants").ClassValue;
|
|
18
17
|
body?: import("tailwind-variants").ClassValue;
|
|
19
18
|
header?: import("tailwind-variants").ClassValue;
|
|
19
|
+
title?: import("tailwind-variants").ClassValue;
|
|
20
20
|
close?: import("tailwind-variants").ClassValue;
|
|
21
21
|
};
|
|
22
22
|
};
|
|
@@ -29,10 +29,10 @@ export declare const popover: import("tailwind-variants").TVReturnType<{
|
|
|
29
29
|
}, undefined, {
|
|
30
30
|
[key: string]: {
|
|
31
31
|
[key: string]: import("tailwind-variants").ClassValue | {
|
|
32
|
-
title?: import("tailwind-variants").ClassValue;
|
|
33
32
|
base?: import("tailwind-variants").ClassValue;
|
|
34
33
|
body?: import("tailwind-variants").ClassValue;
|
|
35
34
|
header?: import("tailwind-variants").ClassValue;
|
|
35
|
+
title?: import("tailwind-variants").ClassValue;
|
|
36
36
|
close?: import("tailwind-variants").ClassValue;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
@@ -28,7 +28,7 @@ Checkboxes let users select one or more items from a list, or turn an item on or
|
|
|
28
28
|
const cls = $derived(checkbox({ state, error, disabled }));
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
|
-
<div class="
|
|
31
|
+
<div class="gap-spacing-50 flex cursor-pointer items-center">
|
|
32
32
|
<Checkbox.Root
|
|
33
33
|
bind:checked
|
|
34
34
|
bind:indeterminate
|
|
@@ -21,10 +21,6 @@ export declare const checkbox: import("tailwind-variants").TVReturnType<{
|
|
|
21
21
|
supporting: string;
|
|
22
22
|
};
|
|
23
23
|
};
|
|
24
|
-
align: {
|
|
25
|
-
start: "items-start";
|
|
26
|
-
center: "items-center";
|
|
27
|
-
};
|
|
28
24
|
disabled: {
|
|
29
25
|
true: {
|
|
30
26
|
root: string;
|
|
@@ -66,10 +62,6 @@ export declare const checkbox: import("tailwind-variants").TVReturnType<{
|
|
|
66
62
|
supporting: string;
|
|
67
63
|
};
|
|
68
64
|
};
|
|
69
|
-
align: {
|
|
70
|
-
start: "items-start";
|
|
71
|
-
center: "items-center";
|
|
72
|
-
};
|
|
73
65
|
disabled: {
|
|
74
66
|
true: {
|
|
75
67
|
root: string;
|
|
@@ -111,10 +103,6 @@ export declare const checkbox: import("tailwind-variants").TVReturnType<{
|
|
|
111
103
|
supporting: string;
|
|
112
104
|
};
|
|
113
105
|
};
|
|
114
|
-
align: {
|
|
115
|
-
start: "items-start";
|
|
116
|
-
center: "items-center";
|
|
117
|
-
};
|
|
118
106
|
disabled: {
|
|
119
107
|
true: {
|
|
120
108
|
root: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from '../../../utils/tv.js';
|
|
2
2
|
export const checkbox = tv({
|
|
3
3
|
slots: {
|
|
4
|
-
root: 'group inline-flex
|
|
4
|
+
root: 'group inline-flex size-spacing-500 shrink-0 select-none items-center justify-center cursor-pointer',
|
|
5
5
|
container: 'relative inline-flex size-[18px] shrink-0',
|
|
6
6
|
control: 'layer-container absolute -inset-[11px] rounded-full text-md-sys-color-on-surface-variant state-layer before:rounded-full group-focus-visible:outline group-focus-visible:outline-3 group-focus-visible:outline-offset-2 group-focus-visible:outline-md-sys-color-secondary transition-colors md-sys-motion-fast-effects',
|
|
7
7
|
box: 'absolute inset-[11px] rounded-[4px] border-2 border-current bg-md-sys-color-surface transition-colors md-sys-motion-fast-effects',
|
|
@@ -31,10 +31,6 @@ export const checkbox = tv({
|
|
|
31
31
|
supporting: 'text-md-sys-color-error'
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
align: {
|
|
35
|
-
start: 'items-start',
|
|
36
|
-
center: 'items-center'
|
|
37
|
-
},
|
|
38
34
|
disabled: {
|
|
39
35
|
true: {
|
|
40
36
|
root: 'cursor-not-allowed',
|
|
@@ -84,7 +80,6 @@ export const checkbox = tv({
|
|
|
84
80
|
}
|
|
85
81
|
],
|
|
86
82
|
defaultVariants: {
|
|
87
|
-
state: 'unchecked'
|
|
88
|
-
align: 'start'
|
|
83
|
+
state: 'unchecked'
|
|
89
84
|
}
|
|
90
85
|
});
|
|
@@ -5,21 +5,22 @@ import * as SearchStories from './Search.stories.svelte';
|
|
|
5
5
|
|
|
6
6
|
<Title />
|
|
7
7
|
|
|
8
|
-
<Subtitle>
|
|
8
|
+
<Subtitle>
|
|
9
|
+
Search lets users enter a query and pick from suggestions and results in a search view.
|
|
10
|
+
</Subtitle>
|
|
9
11
|
|
|
10
12
|
[M3 spec](https://m3.material.io/components/search/specs) ·
|
|
11
|
-
`import { Search } from '@noxlovette/material';`
|
|
13
|
+
`import { Search, SearchView } from '@noxlovette/material';`
|
|
12
14
|
|
|
13
15
|
<Canvas of={SearchStories.Playground} />
|
|
14
16
|
|
|
15
17
|
<Controls of={SearchStories.Playground} />
|
|
16
18
|
|
|
17
|
-
## Search bar
|
|
19
|
+
## Search bar
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
pass straight through to the input.
|
|
21
|
+
M3's search bar is a 56dp, fully rounded `surface-container-high` field with a leading search
|
|
22
|
+
icon. It's a different thing from a filled text field. Once there's a value, a clear button
|
|
23
|
+
appears at the end. Other native `<input>` attributes pass straight through to the input.
|
|
23
24
|
|
|
24
25
|
```svelte
|
|
25
26
|
<Search placeholder="Search" bind:value={query} />
|
|
@@ -27,7 +28,65 @@ pass straight through to the input.
|
|
|
27
28
|
|
|
28
29
|
<Canvas of={SearchStories.WithActions} />
|
|
29
30
|
|
|
30
|
-
For a search field inside a top app bar, use `AppBar`'s `search` prop instead.
|
|
31
|
+
For a search field inside a top app bar, use `AppBar`'s `search` prop instead. It opens the same
|
|
32
|
+
search view when you give it `searchResults`.
|
|
33
|
+
|
|
34
|
+
## Search view
|
|
35
|
+
|
|
36
|
+
M3 treats search as a bar plus a view. Pass `results` and clicking or typing in the bar opens the
|
|
37
|
+
search view, where the suggestions and results go. The view is empty until you fill it: spread the
|
|
38
|
+
snippet's argument onto a `List`, and give each item `role="option"`.
|
|
39
|
+
|
|
40
|
+
```svelte
|
|
41
|
+
<Search placeholder="Search" bind:value={query} bind:open results={suggestions} />
|
|
42
|
+
|
|
43
|
+
{#snippet suggestions(listbox)}
|
|
44
|
+
<List {...listbox}>
|
|
45
|
+
{#each matches as item (item)}
|
|
46
|
+
<ListItem role="option" asChild headline={item} onclick={() => pick(item)} />
|
|
47
|
+
{/each}
|
|
48
|
+
</List>
|
|
49
|
+
{/snippet}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Picking a result is up to you: set the value and `open = false` in the item's `onclick`.
|
|
53
|
+
|
|
54
|
+
<Canvas of={SearchStories.SearchView} />
|
|
55
|
+
|
|
56
|
+
### Layouts
|
|
57
|
+
|
|
58
|
+
`layout` takes `'fullScreen' | 'docked'`, one value or one per window tier. The default,
|
|
59
|
+
`{ small: 'fullScreen', medium: 'docked' }`, follows M3: full-screen on compact windows, docked
|
|
60
|
+
from medium up. The switch is made in CSS, so there's no flash on load.
|
|
61
|
+
|
|
62
|
+
| Layout | Container | Bar |
|
|
63
|
+
| ----------- | ---------------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
64
|
+
| Full-screen | The whole window, `surface-container-low`, 0 radius | 56dp, circular, `surface-container-high`, 12dp from the sides |
|
|
65
|
+
| Docked | Results 2dp below the bar, 12dp radius, `surface-container-high` | The search bar widened in place: its margins go from 24dp to 12dp |
|
|
66
|
+
|
|
67
|
+
The docked view is 360–720dp wide and 240dp to ⅔ of the window tall. In both layouts the bar's
|
|
68
|
+
leading icon becomes a back button, a clear button appears once there's a query, and `trailing`
|
|
69
|
+
actions (a mic, say) stay in the bar.
|
|
70
|
+
|
|
71
|
+
<Canvas of={SearchStories.FullScreen} />
|
|
72
|
+
|
|
73
|
+
<Canvas of={SearchStories.Docked} />
|
|
74
|
+
|
|
75
|
+
### Motion
|
|
76
|
+
|
|
77
|
+
Opening is an M3 [container transform](https://m3.material.io/styles/motion/transitions/transition-patterns#container-transform):
|
|
78
|
+
the bar grows into the view on the `spatial` spring. Back, Esc and a click outside reverse it.
|
|
79
|
+
Under reduced motion the two crossfade in place. Browsers without the View Transition API switch
|
|
80
|
+
straight to the view.
|
|
81
|
+
|
|
82
|
+
### Your own bar
|
|
83
|
+
|
|
84
|
+
`SearchView` is exported for bars you build yourself. Pass the bar element as `anchor`, and open
|
|
85
|
+
the view on click or typing, not on focus: closing hands focus back to the bar.
|
|
86
|
+
|
|
87
|
+
```svelte
|
|
88
|
+
<SearchView bind:open bind:value anchor={barEl} results={suggestions} />
|
|
89
|
+
```
|
|
31
90
|
|
|
32
91
|
## Accessibility
|
|
33
92
|
|
|
@@ -36,3 +95,12 @@ For a search field inside a top app bar, use `AppBar`'s `search` prop instead.
|
|
|
36
95
|
- **Clear button.** A 48dp `type="button"` labelled by `clearLabel` (default "Clear search"). It
|
|
37
96
|
never submits a form and returns focus to the input.
|
|
38
97
|
- **Focus.** Keyboard focus draws the M3 focus indicator around the bar.
|
|
98
|
+
- **Opening.** With `results`, the bar's input has `aria-haspopup="dialog"` and `aria-expanded`.
|
|
99
|
+
Click, typing or ↓ opens the view; focus alone doesn't.
|
|
100
|
+
- **Search view.** A bits-ui `Dialog`: focus moves into the view's field and stays in the view,
|
|
101
|
+
the page doesn't scroll, and Esc or the back button (`backLabel`, default "Back") closes it and
|
|
102
|
+
returns focus to the bar. The view is named by `resultsLabel` (default: the placeholder).
|
|
103
|
+
- **Suggestions.** The view's field is a `combobox` over the `listbox` you render. ↑/↓ move
|
|
104
|
+
through the `role="option"` items (wrapping, skipping disabled ones) via
|
|
105
|
+
`aria-activedescendant`, so focus stays in the field while typing; Enter picks the highlighted
|
|
106
|
+
one. The highlighted item gets the M3 focus indicator.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
3
|
import Search from './Search.svelte';
|
|
4
4
|
import ButtonIcon from '../../buttons/ButtonIcon.svelte';
|
|
5
|
+
import List from '../../containers/list/List.svelte';
|
|
6
|
+
import ListItem from '../../containers/list/ListItem.svelte';
|
|
7
|
+
import { Icon } from '../../../utils/index.js';
|
|
8
|
+
import type { SearchResultsProps } from './types.js';
|
|
5
9
|
|
|
6
10
|
const { Story } = defineMeta({
|
|
7
11
|
title: 'Forms/Search',
|
|
@@ -17,6 +21,51 @@
|
|
|
17
21
|
});
|
|
18
22
|
</script>
|
|
19
23
|
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
const recent = ['Material Design 3', 'Container transform', 'Search view specs'];
|
|
26
|
+
const topics = [
|
|
27
|
+
'Buttons',
|
|
28
|
+
'Cards',
|
|
29
|
+
'Chips',
|
|
30
|
+
'Dialogs',
|
|
31
|
+
'Lists',
|
|
32
|
+
'Menus',
|
|
33
|
+
'Navigation rail',
|
|
34
|
+
'Search',
|
|
35
|
+
'Sheets',
|
|
36
|
+
'Sliders',
|
|
37
|
+
'Snackbar',
|
|
38
|
+
'Tabs',
|
|
39
|
+
'Text fields'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let query = $state('');
|
|
43
|
+
let open = $state(false);
|
|
44
|
+
let picked = $state('');
|
|
45
|
+
|
|
46
|
+
const matches = $derived(
|
|
47
|
+
query ? topics.filter((t) => t.toLowerCase().includes(query.toLowerCase())) : recent
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const pick = (item: string) => {
|
|
51
|
+
picked = item;
|
|
52
|
+
query = item;
|
|
53
|
+
open = false;
|
|
54
|
+
};
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
{#snippet suggestions(listbox: SearchResultsProps)}
|
|
58
|
+
<List {...listbox}>
|
|
59
|
+
{#each matches as item (item)}
|
|
60
|
+
<ListItem role="option" asChild headline={item} onclick={() => pick(item)}>
|
|
61
|
+
{#snippet leading()}
|
|
62
|
+
<Icon name={query ? 'search' : 'history'} size="sm" />
|
|
63
|
+
{/snippet}
|
|
64
|
+
</ListItem>
|
|
65
|
+
{/each}
|
|
66
|
+
</List>
|
|
67
|
+
{/snippet}
|
|
68
|
+
|
|
20
69
|
<Story name="Playground">
|
|
21
70
|
{#snippet template(args)}
|
|
22
71
|
<div class="p-spacing-300 max-w-md">
|
|
@@ -49,3 +98,61 @@
|
|
|
49
98
|
<Search value="query" leadingIconProps={null} placeholder="No leading icon" />
|
|
50
99
|
</div>
|
|
51
100
|
</Story>
|
|
101
|
+
|
|
102
|
+
<!--
|
|
103
|
+
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.
|
|
105
|
+
-->
|
|
106
|
+
<Story
|
|
107
|
+
name="Search view"
|
|
108
|
+
asChild
|
|
109
|
+
parameters={{
|
|
110
|
+
layout: 'fullscreen',
|
|
111
|
+
docs: { story: { inline: false, height: '560px' } }
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<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}>
|
|
116
|
+
{#snippet trailing()}
|
|
117
|
+
<ButtonIcon variant="standard" iconProps={{ name: 'mic' }} aria-label="Voice search" />
|
|
118
|
+
{/snippet}
|
|
119
|
+
</Search>
|
|
120
|
+
<p class="md-sys-typescale-body-medium text-md-sys-color-on-surface-variant">
|
|
121
|
+
{picked ? `Picked: ${picked}` : 'Nothing picked yet'}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
</Story>
|
|
125
|
+
|
|
126
|
+
<Story
|
|
127
|
+
name="Full-screen"
|
|
128
|
+
asChild
|
|
129
|
+
parameters={{
|
|
130
|
+
layout: 'fullscreen',
|
|
131
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
132
|
+
docs: { story: { inline: false, height: '560px' } }
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<div class="p-spacing-300 flex min-h-dvh flex-col items-center">
|
|
136
|
+
<Search
|
|
137
|
+
placeholder="Search components"
|
|
138
|
+
bind:value={query}
|
|
139
|
+
layout="fullScreen"
|
|
140
|
+
results={suggestions}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</Story>
|
|
144
|
+
|
|
145
|
+
<Story
|
|
146
|
+
name="Docked"
|
|
147
|
+
asChild
|
|
148
|
+
parameters={{ layout: 'fullscreen', docs: { story: { inline: false, height: '560px' } } }}
|
|
149
|
+
>
|
|
150
|
+
<div class="p-spacing-300 flex min-h-dvh flex-col items-center">
|
|
151
|
+
<Search
|
|
152
|
+
placeholder="Search components"
|
|
153
|
+
bind:value={query}
|
|
154
|
+
layout="docked"
|
|
155
|
+
results={suggestions}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</Story>
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
import Search from './Search.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 Search: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
-
[evt: string]: CustomEvent<any>;
|
|
17
|
-
}, {}, {}, string>;
|
|
18
|
-
type Search = InstanceType<typeof Search>;
|
|
2
|
+
declare const Search: import("svelte").Component<Record<string, never>, {}, "">;
|
|
3
|
+
type Search = ReturnType<typeof Search>;
|
|
19
4
|
export default Search;
|
|
@@ -4,6 +4,9 @@ Material 3 Search Bar (contained, M3 Expressive).
|
|
|
4
4
|
|
|
5
5
|
Search bars allow users to enter a query to find specific information within an app.
|
|
6
6
|
|
|
7
|
+
Give it `results` and the bar opens a search view (`SearchView`) when clicked or typed in:
|
|
8
|
+
full-screen on compact windows, docked from medium up, with a container transform between them.
|
|
9
|
+
|
|
7
10
|
@see https://m3.material.io/components/search/specs
|
|
8
11
|
-->
|
|
9
12
|
<script lang="ts">
|
|
@@ -12,6 +15,7 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
12
15
|
import type { SearchProps } from './types.js';
|
|
13
16
|
import { Icon } from '../../../utils/index.js';
|
|
14
17
|
import ButtonIcon from '../../buttons/ButtonIcon.svelte';
|
|
18
|
+
import SearchView from './SearchView.svelte';
|
|
15
19
|
|
|
16
20
|
const uid = $props.id();
|
|
17
21
|
|
|
@@ -24,6 +28,11 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
24
28
|
trailingIconProps = { name: 'close' },
|
|
25
29
|
leadingIconProps = { name: 'search' },
|
|
26
30
|
clearLabel = 'Clear search',
|
|
31
|
+
open = $bindable(false),
|
|
32
|
+
results,
|
|
33
|
+
layout,
|
|
34
|
+
backLabel,
|
|
35
|
+
resultsLabel,
|
|
27
36
|
class: className,
|
|
28
37
|
id = uid,
|
|
29
38
|
trailingClick = () => {
|
|
@@ -33,6 +42,34 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
33
42
|
...restProps
|
|
34
43
|
}: SearchProps = $props();
|
|
35
44
|
|
|
45
|
+
let bar = $state<HTMLElement>();
|
|
46
|
+
|
|
47
|
+
// With a search view, clicking the bar, typing in it or pressing ↓ opens the view. Not focus:
|
|
48
|
+
// closing the view hands focus back here, which must not reopen it.
|
|
49
|
+
const opener = $derived(
|
|
50
|
+
results
|
|
51
|
+
? {
|
|
52
|
+
'aria-haspopup': 'dialog' as const,
|
|
53
|
+
'aria-expanded': open,
|
|
54
|
+
onclick: (e: MouseEvent & { currentTarget: HTMLInputElement }) => {
|
|
55
|
+
restProps.onclick?.(e);
|
|
56
|
+
if (!e.defaultPrevented) open = true;
|
|
57
|
+
},
|
|
58
|
+
oninput: (e: Event & { currentTarget: HTMLInputElement }) => {
|
|
59
|
+
restProps.oninput?.(e);
|
|
60
|
+
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
|
+
}
|
|
69
|
+
}
|
|
70
|
+
: {}
|
|
71
|
+
);
|
|
72
|
+
|
|
36
73
|
const showClear = $derived(!!trailingIconProps && !!value);
|
|
37
74
|
|
|
38
75
|
const s = $derived(
|
|
@@ -43,7 +80,7 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
43
80
|
);
|
|
44
81
|
</script>
|
|
45
82
|
|
|
46
|
-
<label for={id} class={s.base({ class: clsx(className) })}>
|
|
83
|
+
<label for={id} class={s.base({ class: clsx(className) })} bind:this={bar}>
|
|
47
84
|
{#if leading}
|
|
48
85
|
<span class={s.leading()}>{@render leading()}</span>
|
|
49
86
|
{:else if leadingIconProps}
|
|
@@ -51,8 +88,9 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
51
88
|
{/if}
|
|
52
89
|
<input
|
|
53
90
|
{...restProps}
|
|
91
|
+
{...opener}
|
|
54
92
|
{id}
|
|
55
|
-
{placeholder}
|
|
93
|
+
placeholder={placeholder ?? undefined}
|
|
56
94
|
bind:this={elementRef}
|
|
57
95
|
bind:value
|
|
58
96
|
type="search"
|
|
@@ -73,3 +111,18 @@ Search bars allow users to enter a query to find specific information within an
|
|
|
73
111
|
</span>
|
|
74
112
|
{/if}
|
|
75
113
|
</label>
|
|
114
|
+
|
|
115
|
+
{#if results}
|
|
116
|
+
<SearchView
|
|
117
|
+
bind:open
|
|
118
|
+
bind:value
|
|
119
|
+
anchor={bar}
|
|
120
|
+
{results}
|
|
121
|
+
{layout}
|
|
122
|
+
placeholder={placeholder ?? undefined}
|
|
123
|
+
{backLabel}
|
|
124
|
+
{resultsLabel}
|
|
125
|
+
{clearLabel}
|
|
126
|
+
{trailing}
|
|
127
|
+
/>
|
|
128
|
+
{/if}
|
|
@@ -4,8 +4,11 @@ import type { SearchProps } from './types.js';
|
|
|
4
4
|
*
|
|
5
5
|
* Search bars allow users to enter a query to find specific information within an app.
|
|
6
6
|
*
|
|
7
|
+
* Give it `results` and the bar opens a search view (`SearchView`) when clicked or typed in:
|
|
8
|
+
* full-screen on compact windows, docked from medium up, with a container transform between them.
|
|
9
|
+
*
|
|
7
10
|
* @see https://m3.material.io/components/search/specs
|
|
8
11
|
*/
|
|
9
|
-
declare const Search: import("svelte").Component<SearchProps, {}, "value" | "elementRef">;
|
|
12
|
+
declare const Search: import("svelte").Component<SearchProps, {}, "value" | "open" | "elementRef">;
|
|
10
13
|
type Search = ReturnType<typeof Search>;
|
|
11
14
|
export default Search;
|