@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.
Files changed (144) hide show
  1. package/dist/animation/Shapes.stories.svelte +56 -0
  2. package/dist/animation/Shapes.stories.svelte.d.ts +3 -0
  3. package/dist/animation/TransitionPatterns.stories.svelte +359 -0
  4. package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
  5. package/dist/animation/containerTransform.d.ts +35 -12
  6. package/dist/animation/containerTransform.js +31 -138
  7. package/dist/animation/enterExit.d.ts +30 -3
  8. package/dist/animation/enterExit.js +69 -22
  9. package/dist/animation/index.d.ts +2 -1
  10. package/dist/animation/index.js +2 -1
  11. package/dist/animation/presence.svelte.d.ts +47 -0
  12. package/dist/animation/presence.svelte.js +84 -0
  13. package/dist/animation/shapeMorph.svelte.d.ts +52 -0
  14. package/dist/animation/shapeMorph.svelte.js +112 -0
  15. package/dist/animation/shapesAnimatableSmall.d.ts +28 -22
  16. package/dist/animation/shapesAnimatableSmall.js +32 -23
  17. package/dist/animation/sharedAxisTransition.d.ts +57 -37
  18. package/dist/animation/sharedAxisTransition.js +74 -22
  19. package/dist/animation/skeleton.d.ts +16 -0
  20. package/dist/animation/skeleton.js +28 -0
  21. package/dist/animation/spring.d.ts +61 -0
  22. package/dist/animation/spring.js +53 -0
  23. package/dist/animation/springValue.svelte.d.ts +22 -0
  24. package/dist/animation/springValue.svelte.js +42 -0
  25. package/dist/animation/transition.d.ts +3 -1
  26. package/dist/animation/transition.js +3 -1
  27. package/dist/attachments/clickOutside.d.ts +13 -0
  28. package/dist/attachments/clickOutside.js +18 -0
  29. package/dist/attachments/index.d.ts +1 -0
  30. package/dist/attachments/index.js +1 -0
  31. package/dist/components/buttons/FAB.svelte +23 -16
  32. package/dist/components/buttons/FABMenu.svelte +24 -29
  33. package/dist/components/buttons/FABMenu.svelte.d.ts +2 -1
  34. package/dist/components/buttons/FABMenuItem.svelte +23 -20
  35. package/dist/components/buttons/FABMenuItem.svelte.d.ts +5 -1
  36. package/dist/components/buttons/split-button/SplitButton.svelte +10 -17
  37. package/dist/components/buttons/split-button/theme.js +1 -1
  38. package/dist/components/buttons/types.d.ts +3 -3
  39. package/dist/components/cards/theme.js +1 -1
  40. package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +8 -12
  41. package/dist/components/containers/bottom-sheet/BottomSheet.svelte +67 -55
  42. package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +1 -1
  43. package/dist/components/containers/bottom-sheet/types.d.ts +8 -2
  44. package/dist/components/containers/context-menu/ContextMenu.svelte +23 -36
  45. package/dist/components/containers/context-menu/theme.js +2 -2
  46. package/dist/components/containers/dialogue/Dialogue.svelte +49 -65
  47. package/dist/components/containers/link-preview/LinkPreview.svelte +12 -23
  48. package/dist/components/containers/link-preview/theme.d.ts +3 -3
  49. package/dist/components/containers/menu/Menu.svelte +14 -21
  50. package/dist/components/containers/menu/MenuSub.svelte +10 -17
  51. package/dist/components/containers/pane/DraggablePane.svelte +90 -5
  52. package/dist/components/containers/pane/DraggablePane.svelte.d.ts +1 -1
  53. package/dist/components/containers/pane/theme.js +2 -2
  54. package/dist/components/containers/popover/Popover.svelte +31 -42
  55. package/dist/components/containers/popover/theme.d.ts +3 -3
  56. package/dist/components/containers/scroll-area/theme.js +2 -2
  57. package/dist/components/containers/side-sheet/SideSheet.stories.svelte +10 -15
  58. package/dist/components/containers/side-sheet/SideSheet.svelte +44 -44
  59. package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +4 -3
  60. package/dist/components/containers/side-sheet/types.d.ts +8 -2
  61. package/dist/components/date/DateField.svelte +99 -92
  62. package/dist/components/date/DateField.svelte.d.ts +4 -5
  63. package/dist/components/date/DateRangeField.svelte +127 -155
  64. package/dist/components/date/DateRangeField.svelte.d.ts +4 -5
  65. package/dist/components/date/calendarMotion.d.ts +9 -0
  66. package/dist/components/date/calendarMotion.js +23 -0
  67. package/dist/components/date/theme.d.ts +95 -0
  68. package/dist/components/date/theme.js +103 -14
  69. package/dist/components/date/types.d.ts +13 -0
  70. package/dist/components/forms/checkbox/theme.js +2 -2
  71. package/dist/components/forms/command/theme.d.ts +3 -3
  72. package/dist/components/forms/command/theme.js +1 -1
  73. package/dist/components/forms/pin/theme.js +2 -2
  74. package/dist/components/forms/radio-group/theme.js +2 -2
  75. package/dist/components/forms/select/Select.svelte +42 -54
  76. package/dist/components/forms/select/theme.js +4 -4
  77. package/dist/components/forms/slider/Slider.svelte +144 -126
  78. package/dist/components/forms/slider/Slider.svelte.d.ts +3 -0
  79. package/dist/components/forms/slider/theme.d.ts +154 -48
  80. package/dist/components/forms/slider/theme.js +60 -20
  81. package/dist/components/forms/slider/types.d.ts +1 -1
  82. package/dist/components/forms/switch/theme.js +4 -4
  83. package/dist/components/forms/textfield/Textfield.svelte +1 -1
  84. package/dist/components/forms/textfield/theme.d.ts +12 -0
  85. package/dist/components/forms/textfield/theme.js +30 -10
  86. package/dist/components/forms/toggle-group/theme.js +1 -1
  87. package/dist/components/forms/tooltip/Tooltip.svelte +24 -35
  88. package/dist/components/nav/breadcrumb/theme.js +1 -1
  89. package/dist/components/nav/navbar/theme.js +2 -2
  90. package/dist/components/nav/rail/Rail.svelte +4 -5
  91. package/dist/components/nav/rail/theme.js +7 -7
  92. package/dist/components/nav/tabs/TabHolder.svelte +29 -3
  93. package/dist/components/nav/tabs/TabHolder.svelte.d.ts +3 -1
  94. package/dist/components/nav/tabs/theme.js +1 -1
  95. package/dist/components/progress/CircularProgress.svelte +78 -4
  96. package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
  97. package/dist/components/progress/LinearProgress.svelte +70 -7
  98. package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
  99. package/dist/components/progress/WavyLinearProgress.svelte +27 -3
  100. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
  101. package/dist/components/progress/_wavy.d.ts +2 -0
  102. package/dist/components/progress/_wavy.js +3 -1
  103. package/dist/components/snackbar/Snackbar.svelte +13 -17
  104. package/dist/components/table/TableBody.svelte +3 -16
  105. package/dist/components/table/TableBody.svelte.d.ts +1 -5
  106. package/dist/components/table/TableHeader.svelte +1 -1
  107. package/dist/components/table/theme.js +1 -1
  108. package/dist/components/table/types.d.ts +0 -7
  109. package/dist/components/time/TimeField.stories.svelte +41 -0
  110. package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
  111. package/dist/components/time/TimeField.svelte +69 -62
  112. package/dist/components/time/TimeField.svelte.d.ts +6 -0
  113. package/dist/components/time/TimepickerInput.svelte +8 -4
  114. package/dist/components/time/types.d.ts +11 -1
  115. package/dist/components/toolbar/theme.js +2 -2
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/styles/component.css +21 -22
  119. package/dist/styles/motion.css +317 -38
  120. package/dist/utils/Layer.svelte +16 -29
  121. package/dist/utils/icon/LoadingIndicator.svelte +61 -22
  122. package/dist/utils/icon/loadingShapes.d.ts +1 -0
  123. package/dist/utils/icon/loadingShapes.js +16 -0
  124. package/package.json +5 -2
  125. package/dist/actions/clickOutside.d.ts +0 -3
  126. package/dist/actions/clickOutside.js +0 -13
  127. package/dist/actions/floating.d.ts +0 -20
  128. package/dist/actions/floating.js +0 -87
  129. package/dist/actions/index.d.ts +0 -6
  130. package/dist/actions/index.js +0 -6
  131. package/dist/actions/keyboard.d.ts +0 -3
  132. package/dist/actions/keyboard.js +0 -14
  133. package/dist/actions/portal.d.ts +0 -3
  134. package/dist/actions/portal.js +0 -8
  135. package/dist/actions/positionFloating.d.ts +0 -10
  136. package/dist/actions/positionFloating.js +0 -59
  137. package/dist/actions/rovingFocusGroup.d.ts +0 -20
  138. package/dist/actions/rovingFocusGroup.js +0 -82
  139. package/dist/animation/easing.d.ts +0 -6
  140. package/dist/animation/easing.js +0 -78
  141. package/dist/animation/outroClass.d.ts +0 -3
  142. package/dist/animation/outroClass.js +0 -20
  143. package/dist/animation/transitionTypes.d.ts +0 -8
  144. 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,3 @@
