@noxlovette/material 0.6.1 → 0.7.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 +5 -2
- package/claude-skill/material-design/references/accessibility-checklist.md +5 -0
- package/claude-skill/material-design/references/component-patterns.md +24 -3
- package/claude-skill/material-design/references/motion-guide.md +53 -11
- package/claude-skill/material-design/references/tokens-and-styles.md +4 -0
- package/dist/animation/TransitionPatterns.stories.svelte +50 -0
- package/dist/animation/containerTransform.js +10 -4
- package/dist/animation/reducedMotion.d.ts +2 -0
- package/dist/animation/reducedMotion.js +2 -0
- package/dist/animation/sharedAxisTransition.d.ts +20 -3
- package/dist/animation/sharedAxisTransition.js +67 -8
- package/dist/attachments/drag.d.ts +52 -0
- package/dist/attachments/drag.js +217 -0
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/components/chips/Chip.mdx +24 -11
- package/dist/components/chips/Chip.stories.svelte +10 -0
- package/dist/components/chips/Chip.svelte +58 -18
- package/dist/components/chips/Chip.svelte.d.ts +3 -1
- package/dist/components/chips/ChipGroup.stories.svelte +73 -0
- package/dist/components/chips/ChipGroup.stories.svelte.d.ts +4 -0
- package/dist/components/chips/ChipGroup.svelte +242 -0
- package/dist/components/chips/ChipGroup.svelte.d.ts +33 -0
- package/dist/components/chips/index.d.ts +1 -0
- package/dist/components/chips/index.js +1 -0
- package/dist/components/chips/theme.d.ts +182 -21
- package/dist/components/chips/theme.js +116 -25
- package/dist/components/chips/types.d.ts +36 -2
- package/dist/components/containers/app/theme.d.ts +1 -1
- package/dist/components/containers/app/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +1 -1
- package/dist/components/containers/list/List.svelte +8 -2
- package/dist/components/containers/list/ListItem.mdx +6 -0
- package/dist/components/containers/list/ListItem.svelte +77 -2
- package/dist/components/containers/list/context.d.ts +14 -0
- package/dist/components/containers/list/theme.d.ts +6 -0
- package/dist/components/containers/list/theme.js +10 -2
- package/dist/components/containers/pane/DraggablePane.svelte +25 -63
- package/dist/components/containers/pane/Pane.svelte +1 -1
- package/dist/components/containers/pane/theme.js +32 -6
- package/dist/components/containers/popover/theme.d.ts +3 -3
- package/dist/components/date/theme.d.ts +6 -6
- package/dist/components/forms/checkbox/theme.js +1 -1
- package/dist/components/forms/command/theme.d.ts +3 -3
- package/dist/components/misc/Avatar.stories.svelte +56 -7
- package/dist/components/misc/Avatar.stories.svelte.d.ts +2 -17
- package/dist/components/misc/Avatar.svelte +82 -8
- package/dist/components/misc/Avatar.svelte.d.ts +5 -0
- package/dist/components/misc/theme.d.ts +6 -0
- package/dist/components/misc/theme.js +5 -3
- package/dist/components/misc/types.d.ts +17 -1
- package/dist/components/nav/appbar/theme.js +2 -1
- package/dist/components/nav/currentRoute.d.ts +11 -0
- package/dist/components/nav/currentRoute.js +28 -0
- package/dist/components/nav/navbar/NavbarItem.svelte +3 -6
- package/dist/components/nav/navbar/theme.d.ts +3 -3
- package/dist/components/nav/rail/Rail.mdx +65 -23
- package/dist/components/nav/rail/Rail.stories.svelte +24 -4
- package/dist/components/nav/rail/Rail.stories.svelte.d.ts +2 -17
- package/dist/components/nav/rail/Rail.svelte +163 -48
- package/dist/components/nav/rail/Rail.svelte.d.ts +6 -3
- package/dist/components/nav/rail/RailItem.svelte +122 -65
- package/dist/components/nav/rail/RailItem.svelte.d.ts +3 -2
- package/dist/components/nav/rail/theme.d.ts +69 -84
- package/dist/components/nav/rail/theme.js +79 -66
- package/dist/components/nav/tabs/Tabs.stories.svelte +2 -1
- package/dist/index.css +2 -0
- package/dist/styles/components.css +50 -0
- package/dist/styles/icon.css +13 -0
- package/dist/utils/icon/Icon.stories.svelte +97 -0
- package/dist/utils/icon/Icon.stories.svelte.d.ts +4 -0
- package/dist/utils/icon/Icon.svelte +101 -12
- package/dist/utils/icon/Icon.svelte.d.ts +9 -0
- package/dist/utils/icon/MaterialSymbolsProvider.svelte +11 -4
- package/dist/utils/icon/base-icons.d.ts +1 -1
- package/dist/utils/icon/base-icons.js +1 -1
- package/dist/utils/icon/icon-names.d.ts +2 -0
- package/dist/utils/icon/icon-names.js +2 -0
- package/dist/utils/icon/swap.d.ts +6 -0
- package/dist/utils/icon/swap.js +45 -0
- package/dist/utils/icon/types.d.ts +59 -7
- package/dist/utils/tv.d.ts +3 -4
- package/dist/utils/tv.js +14 -6
- package/package.json +2 -1
|
@@ -18,8 +18,9 @@ import * as RailStories from './Rail.stories.svelte';
|
|
|
18
18
|
|
|
19
19
|
## Anatomy
|
|
20
20
|
|
|
21
|
-
- **`Rail`** is the container: the sidebar,
|
|
22
|
-
scrim shown while it's expanded on medium windows.
|
|
21
|
+
- **`Rail`** is the container: the sidebar, the room it takes in the page (see Layout), and the
|
|
22
|
+
scrim shown while it's expanded on medium windows. An optional header holds the menu toggle and
|
|
23
|
+
a FAB.
|
|
23
24
|
- **`RailItem`** is one destination. It detects the active route, shows an optional `badge` (a
|
|
24
25
|
count, or `-1` for a dot), and lays itself out for the collapsed or expanded rail.
|
|
25
26
|
- **`railStore`** exposes the collapsed state to components outside the rail.
|
|
@@ -29,40 +30,79 @@ import * as RailStories from './Rail.stories.svelte';
|
|
|
29
30
|
The rail shows from `md` up and is hidden below it. For small windows, mount a `Navbar` with the
|
|
30
31
|
same destinations; it hides itself from `md` up, so the two never show together.
|
|
31
32
|
|
|
32
|
-
| Window | Collapsed rail | Expanded rail
|
|
33
|
-
| -------------- | -------------- |
|
|
34
|
-
| Small (< `md`) | Hidden | Hidden
|
|
35
|
-
| Medium (`md`) | 96dp |
|
|
36
|
-
| Large (`lg`+) | 96dp |
|
|
33
|
+
| Window | Collapsed rail | Expanded rail |
|
|
34
|
+
| -------------- | -------------- | ------------------------------------------------------------------------ |
|
|
35
|
+
| Small (< `md`) | Hidden | Hidden |
|
|
36
|
+
| Medium (`md`) | 96dp | **Modal**: over a scrim; closes on scrim click, Escape, or a destination |
|
|
37
|
+
| Large (`lg`+) | 96dp | **Standard**: pushes content aside, stays open until toggled |
|
|
38
|
+
|
|
39
|
+
## Spec
|
|
40
|
+
|
|
41
|
+
Values follow the M3 Expressive
|
|
42
|
+
[navigation rail tokens](https://m3.material.io/components/navigation-rail/specs). The container
|
|
43
|
+
uses the spec's optional `surface-container` role rather than the default `surface`: that's the
|
|
44
|
+
window colour `App` paints, so the rail belongs to the window and `surface` panes sit beside it. The off-grid
|
|
45
|
+
ones are `--md-comp-nav-rail-*` properties in `styles/components.css`, so a theme can override
|
|
46
|
+
them.
|
|
47
|
+
|
|
48
|
+
| | Collapsed | Expanded |
|
|
49
|
+
| ---------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
50
|
+
| Container | 96dp, `surface` | Hugs content, 220–360dp. Modal adds elevation 3 and 16dp corners |
|
|
51
|
+
| Top space | 44dp, then at least 40dp below the header | Same |
|
|
52
|
+
| Destination | 64dp tall, 4dp apart | 56dp tall, touching, 20dp side padding |
|
|
53
|
+
| Active indicator | 56×32dp around the icon, label 4dp below | 56dp pill around icon and label, 16dp padding, 8dp gap |
|
|
54
|
+
| Label | Label medium; active `secondary` | Label large; active `on-secondary-container` |
|
|
55
|
+
| States | `on-secondary-container` layer: 8% hover, 10% focus and pressed, on the indicator | Same |
|
|
56
|
+
|
|
57
|
+
The whole row is the target, however narrow the indicator. The active icon fills; its weight
|
|
58
|
+
stays the same.
|
|
59
|
+
|
|
60
|
+
## Expanding
|
|
61
|
+
|
|
62
|
+
Expanding is one spatial spring, following Compose's `WideNavigationRail`. The width, the gap
|
|
63
|
+
between destinations (4dp → 0), each destination's height (64 → 56dp), and its indicator
|
|
64
|
+
(56×32 → the full pill) all move together. The indicator stays anchored and the icon keeps its x,
|
|
65
|
+
so nothing moves sideways. The label fades out, switches from under the icon to beside it at the
|
|
66
|
+
halfway point (label medium → label large), then slides in as it fades back. An interrupted
|
|
67
|
+
spring reverses from where it is. Under reduced motion the rail jumps to the end state.
|
|
68
|
+
|
|
69
|
+
Selecting a destination grows its active indicator from the centre out to its edges on
|
|
70
|
+
`fastSpatial`, fading in as it grows, and the one losing the selection shrinks back the same way
|
|
71
|
+
(after Compose's `NavigationRailItem`). The state layer stays on top of it. Under reduced motion
|
|
72
|
+
the indicator just appears.
|
|
37
73
|
|
|
38
74
|
## Layout
|
|
39
75
|
|
|
40
|
-
|
|
41
|
-
|
|
76
|
+
Mount the rail anywhere inside `App`; the page makes room for it on its own. A
|
|
77
|
+
viewport-anchored rail publishes `--md-rail-inset` on `<html>`: 0 below `md`, the collapsed
|
|
78
|
+
width on medium windows (the expanded rail is modal there and overlays), and the rail's live
|
|
79
|
+
width from `lg`, so the content moves aside in step with the spring. `App`'s shell pads by it and
|
|
80
|
+
`AppBar` starts at it, so there's no `md:ml-24` anywhere. A custom shell or another fixed surface
|
|
81
|
+
uses `ps-(--md-rail-inset)` / `left-(--md-rail-inset)`.
|
|
42
82
|
|
|
43
83
|
```svelte
|
|
44
|
-
<
|
|
84
|
+
<App>
|
|
45
85
|
<Rail bind:collapsed>
|
|
46
86
|
<RailItem label="Home" href="/" iconProps={{ name: 'house' }} />
|
|
47
87
|
<RailItem label="Messages" href="/messages" iconProps={{ name: 'mail' }} badge={5} />
|
|
48
88
|
</Rail>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<NavbarItem label="Messages" href="/messages" iconProps={{ name: 'mail' }} badge={5} />
|
|
57
|
-
</Navbar>
|
|
89
|
+
<AppBar title="Home" />
|
|
90
|
+
{@render children()}
|
|
91
|
+
<Navbar>
|
|
92
|
+
<NavbarItem label="Home" href="/" iconProps={{ name: 'house' }} />
|
|
93
|
+
<NavbarItem label="Messages" href="/messages" iconProps={{ name: 'mail' }} badge={5} />
|
|
94
|
+
</Navbar>
|
|
95
|
+
</App>
|
|
58
96
|
```
|
|
59
97
|
|
|
60
98
|
- **Under an app bar:** pass the bar's height in px to `railTop` so the rail starts below it.
|
|
61
99
|
- **Anchoring:** `anchor="viewport"` (the default) fixes the rail to the window's leading edge.
|
|
62
100
|
`anchor="parent"` positions it inside the nearest positioned ancestor, for embedded layouts
|
|
63
|
-
and demos.
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
and demos. It doesn't touch `--md-rail-inset`; instead it renders a spacer beside itself, so
|
|
102
|
+
put it and the content in a flex row.
|
|
103
|
+
- **Extras:** the `fab` snippet places a FAB above the destinations, in the same 56dp column as
|
|
104
|
+
the indicators. `expandable={false}` removes the menu toggle; with no FAB either, the header
|
|
105
|
+
and its 40dp gap go too.
|
|
66
106
|
|
|
67
107
|
<Canvas of={RailStories.WithFAB} />
|
|
68
108
|
|
|
@@ -70,4 +110,6 @@ content in a flex row and the content moves aside on its own, with no `margin-le
|
|
|
70
110
|
|
|
71
111
|
- **Active item.** The active item gets `aria-current="page"`.
|
|
72
112
|
- **Disabled items** set `aria-disabled` and leave the tab order but stay visible.
|
|
73
|
-
- **Keyboard.** Every item is a link reachable with Tab; the
|
|
113
|
+
- **Keyboard.** Every item is a link reachable with Tab; the focus ring goes around the
|
|
114
|
+
indicator. The menu toggle is a real button with `aria-expanded`, and Escape closes the modal
|
|
115
|
+
rail.
|
|
@@ -25,6 +25,17 @@
|
|
|
25
25
|
});
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
// Playground: clicking a destination selects it, to show the indicator growing from its centre.
|
|
30
|
+
const destinations = [
|
|
31
|
+
{ label: 'Home', icon: 'house', badge: 0 },
|
|
32
|
+
{ label: 'Inbox', icon: 'mail', badge: 4 },
|
|
33
|
+
{ label: 'Chat', icon: 'chat', badge: -1 },
|
|
34
|
+
{ label: 'Settings', icon: 'settings', badge: 0 }
|
|
35
|
+
] as const;
|
|
36
|
+
let current = $state<string>('Home');
|
|
37
|
+
</script>
|
|
38
|
+
|
|
28
39
|
<Story name="Playground">
|
|
29
40
|
{#snippet template(args)}
|
|
30
41
|
<div class="bg-md-sys-color-surface-variant relative h-[420px] overflow-hidden rounded-2xl">
|
|
@@ -34,10 +45,19 @@
|
|
|
34
45
|
expandable={args.expandable}
|
|
35
46
|
rounded={args.rounded}
|
|
36
47
|
>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
{#each destinations as d (d.label)}
|
|
49
|
+
<RailItem
|
|
50
|
+
label={d.label}
|
|
51
|
+
href="#{d.label.toLowerCase()}"
|
|
52
|
+
iconProps={{ name: d.icon }}
|
|
53
|
+
badge={d.badge}
|
|
54
|
+
selected={current === d.label}
|
|
55
|
+
onclick={(e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
current = d.label;
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
{/each}
|
|
41
61
|
<RailItem label="Disabled" href="#" iconProps={{ name: 'block' }} disabled />
|
|
42
62
|
</Rail>
|
|
43
63
|
</div>
|
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
import Rail from './Rail.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 Rail: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
-
[evt: string]: CustomEvent<any>;
|
|
17
|
-
}, {}, {}, string>;
|
|
18
|
-
type Rail = InstanceType<typeof Rail>;
|
|
2
|
+
declare const Rail: import("svelte").Component<Record<string, never>, {}, "">;
|
|
3
|
+
type Rail = ReturnType<typeof Rail>;
|
|
19
4
|
export default Rail;
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
@component
|
|
3
3
|
Material 3 Navigation Rail.
|
|
4
4
|
|
|
5
|
-
Navigation rails
|
|
6
|
-
|
|
5
|
+
Navigation rails give access to primary destinations on medium and larger windows. Collapsed it's
|
|
6
|
+
96dp wide; expanded it shows labels beside the icons. On large windows the expanded rail pushes
|
|
7
|
+
the content aside (standard); on medium windows it opens over it, above a scrim (modal), and
|
|
8
|
+
closes on a scrim click, Escape, or picking a destination. Below `md` it's hidden: mount a
|
|
9
|
+
`Navbar` for small windows.
|
|
7
10
|
|
|
8
|
-
@see https://m3.material.io/components/navigation-rail/
|
|
11
|
+
@see https://m3.material.io/components/navigation-rail/specs
|
|
9
12
|
-->
|
|
10
13
|
<script lang="ts">
|
|
11
|
-
import { clickOutside } from '../../../attachments/index.js';
|
|
12
14
|
import clsx from 'clsx';
|
|
13
15
|
import { setContext, untrack } from 'svelte';
|
|
16
|
+
import { animate, type AnimationPlaybackControls } from 'motion';
|
|
14
17
|
import type { RailProps } from './types';
|
|
15
18
|
import ButtonIcon from '../../buttons/ButtonIcon.svelte';
|
|
19
|
+
import { springTokens, springTransition } from '../../../animation/spring.js';
|
|
16
20
|
import { rail } from './theme';
|
|
17
21
|
import { NavigationMenu } from 'bits-ui';
|
|
18
22
|
import { railStore } from './railStore.svelte.js';
|
|
@@ -27,13 +31,19 @@ On mobile (< md) it automatically renders as a bottom navigation bar.
|
|
|
27
31
|
railTop = 0,
|
|
28
32
|
expandLabel = 'Expand navigation',
|
|
29
33
|
collapseLabel = 'Collapse navigation',
|
|
30
|
-
class: className
|
|
34
|
+
class: className,
|
|
35
|
+
showHelp: _showHelp,
|
|
36
|
+
withNavbar: _withNavbar,
|
|
37
|
+
...restProps
|
|
31
38
|
}: RailProps = $props();
|
|
32
39
|
|
|
33
40
|
const expanded = $derived(!collapsed);
|
|
34
41
|
const railCtx = {
|
|
35
42
|
get collapsed() {
|
|
36
43
|
return collapsed;
|
|
44
|
+
},
|
|
45
|
+
get progress() {
|
|
46
|
+
return progress;
|
|
37
47
|
}
|
|
38
48
|
};
|
|
39
49
|
setContext('rail', railCtx);
|
|
@@ -42,64 +52,169 @@ On mobile (< md) it automatically renders as a bottom navigation bar.
|
|
|
42
52
|
railStore.collapsed = collapsed;
|
|
43
53
|
});
|
|
44
54
|
|
|
45
|
-
const
|
|
46
|
-
const railBaseClass = $derived(`${base({ class: clsx(className) })} rail-base`);
|
|
55
|
+
const styles = $derived(rail({ expanded, anchor, rounded, header: expandable || !!fab }));
|
|
47
56
|
const toggleClass = $derived(collapsed ? 'cursor-e-resize' : 'cursor-w-resize');
|
|
48
|
-
|
|
57
|
+
|
|
58
|
+
let railEl = $state<HTMLElement>();
|
|
59
|
+
let ghostEl = $state<HTMLElement>();
|
|
60
|
+
let scrimEl = $state<HTMLElement>();
|
|
61
|
+
|
|
62
|
+
/* The scrim is only displayed while the expanded rail is modal (medium windows). */
|
|
63
|
+
const isModal = () => !!scrimEl && getComputedStyle(scrimEl).display !== 'none';
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
Making room for the rail. A viewport-anchored rail publishes its drawn width on <html>
|
|
67
|
+
(--md-rail-width, every frame while it springs) plus --md-rail-rest, its collapsed width;
|
|
68
|
+
styles/components.css turns them into --md-rail-inset per window size, which App's shell and
|
|
69
|
+
AppBar use. A parent-anchored rail instead renders a ghost in the layout that holds its place
|
|
70
|
+
the same way: the rail's width from lg, the collapsed width on medium windows, where the
|
|
71
|
+
expanded rail overlays the content.
|
|
72
|
+
*/
|
|
73
|
+
$effect(() => {
|
|
74
|
+
const el = railEl;
|
|
75
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
76
|
+
const target = anchor === 'viewport' ? document.documentElement : ghostEl;
|
|
77
|
+
if (!target) return;
|
|
78
|
+
if (anchor === 'viewport') {
|
|
79
|
+
target.style.setProperty(
|
|
80
|
+
'--md-rail-rest',
|
|
81
|
+
'var(--md-comp-nav-rail-collapsed-container-width)'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const property = anchor === 'viewport' ? '--md-rail-width' : '--rail-width';
|
|
85
|
+
const observer = new ResizeObserver(() => {
|
|
86
|
+
target.style.setProperty(property, `${el.offsetWidth}px`);
|
|
87
|
+
});
|
|
88
|
+
observer.observe(el);
|
|
89
|
+
return () => {
|
|
90
|
+
observer.disconnect();
|
|
91
|
+
target.style.removeProperty(property);
|
|
92
|
+
if (anchor === 'viewport') target.style.removeProperty('--md-rail-rest');
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
Expanding is one spring on `progress`, 0 collapsed to 1 expanded, on spatial: a container
|
|
98
|
+
reshaping, not a navigation transition, so no fastSpatial bounce. It's the choreography of
|
|
99
|
+
Compose's WideNavigationRail: `--rail-p` carries the progress to the destinations (their
|
|
100
|
+
height, indicator and icon interpolate on it; the label fades out, swaps sides at the halfway
|
|
101
|
+
point, and fades back in) and to the gap between them, and the width follows it here.
|
|
102
|
+
|
|
103
|
+
The classes hold each end state, so SSR and reduced motion need no JS. For the width, measure
|
|
104
|
+
where the rail is drawn before the change and where the new classes put it, and map the
|
|
105
|
+
progress onto that span; an interrupted spring picks up from where it's drawn.
|
|
106
|
+
*/
|
|
107
|
+
const SPRING = springTransition(springTokens.spatial);
|
|
108
|
+
let progress = $state(untrack(() => (collapsed ? 0 : 1)));
|
|
109
|
+
let widthFrom: number | null = null;
|
|
110
|
+
let progressFrom = 0;
|
|
111
|
+
let spring: AnimationPlaybackControls | undefined;
|
|
112
|
+
|
|
113
|
+
const clearWidth = (el: HTMLElement) =>
|
|
114
|
+
Object.assign(el.style, { width: '', minWidth: '', maxWidth: '' });
|
|
115
|
+
|
|
116
|
+
$effect.pre(() => {
|
|
117
|
+
void expanded;
|
|
118
|
+
if (!railEl) return;
|
|
119
|
+
widthFrom = railEl.getBoundingClientRect().width;
|
|
120
|
+
progressFrom = untrack(() => progress);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
$effect(() => {
|
|
124
|
+
const target = expanded ? 1 : 0;
|
|
125
|
+
const el = railEl;
|
|
126
|
+
const from = widthFrom;
|
|
127
|
+
widthFrom = null;
|
|
128
|
+
if (!el || from === null) return;
|
|
129
|
+
|
|
130
|
+
spring?.stop();
|
|
131
|
+
clearWidth(el);
|
|
132
|
+
const to = el.getBoundingClientRect().width;
|
|
133
|
+
if (progressFrom === target || matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
134
|
+
progress = target;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Lift the expanded min/max for the ride, or the width would snap to 220dp at the start.
|
|
139
|
+
Object.assign(el.style, { width: `${from}px`, minWidth: '0', maxWidth: 'none' });
|
|
140
|
+
const start = progressFrom;
|
|
141
|
+
const controls = animate(start, target, {
|
|
142
|
+
...SPRING,
|
|
143
|
+
onUpdate: (p) => {
|
|
144
|
+
progress = p;
|
|
145
|
+
el.style.width = `${from + ((to - from) * (p - start)) / (target - start)}px`;
|
|
146
|
+
},
|
|
147
|
+
onComplete: () => {
|
|
148
|
+
progress = target;
|
|
149
|
+
clearWidth(el);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
spring = controls;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function onKeydown(event: KeyboardEvent) {
|
|
156
|
+
if (event.key === 'Escape' && expanded && isModal()) {
|
|
157
|
+
collapsed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Picking a destination closes the modal rail; the standard one stays as it is.
|
|
162
|
+
function onNavClick(event: MouseEvent) {
|
|
163
|
+
if (expanded && isModal() && (event.target as Element).closest('a[href]')) {
|
|
164
|
+
collapsed = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
49
167
|
</script>
|
|
50
168
|
|
|
51
|
-
|
|
52
|
-
|
|
169
|
+
<svelte:window onkeydown={onKeydown} />
|
|
170
|
+
|
|
171
|
+
{#if anchor === 'parent'}
|
|
172
|
+
<div
|
|
173
|
+
bind:this={ghostEl}
|
|
174
|
+
class={styles.ghost()}
|
|
175
|
+
style="--rail-width: var(--md-comp-nav-rail-collapsed-container-width)"
|
|
176
|
+
></div>
|
|
177
|
+
{/if}
|
|
53
178
|
|
|
54
|
-
<!-- Tablet scrim (md:block lg:hidden) — overlays content on expand -->
|
|
55
179
|
<div
|
|
56
|
-
|
|
57
|
-
|
|
180
|
+
bind:this={scrimEl}
|
|
181
|
+
class={styles.scrim()}
|
|
58
182
|
onclick={() => (collapsed = true)}
|
|
59
183
|
role="presentation"
|
|
60
184
|
></div>
|
|
61
185
|
|
|
62
|
-
<!-- Desktop/tablet sidebar rail -->
|
|
63
186
|
<div
|
|
64
|
-
|
|
65
|
-
|
|
187
|
+
bind:this={railEl}
|
|
188
|
+
{...restProps}
|
|
189
|
+
class={styles.base({ class: clsx(className) })}
|
|
190
|
+
style:--rail-top="{railTop}px"
|
|
191
|
+
style:--rail-p={progress}
|
|
66
192
|
data-expanded={expanded}
|
|
67
|
-
{@attach expanded && clickOutside(() => (collapsed = true))}
|
|
68
193
|
>
|
|
69
|
-
{#if expandable}
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
194
|
+
{#if expandable || fab}
|
|
195
|
+
<div class={styles.header()}>
|
|
196
|
+
{#if expandable}
|
|
197
|
+
<div class={styles.menu()}>
|
|
198
|
+
<ButtonIcon
|
|
199
|
+
variant="standard"
|
|
200
|
+
type="button"
|
|
201
|
+
tooltipContent={collapsed ? expandLabel : collapseLabel}
|
|
202
|
+
iconProps={{ name: collapsed ? 'menu' : 'menu_open', transition: 'rotate' }}
|
|
203
|
+
aria-expanded={expanded}
|
|
204
|
+
class={toggleClass}
|
|
205
|
+
onclick={() => (collapsed = !collapsed)}
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
79
209
|
|
|
80
|
-
|
|
210
|
+
{@render fab?.()}
|
|
211
|
+
</div>
|
|
212
|
+
{/if}
|
|
81
213
|
|
|
82
|
-
|
|
83
|
-
|
|
214
|
+
<!-- The click is delegated from the links inside; Enter on a link fires it too. -->
|
|
215
|
+
<NavigationMenu.Root orientation="vertical" class={styles.nav()} onclick={onNavClick}>
|
|
216
|
+
<NavigationMenu.List class={styles.items()}>
|
|
84
217
|
{@render children?.()}
|
|
85
218
|
</NavigationMenu.List>
|
|
86
219
|
</NavigationMenu.Root>
|
|
87
220
|
</div>
|
|
88
|
-
|
|
89
|
-
<style>
|
|
90
|
-
.rail-scrim {
|
|
91
|
-
opacity: 0;
|
|
92
|
-
pointer-events: none;
|
|
93
|
-
background-color: transparent;
|
|
94
|
-
z-index: -1;
|
|
95
|
-
transition: opacity var(--md-sys-motion-duration-effects-spring)
|
|
96
|
-
var(--md-sys-motion-timing-function-effects-spring);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.rail-scrim[data-expanded='true'] {
|
|
100
|
-
opacity: 1;
|
|
101
|
-
pointer-events: auto;
|
|
102
|
-
background-color: rgb(0 0 0 / 0.4);
|
|
103
|
-
z-index: var(--z-layer-rail);
|
|
104
|
-
}
|
|
105
|
-
</style>
|
|
@@ -2,10 +2,13 @@ import type { RailProps } from './types';
|
|
|
2
2
|
/**
|
|
3
3
|
* Material 3 Navigation Rail.
|
|
4
4
|
*
|
|
5
|
-
* Navigation rails
|
|
6
|
-
*
|
|
5
|
+
* Navigation rails give access to primary destinations on medium and larger windows. Collapsed it's
|
|
6
|
+
* 96dp wide; expanded it shows labels beside the icons. On large windows the expanded rail pushes
|
|
7
|
+
* the content aside (standard); on medium windows it opens over it, above a scrim (modal), and
|
|
8
|
+
* closes on a scrim click, Escape, or picking a destination. Below `md` it's hidden: mount a
|
|
9
|
+
* `Navbar` for small windows.
|
|
7
10
|
*
|
|
8
|
-
* @see https://m3.material.io/components/navigation-rail/
|
|
11
|
+
* @see https://m3.material.io/components/navigation-rail/specs
|
|
9
12
|
*/
|
|
10
13
|
declare const Rail: import("svelte").Component<RailProps, {}, "collapsed">;
|
|
11
14
|
type Rail = ReturnType<typeof Rail>;
|