@noxlovette/material 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/animation/TransitionPatterns.stories.svelte +357 -0
  2. package/dist/animation/TransitionPatterns.stories.svelte.d.ts +3 -0
  3. package/dist/animation/containerTransform.d.ts +26 -12
  4. package/dist/animation/containerTransform.js +22 -138
  5. package/dist/animation/enterExit.d.ts +21 -3
  6. package/dist/animation/enterExit.js +69 -22
  7. package/dist/animation/index.d.ts +1 -1
  8. package/dist/animation/index.js +1 -1
  9. package/dist/animation/presence.svelte.d.ts +47 -0
  10. package/dist/animation/presence.svelte.js +84 -0
  11. package/dist/animation/sharedAxisTransition.d.ts +35 -37
  12. package/dist/animation/sharedAxisTransition.js +50 -22
  13. package/dist/animation/skeleton.d.ts +11 -0
  14. package/dist/animation/skeleton.js +23 -0
  15. package/dist/animation/spring.d.ts +61 -0
  16. package/dist/animation/spring.js +53 -0
  17. package/dist/animation/springValue.svelte.d.ts +22 -0
  18. package/dist/animation/springValue.svelte.js +42 -0
  19. package/dist/animation/transition.d.ts +3 -1
  20. package/dist/animation/transition.js +3 -1
  21. package/dist/attachments/clickOutside.d.ts +13 -0
  22. package/dist/attachments/clickOutside.js +18 -0
  23. package/dist/attachments/index.d.ts +1 -0
  24. package/dist/attachments/index.js +1 -0
  25. package/dist/components/buttons/FAB.svelte +23 -16
  26. package/dist/components/buttons/FABMenu.svelte +24 -29
  27. package/dist/components/buttons/FABMenu.svelte.d.ts +2 -1
  28. package/dist/components/buttons/FABMenuItem.svelte +23 -20
  29. package/dist/components/buttons/FABMenuItem.svelte.d.ts +5 -1
  30. package/dist/components/buttons/split-button/SplitButton.svelte +10 -17
  31. package/dist/components/buttons/split-button/theme.js +1 -1
  32. package/dist/components/buttons/types.d.ts +3 -3
  33. package/dist/components/cards/theme.js +1 -1
  34. package/dist/components/containers/bottom-sheet/BottomSheet.stories.svelte +8 -12
  35. package/dist/components/containers/bottom-sheet/BottomSheet.svelte +67 -55
  36. package/dist/components/containers/bottom-sheet/BottomSheet.svelte.d.ts +1 -1
  37. package/dist/components/containers/bottom-sheet/types.d.ts +8 -2
  38. package/dist/components/containers/context-menu/ContextMenu.svelte +23 -36
  39. package/dist/components/containers/context-menu/theme.js +2 -2
  40. package/dist/components/containers/dialogue/Dialogue.svelte +49 -65
  41. package/dist/components/containers/link-preview/LinkPreview.svelte +12 -23
  42. package/dist/components/containers/link-preview/theme.d.ts +3 -3
  43. package/dist/components/containers/menu/Menu.svelte +14 -21
  44. package/dist/components/containers/menu/MenuSub.svelte +10 -17
  45. package/dist/components/containers/pane/DraggablePane.svelte +90 -5
  46. package/dist/components/containers/pane/DraggablePane.svelte.d.ts +1 -1
  47. package/dist/components/containers/pane/theme.js +2 -2
  48. package/dist/components/containers/popover/Popover.svelte +31 -42
  49. package/dist/components/containers/popover/theme.d.ts +3 -3
  50. package/dist/components/containers/scroll-area/theme.js +2 -2
  51. package/dist/components/containers/side-sheet/SideSheet.stories.svelte +10 -15
  52. package/dist/components/containers/side-sheet/SideSheet.svelte +44 -44
  53. package/dist/components/containers/side-sheet/SideSheet.svelte.d.ts +4 -3
  54. package/dist/components/containers/side-sheet/types.d.ts +8 -2
  55. package/dist/components/date/DateField.svelte +85 -92
  56. package/dist/components/date/DateField.svelte.d.ts +2 -5
  57. package/dist/components/date/DateRangeField.svelte +113 -155
  58. package/dist/components/date/DateRangeField.svelte.d.ts +2 -5
  59. package/dist/components/date/theme.d.ts +95 -0
  60. package/dist/components/date/theme.js +99 -14
  61. package/dist/components/date/types.d.ts +13 -0
  62. package/dist/components/forms/checkbox/theme.js +2 -2
  63. package/dist/components/forms/command/theme.d.ts +3 -3
  64. package/dist/components/forms/command/theme.js +1 -1
  65. package/dist/components/forms/pin/theme.js +2 -2
  66. package/dist/components/forms/radio-group/theme.js +2 -2
  67. package/dist/components/forms/select/Select.svelte +42 -54
  68. package/dist/components/forms/select/theme.js +4 -4
  69. package/dist/components/forms/slider/Slider.svelte +144 -126
  70. package/dist/components/forms/slider/Slider.svelte.d.ts +3 -0
  71. package/dist/components/forms/slider/theme.d.ts +154 -48
  72. package/dist/components/forms/slider/theme.js +60 -20
  73. package/dist/components/forms/slider/types.d.ts +1 -1
  74. package/dist/components/forms/switch/theme.js +4 -4
  75. package/dist/components/forms/textfield/Textfield.svelte +1 -1
  76. package/dist/components/forms/textfield/theme.d.ts +9 -0
  77. package/dist/components/forms/textfield/theme.js +26 -9
  78. package/dist/components/forms/toggle-group/theme.js +1 -1
  79. package/dist/components/forms/tooltip/Tooltip.svelte +24 -35
  80. package/dist/components/nav/breadcrumb/theme.js +1 -1
  81. package/dist/components/nav/navbar/theme.js +2 -2
  82. package/dist/components/nav/rail/Rail.svelte +4 -5
  83. package/dist/components/nav/rail/theme.js +7 -7
  84. package/dist/components/nav/tabs/theme.js +1 -1
  85. package/dist/components/progress/CircularProgress.svelte +78 -4
  86. package/dist/components/progress/CircularProgress.svelte.d.ts +5 -0
  87. package/dist/components/progress/LinearProgress.svelte +70 -7
  88. package/dist/components/progress/LinearProgress.svelte.d.ts +5 -0
  89. package/dist/components/progress/WavyLinearProgress.svelte +27 -3
  90. package/dist/components/progress/WavyLinearProgress.svelte.d.ts +3 -0
  91. package/dist/components/progress/_wavy.d.ts +2 -0
  92. package/dist/components/progress/_wavy.js +3 -1
  93. package/dist/components/snackbar/Snackbar.svelte +13 -17
  94. package/dist/components/table/TableBody.svelte +3 -16
  95. package/dist/components/table/TableBody.svelte.d.ts +1 -5
  96. package/dist/components/table/TableHeader.svelte +1 -1
  97. package/dist/components/table/theme.js +1 -1
  98. package/dist/components/table/types.d.ts +0 -7
  99. package/dist/components/time/TimeField.stories.svelte +41 -0
  100. package/dist/components/time/TimeField.stories.svelte.d.ts +19 -0
  101. package/dist/components/time/TimeField.svelte +62 -61
  102. package/dist/components/toolbar/theme.js +2 -2
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.js +1 -1
  105. package/dist/styles/component.css +21 -22
  106. package/dist/styles/motion.css +317 -38
  107. package/dist/utils/Layer.svelte +16 -29
  108. package/dist/utils/icon/LoadingIndicator.svelte +61 -22
  109. package/dist/utils/icon/loadingShapes.d.ts +1 -0
  110. package/dist/utils/icon/loadingShapes.js +15 -0
  111. package/package.json +5 -2
  112. package/dist/actions/clickOutside.d.ts +0 -3
  113. package/dist/actions/clickOutside.js +0 -13
  114. package/dist/actions/floating.d.ts +0 -20
  115. package/dist/actions/floating.js +0 -87
  116. package/dist/actions/index.d.ts +0 -6
  117. package/dist/actions/index.js +0 -6
  118. package/dist/actions/keyboard.d.ts +0 -3
  119. package/dist/actions/keyboard.js +0 -14
  120. package/dist/actions/portal.d.ts +0 -3
  121. package/dist/actions/portal.js +0 -8
  122. package/dist/actions/positionFloating.d.ts +0 -10
  123. package/dist/actions/positionFloating.js +0 -59
  124. package/dist/actions/rovingFocusGroup.d.ts +0 -20
  125. package/dist/actions/rovingFocusGroup.js +0 -82
  126. package/dist/animation/easing.d.ts +0 -6
  127. package/dist/animation/easing.js +0 -78
  128. package/dist/animation/outroClass.d.ts +0 -3
  129. package/dist/animation/outroClass.js +0 -20
  130. package/dist/animation/transitionTypes.d.ts +0 -8
  131. package/dist/animation/transitionTypes.js +0 -1
