@noxlovette/material 0.4.8 → 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.
Files changed (131) hide show
  1. package/dist/animation/TransitionPatterns.stories.svelte +357 -0
  2. package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
  3. package/dist/animation/containerTransform.d.ts +26 -12
  4. package/dist/animation/containerTransform.js +22 -138
  5. package/dist/animation/enterExit.d.ts +21 -3
  6. package/dist/animation/enterExit.js +69 -22
  7. package/dist/animation/index.d.ts +1 -1
  8. package/dist/animation/index.js +1 -1
  9. package/dist/animation/presence.svelte.d.ts +47 -0
  10. package/dist/animation/presence.svelte.js +84 -0
  11. package/dist/animation/sharedAxisTransition.d.ts +35 -37
  12. package/dist/animation/sharedAxisTransition.js +50 -22
  13. package/dist/animation/skeleton.d.ts +11 -0
  14. package/dist/animation/skeleton.js +23 -0
  15. package/dist/animation/spring.d.ts +61 -0
  16. package/dist/animation/spring.js +53 -0
  17. package/dist/animation/springValue.svelte.d.ts +22 -0
  18. package/dist/animation/springValue.svelte.js +42 -0
  19. package/dist/animation/transition.d.ts +3 -1
  20. package/dist/animation/transition.js +3 -1
  21. package/dist/attachments/clickOutside.d.ts +13 -0
  22. package/dist/attachments/clickOutside.js +18 -0
  23. package/dist/attachments/index.d.ts +1 -0
  24. package/dist/attachments/index.js +1 -0
  25. package/dist/components/buttons/FAB.svelte +23 -16
  26. package/dist/components/buttons/FABMenu.svelte +24 -29
  27. package/dist/components/buttons/FABMenu.svelte.d.ts +2 -1
  28. package/dist/components/buttons/FABMenuItem.svelte +23 -20
  29. package/dist/components/buttons/FABMenuItem.svelte.d.ts +5 -1
  30. package/dist/components/buttons/split-button/SplitButton.svelte +10 -17
  31. package/dist/components/buttons/split-button/theme.js +1 -1
  32. package/dist/components/buttons/types.d.ts +3 -3
  33. package/dist/components/cards/theme.js +1 -1
  34. package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +8 -12
  35. package/dist/components/containers/bottom-sheet/BottomSheet.svelte +67 -55
  36. package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +1 -1
  37. package/dist/components/containers/bottom-sheet/types.d.ts +8 -2
  38. package/dist/components/containers/context-menu/ContextMenu.svelte +23 -36
  39. package/dist/components/containers/context-menu/theme.js +2 -2
  40. package/dist/components/containers/dialogue/Dialogue.svelte +49 -65
  41. package/dist/components/containers/link-preview/LinkPreview.svelte +12 -23
  42. package/dist/components/containers/link-preview/theme.d.ts +3 -3
  43. package/dist/components/containers/menu/Menu.svelte +14 -21
  44. package/dist/components/containers/menu/MenuSub.svelte +10 -17
  45. package/dist/components/containers/pane/DraggablePane.svelte +90 -5
  46. package/dist/components/containers/pane/DraggablePane.svelte.d.ts +1 -1
  47. package/dist/components/containers/pane/theme.js +2 -2
  48. package/dist/components/containers/popover/Popover.svelte +31 -42
  49. package/dist/components/containers/popover/theme.d.ts +3 -3
  50. package/dist/components/containers/scroll-area/theme.js +2 -2
  51. package/dist/components/containers/side-sheet/SideSheet.stories.svelte +10 -15
  52. package/dist/components/containers/side-sheet/SideSheet.svelte +44 -44
  53. package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +4 -3
  54. package/dist/components/containers/side-sheet/types.d.ts +8 -2
  55. package/dist/components/date/DateField.svelte +85 -92
  56. package/dist/components/date/DateField.svelte.d.ts +2 -5
  57. package/dist/components/date/DateRangeField.svelte +113 -155
  58. package/dist/components/date/DateRangeField.svelte.d.ts +2 -5
  59. package/dist/components/date/theme.d.ts +95 -0
  60. package/dist/components/date/theme.js +99 -14
  61. package/dist/components/date/types.d.ts +13 -0
  62. package/dist/components/forms/checkbox/theme.js +2 -2
  63. package/dist/components/forms/command/theme.d.ts +3 -3
  64. package/dist/components/forms/command/theme.js +1 -1
  65. package/dist/components/forms/pin/theme.js +2 -2
  66. package/dist/components/forms/radio-group/theme.js +2 -2
  67. package/dist/components/forms/select/Select.svelte +42 -54
  68. package/dist/components/forms/select/theme.js +4 -4
  69. package/dist/components/forms/slider/Slider.svelte +144 -126
  70. package/dist/components/forms/slider/Slider.svelte.d.ts +3 -0
  71. package/dist/components/forms/slider/theme.d.ts +154 -48
  72. package/dist/components/forms/slider/theme.js +60 -20
  73. package/dist/components/forms/slider/types.d.ts +1 -1
  74. package/dist/components/forms/switch/theme.js +4 -4
  75. package/dist/components/forms/textfield/Textfield.svelte +1 -1
  76. package/dist/components/forms/textfield/theme.d.ts +9 -0
  77. package/dist/components/forms/textfield/theme.js +26 -9
  78. package/dist/components/forms/toggle-group/theme.js +1 -1
  79. package/dist/components/forms/tooltip/Tooltip.svelte +24 -35
  80. package/dist/components/nav/breadcrumb/theme.js +1 -1
  81. package/dist/components/nav/navbar/theme.js +2 -2
  82. package/dist/components/nav/rail/Rail.svelte +4 -5
  83. package/dist/components/nav/rail/theme.js +7 -7
  84. package/dist/components/nav/tabs/theme.js +1 -1
  85. package/dist/components/progress/CircularProgress.svelte +78 -4
  86. package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
  87. package/dist/components/progress/LinearProgress.svelte +70 -7
  88. package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
  89. package/dist/components/progress/WavyLinearProgress.svelte +27 -3
  90. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
  91. package/dist/components/progress/_wavy.d.ts +2 -0
  92. package/dist/components/progress/_wavy.js +3 -1
  93. package/dist/components/snackbar/Snackbar.svelte +13 -17
  94. package/dist/components/table/TableBody.svelte +3 -16
  95. package/dist/components/table/TableBody.svelte.d.ts +1 -5
  96. package/dist/components/table/TableHeader.svelte +1 -1
  97. package/dist/components/table/theme.js +1 -1
  98. package/dist/components/table/types.d.ts +0 -7
  99. package/dist/components/time/TimeField.stories.svelte +41 -0
  100. package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
  101. package/dist/components/time/TimeField.svelte +62 -61
  102. package/dist/components/toolbar/theme.js +2 -2
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.js +1 -1
  105. package/dist/styles/component.css +21 -22
  106. package/dist/styles/motion.css +317 -38
  107. package/dist/utils/Layer.svelte +16 -29
  108. package/dist/utils/icon/LoadingIndicator.svelte +61 -22
  109. package/dist/utils/icon/loadingShapes.d.ts +1 -0
  110. package/dist/utils/icon/loadingShapes.js +15 -0
  111. package/package.json +5 -2
  112. package/dist/actions/clickOutside.d.ts +0 -3
  113. package/dist/actions/clickOutside.js +0 -13
  114. package/dist/actions/floating.d.ts +0 -20
  115. package/dist/actions/floating.js +0 -87
  116. package/dist/actions/index.d.ts +0 -6
  117. package/dist/actions/index.js +0 -6
  118. package/dist/actions/keyboard.d.ts +0 -3
  119. package/dist/actions/keyboard.js +0 -14
  120. package/dist/actions/portal.d.ts +0 -3
  121. package/dist/actions/portal.js +0 -8
  122. package/dist/actions/positionFloating.d.ts +0 -10
  123. package/dist/actions/positionFloating.js +0 -59
  124. package/dist/actions/rovingFocusGroup.d.ts +0 -20
  125. package/dist/actions/rovingFocusGroup.js +0 -82
  126. package/dist/animation/easing.d.ts +0 -6
  127. package/dist/animation/easing.js +0 -78
  128. package/dist/animation/outroClass.d.ts +0 -3
  129. package/dist/animation/outroClass.js +0 -20
  130. package/dist/animation/transitionTypes.d.ts +0 -8
  131. 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>
@@ -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,27 @@
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
+ * 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 { 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
+ * 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 { TransitionConfig } from 'svelte/transition';
2
- import type { TransitionOptions } from './transitionTypes.js';
3
- export declare const enterExit: (node: Element, options?: TransitionOptions) => TransitionConfig;
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>;