@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,38 +1,58 @@
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
+ * The default for common hierarchical navigation (https://m3.material.io/styles/motion/transitions/applying-transitions).
21
+ * Don't use it for hero moments; use `containerTransform` for those. Keep one axis per relationship
22
+ * across the app (coherent spatial model), and move one `target` region as a unit instead of
23
+ * animating many elements independently.
24
+ *
25
+ * `update` performs the DOM change (`async () => { step++; await tick(); }`). Built on Motion's
26
+ * `animateView()` — both states never need to coexist in the DOM.
27
+ *
28
+ * Not built into any component: hierarchy levels and wizard steps live in the consuming app.
29
+ */
30
+ export declare const sharedAxis: (update: () => void | Promise<void>, { target, axis, direction, spring }?: SharedAxisOptions) => import("motion-dom").ViewTransitionBuilder;
31
+ /**
32
+ * M3 lateral: peer screens at the same level (tabs, carousels) slide past each other edge to edge,
33
+ * without fading — the new screen pushes the old one out.
34
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#lateral
35
+ *
36
+ * Only for peers in one set (https://m3.material.io/styles/motion/transitions/applying-transitions). Never use it for hierarchical screens
37
+ * (use `sharedAxis`) or navbar/rail/drawer destinations (use `fadeThrough`, since the implied swipe
38
+ * conflicts with carousels and swipeable list items). Don't add a fade: it hides the peer
39
+ * relationship and makes the slide look like forward/backward.
40
+ *
41
+ * Built in: `TabHolder` (switching content panels) and `DateField`/`DateRangeField` (changing the
42
+ * visible month). Any new component that pages between peer views must use this too.
43
+ */
44
+ export declare const lateral: (update: () => void | Promise<void>, { target, direction, spring }?: NavigationOptions) => import("motion-dom").ViewTransitionBuilder;
45
+ /**
46
+ * M3 top level (fade through): destinations with no spatial relationship, e.g. navigation bar
47
+ * items. The outgoing screen fades out, then the incoming one fades in while scaling up from 92%.
48
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#top-level
49
+ *
50
+ * The quick fade for navigation bar, rail and drawer destinations (https://m3.material.io/styles/motion/transitions/applying-transitions).
51
+ * It deliberately creates no spatial connection between screens, and it's the pattern to use
52
+ * instead of `lateral` there.
53
+ *
54
+ * Not built into `Navbar`/`Rail`: they don't own the content region that changes, so the app wraps
55
+ * its own route change in this.
56
+ */
57
+ export declare const fadeThrough: (update: () => void | Promise<void>, { target, spring }?: Omit<NavigationOptions, "direction">) => import("motion-dom").ViewTransitionBuilder;
38
58
  export {};
@@ -1,23 +1,75 @@
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
+ M3 "clean fades" (https://m3.material.io/styles/motion/transitions/applying-transitions): fade out before fading in; any cross-fade stays short
10
+ and hidden in the fastest part of the motion. Don't lengthen these. */
11
+ const fadeOut = springTransition(springTokens.fastEffects);
12
+ const fadeIn = { ...springTransition(springTokens.effects), delay: 0.05 };
13
+ /**
14
+ * M3 forward and backward (shared axis): outgoing and incoming content travel together along one
15
+ * axis while fading through each other.
16
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#forward-and-backward
17
+ *
18
+ * The default for common hierarchical navigation (https://m3.material.io/styles/motion/transitions/applying-transitions).
19
+ * Don't use it for hero moments; use `containerTransform` for those. Keep one axis per relationship
20
+ * across the app (coherent spatial model), and move one `target` region as a unit instead of
21
+ * animating many elements independently.
22
+ *
23
+ * `update` performs the DOM change (`async () => { step++; await tick(); }`). Built on Motion's
24
+ * `animateView()` — both states never need to coexist in the DOM.
25
+ *
26
+ * Not built into any component: hierarchy levels and wizard steps live in the consuming app.
27
+ */
28
+ export const sharedAxis = (update, { target, axis = 'x', direction = 'forward', spring = springTokens.spatial } = {}) => {
29
+ const forward = direction === 'forward';
30
+ const [outgoing, incoming] = axis === 'z'
31
+ ? [`scale(${forward ? 1.1 : 0.8})`, `scale(${forward ? 0.8 : 1.1})`]
32
+ : [
33
+ `translate${axis.toUpperCase()}(${forward ? -SHARED_AXIS_OFFSET_PX : SHARED_AXIS_OFFSET_PX}px)`,
34
+ `translate${axis.toUpperCase()}(${forward ? SHARED_AXIS_OFFSET_PX : -SHARED_AXIS_OFFSET_PX}px)`
35
+ ];
36
+ const rest = axis === 'z' ? 'scale(1)' : `translate${axis.toUpperCase()}(0px)`;
37
+ return view(update, target, spring)
38
+ .old({ opacity: [1, 0], transform: [rest, outgoing] }, { opacity: fadeOut })
39
+ .new({ opacity: [0, 1], transform: [incoming, rest] }, { opacity: fadeIn });
40
+ };
41
+ /**
42
+ * M3 lateral: peer screens at the same level (tabs, carousels) slide past each other edge to edge,
43
+ * without fading — the new screen pushes the old one out.
44
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#lateral
45
+ *
46
+ * Only for peers in one set (https://m3.material.io/styles/motion/transitions/applying-transitions). Never use it for hierarchical screens
47
+ * (use `sharedAxis`) or navbar/rail/drawer destinations (use `fadeThrough`, since the implied swipe
48
+ * conflicts with carousels and swipeable list items). Don't add a fade: it hides the peer
49
+ * relationship and makes the slide look like forward/backward.
50
+ *
51
+ * Built in: `TabHolder` (switching content panels) and `DateField`/`DateRangeField` (changing the
52
+ * visible month). Any new component that pages between peer views must use this too.
53
+ */
54
+ export const lateral = (update, { target, direction = 'forward', spring = springTokens.spatial } = {}) => {
55
+ const sign = direction === 'forward' ? 1 : -1;
56
+ const builder = view(update, target, spring)
57
+ .old({ transform: ['translateX(0%)', `translateX(${-sign * 100}%)`] })
58
+ .new({ transform: [`translateX(${sign * 100}%)`, 'translateX(0%)'] });
59
+ return target ? builder.crop(true) : builder;
60
+ };
61
+ /**
62
+ * M3 top level (fade through): destinations with no spatial relationship, e.g. navigation bar
63
+ * items. The outgoing screen fades out, then the incoming one fades in while scaling up from 92%.
64
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#top-level
65
+ *
66
+ * The quick fade for navigation bar, rail and drawer destinations (https://m3.material.io/styles/motion/transitions/applying-transitions).
67
+ * It deliberately creates no spatial connection between screens, and it's the pattern to use
68
+ * instead of `lateral` there.
69
+ *
70
+ * Not built into `Navbar`/`Rail`: they don't own the content region that changes, so the app wraps
71
+ * its own route change in this.
72
+ */
73
+ export const fadeThrough = (update, { target, spring = springTokens.spatial } = {}) => view(update, target, spring)
74
+ .old({ opacity: [1, 0] }, fadeOut)
75
+ .new({ opacity: [0, 1], transform: ['scale(0.92)', 'scale(1)'] }, { opacity: { ...springTransition(springTokens.effects), delay: 0.09 } });
@@ -0,0 +1,16 @@
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
+ * M3 "stable layouts" (https://m3.material.io/styles/motion/transitions/applying-transitions): the placeholder takes the loaded
8
+ * content's footprint so nothing pops in or shifts position when the data arrives.
9
+ *
10
+ * Not built into any component — none of them load their own data. Apps apply it to placeholders.
11
+ *
12
+ * ```svelte
13
+ * <div class="bg-md-sys-color-surface-container-highest h-4 rounded-sm" {@attach skeleton}></div>
14
+ * ```
15
+ */
16
+ export declare const skeleton: Attachment<HTMLElement>;
@@ -0,0 +1,28 @@
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
+ * M3 "stable layouts" (https://m3.material.io/styles/motion/transitions/applying-transitions): the placeholder takes the loaded
12
+ * content's footprint so nothing pops in or shifts position when the data arrives.
13
+ *
14
+ * Not built into any component — none of them load their own data. Apps apply it to placeholders.
15
+ *
16
+ * ```svelte
17
+ * <div class="bg-md-sys-color-surface-container-highest h-4 rounded-sm" {@attach skeleton}></div>
18
+ * ```
19
+ */
20
+ export const skeleton = (node) => {
21
+ const controls = animate(node, { opacity: [1, 0.4] }, {
22
+ ...springTransition(springTokens.slowEffects),
23
+ repeat: Infinity,
24
+ repeatType: 'reverse',
25
+ repeatDelay: PULSE_HOLD_S
26
+ });
27
+ return () => controls.stop();
28
+ };
@@ -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>;
@@ -0,0 +1,18 @@
1
+ import { on } from 'svelte/events';
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 function clickOutside(handler) {
14
+ return (node) => on(document, 'click', (event) => {
15
+ if (!event.composedPath().includes(node))
16
+ handler(event);
17
+ }, { capture: true });
18
+ }
@@ -0,0 +1 @@
1
+ export * from './clickOutside.js';
@@ -0,0 +1 @@
1
+ export * from './clickOutside.js';
@@ -15,8 +15,7 @@ Floating action buttons (FABs) help people take primary actions.
15
15
  import type { FABProps } from './types.js';
16
16
  import FABMenu from './FABMenu.svelte';
17
17
  import { Layer, Icon, LoadingIndicator } from '../../utils/index.js';
18
- import { clickOutside } from '../../actions/index.js';
19
- import { Button, type ButtonRootProps } from 'bits-ui';
18
+ import { Button, DropdownMenu, type ButtonRootProps } from 'bits-ui';
20
19
 
21
20
  let {
22
21
  children,
@@ -30,44 +29,43 @@ Floating action buttons (FABs) help people take primary actions.
30
29
  expanded = false,
31
30
  class: className,
32
31
  withMenu,
32
+ onclick,
33
33
  ...restProps
34
34
  }: FABProps = $props();
35
35
 
36
36
  const { base, icon, label: labelClass } = $derived(fab({ size, config, expanded }));
37
37
 
38
38
  let showMenu = $state(false);
39
- let wrapperEl = $state<HTMLElement | null>(null);
40
39
 
41
40
  const btnCls = $derived(
42
41
  base({
43
42
  class: clsx(
44
43
  className,
45
- 'transition-[border-radius] duration-300',
44
+ 'transition-[border-radius] md-sys-motion-fast-spatial',
46
45
  withMenu && showMenu && '!rounded-full'
47
46
  )
48
47
  })
49
48
  );
50
49
 
51
- function handleClick(e: MouseEvent) {
50
+ function handleClick(e: MouseEvent, triggerClick?: unknown) {
52
51
  if (disabled || loading) {
53
52
  e.preventDefault();
54
53
  return;
55
54
  }
56
- if (withMenu) {
57
- e.preventDefault();
58
- showMenu = !showMenu;
59
- }
55
+ (onclick as ((e: MouseEvent) => void) | null | undefined)?.(e);
56
+ if (typeof triggerClick === 'function') triggerClick(e);
60
57
  }
61
58
  </script>
62
59
 
63
- <div class="contents" bind:this={wrapperEl} use:clickOutside={() => (showMenu = false)}>
60
+ {#snippet fabButton(triggerProps: Record<string, unknown>)}
64
61
  <Button.Root
65
62
  {disabled}
66
63
  {formaction}
67
- class={btnCls}
68
- onclick={handleClick}
69
64
  data-cy="m3-fab"
70
65
  {...restProps as ButtonRootProps}
66
+ {...triggerProps}
67
+ class={btnCls}
68
+ onclick={(e: MouseEvent) => handleClick(e, triggerProps.onclick)}
71
69
  >
72
70
  {#if loading}
73
71
  <LoadingIndicator />
@@ -81,10 +79,19 @@ Floating action buttons (FABs) help people take primary actions.
81
79
  </p>
82
80
  {/if}
83
81
  </Button.Root>
82
+ {/snippet}
84
83
 
85
- {#if withMenu && showMenu && wrapperEl?.firstElementChild}
86
- <FABMenu anchorEl={wrapperEl.firstElementChild as HTMLElement}>
84
+ {#if withMenu}
85
+ <DropdownMenu.Root bind:open={showMenu}>
86
+ <DropdownMenu.Trigger disabled={disabled || loading}>
87
+ {#snippet child({ props })}
88
+ {@render fabButton(props)}
89
+ {/snippet}
90
+ </DropdownMenu.Trigger>
91
+ <FABMenu>
87
92
  {@render children?.()}
88
93
  </FABMenu>
89
- {/if}
90
- </div>
94
+ </DropdownMenu.Root>
95
+ {:else}
96
+ {@render fabButton({})}
97
+ {/if}
@@ -1,42 +1,37 @@
1
1
  <!--
2
2
  @component
3
- A menu container for multiple FAB actions.
3
+ The floating panel of a FAB menu. Rendered by `FAB` when `withMenu` is set — must live inside
4
+ its `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape dismissal.
4
5
  Should be used with `FABMenuItem` as children.
5
6
  -->
6
7
  <script lang="ts">
7
8
  import clsx from 'clsx';
8
- import { flip } from '@floating-ui/dom';
9
+ import { DropdownMenu } from 'bits-ui';
9
10
  import { fabMenu } from './theme.js';
10
11
  import type { FABMenuProps } from './types.js';
11
- import { enterExit } from '../../animation/enterExit.js';
12
- import { easeEmphasizedAccel, easeEmphasizedDecel } from '../../animation/easing.js';
13
- import { floating, portal } from '../../actions/index.js';
12
+ import { enterExit, presence } from '../../animation/index.js';
14
13
 
15
- let { children, class: className, anchorEl, ...restProps }: FABMenuProps = $props();
14
+ let { children, class: className, ...restProps }: FABMenuProps = $props();
16
15
 
17
16
  const { base } = fabMenu();
18
17
  </script>
19
18
 
20
- <ul
21
- data-cy="m3-fab-menu"
22
- {...restProps}
23
- class={base({ class: clsx(className, 'fab-menu overflow-y-auto') })}
24
- in:enterExit={{ duration: 300, easing: easeEmphasizedDecel, mode: 'scale' }}
25
- out:enterExit={{ duration: 200, easing: easeEmphasizedAccel, mode: 'scale' }}
26
- use:portal
27
- use:floating={{
28
- reference: anchorEl,
29
- strategy: 'fixed',
30
- placement: 'top-end',
31
- offsetPx: 8,
32
- middleware: [flip()]
33
- }}
34
- >
35
- {@render children()}
36
- </ul>
37
-
38
- <style>
39
- .fab-menu {
40
- transform-origin: bottom right;
41
- }
42
- </style>
19
+ <DropdownMenu.Portal>
20
+ <DropdownMenu.Content side="top" align="end" sideOffset={8} collisionPadding={8}>
21
+ {#snippet child({ wrapperProps, props, open })}
22
+ <div {...wrapperProps}>
23
+ <div
24
+ data-cy="m3-fab-menu"
25
+ {...restProps}
26
+ {...props}
27
+ class={base({
28
+ class: clsx(className, 'overflow-y-auto')
29
+ })}
30
+ {@attach presence(() => open, enterExit.scale)}
31
+ >
32
+ {@render children()}
33
+ </div>
34
+ </div>
35
+ {/snippet}
36
+ </DropdownMenu.Content>
37
+ </DropdownMenu.Portal>
@@ -1,6 +1,7 @@
1
1
  import type { FABMenuProps } from './types.js';
2
2
  /**
3
- * A menu container for multiple FAB actions.
3
+ * The floating panel of a FAB menu. Rendered by `FAB` when `withMenu` is set — must live inside
4
+ * its `DropdownMenu.Root`, which owns open state, positioning, and outside-click/Escape dismissal.
4
5
  * Should be used with `FABMenuItem` as children.
5
6
  */
6
7
  declare const FABMenu: import("svelte").Component<FABMenuProps, {}, "">;