@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
@@ -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
 
@@ -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,7 +5,7 @@ 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',
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
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
  },
@@ -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;
@@ -6,15 +6,16 @@ It combines a Material 3 Textfield with a TimepickerInput for intuitive time sel
6
6
  @see https://m3.material.io/components/time-pickers/overview
7
7
  -->
8
8
  <script lang="ts">
9
+ import { enterExit, presence } from '../../animation/index.js';
9
10
  import type { HTMLInputAttributes } from 'svelte/elements';
10
11
  import Textfield from '../forms/textfield/Textfield.svelte';
11
12
  import TimepickerInput from './TimepickerInput.svelte';
12
- import { clickOutside, positionFloating } from '../../actions/index.js';
13
- import { enterExit } from '../../animation/enterExit.js';
14
- import { easeEmphasizedDecel, easeEmphasizedAccel } from '../../animation/easing.js';
13
+ import { Popover } from 'bits-ui';
14
+ import type { Snippet } from 'svelte';
15
+ import type { TimepickerLabels } from './types.js';
15
16
 
16
17
  let {
17
- label = 'Время',
18
+ label = 'Time',
18
19
  value = $bindable(),
19
20
  required = false,
20
21
  disabled = false,
@@ -22,6 +23,8 @@ It combines a Material 3 Textfield with a TimepickerInput for intuitive time sel
22
23
  datePickerTitle = 'Pick date',
23
24
  name,
24
25
  leadingIconProps,
26
+ supportingText,
27
+ pickerLabels,
25
28
  ...restProps
26
29
  }: {
27
30
  /** The label for the text field. */
@@ -40,6 +43,10 @@ It combines a Material 3 Textfield with a TimepickerInput for intuitive time sel
40
43
  datePickerTitle?: string;
41
44
  /** Props for the leading icon. */
42
45
  leadingIconProps?: any;
46
+ /** Supporting text under the field. */
47
+ supportingText?: Snippet;
48
+ /** Headline, input and button strings for the picker overlay (English by default). */
49
+ pickerLabels?: TimepickerLabels;
43
50
  } & HTMLInputAttributes = $props();
44
51
 
45
52
  const id = $props.id();
@@ -48,64 +55,64 @@ It combines a Material 3 Textfield with a TimepickerInput for intuitive time sel
48
55
  let anchorEl = $state<HTMLDivElement>();
49
56
  </script>
50
57
 