1
+ declare const Shapes: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type Shapes = ReturnType<typeof Shapes>;
3
+ export default Shapes;
@@ -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>
@@ -0,0 +1,3 @@
1
+ declare const TransitionPatterns: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TransitionPatterns = ReturnType<typeof TransitionPatterns>;
3
+ export default TransitionPatterns;
@@ -1,13 +1,36 @@
1
- import type { TransitionConfig } from 'svelte/transition';
2
- import type { TransitionOptions } from './transitionTypes.js';
3
- interface ContainerOptions {
4
- fallback?: (node: Element, params: TransitionOptions & ContainerOptions & ContainerParamOptions, intro: boolean) => TransitionConfig;
5
- bgContainerZ?: number;
6
- fgContainerZ?: number;
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
- interface ContainerParamOptions {
9
- key: string;
10
- }
11
- export declare const containerTransform: ({ fallback, ...defaults }: TransitionOptions & ContainerOptions) => ((node: Element, params: TransitionOptions & ContainerOptions & ContainerParamOptions) => () => TransitionConfig)[];
12
- export declare const parseSize: (size: string) => number;
13
- export {};
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 { easeEmphasized } from './easing.js';
2
- const getBackgroundColor = (node, defaultColor) => {
3
- if (!defaultColor) {
4
- const tmp = document.createElement('div');
5
- document.body.appendChild(tmp);
6
- defaultColor = getComputedStyle(tmp).backgroundColor;
7
- tmp.remove();
8
- }
9
- const color = getComputedStyle(node).backgroundColor;
10
- if (color != defaultColor)
11
- return color;
12
- if (node.parentElement)
13
- return getBackgroundColor(node.parentElement, defaultColor);
14
- return defaultColor;
15
- };
16
- const parseColor = (color) => {
17
- const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
18
- if (match) {
19
- const [r, g, b, opacity = 1.0] = match.slice(1, 5).map((val) => val && parseFloat(val));
20
- if (typeof r != 'number' ||
21
- typeof g != 'number' ||
22
- typeof b != 'number' ||
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
+ * 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 });