@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
@@ -1,23 +1,70 @@
1
- import { easeEmphasized } from './easing.js';
2
- export const enterExit = (node, options = {}) => {
3
- const mode = options.mode ?? 'fade';
4
- return {
5
- delay: options.delay ?? 0,
6
- duration: options.duration ?? 300,
7
- easing: options.easing ?? easeEmphasized,
8
- css: (t) => {
9
- switch (mode) {
10
- case 'scale':
11
- return `opacity: ${t}; transform: scale(${0.85 + t * 0.15});`;
12
- case 'slide-up':
13
- return `opacity: ${t}; transform: translateY(${(1 - t) * 12}px);`;
14
- case 'dialog':
15
- return `opacity: ${t}; transform: translateY(${(1 - t) * -24}px) scale(${0.9 + t * 0.1});`;
16
- case 'dialog-m3':
17
- return `opacity: ${t}; transform: scale(${0.9 + t * 0.1});`;
18
- default:
19
- return `opacity: ${t};`;
20
- }
21
- }
22
- };
1
+ import { springTokens, springTransition } from './spring.js';
2
+ /*
3
+ Scale and offset values come from Material Components Android, the reference implementation of
4
+ the M3 transition patterns (github.com/material-components/material-components-android,
5
+ lib/java/com/google/android/material/transition):
6
+ - MaterialFade: enters from 0.8 scale, opacity done in the first 30%; exits by fading only.
7
+ - BaseTransientBottomBar (snackbar), slide mode: translates by its own full height.
8
+ Spring timings replace MDC's duration/easing pairs.
9
+ */
10
+ const MATERIAL_FADE_START_SCALE = 0.8;
11
+ /* Spatial spring for the movement; the fade uses the fast effects spring so opacity lands early
12
+ (MDC fades in over the first 30% of the enter) and never overshoots. */
13
+ const enterWith = (spatial) => ({
14
+ ...springTransition(spatial),
15
+ opacity: springTransition(springTokens.fastEffects)
16
+ });
17
+ /* Exits leave quickly and don't bounce on the way out: critically damped throughout. */
18
+ const exit = {
19
+ ...springTransition(springTokens.effects),
20
+ opacity: springTransition(springTokens.fastEffects)
21
+ };
22
+ /* bits-ui sets this on every floating wrapper: the side of the content facing its anchor. */
23
+ const ANCHOR_ORIGIN = 'var(--bits-floating-transform-origin, center)';
24
+ export const enterExit = {
25
+ /** Opacity only — scrims, overlays, content swapped in place. */
26
+ fade: {
27
+ hidden: { opacity: 0 },
28
+ shown: { opacity: 1 },
29
+ enter: springTransition(springTokens.effects),
30
+ exit: springTransition(springTokens.fastEffects)
31
+ },
32
+ /** Anchored surfaces — menus, popovers, tooltips, pickers. Grows out of its anchor (MaterialFade). */
33
+ scale: {
34
+ hidden: { opacity: 0, transform: `scale(${MATERIAL_FADE_START_SCALE})` },
35
+ shown: { opacity: 1, transform: 'scale(1)' },
36
+ exited: { opacity: 0 },
37
+ origin: ANCHOR_ORIGIN,
38
+ enter: enterWith(springTokens.fastSpatial),
39
+ exit
40
+ },
41
+ /** Snackbars — slide in from below by their own height (MDC slide mode). */
42
+ slideUp: {
43
+ hidden: { opacity: 0, transform: 'translateY(100%)' },
44
+ shown: { opacity: 1, transform: 'translateY(0%)' },
45
+ enter: enterWith(springTokens.fastSpatial),
46
+ exit
47
+ },
48
+ /** Modal dialogs — MaterialFade from the center. */
49
+ dialog: {
50
+ hidden: { opacity: 0, transform: `scale(${MATERIAL_FADE_START_SCALE})` },
51
+ shown: { opacity: 1, transform: 'scale(1)' },
52
+ exited: { opacity: 0 },
53
+ enter: enterWith(springTokens.spatial),
54
+ exit
55
+ },
56
+ /** Side sheet anchored to the inline-end edge. */
57
+ sideSheet: {
58
+ hidden: { transform: 'translateX(100%)' },
59
+ shown: { transform: 'translateX(0%)' },
60
+ enter: springTransition(springTokens.spatial),
61
+ exit: springTransition(springTokens.effects)
62
+ },
63
+ /** Bottom sheet anchored to the bottom edge. */
64
+ bottomSheet: {
65
+ hidden: { transform: 'translateY(100%)' },
66
+ shown: { transform: 'translateY(0%)' },
67
+ enter: springTransition(springTokens.spatial),
68
+ exit: springTransition(springTokens.effects)
69
+ }
23
70
  };
@@ -1,5 +1,5 @@
1
- export * from './easing.js';
2
1
  export * from './shapes.js';
3
2
  export * from './shapesAnimatable.js';
4
3
  export * from './shapesAnimatableSmall.js';
4
+ export * from './spring.js';
5
5
  export * from './transition.js';
@@ -1,5 +1,5 @@
1
- export * from './easing.js';
2
1
  export * from './shapes.js';
3
2
  export * from './shapesAnimatable.js';
4
3
  export * from './shapesAnimatableSmall.js';
4
+ export * from './spring.js';
5
5
  export * from './transition.js';
@@ -0,0 +1,47 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import type { PresenceTransition } from './enterExit.js';
3
+ /**
4
+ * Enter/exit attachment driven by Motion's `animate()`. Plays `transition.enter` from `hidden` to
5
+ * `shown` when the element mounts open, and `transition.exit` to `exited` (default `hidden`) when
6
+ * `isOpen()` turns false. Applies `transition.origin` as the element's `transform-origin`. Reopening mid-exit (or closing mid-enter) retargets from the current value — the
7
+ * spring keeps its velocity instead of restarting.
8
+ *
9
+ * Inside bits-ui content (`Popover.Content`, `DropdownMenu.Content`, `Dialog.Content`, …) drop
10
+ * `forceMount` and attach this to the element receiving `props`: bits-ui's presence layer waits
11
+ * for that element's `getAnimations()` before unmounting, and Motion runs transform/opacity
12
+ * through WAAPI, so the exit plays out in full.
13
+ *
14
+ * ```svelte
15
+ * <Popover.Content>
16
+ * {#snippet child({ wrapperProps, props, open })}
17
+ * <div {...wrapperProps}>
18
+ * <div {...props} {@attach presence(() => open, enterExit.scale)}>…</div>
19
+ * </div>
20
+ * {/snippet}
21
+ * </Popover.Content>
22
+ * ```
23
+ *
24
+ * Outside bits-ui, use {@link Presence} so the element stays mounted until the exit finishes.
25
+ */
26
+ export declare const presence: (isOpen: () => boolean, transition: PresenceTransition, onExitComplete?: () => void) => Attachment<HTMLElement>;
27
+ /**
28
+ * Keeps an element mounted until its exit animation finishes — the Motion replacement for a
29
+ * Svelte `out:` transition. Construct during component init.
30
+ *
31
+ * ```svelte
32
+ * <script>
33
+ * const sheet = new Presence(() => open);
34
+ * </script>
35
+ *
36
+ * {#if sheet.mounted}
37
+ * <div {@attach sheet.attach(enterExit.sideSheet)}>…</div>
38
+ * {/if}
39
+ * ```
40
+ */
41
+ export declare class Presence {
42
+ #private;
43
+ constructor(isOpen: () => boolean);
44
+ /** True while open, and while the exit animation is still playing. */
45
+ mounted: boolean;
46
+ attach: (transition: PresenceTransition) => Attachment<HTMLElement>;
47
+ }
@@ -0,0 +1,84 @@
1
+ import { animate } from 'motion';
2
+ import { untrack } from 'svelte';
3
+ const fromTo = (from, to) => Object.fromEntries(Object.entries(to).map(([key, value]) => [key, [from[key], value]]));
4
+ /**
5
+ * Enter/exit attachment driven by Motion's `animate()`. Plays `transition.enter` from `hidden` to
6
+ * `shown` when the element mounts open, and `transition.exit` to `exited` (default `hidden`) when
7
+ * `isOpen()` turns false. Applies `transition.origin` as the element's `transform-origin`. Reopening mid-exit (or closing mid-enter) retargets from the current value — the
8
+ * spring keeps its velocity instead of restarting.
9
+ *
10
+ * Inside bits-ui content (`Popover.Content`, `DropdownMenu.Content`, `Dialog.Content`, …) drop
11
+ * `forceMount` and attach this to the element receiving `props`: bits-ui's presence layer waits
12
+ * for that element's `getAnimations()` before unmounting, and Motion runs transform/opacity
13
+ * through WAAPI, so the exit plays out in full.
14
+ *
15
+ * ```svelte
16
+ * <Popover.Content>
17
+ * {#snippet child({ wrapperProps, props, open })}
18
+ * <div {...wrapperProps}>
19
+ * <div {...props} {@attach presence(() => open, enterExit.scale)}>…</div>
20
+ * </div>
21
+ * {/snippet}
22
+ * </Popover.Content>
23
+ * ```
24
+ *
25
+ * Outside bits-ui, use {@link Presence} so the element stays mounted until the exit finishes.
26
+ */
27
+ export const presence = (isOpen, transition, onExitComplete) => (node) => {
28
+ let controls;
29
+ let started = false;
30
+ if (transition.origin)
31
+ node.style.transformOrigin = transition.origin;
32
+ $effect(() => {
33
+ const open = isOpen();
34
+ untrack(() => {
35
+ const { hidden, shown, exited = hidden, enter, exit } = transition;
36
+ if (open) {
37
+ controls = animate(node, started ? shown : fromTo(hidden, shown), enter);
38
+ }
39
+ else {
40
+ const current = (controls = animate(node, exited, exit));
41
+ current.then(() => {
42
+ if (controls === current)
43
+ onExitComplete?.();
44
+ });
45
+ }
46
+ started = true;
47
+ });
48
+ });
49
+ return () => controls?.stop();
50
+ };
51
+ /**
52
+ * Keeps an element mounted until its exit animation finishes — the Motion replacement for a
53
+ * Svelte `out:` transition. Construct during component init.
54
+ *
55
+ * ```svelte
56
+ * <script>
57
+ * const sheet = new Presence(() => open);
58
+ * </script>
59
+ *
60
+ * {#if sheet.mounted}
61
+ * <div {@attach sheet.attach(enterExit.sideSheet)}>…</div>
62
+ * {/if}
63
+ * ```
64
+ */
65
+ export class Presence {
66
+ #isOpen;
67
+ // Plain field on purpose: set while open, read after close to hold the element for its exit.
68
+ #held = false;
69
+ #exits = $state(0);
70
+ constructor(isOpen) {
71
+ this.#isOpen = isOpen;
72
+ }
73
+ /** True while open, and while the exit animation is still playing. */
74
+ mounted = $derived.by(() => {
75
+ void this.#exits;
76
+ if (this.#isOpen())
77
+ this.#held = true;
78
+ return this.#held;
79
+ });
80
+ attach = (transition) => presence(this.#isOpen, transition, () => {
81
+ this.#held = false;
82
+ this.#exits++;
83
+ });
84
+ }
@@ -1,38 +1,36 @@
1
- import type { TransitionOptions } from './transitionTypes.js';
2
- type SharedAxisOptions = {
3
- direction: 'X' | 'Y';
4
- /**
5
- * true if this element is on the top/left of things
6
- * if it's first, then use transition: and set it to true
7
- * if it's last, then use transition: and set it to false
8
- * if it's in between, use separate in: and out: statements:
9
- * > set it to false when it's interacting with the left side, and true when interacting with its right
10
- * > in order to implement this, try something like using a prevPage variable:
11
- * > ```
12
- * > {:else if page == 1}
13
- * > <div
14
- * > in:sharedAxisTransition={{
15
- * > direction: "X",
16
- * > rightSeam: prevPage > 1, (if we're transitioning from a page on the right, rightseam is true)
17
- * > }}
18
- * > out:sharedAxisTransition={{
19
- * > direction: "X",
20
- * > rightSeam: page > 1, (if we're transitioning to a page on the right, rightseam is true)
21
- * > }}
22
- * > >
23
- * > ```
24
- *
25
- * i went insane over figuring this out :)
26
- */
27
- rightSeam: boolean;
28
- } | {
29
- direction: 'Z';
30
- leaving: boolean;
31
- };
32
- export declare const sharedAxisTransition: (node: Element, options: TransitionOptions & SharedAxisOptions) => {
33
- delay: number | undefined;
34
- duration: number;
35
- easing: (t: number) => number;
36
- css: (t: number, u: number) => string;
37
- };
1
+ import { type ViewTransitionTargetDefinition } from 'motion';
2
+ import { type SpringToken } from './spring.js';
3
+ export type NavigationDirection = 'forward' | 'backward';
4
+ interface NavigationOptions {
5
+ /** The region whose content changes. Omit to transition the whole page. */
6
+ target?: ViewTransitionTargetDefinition;
7
+ /** `forward` moves deeper / to the next item, `backward` returns. */
8
+ direction?: NavigationDirection;
9
+ spring?: SpringToken;
10
+ }
11
+ export interface SharedAxisOptions extends NavigationOptions {
12
+ /** `x` for horizontal steps, `y` for vertical, `z` for parent → child depth. */
13
+ axis?: 'x' | 'y' | 'z';
14
+ }
15
+ /**
16
+ * M3 forward and backward (shared axis): outgoing and incoming content travel together along one
17
+ * axis while fading through each other.
18
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#forward-and-backward
19
+ *
20
+ * `update` performs the DOM change (`async () => { step++; await tick(); }`). Built on Motion's
21
+ * `animateView()` — both states never need to coexist in the DOM.
22
+ */
23
+ export declare const sharedAxis: (update: () => void | Promise<void>, { target, axis, direction, spring }?: SharedAxisOptions) => import("motion-dom").ViewTransitionBuilder;
24
+ /**
25
+ * M3 lateral: peer screens at the same level (tabs, carousels) slide past each other edge to edge,
26
+ * without fading — the new screen pushes the old one out.
27
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#lateral
28
+ */
29
+ export declare const lateral: (update: () => void | Promise<void>, { target, direction, spring }?: NavigationOptions) => import("motion-dom").ViewTransitionBuilder;
30
+ /**
31
+ * M3 top level (fade through): destinations with no spatial relationship, e.g. navigation bar
32
+ * items. The outgoing screen fades out, then the incoming one fades in while scaling up from 92%.
33
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#top-level
34
+ */
35
+ export declare const fadeThrough: (update: () => void | Promise<void>, { target, spring }?: Omit<NavigationOptions, "direction">) => import("motion-dom").ViewTransitionBuilder;
38
36
  export {};
@@ -1,23 +1,51 @@
1
- import { easeEmphasized } from './easing.js';
2
- /* protip: set a background color on the items, and utilize position relative + absolute to let them overlap */
3
- export const sharedAxisTransition = (node, options) => {
4
- void node;
5
- return {
6
- delay: options.delay,
7
- duration: options.duration ?? 500,
8
- easing: options.easing ?? easeEmphasized,
9
- css: (t, u) => {
10
- const opacity = (t - 0.35) * (1 / 0.35);
11
- if (options.direction == 'Z') {
12
- const factor = options.leaving ? u * 0.1 + 1 : t * 0.2 + 0.8;
13
- let css = `transform: scale(${factor.toFixed(3)});`;
14
- if (!options.leaving)
15
- css += `opacity: ${opacity.toFixed(3)};`;
16
- return css;
17
- }
18
- const factor = u * (options.rightSeam ? -30 : 30);
19
- return (`transform: translate${options.direction}(${factor.toFixed(3)}px);` +
20
- `opacity: ${opacity.toFixed(3)}`);
21
- }
22
- };
1
+ import { animateView } from 'motion';
2
+ import { springTokens, springTransition } from './spring.js';
3
+ const SHARED_AXIS_OFFSET_PX = 30;
4
+ const view = (update, target, spring) => {
5
+ const builder = animateView(update, springTransition(spring));
6
+ return target ? builder.add(target) : builder;
23
7
  };
8
+ /* Outgoing content clears quickly; incoming content waits a beat so the two never overlap fully. */
9
+ const fadeOut = springTransition(springTokens.fastEffects);
10
+ const fadeIn = { ...springTransition(springTokens.effects), delay: 0.05 };
11
+ /**
12
+ * M3 forward and backward (shared axis): outgoing and incoming content travel together along one
13
+ * axis while fading through each other.
14
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#forward-and-backward
15
+ *
16
+ * `update` performs the DOM change (`async () => { step++; await tick(); }`). Built on Motion's
17
+ * `animateView()` — both states never need to coexist in the DOM.
18
+ */
19
+ export const sharedAxis = (update, { target, axis = 'x', direction = 'forward', spring = springTokens.spatial } = {}) => {
20
+ const forward = direction === 'forward';
21
+ const [outgoing, incoming] = axis === 'z'
22
+ ? [`scale(${forward ? 1.1 : 0.8})`, `scale(${forward ? 0.8 : 1.1})`]
23
+ : [
24
+ `translate${axis.toUpperCase()}(${forward ? -SHARED_AXIS_OFFSET_PX : SHARED_AXIS_OFFSET_PX}px)`,
25
+ `translate${axis.toUpperCase()}(${forward ? SHARED_AXIS_OFFSET_PX : -SHARED_AXIS_OFFSET_PX}px)`
26
+ ];
27
+ const rest = axis === 'z' ? 'scale(1)' : `translate${axis.toUpperCase()}(0px)`;
28
+ return view(update, target, spring)
29
+ .old({ opacity: [1, 0], transform: [rest, outgoing] }, { opacity: fadeOut })
30
+ .new({ opacity: [0, 1], transform: [incoming, rest] }, { opacity: fadeIn });
31
+ };
32
+ /**
33
+ * M3 lateral: peer screens at the same level (tabs, carousels) slide past each other edge to edge,
34
+ * without fading — the new screen pushes the old one out.
35
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#lateral
36
+ */
37
+ export const lateral = (update, { target, direction = 'forward', spring = springTokens.spatial } = {}) => {
38
+ const sign = direction === 'forward' ? 1 : -1;
39
+ const builder = view(update, target, spring)
40
+ .old({ transform: ['translateX(0%)', `translateX(${-sign * 100}%)`] })
41
+ .new({ transform: [`translateX(${sign * 100}%)`, 'translateX(0%)'] });
42
+ return target ? builder.crop(true) : builder;
43
+ };
44
+ /**
45
+ * M3 top level (fade through): destinations with no spatial relationship, e.g. navigation bar
46
+ * items. The outgoing screen fades out, then the incoming one fades in while scaling up from 92%.
47
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#top-level
48
+ */
49
+ export const fadeThrough = (update, { target, spring = springTokens.spatial } = {}) => view(update, target, spring)
50
+ .old({ opacity: [1, 0] }, fadeOut)
51
+ .new({ opacity: [0, 1], transform: ['scale(0.92)', 'scale(1)'] }, { opacity: { ...springTransition(springTokens.effects), delay: 0.09 } });
@@ -0,0 +1,11 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ /**
3
+ * M3 skeleton loader: a placeholder pulses until real content replaces it. Swap the placeholder
4
+ * for the content with `presence(…, enterExit.fade)` so the reveal is a fade, not a cut.
5
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#skeleton-loaders
6
+ *
7
+ * ```svelte
8
+ * <div class="bg-md-sys-color-surface-container-highest h-4 rounded-sm" {@attach skeleton}></div>
9
+ * ```
10
+ */
11
+ export declare const skeleton: Attachment<HTMLElement>;
@@ -0,0 +1,23 @@
1
+ import { animate } from 'motion';
2
+ import { springTokens, springTransition } from './spring.js';
3
+ /* Hold at each end of the pulse — a critically damped spring alone settles in ~370ms, which reads
4
+ as flicker. Not an M3 value: tune by eye. */
5
+ const PULSE_HOLD_S = 0.5;
6
+ /**
7
+ * M3 skeleton loader: a placeholder pulses until real content replaces it. Swap the placeholder
8
+ * for the content with `presence(…, enterExit.fade)` so the reveal is a fade, not a cut.
9
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#skeleton-loaders
10
+ *
11
+ * ```svelte
12
+ * <div class="bg-md-sys-color-surface-container-highest h-4 rounded-sm" {@attach skeleton}></div>
13
+ * ```
14
+ */
15
+ export const skeleton = (node) => {
16
+ const controls = animate(node, { opacity: [1, 0.4] }, {
17
+ ...springTransition(springTokens.slowEffects),
18
+ repeat: Infinity,
19
+ repeatType: 'reverse',
20
+ repeatDelay: PULSE_HOLD_S
21
+ });
22
+ return () => controls.stop();
23
+ };
@@ -0,0 +1,61 @@
1
+ import { spring } from 'motion';
2
+ /**
3
+ * M3 Expressive spring tokens: `stiffness` plus a `dampingRatio`
4
+ *
5
+ * - **spatial** springs move things (position, size, shape) and overshoot slightly.
6
+ * - **effects** springs change things in place (opacity, color) and are critically damped.
7
+ *
8
+ * https://m3.material.io/styles/motion/overview/specs
9
+ */
10
+ export declare const springTokens: {
11
+ readonly fastSpatial: {
12
+ readonly stiffness: 800;
13
+ readonly dampingRatio: 0.6;
14
+ };
15
+ readonly spatial: {
16
+ readonly stiffness: 380;
17
+ readonly dampingRatio: 0.8;
18
+ };
19
+ readonly slowSpatial: {
20
+ readonly stiffness: 200;
21
+ readonly dampingRatio: 0.8;
22
+ };
23
+ readonly fastEffects: {
24
+ readonly stiffness: 3800;
25
+ readonly dampingRatio: 1;
26
+ };
27
+ readonly effects: {
28
+ readonly stiffness: 1600;
29
+ readonly dampingRatio: 1;
30
+ };
31
+ readonly slowEffects: {
32
+ readonly stiffness: 800;
33
+ readonly dampingRatio: 1;
34
+ };
35
+ };
36
+ export type SpringToken = {
37
+ stiffness: number;
38
+ dampingRatio: number;
39
+ };
40
+ /**
41
+ * A spring token as Motion transition options, for `animate()` / `animateView()`. Physics-based
42
+ * (stiffness/damping, mass 1), so an animation that interrupts another inherits its velocity.
43
+ *
44
+ * ```ts
45
+ * animate(node, { transform: 'scale(1)' }, springTransition(springTokens.fastSpatial));
46
+ * ```
47
+ */
48
+ export declare const springTransition: ({ stiffness, dampingRatio }: SpringToken) => {
49
+ readonly type: typeof spring;
50
+ readonly stiffness: number;
51
+ readonly damping: number;
52
+ readonly mass: 1;
53
+ };
54
+ /**
55
+ * A spring as CSS `transition` parts: a settle `duration` and a `linear()` `easing`.
56
+ * Used by `scripts/generate-motion-springs.ts` to bake the tokens into `styles/motion.css`.
57
+ */
58
+ export declare const springCss: (token: SpringToken, resolutionMs?: number) => {
59
+ duration: string;
60
+ easing: string;
61
+ };
@@ -0,0 +1,53 @@
1
+ import { generateLinearEasing, spring } from 'motion';
2
+ /**
3
+ * M3 Expressive spring tokens: `stiffness` plus a `dampingRatio`
4
+ *
5
+ * - **spatial** springs move things (position, size, shape) and overshoot slightly.
6
+ * - **effects** springs change things in place (opacity, color) and are critically damped.
7
+ *
8
+ * https://m3.material.io/styles/motion/overview/specs
9
+ */
10
+ export const springTokens = {
11
+ fastSpatial: { stiffness: 800, dampingRatio: 0.6 },
12
+ spatial: { stiffness: 380, dampingRatio: 0.8 },
13
+ slowSpatial: { stiffness: 200, dampingRatio: 0.8 },
14
+ fastEffects: { stiffness: 3800, dampingRatio: 1 },
15
+ effects: { stiffness: 1600, dampingRatio: 1 },
16
+ slowEffects: { stiffness: 800, dampingRatio: 1 }
17
+ };
18
+ const STEP_MS = 10;
19
+ const MAX_MS = 5000;
20
+ const resolveSpring = (token) => {
21
+ // Motion takes an absolute damping coefficient; M3 specifies a ratio (mass is 1).
22
+ const { stiffness, damping } = springTransition(token);
23
+ const generator = spring({ keyframes: [0, 1], stiffness, damping });
24
+ let duration = 0;
25
+ while (!generator.next(duration).done && duration < MAX_MS)
26
+ duration += STEP_MS;
27
+ return { duration, at: (progress) => generator.next(duration * progress).value };
28
+ };
29
+ /**
30
+ * A spring token as Motion transition options, for `animate()` / `animateView()`. Physics-based
31
+ * (stiffness/damping, mass 1), so an animation that interrupts another inherits its velocity.
32
+ *
33
+ * ```ts
34
+ * animate(node, { transform: 'scale(1)' }, springTransition(springTokens.fastSpatial));
35
+ * ```
36
+ */
37
+ export const springTransition = ({ stiffness, dampingRatio }) => ({
38
+ // The generator itself, not the string 'spring': the view-transition / WAAPI path
39
+ // (`animateView`, NativeAnimation) only bakes springs from the function and otherwise falls
40
+ // back to a 300ms easeOut.
41
+ type: spring,
42
+ stiffness,
43
+ damping: dampingRatio * 2 * Math.sqrt(stiffness),
44
+ mass: 1
45
+ });
46
+ /**
47
+ * A spring as CSS `transition` parts: a settle `duration` and a `linear()` `easing`.
48
+ * Used by `scripts/generate-motion-springs.ts` to bake the tokens into `styles/motion.css`.
49
+ */
50
+ export const springCss = (token, resolutionMs = STEP_MS) => {
51
+ const { duration, at } = resolveSpring(token);
52
+ return { duration: `${duration}ms`, easing: generateLinearEasing(at, duration, resolutionMs) };
53
+ };
@@ -0,0 +1,22 @@
1
+ import { type SpringToken } from './spring.js';
2
+ /**
3
+ * A number that springs towards a reactive target — for values rendered as geometry (a progress
4
+ * fill, a slider handle) rather than as a Motion-animated element. Retargeting mid-flight keeps
5
+ * the spring's velocity. Construct during component init.
6
+ *
7
+ * ```svelte
8
+ * <script>
9
+ * const shown = new SpringValue(() => percent, springTokens.slowEffects);
10
+ * </script>
11
+ *
12
+ * <div style:width="{shown.current}%"></div>
13
+ * ```
14
+ *
15
+ * `immediate` jumps straight to the target while it returns true — e.g. while a handle is dragged,
16
+ * where the value must track the pointer 1:1.
17
+ */
18
+ export declare class SpringValue {
19
+ #private;
20
+ current: number;
21
+ constructor(target: () => number, spring: SpringToken, immediate?: () => boolean);
22
+ }
@@ -0,0 +1,42 @@
1
+ import { animate } from 'motion';
2
+ import { untrack } from 'svelte';
3
+ import { springTransition } from './spring.js';
4
+ /**
5
+ * A number that springs towards a reactive target — for values rendered as geometry (a progress
6
+ * fill, a slider handle) rather than as a Motion-animated element. Retargeting mid-flight keeps
7
+ * the spring's velocity. Construct during component init.
8
+ *
9
+ * ```svelte
10
+ * <script>
11
+ * const shown = new SpringValue(() => percent, springTokens.slowEffects);
12
+ * </script>
13
+ *
14
+ * <div style:width="{shown.current}%"></div>
15
+ * ```
16
+ *
17
+ * `immediate` jumps straight to the target while it returns true — e.g. while a handle is dragged,
18
+ * where the value must track the pointer 1:1.
19
+ */
20
+ export class SpringValue {
21
+ current = $state(0);
22
+ #controls;
23
+ constructor(target, spring, immediate) {
24
+ this.current = untrack(target);
25
+ $effect(() => {
26
+ const to = target();
27
+ const jump = immediate?.() ?? false;
28
+ untrack(() => {
29
+ this.#controls?.stop();
30
+ if (jump) {
31
+ this.current = to;
32
+ return;
33
+ }
34
+ this.#controls = animate(this.current, to, {
35
+ ...springTransition(spring),
36
+ onUpdate: (latest) => (this.current = latest)
37
+ });
38
+ });
39
+ });
40
+ $effect(() => () => this.#controls?.stop());
41
+ }
42
+ }
@@ -1,4 +1,6 @@
1
1
  export * from './containerTransform.js';
2
2
  export * from './enterExit.js';
3
- export * from './outroClass.js';
3
+ export * from './presence.svelte.js';
4
4
  export * from './sharedAxisTransition.js';
5
+ export * from './skeleton.js';
6
+ export * from './springValue.svelte.js';
@@ -1,4 +1,6 @@
1
1
  export * from './containerTransform.js';
2
2
  export * from './enterExit.js';
3
- export * from './outroClass.js';
3
+ export * from './presence.svelte.js';
4
4
  export * from './sharedAxisTransition.js';
5
+ export * from './skeleton.js';
6
+ export * from './springValue.svelte.js';
@@ -0,0 +1,13 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ /**
3
+ * Calls `handler` when a click lands outside the element. Listens in the capture phase so
4
+ * it still fires when something inside the page stops propagation.
5
+ *
6
+ * Attach it conditionally so the document listener only exists while there is something to
7
+ * dismiss:
8
+ *
9
+ * ```svelte
10
+ * <div {@attach open && clickOutside(() => (open = false))}>...</div>
11
+ * ```
12
+ */
13
+ export declare function clickOutside(handler: (event: MouseEvent) => void): Attachment<HTMLElement>;