51
- <div
52
- class="relative w-full"
53
- bind:this={anchorEl}
54
- use:clickOutside={() => {
55
- picker = false;
56
- }}
57
- >
58
- <Textfield
59
- id={id as string}
60
- {label}
61
- {value}
62
- {name}
63
- {disabled}
64
- {error}
65
- {leadingIconProps}
66
- class="pointer-events-none"
67
- trailingIconProps={{ name: 'timer' }}
68
- trailingOnClick={() => !disabled && (picker = !picker)}
69
- {...restProps as any}
70
- >
71
- {#snippet supportingText()}
72
- ЧЧ-ММ
73
- {/snippet}
74
- </Textfield>
58
+ <Popover.Root bind:open={picker}>
59
+ <div class="relative w-full" bind:this={anchorEl}>
60
+ <Textfield
61
+ id={id as string}
62
+ {label}
63
+ {value}
64
+ {name}
65
+ {disabled}
66
+ {error}
67
+ {leadingIconProps}
68
+ class="pointer-events-none"
69
+ trailingIconProps={{ name: 'schedule' }}
70
+ trailingOnClick={() => !disabled && (picker = !picker)}
71
+ {supportingText}
72
+ {...restProps as any}
73
+ />
75
74
 
76
- <button
77
- {disabled}
78
- title="time-overlay"
79
- class="absolute inset-0 cursor-pointer disabled:cursor-not-allowed"
80
- type="button"
81
- onclick={() => !disabled && (picker = !picker)}
82
- data-cy="calendar-time-toggle"
83
- ></button>
75
+ <Popover.Trigger {disabled}>
76
+ {#snippet child({ props })}
77
+ <button
78
+ {...props}
79
+ title="time-overlay"
80
+ class="absolute inset-0 cursor-pointer disabled:cursor-not-allowed"
81
+ type="button"
82
+ data-cy="calendar-time-toggle"
83
+ ></button>
84
+ {/snippet}
85
+ </Popover.Trigger>
86
+ </div>
84
87
 
85
- {#if picker}
86
- <div
87
- class="picker"
88
- use:positionFloating={{ anchor: anchorEl, offset: 12 }}
89
- in:enterExit={{
90
- duration: 200,
91
- easing: easeEmphasizedDecel,
92
- mode: 'scale'
93
- }}
94
- out:enterExit={{
95
- duration: 150,
96
- easing: easeEmphasizedAccel,
97
- mode: 'scale'
98
- }}
99
- style="transform-origin: top center;"
88
+ <Popover.Portal>
89
+ <Popover.Content
90
+ customAnchor={anchorEl}
91
+ side="bottom"
92
+ align="start"
93
+ sideOffset={12}
94
+ collisionPadding={8}
95
+ class="z-100"
100
96
  >
101
- <TimepickerInput time={value} close={() => (picker = false)} setTime={(t) => (value = t)} />
102
- </div>
103
- {/if}
104
- </div>
105
-
106
- <style>
107
- .picker {
108
- z-index: 1;
109
- position: absolute;
110
- }
111
- </style>
97
+ {#snippet child({ wrapperProps, props, open })}
98
+ <div {...wrapperProps}>
99
+ <div
100
+ {...props}
101
+ class={[
102
+ props.class,
103
+ 'max-h-(--bits-floating-available-height) min-w-(--bits-floating-anchor-width) overflow-auto'
104
+ ]}
105
+ {@attach presence(() => open, enterExit.scale)}
106
+ >
107
+ <TimepickerInput
108
+ {...pickerLabels}
109
+ time={value}
110
+ close={() => (picker = false)}
111
+ setTime={(t) => (value = t)}
112
+ />
113
+ </div>
114
+ </div>
115
+ {/snippet}
116
+ </Popover.Content>
117
+ </Popover.Portal>
118
+ </Popover.Root>
@@ -1,4 +1,6 @@
1
1
  import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { Snippet } from 'svelte';
3
+ import type { TimepickerLabels } from './types.js';
2
4
  type $$ComponentProps = {
3
5
  /** The label for the text field. */
4
6
  label?: string;
@@ -16,6 +18,10 @@ type $$ComponentProps = {
16
18
  datePickerTitle?: string;
17
19
  /** Props for the leading icon. */
18
20
  leadingIconProps?: any;
21
+ /** Supporting text under the field. */
22
+ supportingText?: Snippet;
23
+ /** Headline, input and button strings for the picker overlay (English by default). */
24
+ pickerLabels?: TimepickerLabels;
19
25
  } & HTMLInputAttributes;
20
26
  /**
21
27
  * TimeField component provides a text input that opens a Timepicker overlay when interacted with.
@@ -15,7 +15,11 @@ It follows the Material 3 design for time input pickers.
15
15
  import { ButtonIcon } from '../buttons/index.js';
16
16
 
17
17
  const {
18
- label = 'Введите время',
18
+ label = 'Enter time',
19
+ hourLabel = 'Hour',
20
+ minuteLabel = 'Minute',
21
+ cancelText = 'Cancel',
22
+ confirmText = 'OK',
19
23
  time = '09:41',
20
24
  close,
21
25
  setTime,
@@ -48,7 +52,7 @@ It follows the Material 3 design for time input pickers.
48
52
  {value}
49
53
  </TimeField.Segment>
50
54
  <p class={cls.timeInputSupportingText()}>
51
- {part === 'hour' ? 'Часы' : 'Минуты'}
55
+ {part === 'hour' ? hourLabel : minuteLabel}
52
56
  </p>
53
57
  </div>
54
58
  {/if}
@@ -61,7 +65,7 @@ It follows the Material 3 design for time input pickers.
61
65
  <div class={cls.buttonDiv()}>
62
66
  <ButtonIcon type="button" iconProps={{ name: 'schedule' }} />
63
67
  <div>
64
- <Button variant="text" type="button" onclick={() => close()}>Отмена</Button>
68
+ <Button variant="text" type="button" onclick={() => close()}>{cancelText}</Button>
65
69
  <Button
66
70
  variant="text"
67
71
  type="button"
@@ -69,7 +73,7 @@ It follows the Material 3 design for time input pickers.
69
73
  setTime(output);
70
74
  close();
71
75
  }}
72
- data-cy="calendar-time-confirm">Готово</Button
76
+ data-cy="calendar-time-confirm">{confirmText}</Button
73
77
  >
74
78
  </div>
75
79
  </div>
@@ -9,9 +9,17 @@ import type { TimepickerVariants } from './theme.js';
9
9
  export type TimepickerProps = TimepickerVariants & HTMLAttributes<HTMLInputElement> & {
10
10
  /**
11
11
  * The label displayed at the top of the timepicker.
12
- * @default 'Введите время'
12
+ * @default 'Enter time'
13
13
  */
14
14
  label?: string;
15
+ /** Supporting text under the hour input. @default 'Hour' */
16
+ hourLabel?: string;
17
+ /** Supporting text under the minute input. @default 'Minute' */
18
+ minuteLabel?: string;
19
+ /** Label of the dismiss button. @default 'Cancel' */
20
+ cancelText?: string;
21
+ /** Label of the confirm button. @default 'OK' */
22
+ confirmText?: string;
15
23
  /**
16
24
  * The current time value in HH:MM format.
17
25
  * @default '09:41'
@@ -32,3 +40,5 @@ export type TimepickerProps = TimepickerVariants & HTMLAttributes<HTMLInputEleme
32
40
  */
33
41
  setTime: (time: string) => void;
34
42
  };
43
+ /** The TimepickerInput strings a TimeField forwards to its picker overlay. */
44
+ export type TimepickerLabels = Partial<Pick<TimepickerProps, 'label' | 'hourLabel' | 'minuteLabel' | 'cancelText' | 'confirmText'>>;
@@ -48,7 +48,7 @@ export const toolbar = tv({
48
48
  });
49
49
  export const toolbarButton = tv({
50
50
  slots: {
51
- base: 'layer-container state-layer before:rounded-full relative inline-flex size-10 items-center justify-center rounded-full outline-none transition-colors disabled:cursor-not-allowed disabled:opacity-38',
51
+ base: 'layer-container state-layer before:rounded-full relative inline-flex size-10 items-center justify-center rounded-full outline-none transition-colors md-sys-motion-fast-effects disabled:cursor-not-allowed disabled:opacity-38',
52
52
  icon: 'size-5 text-[20px]'
53
53
  },
54
54
  variants: {
@@ -61,7 +61,7 @@ export const toolbarButton = tv({
61
61
  });
62
62
  export const toolbarGroupItem = tv({
63
63
  slots: {
64
- base: 'layer-container state-layer before:rounded-full relative inline-flex size-10 items-center justify-center rounded-full outline-none transition-colors disabled:cursor-not-allowed disabled:opacity-38',
64
+ base: 'layer-container state-layer before:rounded-full relative inline-flex size-10 items-center justify-center rounded-full outline-none transition-colors md-sys-motion-fast-effects disabled:cursor-not-allowed disabled:opacity-38',
65
65
  icon: 'size-5 text-[20px]'
66
66
  },
67
67
  variants: {