@noxlovette/material 0.4.8 → 0.5.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/dist/animation/Shapes.stories.svelte +56 -0
- package/dist/animation/Shapes.stories.svelte.d.ts +3 -0
- package/dist/animation/TransitionPatterns.stories.svelte +359 -0
- package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
- package/dist/animation/containerTransform.d.ts +35 -12
- package/dist/animation/containerTransform.js +31 -138
- package/dist/animation/enterExit.d.ts +30 -3
- package/dist/animation/enterExit.js +69 -22
- package/dist/animation/index.d.ts +2 -1
- package/dist/animation/index.js +2 -1
- package/dist/animation/presence.svelte.d.ts +47 -0
- package/dist/animation/presence.svelte.js +84 -0
- package/dist/animation/shapeMorph.svelte.d.ts +52 -0
- package/dist/animation/shapeMorph.svelte.js +112 -0
- package/dist/animation/shapesAnimatableSmall.d.ts +28 -22
- package/dist/animation/shapesAnimatableSmall.js +32 -23
- package/dist/animation/sharedAxisTransition.d.ts +57 -37
- package/dist/animation/sharedAxisTransition.js +74 -22
- package/dist/animation/skeleton.d.ts +16 -0
- package/dist/animation/skeleton.js +28 -0
- package/dist/animation/spring.d.ts +61 -0
- package/dist/animation/spring.js +53 -0
- package/dist/animation/springValue.svelte.d.ts +22 -0
- package/dist/animation/springValue.svelte.js +42 -0
- package/dist/animation/transition.d.ts +3 -1
- package/dist/animation/transition.js +3 -1
- package/dist/attachments/clickOutside.d.ts +13 -0
- package/dist/attachments/clickOutside.js +18 -0
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/components/buttons/FAB.svelte +23 -16
- package/dist/components/buttons/FABMenu.svelte +24 -29
- package/dist/components/buttons/FABMenu.svelte.d.ts +2 -1
- package/dist/components/buttons/FABMenuItem.svelte +23 -20
- package/dist/components/buttons/FABMenuItem.svelte.d.ts +5 -1
- package/dist/components/buttons/split-button/SplitButton.svelte +10 -17
- package/dist/components/buttons/split-button/theme.js +1 -1
- package/dist/components/buttons/types.d.ts +3 -3
- package/dist/components/cards/theme.js +1 -1
- package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +8 -12
- package/dist/components/containers/bottom-sheet/BottomSheet.svelte +67 -55
- package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +1 -1
- package/dist/components/containers/bottom-sheet/types.d.ts +8 -2
- package/dist/components/containers/context-menu/ContextMenu.svelte +23 -36
- package/dist/components/containers/context-menu/theme.js +2 -2
- package/dist/components/containers/dialogue/Dialogue.svelte +49 -65
- package/dist/components/containers/link-preview/LinkPreview.svelte +12 -23
- package/dist/components/containers/link-preview/theme.d.ts +3 -3
- package/dist/components/containers/menu/Menu.svelte +14 -21
- package/dist/components/containers/menu/MenuSub.svelte +10 -17
- package/dist/components/containers/pane/DraggablePane.svelte +90 -5
- package/dist/components/containers/pane/DraggablePane.svelte.d.ts +1 -1
- package/dist/components/containers/pane/theme.js +2 -2
- package/dist/components/containers/popover/Popover.svelte +31 -42
- package/dist/components/containers/popover/theme.d.ts +3 -3
- package/dist/components/containers/scroll-area/theme.js +2 -2
- package/dist/components/containers/side-sheet/SideSheet.stories.svelte +10 -15
- package/dist/components/containers/side-sheet/SideSheet.svelte +44 -44
- package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +4 -3
- package/dist/components/containers/side-sheet/types.d.ts +8 -2
- package/dist/components/date/DateField.svelte +99 -92
- package/dist/components/date/DateField.svelte.d.ts +4 -5
- package/dist/components/date/DateRangeField.svelte +127 -155
- package/dist/components/date/DateRangeField.svelte.d.ts +4 -5
- package/dist/components/date/calendarMotion.d.ts +9 -0
- package/dist/components/date/calendarMotion.js +23 -0
- package/dist/components/date/theme.d.ts +95 -0
- package/dist/components/date/theme.js +103 -14
- package/dist/components/date/types.d.ts +13 -0
- package/dist/components/forms/checkbox/theme.js +2 -2
- package/dist/components/forms/command/theme.d.ts +3 -3
- package/dist/components/forms/command/theme.js +1 -1
- package/dist/components/forms/pin/theme.js +2 -2
- package/dist/components/forms/radio-group/theme.js +2 -2
- package/dist/components/forms/select/Select.svelte +42 -54
- package/dist/components/forms/select/theme.js +4 -4
- package/dist/components/forms/slider/Slider.svelte +144 -126
- package/dist/components/forms/slider/Slider.svelte.d.ts +3 -0
- package/dist/components/forms/slider/theme.d.ts +154 -48
- package/dist/components/forms/slider/theme.js +60 -20
- package/dist/components/forms/slider/types.d.ts +1 -1
- package/dist/components/forms/switch/theme.js +4 -4
- package/dist/components/forms/textfield/Textfield.svelte +1 -1
- package/dist/components/forms/textfield/theme.d.ts +12 -0
- package/dist/components/forms/textfield/theme.js +30 -10
- package/dist/components/forms/toggle-group/theme.js +1 -1
- package/dist/components/forms/tooltip/Tooltip.svelte +24 -35
- package/dist/components/nav/breadcrumb/theme.js +1 -1
- package/dist/components/nav/navbar/theme.js +2 -2
- package/dist/components/nav/rail/Rail.svelte +4 -5
- package/dist/components/nav/rail/theme.js +7 -7
- package/dist/components/nav/tabs/TabHolder.svelte +29 -3
- package/dist/components/nav/tabs/TabHolder.svelte.d.ts +3 -1
- package/dist/components/nav/tabs/theme.js +1 -1
- package/dist/components/progress/CircularProgress.svelte +78 -4
- package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
- package/dist/components/progress/LinearProgress.svelte +70 -7
- package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
- package/dist/components/progress/WavyLinearProgress.svelte +27 -3
- package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
- package/dist/components/progress/_wavy.d.ts +2 -0
- package/dist/components/progress/_wavy.js +3 -1
- package/dist/components/snackbar/Snackbar.svelte +13 -17
- package/dist/components/table/TableBody.svelte +3 -16
- package/dist/components/table/TableBody.svelte.d.ts +1 -5
- package/dist/components/table/TableHeader.svelte +1 -1
- package/dist/components/table/theme.js +1 -1
- package/dist/components/table/types.d.ts +0 -7
- package/dist/components/time/TimeField.stories.svelte +41 -0
- package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
- package/dist/components/time/TimeField.svelte +69 -62
- package/dist/components/time/TimeField.svelte.d.ts +6 -0
- package/dist/components/time/TimepickerInput.svelte +8 -4
- package/dist/components/time/types.d.ts +11 -1
- package/dist/components/toolbar/theme.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/component.css +21 -22
- package/dist/styles/motion.css +317 -38
- package/dist/utils/Layer.svelte +16 -29
- package/dist/utils/icon/LoadingIndicator.svelte +61 -22
- package/dist/utils/icon/loadingShapes.d.ts +1 -0
- package/dist/utils/icon/loadingShapes.js +16 -0
- package/package.json +5 -2
- package/dist/actions/clickOutside.d.ts +0 -3
- package/dist/actions/clickOutside.js +0 -13
- package/dist/actions/floating.d.ts +0 -20
- package/dist/actions/floating.js +0 -87
- package/dist/actions/index.d.ts +0 -6
- package/dist/actions/index.js +0 -6
- package/dist/actions/keyboard.d.ts +0 -3
- package/dist/actions/keyboard.js +0 -14
- package/dist/actions/portal.d.ts +0 -3
- package/dist/actions/portal.js +0 -8
- package/dist/actions/positionFloating.d.ts +0 -10
- package/dist/actions/positionFloating.js +0 -59
- package/dist/actions/rovingFocusGroup.d.ts +0 -20
- package/dist/actions/rovingFocusGroup.js +0 -82
- package/dist/animation/easing.d.ts +0 -6
- package/dist/animation/easing.js +0 -78
- package/dist/animation/outroClass.d.ts +0 -3
- package/dist/animation/outroClass.js +0 -20
- package/dist/animation/transitionTypes.d.ts +0 -8
- package/dist/animation/transitionTypes.js +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The 35 M3 Expressive shapes (https://m3.material.io/styles/shape), all morphable with Motion.
|
|
3
|
+
Pick a shape to morph the hero (720-point `animatableShapes`) and the icon-sized preview
|
|
4
|
+
(120-point `animatableShapesSmall`) to it on the `fastSpatial` spring.
|
|
5
|
+
-->
|
|
6
|
+
<script module lang="ts">
|
|
7
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
8
|
+
|
|
9
|
+
const { Story } = defineMeta({
|
|
10
|
+
title: 'Motion/Shapes',
|
|
11
|
+
tags: ['!autodocs']
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import Body from '../components/typography/body/Body.svelte';
|
|
17
|
+
import {
|
|
18
|
+
animatableShapes,
|
|
19
|
+
animatableShapesSmall,
|
|
20
|
+
shapeMorph,
|
|
21
|
+
shapeNames,
|
|
22
|
+
type ShapeName
|
|
23
|
+
} from './shapeMorph.svelte.js';
|
|
24
|
+
|
|
25
|
+
let selected = $state<ShapeName>('circle');
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<Story name="Shape library" asChild>
|
|
29
|
+
<div class="flex flex-col gap-6">
|
|
30
|
+
<div class="flex items-center gap-6">
|
|
31
|
+
<svg viewBox="0 0 380 380" class="text-md-sys-color-primary size-48" aria-hidden="true">
|
|
32
|
+
<path fill="currentColor" {@attach shapeMorph(() => animatableShapes[selected])} />
|
|
33
|
+
</svg>
|
|
34
|
+
<svg viewBox="0 0 48 48" class="text-md-sys-color-tertiary size-12" aria-hidden="true">
|
|
35
|
+
<path fill="currentColor" {@attach shapeMorph(() => animatableShapesSmall[selected])} />
|
|
36
|
+
</svg>
|
|
37
|
+
<Body>{selected}</Body>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="grid grid-cols-5 gap-2 sm:grid-cols-7">
|
|
41
|
+
{#each shapeNames as name (name)}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
aria-pressed={selected === name}
|
|
45
|
+
onclick={() => (selected = name)}
|
|
46
|
+
class="md-sys-typescale-label-small text-md-sys-color-on-surface-variant aria-pressed:bg-md-sys-color-secondary-container aria-pressed:text-md-sys-color-on-secondary-container flex flex-col items-center gap-1 rounded-md p-2"
|
|
47
|
+
>
|
|
48
|
+
<svg viewBox="0 0 48 48" class="size-10" aria-hidden="true">
|
|
49
|
+
<path fill="currentColor" d={animatableShapesSmall[name]} />
|
|
50
|
+
</svg>
|
|
51
|
+
{name}
|
|
52
|
+
</button>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</Story>
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The six M3 transition patterns, each on its own M3 shape, driven by `src/lib/animation/`.
|
|
3
|
+
https://m3.material.io/styles/motion/transitions/transition-patterns
|
|
4
|
+
Choosing and applying them: https://m3.material.io/styles/motion/transitions/applying-transitions
|
|
5
|
+
(summarized in .claude/skills/material-design/references/motion-guide.md → "Applying transitions").
|
|
6
|
+
|
|
7
|
+
Every demo loops by default; "Replay" retriggers it on demand. The Overview plays the six in
|
|
8
|
+
turn rather than all at once: container transform, forward/backward, lateral and top level run
|
|
9
|
+
on the View Transition API, which allows one transition per document at a time.
|
|
10
|
+
|
|
11
|
+
Excluded from autodocs — the docs page renders every story into one document, which would
|
|
12
|
+
duplicate the `data-demo` targets these transitions resolve.
|
|
13
|
+
-->
|
|
14
|
+
<script module lang="ts">
|
|
15
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
16
|
+
|
|
17
|
+
const { Story } = defineMeta({
|
|
18
|
+
title: 'Motion/Transition patterns',
|
|
19
|
+
tags: ['!autodocs'],
|
|
20
|
+
parameters: { layout: 'fullscreen' }
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import { tick } from 'svelte';
|
|
26
|
+
import type { Attachment } from 'svelte/attachments';
|
|
27
|
+
import Button from '../components/buttons/Button.svelte';
|
|
28
|
+
import Title from '../components/typography/title/Title.svelte';
|
|
29
|
+
import Body from '../components/typography/body/Body.svelte';
|
|
30
|
+
import {
|
|
31
|
+
containerTransform,
|
|
32
|
+
enterExit,
|
|
33
|
+
fadeThrough,
|
|
34
|
+
lateral,
|
|
35
|
+
Presence,
|
|
36
|
+
sharedAxis,
|
|
37
|
+
skeleton,
|
|
38
|
+
type NavigationDirection
|
|
39
|
+
} from './transition.js';
|
|
40
|
+
import {
|
|
41
|
+
pathFourLeafClover,
|
|
42
|
+
pathFourSidedCookie,
|
|
43
|
+
pathGem,
|
|
44
|
+
pathPentagon,
|
|
45
|
+
pathSoftBurst,
|
|
46
|
+
pathSunny
|
|
47
|
+
} from './shapes.js';
|
|
48
|
+
|
|
49
|
+
const LOOP_MS = 2200;
|
|
50
|
+
const OVERVIEW_STEP_MS = 1400;
|
|
51
|
+
|
|
52
|
+
let looping = $state(true);
|
|
53
|
+
|
|
54
|
+
/* Container transform — a cookie card expands into a detail surface and back. */
|
|
55
|
+
let expanded = $state(false);
|
|
56
|
+
const runContainer = () => {
|
|
57
|
+
const [from, to] = expanded
|
|
58
|
+
? ['[data-demo="ct-detail"]', '[data-demo="ct-card"]']
|
|
59
|
+
: ['[data-demo="ct-card"]', '[data-demo="ct-detail"]'];
|
|
60
|
+
return containerTransform(
|
|
61
|
+
async () => {
|
|
62
|
+
expanded = !expanded;
|
|
63
|
+
await tick();
|
|
64
|
+
},
|
|
65
|
+
{ from, to }
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/* Forward and backward — Inbox → Thread → Message and back up the hierarchy. */
|
|
70
|
+
const levels = ['Inbox', 'Thread', 'Message'];
|
|
71
|
+
let level = $state(0);
|
|
72
|
+
let descending = true;
|
|
73
|
+
const runAxis = () => {
|
|
74
|
+
if (level === levels.length - 1) descending = false;
|
|
75
|
+
if (level === 0) descending = true;
|
|
76
|
+
const direction: NavigationDirection = descending ? 'forward' : 'backward';
|
|
77
|
+
return sharedAxis(
|
|
78
|
+
async () => {
|
|
79
|
+
level += descending ? 1 : -1;
|
|
80
|
+
await tick();
|
|
81
|
+
},
|
|
82
|
+
{ target: '[data-demo="axis"]', axis: 'x', direction }
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/* Lateral — peer tabs, wrapping back to the first. */
|
|
87
|
+
const tabs = ['Photos', 'Albums', 'Search'];
|
|
88
|
+
let tab = $state(0);
|
|
89
|
+
const runLateral = () => {
|
|
90
|
+
const next = (tab + 1) % tabs.length;
|
|
91
|
+
const direction: NavigationDirection = next > tab ? 'forward' : 'backward';
|
|
92
|
+
return lateral(
|
|
93
|
+
async () => {
|
|
94
|
+
tab = next;
|
|
95
|
+
await tick();
|
|
96
|
+
},
|
|
97
|
+
{ target: '[data-demo="lateral"]', direction }
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/* Top level — unrelated destinations, as from a navigation bar. */
|
|
102
|
+
const destinations = [
|
|
103
|
+
{ label: 'Home', color: 'bg-md-sys-color-primary-container text-md-sys-color-primary' },
|
|
104
|
+
{ label: 'Library', color: 'bg-md-sys-color-secondary-container text-md-sys-color-secondary' },
|
|
105
|
+
{ label: 'Profile', color: 'bg-md-sys-color-tertiary-container text-md-sys-color-tertiary' }
|
|
106
|
+
];
|
|
107
|
+
let destination = $state(0);
|
|
108
|
+
const runTopLevel = () =>
|
|
109
|
+
fadeThrough(
|
|
110
|
+
async () => {
|
|
111
|
+
destination = (destination + 1) % destinations.length;
|
|
112
|
+
await tick();
|
|
113
|
+
},
|
|
114
|
+
{ target: '[data-demo="top"]' }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/* Enter and exit — a surface appears and leaves within the same screen. */
|
|
118
|
+
let popped = $state(true);
|
|
119
|
+
const pop = new Presence(() => popped);
|
|
120
|
+
const runEnterExit = () => (popped = !popped);
|
|
121
|
+
|
|
122
|
+
/* Skeleton loader — a pulsing placeholder, then the content fades in. */
|
|
123
|
+
let loaded = $state(false);
|
|
124
|
+
const content = new Presence(() => loaded);
|
|
125
|
+
const runSkeleton = () => (loaded = !loaded);
|
|
126
|
+
|
|
127
|
+
type Pattern = { id: string; title: string; summary: string; anchor: string; run: () => unknown };
|
|
128
|
+
|
|
129
|
+
const patterns = {
|
|
130
|
+
container: {
|
|
131
|
+
id: 'container',
|
|
132
|
+
title: 'Container transform',
|
|
133
|
+
summary: 'A container morphs into another, crossfading its content.',
|
|
134
|
+
anchor: 'container-transform',
|
|
135
|
+
run: runContainer
|
|
136
|
+
},
|
|
137
|
+
axis: {
|
|
138
|
+
id: 'axis',
|
|
139
|
+
title: 'Forward and backward',
|
|
140
|
+
summary: 'Content moves along a shared axis between hierarchy levels.',
|
|
141
|
+
anchor: 'forward-and-backward',
|
|
142
|
+
run: runAxis
|
|
143
|
+
},
|
|
144
|
+
lateral: {
|
|
145
|
+
id: 'lateral',
|
|
146
|
+
title: 'Lateral',
|
|
147
|
+
summary: 'Peer screens slide past each other edge to edge.',
|
|
148
|
+
anchor: 'lateral',
|
|
149
|
+
run: runLateral
|
|
150
|
+
},
|
|
151
|
+
top: {
|
|
152
|
+
id: 'top',
|
|
153
|
+
title: 'Top level',
|
|
154
|
+
summary: 'Unrelated destinations fade through each other.',
|
|
155
|
+
anchor: 'top-level',
|
|
156
|
+
run: runTopLevel
|
|
157
|
+
},
|
|
158
|
+
enterExit: {
|
|
159
|
+
id: 'enter-exit',
|
|
160
|
+
title: 'Enter and exit',
|
|
161
|
+
summary: 'Surfaces appear and leave within the current screen.',
|
|
162
|
+
anchor: 'enter-and-exit',
|
|
163
|
+
run: runEnterExit
|
|
164
|
+
},
|
|
165
|
+
skeleton: {
|
|
166
|
+
id: 'skeleton',
|
|
167
|
+
title: 'Skeleton loaders',
|
|
168
|
+
summary: 'A placeholder pulses until real content fades in.',
|
|
169
|
+
anchor: 'skeleton-loaders',
|
|
170
|
+
run: runSkeleton
|
|
171
|
+
}
|
|
172
|
+
} satisfies Record<string, Pattern>;
|
|
173
|
+
|
|
174
|
+
const all = Object.values(patterns);
|
|
175
|
+
|
|
176
|
+
/** Loops `run` while `looping` is on; toggling the loop restarts the interval. */
|
|
177
|
+
const loop =
|
|
178
|
+
(run: () => unknown, every = LOOP_MS): Attachment =>
|
|
179
|
+
() => {
|
|
180
|
+
if (!looping) return;
|
|
181
|
+
const id = setInterval(run, every);
|
|
182
|
+
return () => clearInterval(id);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Overview: one pattern per tick, round-robin, so view transitions never overlap. */
|
|
186
|
+
const roundRobin: Attachment = () => {
|
|
187
|
+
if (!looping) return;
|
|
188
|
+
let i = 0;
|
|
189
|
+
const id = setInterval(() => all[i++ % all.length].run(), OVERVIEW_STEP_MS);
|
|
190
|
+
return () => clearInterval(id);
|
|
191
|
+
};
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
{#snippet shape(path: string, cls: string)}
|
|
195
|
+
<svg viewBox="0 0 380 380" class={cls} aria-hidden="true">
|
|
196
|
+
<path d={path} fill="currentColor" />
|
|
197
|
+
</svg>
|
|
198
|
+
{/snippet}
|
|
199
|
+
|
|
200
|
+
{#snippet stage(pattern: Pattern)}
|
|
201
|
+
{#if pattern.id === 'container'}
|
|
202
|
+
{#if !expanded}
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
data-demo="ct-card"
|
|
206
|
+
class="bg-md-sys-color-primary-container text-md-sys-color-on-primary-container absolute top-4 left-4 flex items-center gap-3 rounded-lg p-3"
|
|
207
|
+
onclick={runContainer}
|
|
208
|
+
>
|
|
209
|
+
{@render shape(pathFourSidedCookie, 'size-10 text-md-sys-color-primary')}
|
|
210
|
+
<span class="md-sys-typescale-label-large">Open</span>
|
|
211
|
+
</button>
|
|
212
|
+
{:else}
|
|
213
|
+
<div
|
|
214
|
+
data-demo="ct-detail"
|
|
215
|
+
class="bg-md-sys-color-surface-container-highest text-md-sys-color-on-surface absolute inset-3 flex flex-col items-center justify-center gap-3 rounded-xl"
|
|
216
|
+
>
|
|
217
|
+
{@render shape(pathFourSidedCookie, 'size-24 text-md-sys-color-primary')}
|
|
218
|
+
<span class="md-sys-typescale-title-medium">Detail</span>
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
{:else if pattern.id === 'axis'}
|
|
222
|
+
<div data-demo="axis" class="absolute inset-0 flex flex-col items-center justify-center gap-3">
|
|
223
|
+
{@render shape(
|
|
224
|
+
pathPentagon,
|
|
225
|
+
[
|
|
226
|
+
'size-16 text-md-sys-color-primary',
|
|
227
|
+
'size-20 text-md-sys-color-secondary',
|
|
228
|
+
'size-24 text-md-sys-color-tertiary'
|
|
229
|
+
][level]
|
|
230
|
+
)}
|
|
231
|
+
<span class="md-sys-typescale-title-medium text-md-sys-color-on-surface">
|
|
232
|
+
{levels.slice(0, level + 1).join(' › ')}
|
|
233
|
+
</span>
|
|
234
|
+
</div>
|
|
235
|
+
{:else if pattern.id === 'lateral'}
|
|
236
|
+
<div class="absolute inset-x-0 top-0 flex justify-center gap-6 pt-3">
|
|
237
|
+
{#each tabs as name, i (name)}
|
|
238
|
+
<span
|
|
239
|
+
class={[
|
|
240
|
+
'md-sys-typescale-label-large',
|
|
241
|
+
i === tab ? 'text-md-sys-color-primary' : 'text-md-sys-color-on-surface-variant'
|
|
242
|
+
]}>{name}</span
|
|
243
|
+
>
|
|
244
|
+
{/each}
|
|
245
|
+
</div>
|
|
246
|
+
<div
|
|
247
|
+
data-demo="lateral"
|
|
248
|
+
class="bg-md-sys-color-surface-container absolute inset-x-3 top-11 bottom-3 flex items-center justify-center rounded-lg"
|
|
249
|
+
>
|
|
250
|
+
{@render shape(pathFourLeafClover, 'size-24 text-md-sys-color-primary')}
|
|
251
|
+
<span
|
|
252
|
+
class="md-sys-typescale-display-small text-md-sys-color-on-primary absolute"
|
|
253
|
+
aria-hidden="true">{tab + 1}</span
|
|
254
|
+
>
|
|
255
|
+
</div>
|
|
256
|
+
{:else if pattern.id === 'top'}
|
|
257
|
+
<div
|
|
258
|
+
data-demo="top"
|
|
259
|
+
class={[
|
|
260
|
+
'absolute inset-3 flex flex-col items-center justify-center gap-3 rounded-lg',
|
|
261
|
+
destinations[destination].color
|
|
262
|
+
]}
|
|
263
|
+
>
|
|
264
|
+
{@render shape(pathSunny, 'size-24')}
|
|
265
|
+
<span class="md-sys-typescale-title-medium">{destinations[destination].label}</span>
|
|
266
|
+
</div>
|
|
267
|
+
{:else if pattern.id === 'enter-exit'}
|
|
268
|
+
{#if pop.mounted}
|
|
269
|
+
<div
|
|
270
|
+
class="bg-md-sys-color-surface-container-high shadow-elevation-3 absolute inset-x-8 inset-y-6 flex items-center justify-center rounded-xl"
|
|
271
|
+
{@attach pop.attach(enterExit.scale)}
|
|
272
|
+
>
|
|
273
|
+
{@render shape(pathSoftBurst, 'size-24 text-md-sys-color-tertiary')}
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
276
|
+
{:else if pattern.id === 'skeleton'}
|
|
277
|
+
<div class="absolute inset-3 grid [&>*]:[grid-area:1/1]">
|
|
278
|
+
{#if !loaded}
|
|
279
|
+
<div class="flex flex-col items-center justify-center gap-3" {@attach skeleton}>
|
|
280
|
+
<div class="bg-md-sys-color-surface-container-highest size-24 rounded-full"></div>
|
|
281
|
+
<div class="bg-md-sys-color-surface-container-highest h-4 w-28 rounded-full"></div>
|
|
282
|
+
</div>
|
|
283
|
+
{/if}
|
|
284
|
+
{#if content.mounted}
|
|
285
|
+
<div
|
|
286
|
+
class="flex flex-col items-center justify-center gap-3"
|
|
287
|
+
{@attach content.attach(enterExit.fade)}
|
|
288
|
+
>
|
|
289
|
+
{@render shape(pathGem, 'size-24 text-md-sys-color-secondary')}
|
|
290
|
+
<span class="md-sys-typescale-title-medium text-md-sys-color-on-surface">Loaded</span>
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
{/snippet}
|
|
296
|
+
|
|
297
|
+
{#snippet demo(pattern: Pattern, tall = false)}
|
|
298
|
+
<section
|
|
299
|
+
class="bg-md-sys-color-surface-container-low text-md-sys-color-on-surface flex flex-col gap-3 rounded-xl p-4"
|
|
300
|
+
>
|
|
301
|
+
<header class="flex items-start justify-between gap-3">
|
|
302
|
+
<div class="flex flex-col gap-1">
|
|
303
|
+
<Title size="medium">{pattern.title}</Title>
|
|
304
|
+
<Body size="small" class="text-md-sys-color-on-surface-variant">{pattern.summary}</Body>
|
|
305
|
+
</div>
|
|
306
|
+
<Button variant="tonal" size="sm" onclick={pattern.run}>Replay</Button>
|
|
307
|
+
</header>
|
|
308
|
+
<div
|
|
309
|
+
class={['bg-md-sys-color-surface relative overflow-clip rounded-lg', tall ? 'h-96' : 'h-60']}
|
|
310
|
+
>
|
|
311
|
+
{@render stage(pattern)}
|
|
312
|
+
</div>
|
|
313
|
+
<a
|
|
314
|
+
class="text-link md-sys-typescale-label-small self-start"
|
|
315
|
+
href="https://m3.material.io/styles/motion/transitions/transition-patterns#{pattern.anchor}"
|
|
316
|
+
target="_blank"
|
|
317
|
+
rel="noreferrer">M3 spec</a
|
|
318
|
+
>
|
|
319
|
+
</section>
|
|
320
|
+
{/snippet}
|
|
321
|
+
|
|
322
|
+
{#snippet toolbar()}
|
|
323
|
+
<div class="flex items-center justify-between gap-4">
|
|
324
|
+
<Title size="large">M3 transition patterns</Title>
|
|
325
|
+
<Button variant={looping ? 'filled' : 'outlined'} onclick={() => (looping = !looping)}>
|
|
326
|
+
{looping ? 'Pause loop' : 'Loop'}
|
|
327
|
+
</Button>
|
|
328
|
+
</div>
|
|
329
|
+
{/snippet}
|
|
330
|
+
|
|
331
|
+
<Story name="Overview" asChild>
|
|
332
|
+
<div class="bg-md-sys-color-surface flex min-h-dvh flex-col gap-6 p-6" {@attach roundRobin}>
|
|
333
|
+
{@render toolbar()}
|
|
334
|
+
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
335
|
+
{#each all as pattern (pattern.id)}
|
|
336
|
+
{@render demo(pattern)}
|
|
337
|
+
{/each}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</Story>
|
|
341
|
+
|
|
342
|
+
{#snippet single(pattern: Pattern)}
|
|
343
|
+
<div
|
|
344
|
+
class="bg-md-sys-color-surface flex min-h-dvh flex-col gap-6 p-6"
|
|
345
|
+
{@attach loop(pattern.run)}
|
|
346
|
+
>
|
|
347
|
+
{@render toolbar()}
|
|
348
|
+
<div class="mx-auto w-full max-w-xl">
|
|
349
|
+
{@render demo(pattern, true)}
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
{/snippet}
|
|
353
|
+
|
|
354
|
+
<Story name="Container transform" asChild>{@render single(patterns.container)}</Story>
|
|
355
|
+
<Story name="Forward and backward" asChild>{@render single(patterns.axis)}</Story>
|
|
356
|
+
<Story name="Lateral" asChild>{@render single(patterns.lateral)}</Story>
|
|
357
|
+
<Story name="Top level" asChild>{@render single(patterns.top)}</Story>
|
|
358
|
+
<Story name="Enter and exit" asChild>{@render single(patterns.enterExit)}</Story>
|
|
359
|
+
<Story name="Skeleton loaders" asChild>{@render single(patterns.skeleton)}</Story>
|
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { type ViewTransitionTargetDefinition } from 'motion';
|
|
2
|
+
import { type SpringToken } from './spring.js';
|
|
3
|
+
export interface ContainerTransformOptions {
|
|
4
|
+
/** The container before the update (e.g. a card). */
|
|
5
|
+
from: ViewTransitionTargetDefinition;
|
|
6
|
+
/** The container after the update (e.g. the detail view). Pass a selector if it isn't mounted yet. */
|
|
7
|
+
to: ViewTransitionTargetDefinition;
|
|
8
|
+
/** Spring for the bounds/shape morph. `slowSpatial` suits full-screen expansions. */
|
|
9
|
+
spring?: SpringToken;
|
|
7
10
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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: `Search` is a plain field with no search view to expand into, and
|
|
27
|
+
* card → detail is the app's own navigation.
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* containerTransform(
|
|
31
|
+
* async () => { expanded = true; await tick(); },
|
|
32
|
+
* { from: cardEl, to: '[data-detail]' }
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare const containerTransform: (update: () => void | Promise<void>, { from, to, spring }: ContainerTransformOptions) => import("motion-dom").ViewTransitionBuilder;
|
|
@@ -1,138 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
https://github.com/sveltejs/svelte/blob/master/src/runtime/transition/index.ts
|
|
33
|
-
If you have an idea for cleaning up this mess of code, please make a PR. */
|
|
34
|
-
const to_receive = new Map();
|
|
35
|
-
const to_send = new Map();
|
|
36
|
-
function calcTransition(from, fromNode, node, params) {
|
|
37
|
-
const to = node.getBoundingClientRect();
|
|
38
|
-
const isEntering = from.width * from.height < to.width * to.height;
|
|
39
|
-
const dx = from.left + from.width / 2 - (to.left + to.width / 2);
|
|
40
|
-
const dy = from.top - to.top;
|
|
41
|
-
const style = getComputedStyle(node);
|
|
42
|
-
const transform = style.transform == 'none' ? '' : style.transform;
|
|
43
|
-
const opacity = +style.opacity;
|
|
44
|
-
const bgContainerZ = params.bgContainerZ ?? defaults.bgContainerZ ?? 4;
|
|
45
|
-
const fgContainerZ = params.fgContainerZ ?? defaults.fgContainerZ ?? 5;
|
|
46
|
-
let container = {
|
|
47
|
-
fromColor: parseColor(getBackgroundColor(node)),
|
|
48
|
-
fromRadius: parseSize(style.borderRadius),
|
|
49
|
-
fromBorderWidth: parseSize(style.borderLeftWidth),
|
|
50
|
-
fromBorderColor: parseColor(style.borderLeftColor),
|
|
51
|
-
toColor: parseColor(getBackgroundColor(fromNode)),
|
|
52
|
-
toRadius: parseSize(getComputedStyle(fromNode).borderRadius),
|
|
53
|
-
toBorderWidth: parseSize(getComputedStyle(fromNode).borderLeftWidth),
|
|
54
|
-
toBorderColor: parseColor(getComputedStyle(fromNode).borderLeftColor)
|
|
55
|
-
};
|
|
56
|
-
return {
|
|
57
|
-
delay: params.delay ?? defaults.delay ?? 0,
|
|
58
|
-
duration: params.duration ?? defaults.duration ?? 500,
|
|
59
|
-
easing: params.easing ?? defaults.easing ?? easeEmphasized,
|
|
60
|
-
css: (t, u) => {
|
|
61
|
-
const dw = t + u * (from.width / to.width);
|
|
62
|
-
const dh = t + u * (from.height / to.height);
|
|
63
|
-
const tOpacity = (isEntering ? (10 * t - 3) / 7 : (-10 / 3) * u + 1) * opacity;
|
|
64
|
-
const tScale = isEntering ? Math.max(dw, dh) : Math.min(dw, dh);
|
|
65
|
-
const horizontalTrim = ((tScale - dw) * to.width) / tScale / 2;
|
|
66
|
-
const verticalTrim = ((tScale - dh) * to.height) / tScale;
|
|
67
|
-
return `
|
|
68
|
-
opacity: ${tOpacity};
|
|
69
|
-
transform-origin: top center;
|
|
70
|
-
transform: ${transform} translate(${u * dx}px, ${u * dy}px) scale(${tScale});
|
|
71
|
-
clip-path: inset(0 ${horizontalTrim}px ${verticalTrim}px ${horizontalTrim}px);
|
|
72
|
-
z-index: ${fgContainerZ};
|
|
73
|
-
${t < 0.98 ? 'background-color: transparent;' : ''}
|
|
74
|
-
border-color: transparent;
|
|
75
|
-
pointer-events: none;
|
|
76
|
-
`;
|
|
77
|
-
},
|
|
78
|
-
tick: (t, u) => {
|
|
79
|
-
if (!isEntering || !container)
|
|
80
|
-
return;
|
|
81
|
-
if (container.backwards == null)
|
|
82
|
-
container.backwards = Boolean(t);
|
|
83
|
-
if (!container.e) {
|
|
84
|
-
container.e = document.createElement('div');
|
|
85
|
-
container.e.style.position = 'fixed';
|
|
86
|
-
container.e.style.zIndex = bgContainerZ.toString();
|
|
87
|
-
container.e.style.boxSizing = 'border-box';
|
|
88
|
-
container.e.style.borderStyle = 'solid';
|
|
89
|
-
document.body.appendChild(container.e);
|
|
90
|
-
}
|
|
91
|
-
else if (t == (container.backwards ? 0 : 1)) {
|
|
92
|
-
document.body.removeChild(container.e);
|
|
93
|
-
container = null;
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (!container?.e)
|
|
97
|
-
return;
|
|
98
|
-
container.e.style.top = (u * from.top + t * to.top).toFixed(1) + 'px';
|
|
99
|
-
container.e.style.left = (u * from.left + t * to.left).toFixed(1) + 'px';
|
|
100
|
-
container.e.style.width = (u * from.width + t * to.width).toFixed(1) + 'px';
|
|
101
|
-
container.e.style.height = (u * from.height + t * to.height).toFixed(1) + 'px';
|
|
102
|
-
const { fromColor, fromRadius, fromBorderWidth, fromBorderColor, toColor, toRadius, toBorderWidth, toBorderColor } = container;
|
|
103
|
-
const interpColor = [0, 0, 0, 0].map((_, i) => Math.trunc(t * fromColor[i] + u * toColor[i]));
|
|
104
|
-
container.e.style.backgroundColor = `rgba(${interpColor.join(',')})`;
|
|
105
|
-
container.e.style.borderRadius = (t * fromRadius + u * toRadius).toFixed(1) + 'px';
|
|
106
|
-
container.e.style.borderWidth = (t * fromBorderWidth + u * toBorderWidth).toFixed(1) + 'px';
|
|
107
|
-
const interpBorder = [0, 0, 0, 0].map((_, i) => Math.trunc(t * fromBorderColor[i] + u * toBorderColor[i]));
|
|
108
|
-
container.e.style.borderColor = `rgba(${interpBorder.join(',')})`;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function makeTransition(items, counterparts, intro) {
|
|
113
|
-
return (node, params) => {
|
|
114
|
-
items.set(params.key, {
|
|
115
|
-
rect: node.getBoundingClientRect(),
|
|
116
|
-
node
|
|
117
|
-
});
|
|
118
|
-
return () => {
|
|
119
|
-
const counterpart = counterparts.get(params.key);
|
|
120
|
-
if (counterpart) {
|
|
121
|
-
counterparts.delete(params.key);
|
|
122
|
-
return calcTransition(counterpart.rect, counterpart.node, node, params);
|
|
123
|
-
}
|
|
124
|
-
// if the node is disappearing altogether
|
|
125
|
-
// (i.e. wasn't claimed by the other list)
|
|
126
|
-
// then we need to supply an outro
|
|
127
|
-
items.delete(params.key);
|
|
128
|
-
return fallback ? fallback(node, params, intro) : {};
|
|
129
|
-
};
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return [makeTransition(to_send, to_receive, false), makeTransition(to_receive, to_send, true)];
|
|
133
|
-
};
|
|
134
|
-
export const parseSize = (size) => (size.endsWith('px')
|
|
135
|
-
? +size.slice(0, -2)
|
|
136
|
-
: size.endsWith('rem')
|
|
137
|
-
? +size.slice(0, -3) * 16
|
|
138
|
-
: null) || 0;
|
|
1
|
+
import { animateView } from 'motion';
|
|
2
|
+
import { springTokens, springTransition } from './spring.js';
|
|
3
|
+
/**
|
|
4
|
+
* M3 container transform: one container morphs its bounds, shape and color into another while
|
|
5
|
+
* the outgoing content fades out and the incoming content fades in on top.
|
|
6
|
+
* https://m3.material.io/styles/motion/transitions/transition-patterns#container-transform
|
|
7
|
+
*
|
|
8
|
+
* The most dramatic pattern (https://m3.material.io/styles/motion/transitions/applying-transitions).
|
|
9
|
+
* Use it for hero moments, shallow expand → collapse hierarchies and seamless element-to-element
|
|
10
|
+
* connections. Don't use it in deep hierarchies or utility-focused navigation, where it becomes
|
|
11
|
+
* excessive. Use `sharedAxis` there. Keep `spring` at `spatial`/`slowSpatial`, never the bouncy
|
|
12
|
+
* `fastSpatial`.
|
|
13
|
+
*
|
|
14
|
+
* Built on Motion's `animateView()` (View Transition API), so `from` and `to` never have to be in
|
|
15
|
+
* the DOM at the same time — `update` swaps one for the other. Browsers without the API just run
|
|
16
|
+
* `update`. A second call while one is running is queued, not interrupted.
|
|
17
|
+
*
|
|
18
|
+
* Not built into any component: `Search` is a plain field with no search view to expand into, and
|
|
19
|
+
* card → detail is the app's own navigation.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* containerTransform(
|
|
23
|
+
* async () => { expanded = true; await tick(); },
|
|
24
|
+
* { from: cardEl, to: '[data-detail]' }
|
|
25
|
+
* );
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const containerTransform = (update, { from, to, spring = springTokens.spatial }) => animateView(update, springTransition(spring))
|
|
29
|
+
.add(from, to)
|
|
30
|
+
.old({ opacity: [1, 0] }, springTransition(springTokens.fastEffects))
|
|
31
|
+
.new({ opacity: [0, 1] }, { ...springTransition(springTokens.effects), delay: 0.05 });
|