@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
@@ -4,7 +4,7 @@ import type { SliderVariants } from './theme.js';
4
4
  /**
5
5
  * Props for the Slider component.
6
6
  */
7
- export type SliderProps = SliderVariants & Omit<HTMLAttributes<HTMLSpanElement>, 'size' | 'value'> & {
7
+ export type SliderProps = Omit<SliderVariants, 'active'> & Omit<HTMLAttributes<HTMLSpanElement>, 'size' | 'value'> & {
8
8
  /**
9
9
  * The current value of the slider.
10
10
  */
@@ -14,14 +14,14 @@ export const toggle = tv({
14
14
  'group-data-[disabled]:cursor-not-allowed',
15
15
  'group-data-[disabled]:border-md-sys-color-on-surface/12 group-data-[disabled]:bg-md-sys-color-surface-container-highest',
16
16
  'group-data-[state=checked]:group-data-[disabled]:border-transparent group-data-[state=checked]:group-data-[disabled]:bg-md-sys-color-on-surface/12',
17
- 'transition-colors duration-200'
17
+ 'transition-colors md-sys-motion-effects'
18
18
  ].join(' '),
19
19
  // Thumb — absolute over track
20
20
  handle: [
21
21
  'pointer-events-none absolute left-2 top-1/2 flex h-4 w-4 -translate-y-1/2',
22
22
  'items-center justify-center rounded-full relative',
23
23
  'bg-md-sys-color-outline',
24
- 'transition-[left,transform,background-color] duration-200 ease-in-out',
24
+ 'transition-[left,transform,background-color] md-sys-motion-fast-spatial',
25
25
  // checked: move right + scale up + on-primary color
26
26
  'group-data-[state=checked]:left-[1.75rem] group-data-[state=checked]:scale-[1.5] group-data-[state=checked]:bg-md-sys-color-on-primary',
27
27
  // disabled
@@ -38,7 +38,7 @@ export const toggle = tv({
38
38
  hover: [
39
39
  'pointer-events-none absolute left-4 top-1/2 h-12 w-12',
40
40
  '-translate-x-1/2 -translate-y-1/2 rounded-full',
41
- 'transition-[left,background-color] duration-200',
41
+ 'transition-[left,background-color] md-sys-motion-fast-spatial',
42
42
  'group-data-[state=checked]:left-9',
43
43
  'group-data-[disabled]:hidden',
44
44
  'group-hover:bg-md-sys-color-on-surface/8',
@@ -47,7 +47,7 @@ export const toggle = tv({
47
47
  // Icon inside thumb - shared base (sized via size="xs" prop on Icon, not here)
48
48
  icon: [
49
49
  'absolute inset-0 m-auto pointer-events-none flex items-center justify-center',
50
- 'opacity-0 transition-opacity duration-150 shrink-0'
50
+ 'opacity-0 transition-opacity md-sys-motion-fast-effects shrink-0'
51
51
  ].join(' '),
52
52
  // Specific icon states - colour matches M3 spec for each thumb background
53
53
  iconChecked: [
@@ -34,7 +34,7 @@ Text fields allow users to enter and edit text.
34
34
  ...restProps
35
35
  }: TextfieldProps = $props();
36
36
 
37
- const cls = $derived(textfield({ disabled, error, variant }));
37
+ const cls = $derived(textfield({ disabled, error, variant, leadingIcon: !!leadingIconProps }));
38
38
 
39
39
  const inputProps = $derived({
40
40
  id,
@@ -4,6 +4,7 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
4
4
  variant: {
5
5
  filled: {
6
6
  base: string;
7
+ input: string;
7
8
  label: string;
8
9
  };
9
10
  outlined: {
@@ -20,6 +21,9 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
20
21
  fieldset: string;
21
22
  };
22
23
  };
24
+ leadingIcon: {
25
+ true: {};
26
+ };
23
27
  disabled: {
24
28
  true: {
25
29
  base: string;
@@ -42,6 +46,7 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
42
46
  variant: {
43
47
  filled: {
44
48
  base: string;
49
+ input: string;
45
50
  label: string;
46
51
  };
47
52
  outlined: {
@@ -58,6 +63,9 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
58
63
  fieldset: string;
59
64
  };
60
65
  };
66
+ leadingIcon: {
67
+ true: {};
68
+ };
61
69
  disabled: {
62
70
  true: {
63
71
  base: string;
@@ -80,6 +88,7 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
80
88
  variant: {
81
89
  filled: {
82
90
  base: string;
91
+ input: string;
83
92
  label: string;
84
93
  };
85
94
  outlined: {
@@ -96,6 +105,9 @@ export declare const textfield: import("tailwind-variants").TVReturnType<{
96
105
  fieldset: string;
97
106
  };
98
107
  };
108
+ leadingIcon: {
109
+ true: {};
110
+ };
99
111
  disabled: {
100
112
  true: {
101
113
  base: string;
@@ -4,11 +4,13 @@ export const textfield = tv({
4
4
  base: `
5
5
  group relative w-full h-14 flex items-center
6
6
  `,
7
+ /* Stretches to the full field height so the label floats relative to the field's top edge,
8
+ not the input's. */
7
9
  inputWrapper: `
8
- relative flex-1 px-4
10
+ relative flex-1 self-stretch flex items-center px-4
9
11
  `,
10
12
  input: `
11
- peer w-full pt-4 bg-transparent outline-none
13
+ peer w-full bg-transparent outline-none
12
14
  md-sys-typescale-body-large text-md-sys-color-on-surface
13
15
  disabled:text-md-sys-color-on-surface/38
14
16
  `,
@@ -17,7 +19,7 @@ export const textfield = tv({
17
19
  md-sys-typescale-body-large
18
20
  text-md-sys-color-on-surface-variant
19
21
  pointer-events-none
20
- transition-[top,font-size,line-height,color,transform] duration-200 ease-in-out
22
+ transition-[top,left,font-size,line-height,color,transform] md-sys-motion-fast-spatial
21
23
 
22
24
  peer-focus:text-md-sys-color-primary
23
25
  `,
@@ -25,7 +27,7 @@ export const textfield = tv({
25
27
  ml-0.5
26
28
  text-md-sys-color-error
27
29
  group-focus-within:text-md-sys-color-primary
28
- transition-colors duration-200
30
+ transition-colors md-sys-motion-effects
29
31
  `,
30
32
  leadingIcon: `
31
33
  text-md-sys-color-on-surface-variant size-6
@@ -43,9 +45,9 @@ export const textfield = tv({
43
45
  text-md-sys-color-on-surface-variant
44
46
  `,
45
47
  fieldset: `
46
- absolute -top-[5px] left-0 right-0 bottom-0 m-0 px-2
48
+ absolute -top-2 left-0 right-0 bottom-0 m-0 px-3
47
49
  pointer-events-none rounded-xs border border-md-sys-color-outline
48
- transition-colors duration-150
50
+ transition-colors md-sys-motion-fast-effects
49
51
 
50
52
  group-hover:border-md-sys-color-on-surface
51
53
  group-focus-within:border-2 group-focus-within:border-md-sys-color-primary
@@ -53,7 +55,7 @@ export const textfield = tv({
53
55
  legend: `
54
56
  invisible float-none block h-auto max-w-[0.01px] overflow-hidden
55
57
  whitespace-nowrap md-sys-typescale-body-small
56
- transition-[max-width] duration-150 ease-in-out
58
+ transition-[max-width] md-sys-motion-fast-spatial
57
59
 
58
60
  group-focus-within:max-w-full
59
61
  group-has-[input:not(:placeholder-shown)]:max-w-full
@@ -69,16 +71,19 @@ export const textfield = tv({
69
71
  px-0 bg-md-sys-color-surface-container-highest
70
72
  rounded-t-xs state-layer before:rounded-xs hover:before:bg-md-sys-color-on-surface/8
71
73
  after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-md-sys-color-on-surface-variant
72
- after:transition-[height,background-color] after:duration-200
74
+ after:transition-[height,background-color] after:md-sys-motion-fast-spatial
73
75
  hover:after:bg-md-sys-color-on-surface
74
76
  focus-within:after:bg-md-sys-color-primary
75
77
  focus-within:after:h-[2px]
76
78
  disabled:bg-md-sys-color-on-surface/4
77
79
  disabled:after:bg-md-sys-color-on-surface/12
78
80
  `,
81
+ /* Room for the label floated inside the container. Outlined labels float into the border
82
+ notch instead, so their input stays vertically centered. */
83
+ input: 'pt-4',
79
84
  label: `
80
- peer-focus:top-1.5 peer-focus:md-sys-typescale-body-small peer-focus:-translate-y-1
81
- peer-not-placeholder-shown:top-1.5 peer-not-placeholder-shown:md-sys-typescale-body-small peer-not-placeholder-shown:-translate-y-1
85
+ peer-focus:top-2 peer-focus:md-sys-typescale-body-small peer-focus:translate-y-0
86
+ peer-not-placeholder-shown:top-2 peer-not-placeholder-shown:md-sys-typescale-body-small peer-not-placeholder-shown:translate-y-0
82
87
  `
83
88
  },
84
89
  outlined: {
@@ -102,6 +107,9 @@ export const textfield = tv({
102
107
  `
103
108
  }
104
109
  },
110
+ leadingIcon: {
111
+ true: {}
112
+ },
105
113
  disabled: {
106
114
  true: {
107
115
  base: 'cursor-not-allowed opacity-60',
@@ -109,6 +117,18 @@ export const textfield = tv({
109
117
  }
110
118
  }
111
119
  },
120
+ compoundVariants: [
121
+ {
122
+ /* M3: an outlined label floats back to the field's start edge, over the leading icon, into
123
+ the notch. The wrapper starts after the icon (ml-3 + size-6 = 36px), so -20px puts the
124
+ label at 16px from the field's edge. */
125
+ variant: 'outlined',
126
+ leadingIcon: true,
127
+ class: {
128
+ label: 'peer-focus:-left-5 peer-not-placeholder-shown:-left-5'
129
+ }
130
+ }
131
+ ],
112
132
  defaultVariants: {
113
133
  variant: 'outlined'
114
134
  }
@@ -13,7 +13,7 @@ export const toggleGroup = tv({
13
13
  });
14
14
  export const toggleGroupItem = tv({
15
15
  slots: {
16
- base: 'layer-container state-layer relative inline-flex min-w-12 flex-1 items-center justify-center gap-2 px-4 md-sys-typescale-label-large text-md-sys-color-on-surface outline-none transition-colors disabled:cursor-not-allowed disabled:opacity-38 [&:not(:last-child)]:border-r [&:not(:last-child)]:border-md-sys-color-outline data-[state=on]:bg-md-sys-color-secondary-container data-[state=on]:text-md-sys-color-on-secondary-container',
16
+ base: 'layer-container state-layer relative inline-flex min-w-12 flex-1 items-center justify-center gap-2 px-4 md-sys-typescale-label-large text-md-sys-color-on-surface outline-none transition-colors md-sys-motion-fast-effects disabled:cursor-not-allowed disabled:opacity-38 [&:not(:last-child)]:border-r [&:not(:last-child)]:border-md-sys-color-outline data-[state=on]:bg-md-sys-color-secondary-container data-[state=on]:text-md-sys-color-on-secondary-container',
17
17
  checkIcon: 'size-[18px] text-[18px]',
18
18
  icon: 'size-[18px] text-[18px]'
19
19
  }
@@ -13,7 +13,7 @@ Plain tooltips are used for simple labels, while rich tooltips can contain forma
13
13
  import { tooltip } from './theme.js';
14
14
  import type { TooltipProps } from './types.js';
15
15
  import { Tooltip } from 'bits-ui';
16
- import { enterExit, easeEmphasizedDecel, easeEmphasizedAccel } from '../../../animation/index.js';
16
+ import { enterExit, presence } from '../../../animation/index.js';
17
17
 
18
18
  let {
19
19
  subhead,
@@ -66,43 +66,32 @@ Plain tooltips are used for simple labels, while rich tooltips can contain forma
66
66
  {/if}
67
67
 
68
68
  {#if hasTooltipContent}
69
- <Tooltip.Content {...contentProps} forceMount>
69
+ <Tooltip.Content {...contentProps}>
70
70
  {#snippet child({ wrapperProps, props, open })}
71
- {#if open}
72
- <div {...wrapperProps} class={wrapperProps.class as any}>
73
- <div
74
- {...props}
75
- class={clsx(baseCls, props.class as any)}
76
- in:enterExit={{
77
- duration: variant === 'rich' ? 200 : 150,
78
- easing: easeEmphasizedDecel,
79
- mode: 'scale'
80
- }}
81
- out:enterExit={{
82
- duration: variant === 'rich' ? 150 : 75,
83
- easing: easeEmphasizedAccel,
84
- mode: 'scale'
85
- }}
86
- >
87
- {#if variant === 'rich'}
88
- <Layer />
89
- {#if subhead}
90
- <Title size="small" class={subheadCls()}>
91
- {subhead}
92
- </Title>
93
- {/if}
94
- <Body class={supportingTextCls()}>
95
- {supportingText}
96
- </Body>
97
- {@render children?.()}
98
- {:else if supportingText}
99
- <Body class={supportingTextCls()}>
100
- {supportingText}
101
- </Body>
71
+ <div {...wrapperProps} class={wrapperProps.class as any}>
72
+ <div
73
+ {...props}
74
+ class={clsx(baseCls, props.class as any)}
75
+ {@attach presence(() => open, enterExit.scale)}
76
+ >
77
+ {#if variant === 'rich'}
78
+ <Layer />
79
+ {#if subhead}
80
+ <Title size="small" class={subheadCls()}>
81
+ {subhead}
82
+ </Title>
102
83
  {/if}
103
- </div>
84
+ <Body class={supportingTextCls()}>
85
+ {supportingText}
86
+ </Body>
87
+ {@render children?.()}
88
+ {:else if supportingText}
89
+ <Body class={supportingTextCls()}>
90
+ {supportingText}
91
+ </Body>
92
+ {/if}
104
93
  </div>
105
- {/if}
94
+ </div>
106
95
  {/snippet}
107
96
  </Tooltip.Content>
108
97
  {/if}
@@ -4,7 +4,7 @@ export const breadcrumb = tv({
4
4
  base: 'flex',
5
5
  list: 'flex flex-wrap items-center gap-1',
6
6
  item: 'flex items-center gap-1',
7
- link: 'md-sys-typescale-label-large text-md-sys-color-on-surface-variant md-sys-state-focus-indicator relative inline-flex min-h-12 items-center rounded-full px-3 transition-colors',
7
+ link: 'md-sys-typescale-label-large text-md-sys-color-on-surface-variant md-sys-state-focus-indicator relative inline-flex min-h-12 items-center rounded-full px-3 transition-colors md-sys-motion-fast-effects',
8
8
  text: 'md-sys-typescale-label-large text-md-sys-color-on-surface-variant px-1 py-0.5',
9
9
  current: 'md-sys-typescale-label-large font-bold px-1 py-0.5',
10
10
  separator: 'text-md-sys-color-on-surface-variant inline-flex shrink-0'
@@ -9,9 +9,9 @@ export const navbar = tv({
9
9
  });
10
10
  export const navitem = tv({
11
11
  slots: {
12
- base: 'group relative flex flex-1 flex-col items-center justify-center min-w-0 max-w-[168px] h-full transition-colors',
12
+ base: 'group relative flex flex-1 flex-col items-center justify-center min-w-0 max-w-[168px] h-full transition-colors md-sys-motion-fast-effects',
13
13
  content: 'flex flex-col items-center justify-center gap-1 w-full',
14
- iconContainer: 'relative flex items-center justify-center h-8 w-16 rounded-full transition-colors duration-200 ease-in-out',
14
+ iconContainer: 'relative flex items-center justify-center h-8 w-16 rounded-full transition-colors md-sys-motion-effects',
15
15
  icon: 'text-[24px] size-6',
16
16
  label: 'md-sys-typescale-label-medium truncate w-full text-center'
17
17
  },
@@ -8,7 +8,7 @@ On mobile (< md) it automatically renders as a bottom navigation bar.
8
8
  @see https://m3.material.io/components/navigation-rail/overview
9
9
  -->
10
10
  <script lang="ts">
11
- import { clickOutside } from '../../../actions/index.js';
11
+ import { clickOutside } from '../../../attachments/index.js';
12
12
  import clsx from 'clsx';
13
13
  import { setContext, untrack } from 'svelte';
14
14
  import type { RailProps } from './types';
@@ -62,9 +62,7 @@ On mobile (< md) it automatically renders as a bottom navigation bar.
62
62
  class={railBaseClass}
63
63
  style={cssVars}
64
64
  data-expanded={expanded}
65
- use:clickOutside={() => {
66
- if (expanded) collapsed = true;
67
- }}
65
+ {@attach expanded && clickOutside(() => (collapsed = true))}
68
66
  >
69
67
  {#if expandable}
70
68
  <ButtonIcon
@@ -91,7 +89,8 @@ On mobile (< md) it automatically renders as a bottom navigation bar.
91
89
  pointer-events: none;
92
90
  background-color: transparent;
93
91
  z-index: -1;
94
- transition: opacity 200ms ease;
92
+ transition: opacity var(--md-sys-motion-duration-effects-spring)
93
+ var(--md-sys-motion-timing-function-effects-spring);
95
94
  }
96
95
 
97
96
  .rail-scrim[data-expanded='true'] {
@@ -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: {
@@ -4,15 +4,18 @@ Material 3 Tabs container.
4
4
 
5
5
  Wraps bits-ui Tabs.Root + Tabs.List and renders a sliding active indicator.
6
6
  Use TabContent for content-panel tabs; omit it for navigation (href) tabs.
7
+ Switching panels runs the M3 lateral transition: the new panel pushes the old one out in the
8
+ direction of the selected tab.
7
9
 
8
10
  @see https://m3.material.io/components/tabs/overview
9
11
  -->
10
12
  <script lang="ts">
13
+ import { lateral } from '../../../animation/index.js';
11
14
  import { tabHolder } from './theme.js';
12
15
  import type { TabHolderProps } from './types.js';
13
16
  import Tab from './Tab.svelte';
14
17
  import { Tabs } from 'bits-ui';
15
- import type { Snippet } from 'svelte';
18
+ import { tick, type Snippet } from 'svelte';
16
19
 
17
20
  let {
18
21
  items,
@@ -27,9 +30,28 @@ Use TabContent for content-panel tabs; omit it for navigation (href) tabs.
27
30
  const idx = $derived(items.findIndex((i) => i.value === value));
28
31
  const count = $derived(items.length);
29
32
  const { base, bar } = $derived(tabHolder({ rounded }));
33
+
34
+ let panels = $state<HTMLElement>();
35
+
36
+ /**
37
+ * Tabs are peers, so switching panels is the M3 lateral transition: the new panel pushes the
38
+ * old one out toward the side of the newly selected tab. Navigation (href) tabs have no panels
39
+ * here — the route change is the app's to animate.
40
+ * https://m3.material.io/styles/motion/transitions/transition-patterns#lateral
41
+ */
42
+ const select = (next: string) => {
43
+ const from = idx;
44
+ const to = items.findIndex((i) => i.value === next);
45
+ const update = async () => {
46
+ value = next;
47
+ await tick();
48
+ };
49
+ if (!children || !panels || from === -1 || to === -1 || from === to) return void update();
50
+ lateral(update, { target: panels, direction: to > from ? 'forward' : 'backward' });
51
+ };
30
52
  </script>
31
53
 
32
- <Tabs.Root bind:value {onValueChange} {activationMode}>
54
+ <Tabs.Root bind:value={() => value, select} {onValueChange} {activationMode}>
33
55
  <Tabs.List class={base()}>
34
56
  {#each items as item (item)}
35
57
  <Tab {...item} {variant} />
@@ -41,5 +63,9 @@ Use TabContent for content-panel tabs; omit it for navigation (href) tabs.
41
63
  ></div>
42
64
  </Tabs.List>
43
65
 
44
- {@render children?.()}
66
+ {#if children}
67
+ <div bind:this={panels}>
68
+ {@render children()}
69
+ </div>
70
+ {/if}
45
71
  </Tabs.Root>
@@ -1,5 +1,5 @@
1
1
  import type { TabHolderProps } from './types.js';
2
- import type { Snippet } from 'svelte';
2
+ import { type Snippet } from 'svelte';
3
3
  type $$ComponentProps = TabHolderProps & {
4
4
  children?: Snippet;
5
5
  };
@@ -8,6 +8,8 @@ type $$ComponentProps = TabHolderProps & {
8
8
  *
9
9
  * Wraps bits-ui Tabs.Root + Tabs.List and renders a sliding active indicator.
10
10
  * Use TabContent for content-panel tabs; omit it for navigation (href) tabs.
11
+ * Switching panels runs the M3 lateral transition: the new panel pushes the old one out in the
12
+ * direction of the selected tab.
11
13
  *
12
14
  * @see https://m3.material.io/components/tabs/overview
13
15
  */
@@ -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: {
@@ -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, {}, "">;