@noxlovette/material 0.4.7 → 0.5.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/dist/animation/TransitionPatterns.stories.svelte +357 -0
- package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
- package/dist/animation/containerTransform.d.ts +26 -12
- package/dist/animation/containerTransform.js +22 -138
- package/dist/animation/enterExit.d.ts +21 -3
- package/dist/animation/enterExit.js +69 -22
- package/dist/animation/index.d.ts +1 -1
- package/dist/animation/index.js +1 -1
- package/dist/animation/presence.svelte.d.ts +47 -0
- package/dist/animation/presence.svelte.js +84 -0
- package/dist/animation/sharedAxisTransition.d.ts +35 -37
- package/dist/animation/sharedAxisTransition.js +50 -22
- package/dist/animation/skeleton.d.ts +11 -0
- package/dist/animation/skeleton.js +23 -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 +85 -92
- package/dist/components/date/DateField.svelte.d.ts +2 -5
- package/dist/components/date/DateRangeField.svelte +113 -155
- package/dist/components/date/DateRangeField.svelte.d.ts +2 -5
- package/dist/components/date/theme.d.ts +95 -0
- package/dist/components/date/theme.js +99 -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 +9 -0
- package/dist/components/forms/textfield/theme.js +26 -9
- 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/theme.js +1 -1
- package/dist/components/pill/theme.d.ts +1 -1
- package/dist/components/pill/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/Table.stories.svelte +24 -0
- 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 +2 -2
- 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 +62 -61
- 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 +15 -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,357 @@
|
|
|
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
|
+
|
|
5
|
+
Every demo loops by default; "Replay" retriggers it on demand. The Overview plays the six in
|
|
6
|
+
turn rather than all at once: container transform, forward/backward, lateral and top level run
|
|
7
|
+
on the View Transition API, which allows one transition per document at a time.
|
|
8
|
+
|
|
9
|
+
Excluded from autodocs — the docs page renders every story into one document, which would
|
|
10
|
+
duplicate the `data-demo` targets these transitions resolve.
|
|
11
|
+
-->
|
|
12
|
+
<script module lang="ts">
|
|
13
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
14
|
+
|
|
15
|
+
const { Story } = defineMeta({
|
|
16
|
+
title: 'Motion/Transition patterns',
|
|
17
|
+
tags: ['!autodocs'],
|
|
18
|
+
parameters: { layout: 'fullscreen' }
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
import { tick } from 'svelte';
|
|
24
|
+
import type { Attachment } from 'svelte/attachments';
|
|
25
|
+
import Button from '../components/buttons/Button.svelte';
|
|
26
|
+
import Title from '../components/typography/title/Title.svelte';
|
|
27
|
+
import Body from '../components/typography/body/Body.svelte';
|
|
28
|
+
import {
|
|
29
|
+
containerTransform,
|
|
30
|
+
enterExit,
|
|
31
|
+
fadeThrough,
|
|
32
|
+
lateral,
|
|
33
|
+
Presence,
|
|
34
|
+
sharedAxis,
|
|
35
|
+
skeleton,
|
|
36
|
+
type NavigationDirection
|
|
37
|
+
} from './transition.js';
|
|
38
|
+
import {
|
|
39
|
+
pathFourLeafClover,
|
|
40
|
+
pathFourSidedCookie,
|
|
41
|
+
pathGem,
|
|
42
|
+
pathPentagon,
|
|
43
|
+
pathSoftBurst,
|
|
44
|
+
pathSunny
|
|
45
|
+
} from './shapes.js';
|
|
46
|
+
|
|
47
|
+
const LOOP_MS = 2200;
|
|
48
|
+
const OVERVIEW_STEP_MS = 1400;
|
|
49
|
+
|
|
50
|
+
let looping = $state(true);
|
|
51
|
+
|
|
52
|
+
/* Container transform — a cookie card expands into a detail surface and back. */
|
|
53
|
+
let expanded = $state(false);
|
|
54
|
+
const runContainer = () => {
|
|
55
|
+
const [from, to] = expanded
|
|
56
|
+
? ['[data-demo="ct-detail"]', '[data-demo="ct-card"]']
|
|
57
|
+
: ['[data-demo="ct-card"]', '[data-demo="ct-detail"]'];
|
|
58
|
+
return containerTransform(
|
|
59
|
+
async () => {
|
|
60
|
+
expanded = !expanded;
|
|
61
|
+
await tick();
|
|
62
|
+
},
|
|
63
|
+
{ from, to }
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/* Forward and backward — Inbox → Thread → Message and back up the hierarchy. */
|
|
68
|
+
const levels = ['Inbox', 'Thread', 'Message'];
|
|
69
|
+
let level = $state(0);
|
|
70
|
+
let descending = true;
|
|
71
|
+
const runAxis = () => {
|
|
72
|
+
if (level === levels.length - 1) descending = false;
|
|
73
|
+
if (level === 0) descending = true;
|
|
74
|
+
const direction: NavigationDirection = descending ? 'forward' : 'backward';
|
|
75
|
+
return sharedAxis(
|
|
76
|
+
async () => {
|
|
77
|
+
level += descending ? 1 : -1;
|
|
78
|
+
await tick();
|
|
79
|
+
},
|
|
80
|
+
{ target: '[data-demo="axis"]', axis: 'x', direction }
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/* Lateral — peer tabs, wrapping back to the first. */
|
|
85
|
+
const tabs = ['Photos', 'Albums', 'Search'];
|
|
86
|
+
let tab = $state(0);
|
|
87
|
+
const runLateral = () => {
|
|
88
|
+
const next = (tab + 1) % tabs.length;
|
|
89
|
+
const direction: NavigationDirection = next > tab ? 'forward' : 'backward';
|
|
90
|
+
return lateral(
|
|
91
|
+
async () => {
|
|
92
|
+
tab = next;
|
|
93
|
+
await tick();
|
|
94
|
+
},
|
|
95
|
+
{ target: '[data-demo="lateral"]', direction }
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/* Top level — unrelated destinations, as from a navigation bar. */
|
|
100
|
+
const destinations = [
|
|
101
|
+
{ label: 'Home', color: 'bg-md-sys-color-primary-container text-md-sys-color-primary' },
|
|
102
|
+
{ label: 'Library', color: 'bg-md-sys-color-secondary-container text-md-sys-color-secondary' },
|
|
103
|
+
{ label: 'Profile', color: 'bg-md-sys-color-tertiary-container text-md-sys-color-tertiary' }
|
|
104
|
+
];
|
|
105
|
+
let destination = $state(0);
|
|
106
|
+
const runTopLevel = () =>
|
|
107
|
+
fadeThrough(
|
|
108
|
+
async () => {
|
|
109
|
+
destination = (destination + 1) % destinations.length;
|
|
110
|
+
await tick();
|
|
111
|
+
},
|
|
112
|
+
{ target: '[data-demo="top"]' }
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/* Enter and exit — a surface appears and leaves within the same screen. */
|
|
116
|
+
let popped = $state(true);
|
|
117
|
+
const pop = new Presence(() => popped);
|
|
118
|
+
const runEnterExit = () => (popped = !popped);
|
|
119
|
+
|
|
120
|
+
/* Skeleton loader — a pulsing placeholder, then the content fades in. */
|
|
121
|
+
let loaded = $state(false);
|
|
122
|
+
const content = new Presence(() => loaded);
|
|
123
|
+
const runSkeleton = () => (loaded = !loaded);
|
|
124
|
+
|
|
125
|
+
type Pattern = { id: string; title: string; summary: string; anchor: string; run: () => unknown };
|
|
126
|
+
|
|
127
|
+
const patterns = {
|
|
128
|
+
container: {
|
|
129
|
+
id: 'container',
|
|
130
|
+
title: 'Container transform',
|
|
131
|
+
summary: 'A container morphs into another, crossfading its content.',
|
|
132
|
+
anchor: 'container-transform',
|
|
133
|
+
run: runContainer
|
|
134
|
+
},
|
|
135
|
+
axis: {
|
|
136
|
+
id: 'axis',
|
|
137
|
+
title: 'Forward and backward',
|
|
138
|
+
summary: 'Content moves along a shared axis between hierarchy levels.',
|
|
139
|
+
anchor: 'forward-and-backward',
|
|
140
|
+
run: runAxis
|
|
141
|
+
},
|
|
142
|
+
lateral: {
|
|
143
|
+
id: 'lateral',
|
|
144
|
+
title: 'Lateral',
|
|
145
|
+
summary: 'Peer screens slide past each other edge to edge.',
|
|
146
|
+
anchor: 'lateral',
|
|
147
|
+
run: runLateral
|
|
148
|
+
},
|
|
149
|
+
top: {
|
|
150
|
+
id: 'top',
|
|
151
|
+
title: 'Top level',
|
|
152
|
+
summary: 'Unrelated destinations fade through each other.',
|
|
153
|
+
anchor: 'top-level',
|
|
154
|
+
run: runTopLevel
|
|
155
|
+
},
|
|
156
|
+
enterExit: {
|
|
157
|
+
id: 'enter-exit',
|
|
158
|
+
title: 'Enter and exit',
|
|
159
|
+
summary: 'Surfaces appear and leave within the current screen.',
|
|
160
|
+
anchor: 'enter-and-exit',
|
|
161
|
+
run: runEnterExit
|
|
162
|
+
},
|
|
163
|
+
skeleton: {
|
|
164
|
+
id: 'skeleton',
|
|
165
|
+
title: 'Skeleton loaders',
|
|
166
|
+
summary: 'A placeholder pulses until real content fades in.',
|
|
167
|
+
anchor: 'skeleton-loaders',
|
|
168
|
+
run: runSkeleton
|
|
169
|
+
}
|
|
170
|
+
} satisfies Record<string, Pattern>;
|
|
171
|
+
|
|
172
|
+
const all = Object.values(patterns);
|
|
173
|
+
|
|
174
|
+
/** Loops `run` while `looping` is on; toggling the loop restarts the interval. */
|
|
175
|
+
const loop =
|
|
176
|
+
(run: () => unknown, every = LOOP_MS): Attachment =>
|
|
177
|
+
() => {
|
|
178
|
+
if (!looping) return;
|
|
179
|
+
const id = setInterval(run, every);
|
|
180
|
+
return () => clearInterval(id);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Overview: one pattern per tick, round-robin, so view transitions never overlap. */
|
|
184
|
+
const roundRobin: Attachment = () => {
|
|
185
|
+
if (!looping) return;
|
|
186
|
+
let i = 0;
|
|
187
|
+
const id = setInterval(() => all[i++ % all.length].run(), OVERVIEW_STEP_MS);
|
|
188
|
+
return () => clearInterval(id);
|
|
189
|
+
};
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
{#snippet shape(path: string, cls: string)}
|
|
193
|
+
<svg viewBox="0 0 380 380" class={cls} aria-hidden="true">
|
|
194
|
+
<path d={path} fill="currentColor" />
|
|
195
|
+
</svg>
|
|
196
|
+
{/snippet}
|
|
197
|
+
|
|
198
|
+
{#snippet stage(pattern: Pattern)}
|
|
199
|
+
{#if pattern.id === 'container'}
|
|
200
|
+
{#if !expanded}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
data-demo="ct-card"
|
|
204
|
+
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"
|
|
205
|
+
onclick={runContainer}
|
|
206
|
+
>
|
|
207
|
+
{@render shape(pathFourSidedCookie, 'size-10 text-md-sys-color-primary')}
|
|
208
|
+
<span class="md-sys-typescale-label-large">Open</span>
|
|
209
|
+
</button>
|
|
210
|
+
{:else}
|
|
211
|
+
<div
|
|
212
|
+
data-demo="ct-detail"
|
|
213
|
+
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"
|
|
214
|
+
>
|
|
215
|
+
{@render shape(pathFourSidedCookie, 'size-24 text-md-sys-color-primary')}
|
|
216
|
+
<span class="md-sys-typescale-title-medium">Detail</span>
|
|
217
|
+
</div>
|
|
218
|
+
{/if}
|
|
219
|
+
{:else if pattern.id === 'axis'}
|
|
220
|
+
<div data-demo="axis" class="absolute inset-0 flex flex-col items-center justify-center gap-3">
|
|
221
|
+
{@render shape(
|
|
222
|
+
pathPentagon,
|
|
223
|
+
[
|
|
224
|
+
'size-16 text-md-sys-color-primary',
|
|
225
|
+
'size-20 text-md-sys-color-secondary',
|
|
226
|
+
'size-24 text-md-sys-color-tertiary'
|
|
227
|
+
][level]
|
|
228
|
+
)}
|
|
229
|
+
<span class="md-sys-typescale-title-medium text-md-sys-color-on-surface">
|
|
230
|
+
{levels.slice(0, level + 1).join(' › ')}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
{:else if pattern.id === 'lateral'}
|
|
234
|
+
<div class="absolute inset-x-0 top-0 flex justify-center gap-6 pt-3">
|
|
235
|
+
{#each tabs as name, i (name)}
|
|
236
|
+
<span
|
|
237
|
+
class={[
|
|
238
|
+
'md-sys-typescale-label-large',
|
|
239
|
+
i === tab ? 'text-md-sys-color-primary' : 'text-md-sys-color-on-surface-variant'
|
|
240
|
+
]}>{name}</span
|
|
241
|
+
>
|
|
242
|
+
{/each}
|
|
243
|
+
</div>
|
|
244
|
+
<div
|
|
245
|
+
data-demo="lateral"
|
|
246
|
+
class="bg-md-sys-color-surface-container absolute inset-x-3 top-11 bottom-3 flex items-center justify-center rounded-lg"
|
|
247
|
+
>
|
|
248
|
+
{@render shape(pathFourLeafClover, 'size-24 text-md-sys-color-primary')}
|
|
249
|
+
<span
|
|
250
|
+
class="md-sys-typescale-display-small text-md-sys-color-on-primary absolute"
|
|
251
|
+
aria-hidden="true">{tab + 1}</span
|
|
252
|
+
>
|
|
253
|
+
</div>
|
|
254
|
+
{:else if pattern.id === 'top'}
|
|
255
|
+
<div
|
|
256
|
+
data-demo="top"
|
|
257
|
+
class={[
|
|
258
|
+
'absolute inset-3 flex flex-col items-center justify-center gap-3 rounded-lg',
|
|
259
|
+
destinations[destination].color
|
|
260
|
+
]}
|
|
261
|
+
>
|
|
262
|
+
{@render shape(pathSunny, 'size-24')}
|
|
263
|
+
<span class="md-sys-typescale-title-medium">{destinations[destination].label}</span>
|
|
264
|
+
</div>
|
|
265
|
+
{:else if pattern.id === 'enter-exit'}
|
|
266
|
+
{#if pop.mounted}
|
|
267
|
+
<div
|
|
268
|
+
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"
|
|
269
|
+
{@attach pop.attach(enterExit.scale)}
|
|
270
|
+
>
|
|
271
|
+
{@render shape(pathSoftBurst, 'size-24 text-md-sys-color-tertiary')}
|
|
272
|
+
</div>
|
|
273
|
+
{/if}
|
|
274
|
+
{:else if pattern.id === 'skeleton'}
|
|
275
|
+
<div class="absolute inset-3 grid [&>*]:[grid-area:1/1]">
|
|
276
|
+
{#if !loaded}
|
|
277
|
+
<div class="flex flex-col items-center justify-center gap-3" {@attach skeleton}>
|
|
278
|
+
<div class="bg-md-sys-color-surface-container-highest size-24 rounded-full"></div>
|
|
279
|
+
<div class="bg-md-sys-color-surface-container-highest h-4 w-28 rounded-full"></div>
|
|
280
|
+
</div>
|
|
281
|
+
{/if}
|
|
282
|
+
{#if content.mounted}
|
|
283
|
+
<div
|
|
284
|
+
class="flex flex-col items-center justify-center gap-3"
|
|
285
|
+
{@attach content.attach(enterExit.fade)}
|
|
286
|
+
>
|
|
287
|
+
{@render shape(pathGem, 'size-24 text-md-sys-color-secondary')}
|
|
288
|
+
<span class="md-sys-typescale-title-medium text-md-sys-color-on-surface">Loaded</span>
|
|
289
|
+
</div>
|
|
290
|
+
{/if}
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
{/snippet}
|
|
294
|
+
|
|
295
|
+
{#snippet demo(pattern: Pattern, tall = false)}
|
|
296
|
+
<section
|
|
297
|
+
class="bg-md-sys-color-surface-container-low text-md-sys-color-on-surface flex flex-col gap-3 rounded-xl p-4"
|
|
298
|
+
>
|
|
299
|
+
<header class="flex items-start justify-between gap-3">
|
|
300
|
+
<div class="flex flex-col gap-1">
|
|
301
|
+
<Title size="medium">{pattern.title}</Title>
|
|
302
|
+
<Body size="small" class="text-md-sys-color-on-surface-variant">{pattern.summary}</Body>
|
|
303
|
+
</div>
|
|
304
|
+
<Button variant="tonal" size="sm" onclick={pattern.run}>Replay</Button>
|
|
305
|
+
</header>
|
|
306
|
+
<div
|
|
307
|
+
class={['bg-md-sys-color-surface relative overflow-clip rounded-lg', tall ? 'h-96' : 'h-60']}
|
|
308
|
+
>
|
|
309
|
+
{@render stage(pattern)}
|
|
310
|
+
</div>
|
|
311
|
+
<a
|
|
312
|
+
class="text-link md-sys-typescale-label-small self-start"
|
|
313
|
+
href="https://m3.material.io/styles/motion/transitions/transition-patterns#{pattern.anchor}"
|
|
314
|
+
target="_blank"
|
|
315
|
+
rel="noreferrer">M3 spec</a
|
|
316
|
+
>
|
|
317
|
+
</section>
|
|
318
|
+
{/snippet}
|
|
319
|
+
|
|
320
|
+
{#snippet toolbar()}
|
|
321
|
+
<div class="flex items-center justify-between gap-4">
|
|
322
|
+
<Title size="large">M3 transition patterns</Title>
|
|
323
|
+
<Button variant={looping ? 'filled' : 'outlined'} onclick={() => (looping = !looping)}>
|
|
324
|
+
{looping ? 'Pause loop' : 'Loop'}
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
{/snippet}
|
|
328
|
+
|
|
329
|
+
<Story name="Overview" asChild>
|
|
330
|
+
<div class="bg-md-sys-color-surface flex min-h-dvh flex-col gap-6 p-6" {@attach roundRobin}>
|
|
331
|
+
{@render toolbar()}
|
|
332
|
+
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
333
|
+
{#each all as pattern (pattern.id)}
|
|
334
|
+
{@render demo(pattern)}
|
|
335
|
+
{/each}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</Story>
|
|
339
|
+
|
|
340
|
+
{#snippet single(pattern: Pattern)}
|
|
341
|
+
<div
|
|
342
|
+
class="bg-md-sys-color-surface flex min-h-dvh flex-col gap-6 p-6"
|
|
343
|
+
{@attach loop(pattern.run)}
|
|
344
|
+
>
|
|
345
|
+
{@render toolbar()}
|
|
346
|
+
<div class="mx-auto w-full max-w-xl">
|
|
347
|
+
{@render demo(pattern, true)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
{/snippet}
|
|
351
|
+
|
|
352
|
+
<Story name="Container transform" asChild>{@render single(patterns.container)}</Story>
|
|
353
|
+
<Story name="Forward and backward" asChild>{@render single(patterns.axis)}</Story>
|
|
354
|
+
<Story name="Lateral" asChild>{@render single(patterns.lateral)}</Story>
|
|
355
|
+
<Story name="Top level" asChild>{@render single(patterns.top)}</Story>
|
|
356
|
+
<Story name="Enter and exit" asChild>{@render single(patterns.enterExit)}</Story>
|
|
357
|
+
<Story name="Skeleton loaders" asChild>{@render single(patterns.skeleton)}</Story>
|
|
@@ -1,13 +1,27 @@
|
|
|
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
|
+
* Built on Motion's `animateView()` (View Transition API), so `from` and `to` never have to be in
|
|
17
|
+
* the DOM at the same time — `update` swaps one for the other. Browsers without the API just run
|
|
18
|
+
* `update`. A second call while one is running is queued, not interrupted.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* containerTransform(
|
|
22
|
+
* async () => { expanded = true; await tick(); },
|
|
23
|
+
* { from: cardEl, to: '[data-detail]' }
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const containerTransform: (update: () => void | Promise<void>, { from, to, spring }: ContainerTransformOptions) => import("motion-dom").ViewTransitionBuilder;
|
|
@@ -1,138 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
typeof opacity != 'number') {
|
|
24
|
-
throw new Error('something went down in the color parser, see previous info');
|
|
25
|
-
}
|
|
26
|
-
return [r, g, b, opacity];
|
|
27
|
-
}
|
|
28
|
-
return [0, 0, 0, 0];
|
|
29
|
-
};
|
|
30
|
-
export const containerTransform = ({ fallback, ...defaults }) => {
|
|
31
|
-
/* This code is based on the crossfade function from Svelte. Svelte is under the MIT license.
|
|
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
|
+
* Built on Motion's `animateView()` (View Transition API), so `from` and `to` never have to be in
|
|
9
|
+
* the DOM at the same time — `update` swaps one for the other. Browsers without the API just run
|
|
10
|
+
* `update`. A second call while one is running is queued, not interrupted.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* containerTransform(
|
|
14
|
+
* async () => { expanded = true; await tick(); },
|
|
15
|
+
* { from: cardEl, to: '[data-detail]' }
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const containerTransform = (update, { from, to, spring = springTokens.spatial }) => animateView(update, springTransition(spring))
|
|
20
|
+
.add(from, to)
|
|
21
|
+
.old({ opacity: [1, 0] }, springTransition(springTokens.fastEffects))
|
|
22
|
+
.new({ opacity: [0, 1] }, { ...springTransition(springTokens.effects), delay: 0.05 });
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { AnimationOptions, DOMKeyframesDefinition } from 'motion';
|
|
2
|
+
/**
|
|
3
|
+
* An M3 enter/exit transition for {@link presence} / `Presence`. Keep keyframes to `transform`
|
|
4
|
+
* and `opacity` so Motion runs them on WAAPI (compositor), which is also what bits-ui's presence
|
|
5
|
+
* layer detects to delay unmounting.
|
|
6
|
+
*
|
|
7
|
+
* https://m3.material.io/styles/motion/transitions/transition-patterns#enter-and-exit
|
|
8
|
+
*/
|
|
9
|
+
export interface PresenceTransition {
|
|
10
|
+
/** Where the enter animation starts. */
|
|
11
|
+
hidden: DOMKeyframesDefinition;
|
|
12
|
+
shown: DOMKeyframesDefinition;
|
|
13
|
+
/** Where the exit animation ends. Defaults to `hidden`. */
|
|
14
|
+
exited?: DOMKeyframesDefinition;
|
|
15
|
+
/** `transform-origin` applied while mounted. */
|
|
16
|
+
origin?: string;
|
|
17
|
+
enter: AnimationOptions;
|
|
18
|
+
exit: AnimationOptions;
|
|
19
|
+
}
|
|
20
|
+
export type EnterExitPreset = 'fade' | 'scale' | 'slideUp' | 'dialog' | 'sideSheet' | 'bottomSheet';
|
|
21
|
+
export declare const enterExit: Record<EnterExitPreset, PresenceTransition>;
|