@noxlovette/material 0.4.7 → 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 (134) 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/pill/theme.d.ts +1 -1
  86. package/dist/components/pill/theme.js +1 -1
  87. package/dist/components/progress/CircularProgress.svelte +78 -4
  88. package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
  89. package/dist/components/progress/LinearProgress.svelte +70 -7
  90. package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
  91. package/dist/components/progress/WavyLinearProgress.svelte +27 -3
  92. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
  93. package/dist/components/progress/_wavy.d.ts +2 -0
  94. package/dist/components/progress/_wavy.js +3 -1
  95. package/dist/components/snackbar/Snackbar.svelte +13 -17
  96. package/dist/components/table/Table.stories.svelte +24 -0
  97. package/dist/components/table/TableBody.svelte +3 -16
  98. package/dist/components/table/TableBody.svelte.d.ts +1 -5
  99. package/dist/components/table/TableHeader.svelte +1 -1
  100. package/dist/components/table/theme.js +2 -2
  101. package/dist/components/table/types.d.ts +0 -7
  102. package/dist/components/time/TimeField.stories.svelte +41 -0
  103. package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
  104. package/dist/components/time/TimeField.svelte +62 -61
  105. package/dist/components/toolbar/theme.js +2 -2
  106. package/dist/index.d.ts +1 -1
  107. package/dist/index.js +1 -1
  108. package/dist/styles/component.css +21 -22
  109. package/dist/styles/motion.css +317 -38
  110. package/dist/utils/Layer.svelte +16 -29
  111. package/dist/utils/icon/LoadingIndicator.svelte +61 -22
  112. package/dist/utils/icon/loadingShapes.d.ts +1 -0
  113. package/dist/utils/icon/loadingShapes.js +15 -0
  114. package/package.json +5 -2
  115. package/dist/actions/clickOutside.d.ts +0 -3
  116. package/dist/actions/clickOutside.js +0 -13
  117. package/dist/actions/floating.d.ts +0 -20
  118. package/dist/actions/floating.js +0 -87
  119. package/dist/actions/index.d.ts +0 -6
  120. package/dist/actions/index.js +0 -6
  121. package/dist/actions/keyboard.d.ts +0 -3
  122. package/dist/actions/keyboard.js +0 -14
  123. package/dist/actions/portal.d.ts +0 -3
  124. package/dist/actions/portal.js +0 -8
  125. package/dist/actions/positionFloating.d.ts +0 -10
  126. package/dist/actions/positionFloating.js +0 -59
  127. package/dist/actions/rovingFocusGroup.d.ts +0 -20
  128. package/dist/actions/rovingFocusGroup.js +0 -82
  129. package/dist/animation/easing.d.ts +0 -6
  130. package/dist/animation/easing.js +0 -78
  131. package/dist/animation/outroClass.d.ts +0 -3
  132. package/dist/animation/outroClass.js +0 -20
  133. package/dist/animation/transitionTypes.d.ts +0 -8
  134. package/dist/animation/transitionTypes.js +0 -1
@@ -1,9 +1,9 @@
1
1
  import { tv } from 'tailwind-variants';
