@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
@@ -1,3 +1,30 @@
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
+ * Introduces a component in the context of the current screen, modal or not
10
+ * (https://m3.material.io/styles/motion/transitions/applying-transitions). Never use it to navigate between hierarchical screens.
11
+ * Clean fades: opacity always runs on `fastEffects` so it never lingers over the content behind.
12
+ * Edge-anchored sheets slide without fading, as M3 asks.
13
+ *
14
+ * Required for every component that mounts a surface within the current screen: dialogs, sheets,
15
+ * menus, popovers, tooltips, pickers, snackbars, the FAB menu. See the ownership table in
16
+ * `.claude/skills/material-design/references/motion-guide.md`.
17
+ */
18
+ export interface PresenceTransition {
19
+ /** Where the enter animation starts. */
20
+ hidden: DOMKeyframesDefinition;
21
+ shown: DOMKeyframesDefinition;
22
+ /** Where the exit animation ends. Defaults to `hidden`. */
23
+ exited?: DOMKeyframesDefinition;
24
+ /** `transform-origin` applied while mounted. */
25
+ origin?: string;
26
+ enter: AnimationOptions;
27
+ exit: AnimationOptions;
28
+ }
29
+ export type EnterExitPreset = 'fade' | 'scale' | 'slideUp' | 'dialog' | 'sideSheet' | 'bottomSheet';
30
+ export declare const enterExit: Record<EnterExitPreset, PresenceTransition>;
@@ -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. Slides only: no opacity (M3: don't fade sheets). */
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. Slides only: no opacity (M3: don't fade sheets). */
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,6 @@
1
- export * from './easing.js';
1
+ export * from './shapeMorph.svelte.js';
2
2
  export * from './shapes.js';
3
3
  export * from './shapesAnimatable.js';
4
4
  export * from './shapesAnimatableSmall.js';
5
+ export * from './spring.js';
5
6
  export * from './transition.js';
@@ -1,5 +1,6 @@
1
- export * from './easing.js';
1
+ export * from './shapeMorph.svelte.js';
2
2
  export * from './shapes.js';
3
3
  export * from './shapesAnimatable.js';
4
4
  export * from './shapesAnimatableSmall.js';
5
+ export * from './spring.js';
5
6
  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
+ }
@@ -0,0 +1,52 @@
1
+ import { type AnimationPlaybackControls } from 'motion';
2
+ import type { Attachment } from 'svelte/attachments';
3
+ import { type SpringToken } from './spring.js';
4
+ /**
5
+ * The 35 shapes of the M3 Expressive shape library, in the order of the spec sheet.
6
+ * https://m3.material.io/styles/shape
7
+ */
8
+ export declare const shapeNames: readonly ["circle", "square", "slanted", "arch", "semicircle", "oval", "pill", "triangle", "arrow", "fan", "diamond", "clamshell", "pentagon", "gem", "verySunny", "sunny", "fourSidedCookie", "sixSidedCookie", "sevenSidedCookie", "nineSidedCookie", "twelveSidedCookie", "fourLeafClover", "eightLeafClover", "burst", "softBurst", "boom", "softBoom", "flower", "puffy", "puffyDiamond", "ghostish", "pixelCircle", "pixelTriangle", "bun", "heart"];
9
+ export type ShapeName = (typeof shapeNames)[number];
10
+ /**
11
+ * Every shape as a 720-point path in a 380 × 380 viewBox (`<svg viewBox="0 0 380 380">`). All
12
+ * share one command structure, start point and winding, so any two morph cleanly. Use these for
13
+ * large, hero-sized shapes; for anything icon-sized prefer {@link animatableShapesSmall}.
14
+ *
15
+ * Importing the map pulls in all 35 paths (~315 KB unminified). To ship only a few, import the
16
+ * individual `pathAnimatable*` constants instead.
17
+ */
18
+ export declare const animatableShapes: Record<"circle" | "square" | "slanted" | "arch" | "semicircle" | "oval" | "pill" | "triangle" | "arrow" | "fan" | "diamond" | "clamshell" | "pentagon" | "gem" | "verySunny" | "sunny" | "fourSidedCookie" | "sixSidedCookie" | "sevenSidedCookie" | "nineSidedCookie" | "twelveSidedCookie" | "fourLeafClover" | "eightLeafClover" | "burst" | "softBurst" | "boom" | "softBoom" | "flower" | "puffy" | "puffyDiamond" | "ghostish" | "pixelCircle" | "pixelTriangle" | "bun" | "heart", string>;
19
+ /**
20
+ * Every shape as a 120-point path in a 48 × 48 viewBox (38 × 38 shape, centred). Same guarantees
21
+ * as {@link animatableShapes} at an eighth of the size: the set for icons, avatars, indicators and
22
+ * FABs. Never mix paths from the two sets in one morph — their point counts differ.
23
+ */
24
+ export declare const animatableShapesSmall: Record<"circle" | "square" | "slanted" | "arch" | "semicircle" | "oval" | "pill" | "triangle" | "arrow" | "fan" | "diamond" | "clamshell" | "pentagon" | "gem" | "verySunny" | "sunny" | "fourSidedCookie" | "sixSidedCookie" | "sevenSidedCookie" | "nineSidedCookie" | "twelveSidedCookie" | "fourLeafClover" | "eightLeafClover" | "burst" | "softBurst" | "boom" | "softBoom" | "flower" | "puffy" | "puffyDiamond" | "ghostish" | "pixelCircle" | "pixelTriangle" | "bun" | "heart", string>;
25
+ /**
26
+ * Morphs an SVG `<path>` from its current `d` to `to` on an M3 spring (default `fastSpatial`,
27
+ * the token for shape morphs). Both paths must come from the same animatable set. Motion drives
28
+ * the progress; the spring's overshoot carries through into the shape. Under
29
+ * `prefers-reduced-motion` the shape swaps instantly (M3: turn off shape morphing).
30
+ *
31
+ * Starting a new morph mid-flight picks up from the in-between shape on screen, so stop the
32
+ * previous controls first — {@link shapeMorph} does this for you.
33
+ *
34
+ * ```ts
35
+ * morphShape(path, animatableShapesSmall.heart);
36
+ * ```
37
+ */
38
+ export declare const morphShape: (path: SVGPathElement, to: string, spring?: SpringToken) => AnimationPlaybackControls | undefined;
39
+ /**
40
+ * Attachment that keeps a `<path>` on `shape()` and morphs whenever it changes. The first shape
41
+ * renders without animating.
42
+ *
43
+ * ```svelte
44
+ * <svg viewBox="0 0 48 48">
45
+ * <path
46
+ * fill="currentColor"
47
+ * {@attach shapeMorph(() => animatableShapesSmall[selected ? 'sunny' : 'circle'])}
48
+ * />
49
+ * </svg>
50
+ * ```
51
+ */
52
+ export declare const shapeMorph: (shape: () => string, spring?: SpringToken) => Attachment<SVGPathElement>;
@@ -0,0 +1,112 @@
1
+ import { animate, mix } from 'motion';
2
+ import { untrack } from 'svelte';
3
+ import * as large from './shapesAnimatable.js';
4
+ import * as small from './shapesAnimatableSmall.js';
5
+ import { springTokens, springTransition } from './spring.js';
6
+ /**
7
+ * The 35 shapes of the M3 Expressive shape library, in the order of the spec sheet.
8
+ * https://m3.material.io/styles/shape
9
+ */
10
+ export const shapeNames = [
11
+ 'circle',
12
+ 'square',
13
+ 'slanted',
14
+ 'arch',
15
+ 'semicircle',
16
+ 'oval',
17
+ 'pill',
18
+ 'triangle',
19
+ 'arrow',
20
+ 'fan',
21
+ 'diamond',
22
+ 'clamshell',
23
+ 'pentagon',
24
+ 'gem',
25
+ 'verySunny',
26
+ 'sunny',
27
+ 'fourSidedCookie',
28
+ 'sixSidedCookie',
29
+ 'sevenSidedCookie',
30
+ 'nineSidedCookie',
31
+ 'twelveSidedCookie',
32
+ 'fourLeafClover',
33
+ 'eightLeafClover',
34
+ 'burst',
35
+ 'softBurst',
36
+ 'boom',
37
+ 'softBoom',
38
+ 'flower',
39
+ 'puffy',
40
+ 'puffyDiamond',
41
+ 'ghostish',
42
+ 'pixelCircle',
43
+ 'pixelTriangle',
44
+ 'bun',
45
+ 'heart'
46
+ ];
47
+ const byName = (paths, prefix) => Object.fromEntries(shapeNames.map((name) => [name, paths[prefix + name[0].toUpperCase() + name.slice(1)]]));
48
+ /**
49
+ * Every shape as a 720-point path in a 380 × 380 viewBox (`<svg viewBox="0 0 380 380">`). All
50
+ * share one command structure, start point and winding, so any two morph cleanly. Use these for
51
+ * large, hero-sized shapes; for anything icon-sized prefer {@link animatableShapesSmall}.
52
+ *
53
+ * Importing the map pulls in all 35 paths (~315 KB unminified). To ship only a few, import the
54
+ * individual `pathAnimatable*` constants instead.
55
+ */
56
+ export const animatableShapes = byName(large, 'pathAnimatable');
57
+ /**
58
+ * Every shape as a 120-point path in a 48 × 48 viewBox (38 × 38 shape, centred). Same guarantees
59
+ * as {@link animatableShapes} at an eighth of the size: the set for icons, avatars, indicators and
60
+ * FABs. Never mix paths from the two sets in one morph — their point counts differ.
61
+ */
62
+ export const animatableShapesSmall = byName(small, 'pathAnimatableSmall');
63
+ const prefersReducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
64
+ /**
65
+ * Morphs an SVG `<path>` from its current `d` to `to` on an M3 spring (default `fastSpatial`,
66
+ * the token for shape morphs). Both paths must come from the same animatable set. Motion drives
67
+ * the progress; the spring's overshoot carries through into the shape. Under
68
+ * `prefers-reduced-motion` the shape swaps instantly (M3: turn off shape morphing).
69
+ *
70
+ * Starting a new morph mid-flight picks up from the in-between shape on screen, so stop the
71
+ * previous controls first — {@link shapeMorph} does this for you.
72
+ *
73
+ * ```ts
74
+ * morphShape(path, animatableShapesSmall.heart);
75
+ * ```
76
+ */
77
+ export const morphShape = (path, to, spring = springTokens.fastSpatial) => {
78
+ const from = path.getAttribute('d');
79
+ if (!from || from === to || prefersReducedMotion()) {
80
+ path.setAttribute('d', to);
81
+ return;
82
+ }
83
+ const shape = mix(from, to);
84
+ return animate(0, 1, {
85
+ ...springTransition(spring),
86
+ onUpdate: (progress) => path.setAttribute('d', shape(progress))
87
+ });
88
+ };
89
+ /**
90
+ * Attachment that keeps a `<path>` on `shape()` and morphs whenever it changes. The first shape
91
+ * renders without animating.
92
+ *
93
+ * ```svelte
94
+ * <svg viewBox="0 0 48 48">
95
+ * <path
96
+ * fill="currentColor"
97
+ * {@attach shapeMorph(() => animatableShapesSmall[selected ? 'sunny' : 'circle'])}
98
+ * />
99
+ * </svg>
100
+ * ```
101
+ */
102
+ export const shapeMorph = (shape, spring) => (path) => {
103
+ let controls;
104
+ $effect(() => {
105
+ const to = shape();
106
+ untrack(() => {
107
+ controls?.stop();
108
+ controls = morphShape(path, to, spring);
109
+ });
110
+ });
111
+ return () => controls?.stop();
112
+ };