@@ -3,12 +3,12 @@ export type CommandVariants = VariantProps<typeof command>;
3
3
  export declare const command: import("tailwind-variants").TVReturnType<{
4
4
  [key: string]: {
5
5
  [key: string]: import("tailwind-variants").ClassValue | {
6
+ item?: import("tailwind-variants").ClassValue;
6
7
  input?: import("tailwind-variants").ClassValue;
7
8
  base?: import("tailwind-variants").ClassValue;
8
9
  group?: import("tailwind-variants").ClassValue;
9
10
  list?: import("tailwind-variants").ClassValue;
10
11
  separator?: import("tailwind-variants").ClassValue;
11
- item?: import("tailwind-variants").ClassValue;
12
12
  inputWrapper?: import("tailwind-variants").ClassValue;
13
13
  inputIcon?: import("tailwind-variants").ClassValue;
14
14
  empty?: import("tailwind-variants").ClassValue;
@@ -18,12 +18,12 @@ export declare const command: import("tailwind-variants").TVReturnType<{
18
18
  } | {
19
19
  [x: string]: {
20
20
  [x: string]: import("tailwind-variants").ClassValue | {
21
+ item?: import("tailwind-variants").ClassValue;
21
22
  input?: import("tailwind-variants").ClassValue;
22
23
  base?: import("tailwind-variants").ClassValue;
23
24
  group?: import("tailwind-variants").ClassValue;
24
25
  list?: import("tailwind-variants").ClassValue;
25
26
  separator?: import("tailwind-variants").ClassValue;
26
- item?: import("tailwind-variants").ClassValue;
27
27
  inputWrapper?: import("tailwind-variants").ClassValue;
28
28
  inputIcon?: import("tailwind-variants").ClassValue;
29
29
  empty?: import("tailwind-variants").ClassValue;
@@ -44,12 +44,12 @@ export declare const command: import("tailwind-variants").TVReturnType<{
44
44
  }, undefined, {
45
45
  [key: string]: {
46
46
  [key: string]: import("tailwind-variants").ClassValue | {
47
+ item?: import("tailwind-variants").ClassValue;
47
48
  input?: import("tailwind-variants").ClassValue;
48
49
  base?: import("tailwind-variants").ClassValue;
49
50
  group?: import("tailwind-variants").ClassValue;
50
51
  list?: import("tailwind-variants").ClassValue;
51
52
  separator?: import("tailwind-variants").ClassValue;
52
- item?: import("tailwind-variants").ClassValue;
53
53
  inputWrapper?: import("tailwind-variants").ClassValue;
54
54
  inputIcon?: import("tailwind-variants").ClassValue;
55
55
  empty?: import("tailwind-variants").ClassValue;
@@ -9,7 +9,7 @@ export const command = tv({
9
9
  empty: 'py-6 text-center md-sys-typescale-body-medium text-md-sys-color-on-surface-variant',
10
10
  group: 'overflow-hidden p-1 text-md-sys-color-on-surface-variant [&_[data-command-group-heading]]:px-3 [&_[data-command-group-heading]]:py-2 [&_[data-command-group-heading]]:md-sys-typescale-label-medium [&_[data-command-group-heading]]:text-md-sys-color-on-surface-variant',
11
11
  separator: '-mx-1 h-px bg-md-sys-color-outline-variant',
12
- item: 'relative flex cursor-default select-none items-center rounded-lg px-3 py-3 md-sys-typescale-body-medium outline-none data-[selected=true]:bg-md-sys-color-secondary-container data-[selected=true]:text-md-sys-color-on-secondary-container data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 transition-colors',
12
+ item: 'relative flex cursor-default select-none items-center rounded-lg px-3 py-3 md-sys-typescale-body-medium outline-none data-[selected=true]:bg-md-sys-color-secondary-container data-[selected=true]:text-md-sys-color-on-secondary-container data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50 transition-colors md-sys-motion-fast-effects',
13
13
  itemIcon: 'mr-3 h-5 w-5 shrink-0'
14
14
  }
15
15
  });
@@ -8,9 +8,9 @@ export const pinInput = tv({
8
8
  relative flex h-14 w-12 items-center justify-center
9
9
  bg-md-sys-color-surface-container-highest rounded-t-xs
10
10
  md-sys-typescale-headline-small text-md-sys-color-on-surface
11
- transition-all duration-200
11
+ transition-colors md-sys-motion-fast-effects
12
12
  after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-md-sys-color-on-surface-variant
13
- after:transition-[height,background-color] after:duration-200
13
+ after:transition-[height,background-color] after:md-sys-motion-fast-spatial
14
14
  data-[focus]:after:bg-md-sys-color-primary data-[focus]:after:h-[2px]
15
15
  data-[placeholder]:text-md-sys-color-on-surface-variant/38
16
16
  `,
@@ -5,8 +5,8 @@ export const radioGroup = tv({
5
5
  item: 'inline-flex min-h-10 cursor-pointer select-none items-center gap-3 text-md-sys-color-on-surface',
6
6
  indicator: 'relative inline-flex size-[20px] shrink-0',
7
7
  control: 'relative layer-container absolute -inset-[10px] rounded-full text-md-sys-color-on-surface-variant state-layer before:rounded-full',
8
- outerRing: 'absolute inset-0 rounded-full border-2 border-md-sys-color-outline transition-colors',
9
- innerDot: 'absolute inset-0 m-auto size-2.5 scale-0 rounded-full bg-md-sys-color-on-primary-container transition-transform duration-[350ms] [transition-timing-function:var(--md-sys-motion-timing-function-fast-spatial)]',
8
+ outerRing: 'absolute inset-0 rounded-full border-2 border-md-sys-color-outline transition-colors md-sys-motion-fast-effects',
9
+ innerDot: 'absolute inset-0 m-auto size-2.5 scale-0 rounded-full bg-md-sys-color-on-primary-container transition-transform md-sys-motion-fast-spatial',
10
10
  label: 'md-sys-typescale-body-large text-md-sys-color-on-surface',
11
11
  supporting: 'md-sys-typescale-body-medium text-md-sys-color-on-surface-variant'
12
12
  },
@@ -8,13 +8,12 @@ Powered by bits-ui for accessibility and behavior.
8
8
  @see https://m3.material.io/components/menus/overview
9
9
  -->
10
10
  <script lang="ts">
11
+ import { enterExit, presence } from '../../../animation/index.js';
11
12
  import { Select } from 'bits-ui';
12
13
  import { select as selectCls } from './theme.js';
13
14
  import { Icon } from '../../../utils/index.js';
14
15
  import type { SelectProps } from './types.js';
15
16
  import clsx from 'clsx';
16
- import { enterExit } from '../../../animation/enterExit.js';
17
- import { easeEmphasized } from '../../../animation/easing.js';
18
17
 
19
18
  let {
20
19
  value = $bindable(),
@@ -66,7 +65,6 @@ Powered by bits-ui for accessibility and behavior.
66
65
 
67
66
  <Select.Portal disabled={portalDisabled}>
68
67
  <Select.Content
69
- forceMount
70
68
  class={cls.content({ class: clsx('z-[100]', contentProps?.class) })}
71
69
  sideOffset={4}
72
70
  align="start"
@@ -74,58 +72,48 @@ Powered by bits-ui for accessibility and behavior.
74
72
  >
75
73
  {#snippet child({ wrapperProps, props, open })}
76
74
  <div {...wrapperProps} class={clsx('z-[100]', wrapperProps.class as any)}>
77
- {#if open}
78
- <div
79
- {...props}
80
- class={clsx('relative flex flex-col outline-none', props.class as any)}
81
- transition:enterExit={{
82
- duration: 200,
83
- easing: easeEmphasized,
84
- mode: 'scale'
85
- }}
86
- style:width="var(--bits-select-anchor-width)"
87
- style:max-width="min(calc(100vw - 32px), 560px)"
88
- >
89
- <Select.Viewport class="w-full">
90
- {#each options as item, i (i)}
91
- {#if item.type === 'group'}
92
- <Select.Group>
93
- {#if item.heading}
94
- <Select.GroupHeading class={cls.groupLabel()}>
95
- {item.heading}
96
- </Select.GroupHeading>
75
+ <div
76
+ {...props}
77
+ class={clsx('relative flex flex-col outline-none', props.class as any)}
78
+ {@attach presence(() => open, enterExit.scale)}
79
+ style:width="var(--bits-select-anchor-width)"
80
+ style:max-width="min(calc(100vw - 32px), 560px)"
81
+ >
82
+ <Select.Viewport class="w-full">
83
+ {#each options as item, i (i)}
84
+ {#if item.type === 'group'}
85
+ <Select.Group>
86
+ {#if item.heading}
87
+ <Select.GroupHeading class={cls.groupLabel()}>
88
+ {item.heading}
89
+ </Select.GroupHeading>
90
+ {/if}
91
+ {#each item.items as innerItem, j (j)}
92
+ {#if innerItem.type !== 'group'}
93
+ <Select.Item {...innerItem} class={cls.item()}>
94
+ {#snippet children({ selected })}
95
+ <span class="flex-1 truncate">{innerItem.label}</span>
96
+ {#if selected}
97
+ <Icon aria-hidden="true" name="check" class="size-5 shrink-0" />
98
+ {/if}
99
+ {/snippet}
100
+ </Select.Item>
97
101
  {/if}
98
- {#each item.items as innerItem, j (j)}
99
- {#if innerItem.type !== 'group'}
100
- <Select.Item {...innerItem} class={cls.item()}>
101
- {#snippet children({ selected })}
102
- <span class="flex-1 truncate">{innerItem.label}</span>
103
- {#if selected}
104
- <Icon
105
- aria-hidden="true"
106
- name="check"
107
- class="size-5 shrink-0"
108
- />
109
- {/if}
110
- {/snippet}
111
- </Select.Item>
112
- {/if}
113
- {/each}
114
- </Select.Group>
115
- {:else}
116
- <Select.Item {...item} class={cls.item()}>
117
- {#snippet children({ selected })}
118
- <span class="flex-1 truncate">{item.label}</span>
119
- {#if selected}
120
- <Icon aria-hidden="true" name="check" class="size-5 shrink-0" />
121
- {/if}
122
- {/snippet}
123
- </Select.Item>
124
- {/if}
125
- {/each}
126
- </Select.Viewport>
127
- </div>
128
- {/if}
102
+ {/each}
103
+ </Select.Group>
104
+ {:else}
105
+ <Select.Item {...item} class={cls.item()}>
106
+ {#snippet children({ selected })}
107
+ <span class="flex-1 truncate">{item.label}</span>
108
+ {#if selected}
109
+ <Icon aria-hidden="true" name="check" class="size-5 shrink-0" />
110
+ {/if}
111
+ {/snippet}
112
+ </Select.Item>
113
+ {/if}
114
+ {/each}
115
+ </Select.Viewport>
116
+ </div>
129
117
  </div>
130
118
  {/snippet}
131
119
  </Select.Content>
@@ -28,7 +28,7 @@ export const select = tv({
28
28
  mr-3 text-md-sys-color-on-surface-variant size-6
29
29
  group-data-[state=open]:text-md-sys-color-primary
30
30
  group-data-[invalid]:text-md-sys-color-error z-20 text-[24px]
31
- transition-transform duration-200
31
+ transition-transform md-sys-motion-fast-spatial
32
32
  group-data-[state=open]:rotate-180
33
33
  `,
34
34
  content: `
@@ -45,7 +45,7 @@ export const select = tv({
45
45
  data-[selected]:bg-md-sys-color-secondary-container
46
46
  data-[selected]:text-md-sys-color-on-secondary-container
47
47
  data-[disabled]:cursor-not-allowed data-[disabled]:opacity-38
48
- transition-colors duration-150
48
+ transition-colors md-sys-motion-fast-effects
49
49
  `,
50
50
  groupLabel: `
51
51
  px-3 pt-2 pb-1 md-sys-typescale-label-medium
@@ -68,7 +68,7 @@ export const select = tv({
68
68
  bg-md-sys-color-surface-container-highest
69
69
  rounded-t-xs state-layer before:rounded-xs hover:before:bg-md-sys-color-on-surface/8
70
70
  after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-md-sys-color-on-surface-variant
71
- after:transition-[height,background-color] after:duration-200
71
+ after:transition-[height,background-color] after:md-sys-motion-fast-spatial
72
72
  hover:after:bg-md-sys-color-on-surface
73
73
  has-[[data-state=open]]:after:bg-md-sys-color-primary
74
74
  has-[[data-state=open]]:after:h-[2px]
@@ -79,7 +79,7 @@ export const select = tv({
79
79
  outlined: {
80
80
  base: `
81
81
  rounded-xs border border-md-sys-color-outline
82
- transition-colors duration-150
82
+ transition-colors md-sys-motion-fast-effects
83
83
  hover:border-md-sys-color-on-surface
84
84
  has-[[data-state=open]]:border-2 has-[[data-state=open]]:border-md-sys-color-primary
85
85
  `
@@ -4,11 +4,17 @@ Material 3 Slider.
4
4
 
5
5
  Sliders allow users to make selections from a range of values.
6
6
 
7
+ The handle follows the pointer 1:1 while dragged; any other change (a keyboard step, a click on
8
+ the track, a new `value` from outside) springs to it on the fast spatial spring.
9
+
7
10
  @see https://m3.material.io/components/sliders/overview
8
11
  -->
9
12
  <script lang="ts">
10
13
  import { Slider } from 'bits-ui';
11
14
  import clsx from 'clsx';
15
+ import type { Attachment } from 'svelte/attachments';
16
+ import { springTokens } from '../../../animation/spring.js';
17
+ import { SpringValue } from '../../../animation/springValue.svelte.js';
12
18
  import { Icon } from '../../../utils/index.js';
13
19
  import { slider } from './theme.js';
14
20
  import type { SliderProps } from './types.js';
@@ -26,58 +32,120 @@ Sliders allow users to make selections from a range of values.
26
32
  stops = false,
27
33
  endStops = true,
28
34
  vertical = false,
29
- format = (n: number) => {
30
- return n.toFixed(0);
31
- },
35
+ format = (n: number) => n.toFixed(0),
32
36
  class: className,
33
37
  dir,
34
38
  id,
35
39
  ...extra
36
40
  }: SliderProps = $props();
37
41
 
38
- const {
39
- root,
40
- leadingIcon,
41
- leadingIconPop,
42
- trailingIcon,
43
- trailingIconPop,
44
- trackFill,
45
- trackRest,
46
- stopBase,
47
- stopFill,
48
- stopRest,
49
- handle: handleCls,
50
- value: valueCls
51
- } = $derived(slider({ size, vertical }));
52
-
53
- const sliderDir = $derived(dir === null || dir === 'auto' ? undefined : dir);
54
- const sliderId = $derived(id === null ? undefined : id);
42
+ /* M3 slider geometry, in px. */
43
+ const HANDLE_PX = 4;
44
+ const GAP_PX = 6;
45
+ const ICON_PADDING_PX = 8;
46
+ /* Stops closer than this to the handle's center are hidden under its gap. */
47
+ const STOP_CLEARANCE_PX = HANDLE_PX / 2 + GAP_PX;
55
48
 
56
- let offsetWidth = $state(600);
57
- let offsetHeight = $state(600);
58
- let inlineSize = $derived(vertical ? offsetHeight : offsetWidth);
49
+ const iconSize = $derived(size === 'xl' ? 'lg' : 'md');
50
+ const iconPx = $derived(size === 'xl' ? 40 : 24);
59
51
 
52
+ const stepValue = $derived(typeof step === 'number' ? step : undefined);
60
53
  const range = $derived(max - min);
61
- const handlePosition = $derived((value - min) / (range || 1));
62
- const stopList = $derived.by(() => {
63
- const output: number[] = [];
64
- const add = (stopValue: number) => {
65
- if (leadingIconProps && stopValue == 0) return;
66
- if (trailingIconProps && stopValue == 1) return;
67
- output.push(stopValue);
54
+ const toFraction = (v: number) => (range > 0 ? (v - min) / range : 0);
55
+
56
+ /* ---- motion ---------------------------------------------------------------------------- */
57
+
58
+ /*
59
+ A press that jumps the value (a click on the track) springs; once the pointer moves while
60
+ pressed, the handle is being dragged and tracks it directly.
61
+ */
62
+ let dragging = $state(false);
63
+
64
+ const trackDrag: Attachment<HTMLElement> = (node) => {
65
+ const move = () => (dragging = true);
66
+ const release = () => {
67
+ dragging = false;
68
+ window.removeEventListener('pointermove', move);
69
+ window.removeEventListener('pointerup', release);
70
+ window.removeEventListener('pointercancel', release);
71
+ };
72
+ const press = () => {
73
+ if (disabled) return;
74
+ window.addEventListener('pointermove', move);
75
+ window.addEventListener('pointerup', release);
76
+ window.addEventListener('pointercancel', release);
77
+ };
78
+ node.addEventListener('pointerdown', press);
79
+ return () => {
80
+ node.removeEventListener('pointerdown', press);
81
+ release();
68
82
  };
83
+ };
69
84
 
70
- if (stops && typeof step == 'number' && range > 0) {
71
- for (let i = 0; i <= range; i += step) add(i / range);
72
- }
73
- if (endStops && !output.includes(1)) {
74
- add(1);
75
- }
85
+ /** The rendered position as a 0–1 fraction; springs towards `value` unless dragging. */
86
+ const fraction = new SpringValue(
87
+ () => toFraction(value),
88
+ springTokens.fastSpatial,
89
+ () => dragging
90
+ );
76
91
 
77
- return output;
92
+ /* The spring may overshoot the ends; the track can't. */
93
+ const pos = $derived(Math.min(1, Math.max(0, fraction.current)));
94
+
95
+ /* ---- geometry -------------------------------------------------------------------------- */
96
+
97
+ let trackWidth = $state(0);
98
+ let trackHeight = $state(0);
99
+ const length = $derived(vertical ? trackHeight : trackWidth);
100
+
101
+ const start = $derived(vertical ? 'bottom' : 'left');
102
+ const extent = $derived(vertical ? 'height' : 'width');
103
+
104
+ /** Handle center: travels the track inset by half a handle so it never overhangs the ends. */
105
+ const center = (f: number) => `calc(${HANDLE_PX / 2}px + ${f} * (100% - ${HANDLE_PX}px))`;
106
+ const centerPx = $derived(HANDLE_PX / 2 + pos * (length - HANDLE_PX));
107
+
108
+ const activePx = $derived(centerPx - HANDLE_PX / 2 - GAP_PX);
109
+ const inactivePx = $derived(length - centerPx - HANDLE_PX / 2 - GAP_PX);
110
+
111
+ const activeStyle = $derived(
112
+ `${extent}: max(0px, calc(${pos} * (100% - ${HANDLE_PX}px) - ${GAP_PX}px))`
113
+ );
114
+ const inactiveStyle = $derived(
115
+ `${extent}: max(0px, calc(${1 - pos} * (100% - ${HANDLE_PX}px) - ${GAP_PX}px))`
116
+ );
117
+
118
+ /* Stops sit inset by half the track thickness so the end stops land in the rounded caps. */
119
+ const stopList = $derived.by(() => {
120
+ const output: number[] = [];
121
+ if (stops && stepValue && range > 0) {
122
+ for (let v = min; v <= max; v += stepValue) output.push(toFraction(v));
123
+ } else if (endStops) {
124
+ output.push(1);
125
+ }
126
+ return output.filter((s) => {
127
+ if (leadingIconProps && s === 0) return false;
128
+ if (trailingIconProps && s === 1) return false;
129
+ return !length || Math.abs(s - pos) * length > STOP_CLEARANCE_PX;
130
+ });
78
131
  });
79
132
 
80
- const stepValue = $derived(typeof step === 'number' ? step : undefined);
133
+ /* Icons ride inside their track; when it gets too short they hop across the handle. */
134
+ const iconFits = (trackPx: number) => trackPx >= iconPx + ICON_PADDING_PX * 2;
135
+ const leadingOnActive = $derived(!length || iconFits(activePx));
136
+ const trailingOnInactive = $derived(!length || iconFits(inactivePx));
137
+
138
+ const leadingOffset = $derived(
139
+ leadingOnActive ? ICON_PADDING_PX : centerPx + HANDLE_PX / 2 + GAP_PX + ICON_PADDING_PX
140
+ );
141
+ const trailingOffset = $derived(
142
+ trailingOnInactive
143
+ ? length - ICON_PADDING_PX - iconPx
144
+ : centerPx - HANDLE_PX / 2 - GAP_PX - ICON_PADDING_PX - iconPx
145
+ );
146
+
147
+ const sliderDir = $derived(dir === null || dir === 'auto' ? undefined : dir);
148
+ const sliderId = $derived(id === null ? undefined : id);
81
149
  </script>
82
150
 
83
151
  <Slider.Root
@@ -93,109 +161,59 @@ Sliders allow users to make selections from a range of values.
93
161
  {...extra}
94
162
  >
95
163
  {#snippet child({ props })}
164
+ {@const cls = slider({ size, vertical, active: dragging })}
96
165
  <div
97
166
  {...props}
98
- class={root({ class: clsx(className) })}
99
- style="--handle: {handlePosition - 0.5}; {props.style ?? ''}"
167
+ class={cls.root({ class: clsx(className) })}
168
+ bind:clientWidth={trackWidth}
169
+ bind:clientHeight={trackHeight}
170
+ {@attach trackDrag}
100
171
  >
101
- <div
102
- class="pointer-events-none absolute inset-0"
103
- bind:offsetWidth
104
- bind:offsetHeight
105
- aria-hidden="true"
106
- ></div>
107
-
108
- <div
109
- class={trackFill({
110
- class: clsx(
111
- vertical &&
112
- '[clip-path:inset(calc(100%-var(--handle-left))_0_0_0_round_var(--m3-slider-track-in-shape)_var(--m3-slider-track-in-shape)_var(--track-radius)_var(--track-radius))]'
113
- )
114
- })}
115
- >
116
- {#each stopList as stop}
117
- <div
118
- class={clsx(
119
- stopBase(),
120
- stopFill(),
121
- vertical ? '-translate-x-1/2 translate-y-1/2' : '-translate-x-1/2 -translate-y-1/2'
122
- )}
123
- style:--x={stop - 0.5}
124
- ></div>
125
- {/each}
126
- </div>
127
-
128
- <div
129
- class={trackRest({
130
- class: clsx(
131
- vertical &&
132
- '[clip-path:inset(0_0_var(--handle-right)_0_round_var(--track-radius)_var(--track-radius)_var(--m3-slider-track-in-shape)_var(--m3-slider-track-in-shape))]'
133
- )
134
- })}
135
- >
136
- {#each stopList as stop}
137
- <div
138
- class={clsx(
139
- stopBase(),
140
- stopRest(),
141
- vertical ? '-translate-x-1/2 translate-y-1/2' : '-translate-x-1/2 -translate-y-1/2'
142
- )}
143
- style:--x={stop - 0.5}
144
- ></div>
145
- {/each}
146
- </div>
172
+ <div class={cls.track({ class: cls.activeTrack() })} style={activeStyle}></div>
173
+ <div class={cls.track({ class: cls.inactiveTrack() })} style={inactiveStyle}></div>
174
+
175
+ {#each stopList as s (s)}
176
+ <div
177
+ class={cls.stop({ class: s < pos ? cls.stopOnActive() : cls.stopOnInactive() })}
178
+ style="{start}: calc(var(--track) / 2 + {s} * (100% - var(--track)))"
179
+ ></div>
180
+ {/each}
147
181
 
148
182
  {#if leadingIconProps}
149
- <Icon
150
- class={leadingIcon({
151
- class: clsx(
152
- leadingIconProps.class,
153
- inlineSize * handlePosition < (size == 'xl' ? 48 : 40) && leadingIconPop(),
154
- vertical && 'translate-x-[-50%] translate-y-0'
155
- )
156
- })}
157
- {...leadingIconProps}
158
- />
183
+ <span
184
+ class={cls.icon({ class: leadingOnActive ? cls.iconOnActive() : cls.iconOnInactive() })}
185
+ style="{start}: {leadingOffset}px"
186
+ >
187
+ <Icon size={iconSize} {...leadingIconProps} />
188
+ </span>
159
189
  {/if}
160
190
  {#if trailingIconProps}
161
- <Icon
162
- class={trailingIcon({
163
- class: clsx(
164
- trailingIconProps.class,
165
- inlineSize * (1 - handlePosition) < (size == 'xl' ? 48 : 40) && trailingIconPop(),
166
- vertical && 'translate-x-[-50%] translate-y-0'
167
- )
191
+ <span
192
+ class={cls.icon({
193
+ class: trailingOnInactive ? cls.iconOnInactive() : cls.iconOnActive()
168
194
  })}
169
- {...trailingIconProps}
170
- />
195
+ style="{start}: {trailingOffset}px"
196
+ >
197
+ <Icon size={iconSize} {...trailingIconProps} />
198
+ </span>
171
199
  {/if}
172
200
 
173
- <Slider.Thumb
174
- index={0}
175
- class={handleCls({
176
- class: clsx(vertical ? 'translate-y-1/2' : '-translate-x-1/2')
177
- })}
178
- />
201
+ <Slider.Thumb index={0}>
202
+ {#snippet child({ props: thumbProps, active })}
203
+ <div
204
+ {...thumbProps}
205
+ class={cls.handle()}
206
+ data-active={active || undefined}
207
+ style="{start}: {center(pos)}"
208
+ ></div>
209
+ {/snippet}
210
+ </Slider.Thumb>
179
211
 
180
212
  {#if showValue}
181
- <div
182
- class={valueCls({
183
- class: clsx(vertical ? 'translate-x-0 translate-y-1/2 rotate-90' : '-translate-x-1/2')
184
- })}
185
- >
213
+ <div class={cls.value()} style="{start}: {center(pos)}" aria-hidden="true">
186
214
  {format(value)}
187
215
  </div>
188
216
  {/if}
189
217
  </div>
190
218
  {/snippet}
191
219
  </Slider.Root>
192
-
193
- <style>
194
- @layer tokens {
195
- :root {
196
- --m3-slider-track-out-shape: 0.5rem;
197
- --m3-slider-track-in-shape: 0.125rem;
198
- --m3-slider-handle-shape: calc(infinity * 1px);
199
- }
200
- }
201
- </style>
@@ -5,6 +5,9 @@ import type { SliderProps } from './types.js';
5
5
  *
6
6
  * Sliders allow users to make selections from a range of values.
7
7
  *
8
+ * The handle follows the pointer 1:1 while dragged; any other change (a keyboard step, a click on
9
+ * the track, a new `value` from outside) springs to it on the fast spatial spring.
10
+ *
8
11
  * @see https://m3.material.io/components/sliders/overview
9
12
  */
10
13
  declare const Slider: import("svelte").Component<SliderProps, {}, "value">;