2
2
  export const rail = tv({
3
3
  slots: {
4
- base: 'py-12 hidden bg-md-sys-color-surface-container z-40 flex-col md:flex gap-6 transition-[width,padding,background-color] duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
4
+ base: 'py-12 hidden bg-md-sys-color-surface-container z-40 flex-col md:flex gap-6 transition-[width,padding,background-color] md-sys-motion-spatial',
5
5
  items: 'flex flex-col gap-3 pt-6 scrollbar-none w-full overflow-x-hidden',
6
- ghost: 'hidden md:block shrink-0 transition-[width] duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
6
+ ghost: 'hidden md:block shrink-0 transition-[width] md-sys-motion-spatial',
7
7
  scrim: 'hidden md:block lg:hidden inset-0'
8
8
  },
9
9
  variants: {
@@ -41,11 +41,11 @@ export const rail = tv({
41
41
  });
42
42
  export const railElement = tv({
43
43
  slots: {
44
- base: 'group relative z-30 flex w-full transition-all duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
45
- content: 'flex items-center min-w-0 rounded-full gap-0.5 py-1.5 transition-all duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
46
- iconContainer: 'relative rounded-full items-center justify-center inline-flex transition-all duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
47
- icon: 'text-[24px] transition-all duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)]',
48
- label: 'transition-all duration-[var(--md-sys-motion-duration-spatial)] ease-[var(--md-sys-motion-timing-function-emphasized)] whitespace-nowrap'
44
+ base: 'group relative z-30 flex w-full transition-all md-sys-motion-spatial',
45
+ content: 'flex items-center min-w-0 rounded-full gap-0.5 py-1.5 transition-all md-sys-motion-spatial',
46
+ iconContainer: 'relative rounded-full items-center justify-center inline-flex transition-all md-sys-motion-spatial',
47
+ icon: 'text-[24px] transition-all md-sys-motion-spatial',
48
+ label: 'transition-all md-sys-motion-spatial whitespace-nowrap'
49
49
  },
50
50
  variants: {
51
51
  active: {
@@ -2,7 +2,7 @@ import { tv } from 'tailwind-variants';
2
2
  export const tabHolder = tv({
3
3
  slots: {
4
4
  base: 'relative flex w-full bg-md-sys-color-surface',
5
- bar: 'absolute bottom-0 h-[3px] rounded-t-full bg-md-sys-color-primary transition-transform duration-200 ease-out'
5
+ bar: 'absolute bottom-0 h-[3px] rounded-t-full bg-md-sys-color-primary transition-transform md-sys-motion-fast-spatial'
6
6
  },
7
7
  variants: {
8
8
  rounded: {
@@ -14,7 +14,7 @@ export declare const pill: import("tailwind-variants").TVReturnType<{
14
14
  yellow: "bg-yellow-400 text-yellow-950 dark:bg-yellow-300 dark:text-yellow-950";
15
15
  green: "bg-green-600 text-white dark:bg-green-500 dark:text-green-950";
16
16
  };
17
- }, undefined, "relative inline-flex items-center justify-center px-4 py-1.5 md-sys-typescale-label-large select-none rounded-full", {
17
+ }, undefined, "relative inline-flex items-center justify-center px-4 py-1.5 md-sys-typescale-label-large select-none rounded-full whitespace-nowrap", {
18
18
  variant: {
19
19
  primary: "md-component-button-filled-primary";
20
20
  secondary: "md-component-button-filled-secondary";
@@ -1,6 +1,6 @@
1
1
  import { tv } from 'tailwind-variants';
2
2
  export const pill = tv({
3
- base: 'relative inline-flex items-center justify-center px-4 py-1.5 md-sys-typescale-label-large select-none rounded-full',
3
+ base: 'relative inline-flex items-center justify-center px-4 py-1.5 md-sys-typescale-label-large select-none rounded-full whitespace-nowrap',
4
4
  variants: {
5
5
  variant: {
6
6
  primary: 'md-component-button-filled-primary',
@@ -2,10 +2,18 @@
2
2
  @component
3
3
  Material 3 Circular Progress Indicator.
4
4
 
5
+ Determinate: the arc springs to each new `percent` on the slow effects spring (critically damped,
6
+ so progress never visibly runs backwards), with a gap separating it from the track.
7
+ Indeterminate: the arc's head and tail take turns springing ahead, so it grows, shrinks and
8
+ travels around the circle.
9
+
5
10
  @see https://m3.material.io/components/progress-indicators/overview#circular-progress-indicators
6
11
  -->
7
12
  <script lang="ts">
8
13
  import { Progress } from 'bits-ui';
14
+ import { animate, type AnimationPlaybackControls } from 'motion';
15
+ import { springTokens, springTransition } from '../../animation/spring.js';
16
+ import { SpringValue } from '../../animation/springValue.svelte.js';
9
17
 
10
18
  let {
11
19
  percent,
@@ -20,8 +28,69 @@ Material 3 Circular Progress Indicator.
20
28
  thickness?: number;
21
29
  } = $props();
22
30
 
23
- let r = $derived(size / 2 - thickness / 2);
24
- let circumference = $derived(Math.PI * r * 2);
31
+ /* M3: 4dp between the indicator and the track, measured between the rounded caps. */
32
+ const GAP_PX = 4;
33
+ /* Indeterminate arc lengths it grows to and shrinks back to. Not M3 values: tune by eye. */
34
+ const MIN_SWEEP_DEG = 20;
35
+ const MAX_SWEEP_DEG = 270;
36
+
37
+ const r = $derived(size / 2 - thickness / 2);
38
+ const circumference = $derived(Math.PI * r * 2);
39
+ /* Round caps overhang each end by half the stroke; the gap sits between those caps. */
40
+ const gapDeg = $derived((((GAP_PX + thickness) / r) * 180) / Math.PI);
41
+
42
+ const shown = new SpringValue(() => percent ?? 0, springTokens.slowEffects);
43
+
44
+ let head = $state(0);
45
+ let tail = $state(0);
46
+
47
+ $effect(() => {
48
+ if (percent != null) return;
49
+ let running = true;
50
+ let controls: AnimationPlaybackControls | undefined;
51
+ const spring = springTransition(springTokens.spatial);
52
+
53
+ (async () => {
54
+ let h = 0;
55
+ let t = 0;
56
+ while (running) {
57
+ controls = animate(h, t + MAX_SWEEP_DEG, { ...spring, onUpdate: (v) => (head = v) });
58
+ await controls;
59
+ h = t + MAX_SWEEP_DEG;
60
+ if (!running) break;
61
+ controls = animate(t, h - MIN_SWEEP_DEG, { ...spring, onUpdate: (v) => (tail = v) });
62
+ await controls;
63
+ t = h - MIN_SWEEP_DEG;
64
+ }
65
+ })();
66
+
67
+ return () => {
68
+ running = false;
69
+ controls?.stop();
70
+ };
71
+ });
72
+
73
+ /** An arc from `fromDeg`, `sweepDeg` long, as stroke-dash values (0° = 12 o'clock). */
74
+ const arc = (fromDeg: number, sweepDeg: number) => ({
75
+ dasharray: `${(Math.max(0, sweepDeg) / 360) * circumference} ${circumference}`,
76
+ dashoffset: (-(fromDeg % 360) / 360) * circumference,
77
+ // A zero-length dash with round caps still paints a dot.
78
+ opacity: sweepDeg > 0.5 ? 1 : 0
79
+ });
80
+
81
+ const from = $derived(percent != null ? 0 : tail);
82
+ const sweep = $derived(percent != null ? (shown.current / 100) * 360 : head - tail);
83
+
84
+ const indicator = $derived(arc(from, sweep));
85
+ /* The track fills the rest of the circle, keeping a gap either side of the indicator. M3 draws
86
+ no track while indeterminate. */
87
+ const track = $derived(
88
+ percent == null
89
+ ? arc(0, 0)
90
+ : indicator.opacity
91
+ ? arc(from + sweep + gapDeg, 360 - sweep - gapDeg * 2)
92
+ : arc(0, 360)
93
+ );
25
94
  </script>
26
95
 
27
96
  <Progress.Root value={percent ?? null} max={100} data-cy="m3-circular-progress">
@@ -40,6 +109,10 @@ Material 3 Circular Progress Indicator.
40
109
  {r}
41
110
  stroke="var(--color-md-sys-color-secondary-container)"
42
111
  stroke-width={thickness}
112
+ stroke-dasharray={track.dasharray}
113
+ stroke-dashoffset={track.dashoffset}
114
+ opacity={track.opacity}
115
+ stroke-linecap="round"
43
116
  fill="none"
44
117
  />
45
118
  <circle
@@ -48,8 +121,9 @@ Material 3 Circular Progress Indicator.
48
121
  {r}
49
122
  stroke="var(--color-md-sys-color-primary)"
50
123
  stroke-width={thickness}
51
- stroke-dasharray="{circumference} {circumference}"
52
- stroke-dashoffset={(percent != null ? (percent / -100) * circumference : 0) + circumference}
124
+ stroke-dasharray={indicator.dasharray}
125
+ stroke-dashoffset={indicator.dashoffset}
126
+ opacity={indicator.opacity}
53
127
  stroke-linecap="round"
54
128
  fill="none"
55
129
  />
@@ -9,6 +9,11 @@ type $$ComponentProps = {
9
9
  /**
10
10
  * Material 3 Circular Progress Indicator.
11
11
  *
12
+ * Determinate: the arc springs to each new `percent` on the slow effects spring (critically damped,
13
+ * so progress never visibly runs backwards), with a gap separating it from the track.
14
+ * Indeterminate: the arc's head and tail take turns springing ahead, so it grows, shrinks and
15
+ * travels around the circle.
16
+ *
12
17
  * @see https://m3.material.io/components/progress-indicators/overview#circular-progress-indicators
13
18
  */
14
19
  declare const CircularProgress: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -2,12 +2,20 @@
2
2
  @component
3
3
  Material 3 Linear Progress Indicator.
4
4
 
5
+ Determinate: the bar springs to each new `percent` on the slow effects spring (critically damped,
6
+ so progress never visibly runs backwards). Indeterminate: the bar's head and tail spring across
7
+ the track on springs of different stiffness, so it stretches out and catches up on every pass.
8
+ The track always keeps a gap either side of the bar.
9
+
5
10
  @see https://m3.material.io/components/progress-indicators/overview#linear-progress-indicators
6
11
  -->
7
12
  <script lang="ts">
8
13
  import clsx from 'clsx';
9
14
  import { tv } from 'tailwind-variants';
10
15
  import { Progress } from 'bits-ui';
16
+ import { animate, type AnimationPlaybackControls } from 'motion';
17
+ import { springTokens, springTransition } from '../../animation/spring.js';
18
+ import { SpringValue } from '../../animation/springValue.svelte.js';
11
19
 
12
20
  let {
13
21
  percent,
@@ -21,15 +29,58 @@ Material 3 Linear Progress Indicator.
21
29
  class?: string;
22
30
  } = $props();
23
31
 
32
+ /* M3: 4dp between the indicator and the track. */
33
+ const GAP_PX = 4;
34
+
24
35
  const linearProgress = tv({
25
36
  slots: {
26
- container: 'flex gap-1',
27
- fill: 'bg-md-sys-color-primary rounded-full shrink',
28
- track: 'bg-md-sys-color-secondary-container rounded-full grow'
37
+ container: 'relative overflow-hidden',
38
+ segment: 'absolute inset-y-0 rounded-full',
39
+ bar: 'bg-md-sys-color-primary',
40
+ track: 'bg-md-sys-color-secondary-container'
29
41
  }
30
42
  });
31
43
 
32
- const { container, fill, track } = linearProgress();
44
+ const { container, segment, bar, track } = linearProgress();
45
+
46
+ const shown = new SpringValue(() => (percent ?? 0) / 100, springTokens.slowEffects);
47
+
48
+ /* Indeterminate pass: the head leads on the spatial spring, the tail trails on the slower one. */
49
+ let head = $state(0);
50
+ let tail = $state(0);
51
+
52
+ $effect(() => {
53
+ if (percent != null) return;
54
+ let running = true;
55
+ let controls: AnimationPlaybackControls[] = [];
56
+
57
+ (async () => {
58
+ while (running) {
59
+ head = 0;
60
+ tail = 0;
61
+ controls = [
62
+ animate(0, 1, {
63
+ ...springTransition(springTokens.spatial),
64
+ onUpdate: (v) => (head = v)
65
+ }),
66
+ animate(0, 1, {
67
+ ...springTransition(springTokens.slowSpatial),
68
+ onUpdate: (v) => (tail = v)
69
+ })
70
+ ];
71
+ await Promise.all(controls);
72
+ }
73
+ })();
74
+
75
+ return () => {
76
+ running = false;
77
+ controls.forEach((c) => c.stop());
78
+ };
79
+ });
80
+
81
+ const clamp01 = (v: number) => Math.min(1, Math.max(0, v));
82
+ const start = $derived(percent != null ? 0 : clamp01(tail));
83
+ const end = $derived(percent != null ? clamp01(shown.current) : clamp01(head));
33
84
  </script>
34
85
 
35
86
  <Progress.Root
@@ -39,8 +90,20 @@ Material 3 Linear Progress Indicator.
39
90
  style="height: {height / 16}rem"
40
91
  data-cy="m3-linear-progress"
41
92
  >
42
- {#if percent != null}
43
- <div class={fill()} style:width="{percent}%"></div>
93
+ {#if start > 0}
94
+ <div
95
+ class={segment({ class: track() })}
96
+ style="left: 0; width: max(0px, calc({start} * 100% - {GAP_PX}px))"
97
+ ></div>
98
+ {/if}
99
+ {#if end > start}
100
+ <div
101
+ class={segment({ class: bar() })}
102
+ style="left: calc({start} * 100%); width: calc({end - start} * 100%)"
103
+ ></div>
44
104
  {/if}
45
- <div class={track()}></div>
105
+ <div
106
+ class={segment({ class: track() })}
107
+ style="right: 0; width: max(0px, calc({1 - end} * 100% - {end > start ? GAP_PX : 0}px))"
108
+ ></div>
46
109
  </Progress.Root>
@@ -8,6 +8,11 @@ type $$ComponentProps = {
8
8
  /**
9
9
  * Material 3 Linear Progress Indicator.
10
10
  *
11
+ * Determinate: the bar springs to each new `percent` on the slow effects spring (critically damped,
12
+ * so progress never visibly runs backwards). Indeterminate: the bar's head and tail spring across
13
+ * the track on springs of different stiffness, so it stretches out and catches up on every pass.
14
+ * The track always keeps a gap either side of the bar.
15
+ *
11
16
  * @see https://m3.material.io/components/progress-indicators/overview#linear-progress-indicators
12
17
  */
13
18
  declare const LinearProgress: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -2,11 +2,17 @@
2
2
  @component
3
3
  Wavy linear progress indicator — a custom M3-inspired variant.
4
4
 
5
+ The wave travels continuously; the filled length springs to each new `percent` on the slow
6
+ effects spring (critically damped, so progress never visibly runs backwards).
7
+
5
8
  @see https://m3.material.io/components/progress-indicators/overview
6
9
  -->
7
10
  <script lang="ts">
8
11
  import { Progress } from 'bits-ui';
9
- import { linear, trackOpacity } from './_wavy.js';
12
+ import { animate } from 'motion';
13
+ import { springTokens } from '../../animation/spring.js';
14
+ import { SpringValue } from '../../animation/springValue.svelte.js';
15
+ import { linear, trackOpacity, WAVE_PERIOD_MS } from './_wavy.js';
10
16
 
11
17
  let {
12
18
  width = 600,
@@ -26,10 +32,28 @@ Wavy linear progress indicator — a custom M3-inspired variant.
26
32
  class?: string;
27
33
  } = $props();
28
34
 
35
+ const shown = new SpringValue(() => percent ?? 0, springTokens.slowEffects);
36
+
37
+ /* The wave's phase: a travelling wave moves at constant speed, one wavelength per period. */
38
+ let time = $state(0);
39
+ $effect(() => {
40
+ const controls = animate(0, WAVE_PERIOD_MS, {
41
+ duration: WAVE_PERIOD_MS / 1000,
42
+ ease: 'linear',
43
+ repeat: Infinity,
44
+ onUpdate: (v) => (time = v)
45
+ });
46
+ return () => controls.stop();
47
+ });
48
+
29
49
  let left = $derived(thickness * 0.5);
30
50
  let right = $derived(width - thickness * 0.5);
31
- let percentX = $derived(percent != null ? (percent / 100) * (right - left) + left : left);
32
- let wavePath = $derived(linear(height / 2 - thickness / 2, height / 2, left, percentX, 0));
51
+ let percentX = $derived(
52
+ percent != null
53
+ ? (Math.min(100, Math.max(0, shown.current)) / 100) * (right - left) + left
54
+ : left
55
+ );
56
+ let wavePath = $derived(linear(height / 2 - thickness / 2, height / 2, left, percentX, time));
33
57
  </script>
34
58
 
35
59
  <Progress.Root value={percent ?? null} max={100}>
@@ -12,6 +12,9 @@ type $$ComponentProps = {
12
12
  /**
13
13
  * Wavy linear progress indicator — a custom M3-inspired variant.
14
14
  *
15
+ * The wave travels continuously; the filled length springs to each new `percent` on the slow
16
+ * effects spring (critically damped, so progress never visibly runs backwards).
17
+ *
15
18
  * @see https://m3.material.io/components/progress-indicators/overview
16
19
  */
17
20
  declare const WavyLinearProgress: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,2 +1,4 @@
1
+ /** One full wave cycle, in ms. */
2
+ export declare const WAVE_PERIOD_MS = 1000;
1
3
  export declare const linear: (amp: number, center: number, from: number, to: number, time: number, cutoffTo?: number) => string;
2
4
  export declare const trackOpacity: (right: number, x: number) => number;
@@ -1,4 +1,6 @@
1
- const frequencyT = (Math.PI * 2) / 1000;
1
+ /** One full wave cycle, in ms. */
2
+ export const WAVE_PERIOD_MS = 1000;
3
+ const frequencyT = (Math.PI * 2) / WAVE_PERIOD_MS;
2
4
  const frequencyX = (Math.PI * 2) / 40;
3
5
  const round2 = (x) => Math.round(x * 100) / 100;
4
6
  const round1 = (x) => Math.round(x * 10) / 10;
@@ -9,8 +9,7 @@ Snackbars provide brief messages about app processes at the bottom of the screen
9
9
  import type { SnackBarProps } from './types.js';
10
10
  import Icon from '../../utils/icon/Icon.svelte';
11
11
  import { Layer } from '../../utils/index.js';
12
- import { enterExit } from '../../animation/enterExit.js';
13
- import { easeEmphasizedDecel, easeEmphasizedAccel } from '../../animation/easing.js';
12
+ import { enterExit, Presence } from '../../animation/index.js';
14
13
 
15
14
  let {
16
15
  message = $bindable(''),
@@ -49,6 +48,12 @@ Snackbars provide brief messages about app processes at the bottom of the screen
49
48
  };
50
49
  });
51
50
 
51
+ const snack = new Presence(() => Boolean(message) && !dismissed);
52
+
53
+ // Dismissing clears `message`; keep rendering the last one while the exit animation plays.
54
+ let lastMessage: SnackBarProps['message'] = '';
55
+ const shownMessage = $derived.by(() => (message ? (lastMessage = message) : lastMessage));
56
+
52
57
  const {
53
58
  base,
54
59
  icon,
@@ -58,27 +63,18 @@ Snackbars provide brief messages about app processes at the bottom of the screen
58
63
  } = $derived(snackbar({ fixed }));
59
64
  </script>
60
65
 
61
- {#if message && !dismissed}
66
+ {#if snack.mounted}
62
67
  <div
63
68
  class={base()}
64
69
  data-cy="m3-snackbar"
65
70
  {...restProps}
66
- in:enterExit={{
67
- duration: 400,
68
- easing: easeEmphasizedDecel,
69
- mode: 'slide-up'
70
- }}
71
- out:enterExit={{
72
- duration: 200,
73
- easing: easeEmphasizedAccel,
74
- mode: 'slide-up'
75
- }}
71
+ {@attach snack.attach(enterExit.slideUp)}
76
72
  >
77
- {#if typeof message === 'string'}
78
- <p class={supportingText()}>{message}</p>
79
- {:else}
73
+ {#if typeof shownMessage === 'string'}
74
+ <p class={supportingText()}>{shownMessage}</p>
75
+ {:else if shownMessage}
80
76
  <p class={supportingText()}>
81
- {@render message()}
77
+ {@render shownMessage()}
82
78
  </p>
83
79
  {/if}
84
80
 
@@ -7,6 +7,7 @@
7
7
  import TableRow from './TableRow.svelte';
8
8
  import TableHeader from './TableHeader.svelte';
9
9
  import TableCell from './TableCell.svelte';
10
+ import Pill from '../pill/Pill.svelte';
10
11
 
11
12
  const { Story } = defineMeta({
12
13
  title: 'Table/Table',
@@ -247,6 +248,29 @@
247
248
  </Table>
248
249
  </Story>
249
250
 
251
+ <Story name="Extreme: Narrow Viewport With Pill" asChild>
252
+ <div class="w-[390px]">
253
+ <Table rounded>
254
+ <TableHead>
255
+ <TableRow>
256
+ <TableHeader>Student</TableHeader>
257
+ <TableHeader>Lesson</TableHeader>
258
+ <TableHeader>Status</TableHeader>
259
+ <TableHeader>Paid</TableHeader>
260
+ </TableRow>
261
+ </TableHead>
262
+ <TableBody>
263
+ <TableRow>
264
+ <TableCell>Student 8</TableCell>
265
+ <TableCell>English, 60 minutes</TableCell>
266
+ <TableCell><Pill variant="error">Error</Pill></TableCell>
267
+ <TableCell>Not paid</TableCell>
268
+ </TableRow>
269
+ </TableBody>
270
+ </Table>
271
+ </div>
272
+ </Story>
273
+
250
274
  <Story name="Mixed Alignment" asChild>
251
275
  <Table rounded>
252
276
  <TableHead>
@@ -1,30 +1,17 @@
1
1
  <!--
2
2
  @component
3
- Wraps the data rows of a Table. Rows are keyboard-navigable by default: Tab/Shift+Tab
4
- stop on the table once (roving tabindex), and ArrowUp/ArrowDown or k/j move focus
5
- between rows. Set `navigable={false}` to opt a purely presentational table out.
3
+ Wraps the data rows of a Table.
6
4
  -->
7
5
  <script lang="ts">
8
6
  import type { TableBodyProps } from './types.js';
9
7
  import { table } from './theme.js';
10
- import { rovingFocusGroup } from '../../actions/index.js';
11
8
  import clsx from 'clsx';
12
9
 
13
- let {
14
- children,
15
- navigable = true,
16
- loop = false,
17
- class: className,
18
- ...restProps
19
- }: TableBodyProps = $props();
10
+ let { children, class: className, ...restProps }: TableBodyProps = $props();
20
11
 
21
12
  const { body } = table();
22
13
  </script>
23
14
 
24
- <tbody
25
- class={body({ class: clsx(className) })}
26
- use:rovingFocusGroup={{ candidateSelector: '[data-table-row]', loop, enabled: navigable }}
27
- {...restProps}
28
- >
15
+ <tbody class={body({ class: clsx(className) })} {...restProps}>
29
16
  {@render children()}
30
17
  </tbody>
@@ -1,9 +1,5 @@
1
1
  import type { TableBodyProps } from './types.js';
2
- /**
3
- * Wraps the data rows of a Table. Rows are keyboard-navigable by default: Tab/Shift+Tab
4
- * stop on the table once (roving tabindex), and ArrowUp/ArrowDown or k/j move focus
5
- * between rows. Set `navigable={false}` to opt a purely presentational table out.
6
- */
2
+ /** Wraps the data rows of a Table. */
7
3
  declare const TableBody: import("svelte").Component<TableBodyProps, {}, "">;
8
4
  type TableBody = ReturnType<typeof TableBody>;
9
5
  export default TableBody;
@@ -30,7 +30,7 @@ and `sortDirection` props. Defaults to `scope="col"`.
30
30
  {#if sortable}
31
31
  <span
32
32
  class={[
33
- 'transition-[opacity,transform] duration-200',
33
+ 'md-sys-motion-fast-spatial transition-[opacity,transform]',
34
34
  sorted ? 'opacity-100' : 'opacity-0 group-hover:opacity-40',
35
35
  sortDirection === 'desc' ? 'rotate-180' : 'rotate-0'
36
36
  ].join(' ')}
@@ -5,8 +5,8 @@ export const table = tv({
5
5
  head: '',
6
6
  body: '[&_tr:last-child]:border-b-0',
7
7
  foot: 'border-t border-md-sys-color-outline-variant',
8
- row: 'border-b border-md-sys-color-outline-variant transition-colors focus-visible:outline focus-visible:-outline-offset-2 focus-visible:outline-md-sys-color-primary',
9
- cell: 'px-4 py-3 align-middle wrap-anywhere',
8
+ row: 'border-b border-md-sys-color-outline-variant transition-colors md-sys-motion-fast-effects focus-visible:outline focus-visible:-outline-offset-2 focus-visible:outline-md-sys-color-primary',
9
+ cell: 'px-4 py-3 align-middle',
10
10
  header: 'px-4 py-4 align-middle md-sys-typescale-label-large text-md-sys-color-on-surface font-medium whitespace-nowrap'
11
11
  },
12
12
  variants: {
@@ -13,13 +13,6 @@ export type TableHeadProps = HTMLAttributes<HTMLTableSectionElement> & {
13
13
  };
14
14
  export type TableBodyProps = HTMLAttributes<HTMLTableSectionElement> & {
15
15
  children: Snippet;
16
- /**
17
- * Make rows keyboard-navigable: roving tabindex so Tab/Shift+Tab stop on the table
18
- * once, and ArrowUp/ArrowDown (or k/j) move focus between rows. Default `true`.
19
- */
20
- navigable?: boolean;
21
- /** Loop focus from the last row back to the first (and vice versa). Default `false`. */
22
- loop?: boolean;
23
16
  };
24
17
  export type TableFootProps = HTMLAttributes<HTMLTableSectionElement> & {
25
18
  children: Snippet;
@@ -0,0 +1,41 @@
1
+ <!--
2
+ TimeField anchors its TimepickerInput overlay to the whole field via a bits-ui Popover
3
+ (`customAnchor`), so the panel is portaled, flips above the field when there's no room below,
4
+ and dismisses on outside click / Escape.
5
+ -->
6
+ <script module lang="ts">
7
+ import { defineMeta } from '@storybook/addon-svelte-csf';
8
+ import TimeField from './TimeField.svelte';
9
+
10
+ const { Story } = defineMeta({
11
+ title: 'Time/TimeField',
12
+ component: TimeField,
13
+ argTypes: {
14
+ label: { control: 'text' },
15
+ disabled: { control: 'boolean' },
16
+ error: { control: 'boolean' },
17
+ required: { control: 'boolean' }
18
+ },
19
+ args: {
20
+ label: 'Time',
21
+ value: '09:30',
22
+ disabled: false,
23
+ error: false,
24
+ required: false
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <Story name="Playground">
30
+ {#snippet template(args)}
31
+ <div class="max-w-xs p-8">
32
+ <TimeField {...args} />
33
+ </div>
34
+ {/snippet}
35
+ </Story>
36
+
37
+ <Story name="Near Bottom Edge" asChild>
38
+ <div class="flex min-h-dvh max-w-xs flex-col justify-end p-8">
39
+ <TimeField label="Time" value="18:45" />
40
+ </div>
41
+ </Story>
@@ -0,0 +1,19 @@
1
+ import TimeField from './TimeField.svelte';
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const TimeField: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type TimeField = InstanceType<typeof TimeField>;
19
+ export default TimeField;