@boyernick/standard-ui-react 0.1.1-canary.2 → 0.1.1-canary.22

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 (56) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +1 -1
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/autocomplete.tsx +12 -11
  5. package/src/badge.tsx +73 -9
  6. package/src/brand.tsx +2 -2
  7. package/src/breadcrumb.tsx +38 -20
  8. package/src/button.tsx +2 -2
  9. package/src/calendar.tsx +22 -11
  10. package/src/card.tsx +57 -14
  11. package/src/carousel.tsx +163 -6
  12. package/src/checkbox.tsx +35 -13
  13. package/src/collapsible.tsx +4 -2
  14. package/src/combobox.tsx +13 -12
  15. package/src/command.tsx +38 -14
  16. package/src/context-menu.tsx +6 -3
  17. package/src/date-picker.tsx +174 -0
  18. package/src/dialog.tsx +33 -10
  19. package/src/drawer.tsx +1 -1
  20. package/src/empty.tsx +4 -4
  21. package/src/field.tsx +1 -1
  22. package/src/filter-group.tsx +139 -0
  23. package/src/icons.tsx +39 -0
  24. package/src/illustrations.tsx +219 -141
  25. package/src/image-modal.tsx +440 -49
  26. package/src/index.ts +188 -0
  27. package/src/input.tsx +1 -1
  28. package/src/kbd.tsx +49 -0
  29. package/src/lib/motion.ts +128 -18
  30. package/src/lib/popup.ts +45 -0
  31. package/src/menu.tsx +7 -4
  32. package/src/menubar.tsx +5 -0
  33. package/src/meter.tsx +1 -1
  34. package/src/minimap.tsx +267 -0
  35. package/src/navigation-menu.tsx +336 -67
  36. package/src/number-field.tsx +327 -61
  37. package/src/pagination.tsx +258 -42
  38. package/src/popover.tsx +3 -1
  39. package/src/preview-card.tsx +3 -1
  40. package/src/progress.tsx +6 -1
  41. package/src/questionnaire.tsx +279 -0
  42. package/src/scroll-area.tsx +1 -1
  43. package/src/select.tsx +8 -6
  44. package/src/sidebar.tsx +109 -28
  45. package/src/slider.tsx +64 -6
  46. package/src/sounds.tsx +6 -10
  47. package/src/spinner.tsx +105 -32
  48. package/src/switch.tsx +2 -2
  49. package/src/table.tsx +82 -15
  50. package/src/tabs.tsx +184 -38
  51. package/src/text-animate.tsx +71 -28
  52. package/src/textarea.tsx +1 -1
  53. package/src/timeline.tsx +321 -0
  54. package/src/toast.tsx +156 -23
  55. package/src/tooltip.tsx +43 -3
  56. package/src/video-player.tsx +296 -120
package/src/select.tsx CHANGED
@@ -5,6 +5,7 @@ import type { ComponentProps } from "react"
5
5
  import { IconChevronDownSmall } from "./icons"
6
6
  import { cn } from "./lib/cn"
7
7
  import { motion } from "./lib/motion"
8
+ import { popupInset, popupItem, popupLabel, popupSurface } from "./lib/popup"
8
9
 
9
10
  export type SelectProps = ComponentProps<typeof BaseSelect.Root>
10
11
  export type SelectTriggerProps = ComponentProps<typeof BaseSelect.Trigger>
@@ -33,7 +34,7 @@ export const SelectTrigger = ({
33
34
  className={cn(
34
35
  "flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border-secondary bg-surface px-3 text-sm text-fg-primary inset-shadow-outline-top outline-none",
35
36
  motion.all,
36
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-placeholder:text-fg-quaternary data-popup-open:border-border-secondary data-popup-open:bg-background-tertiary/60",
37
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/20 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-placeholder:text-fg-quaternary data-popup-open:border-border-secondary",
37
38
  className,
38
39
  )}
39
40
  {...props}
@@ -71,7 +72,7 @@ export const SelectPortal = (props: SelectPortalProps) => (
71
72
 
72
73
  export const SelectPositioner = ({
73
74
  alignItemWithTrigger = false,
74
- sideOffset = 4,
75
+ sideOffset = 6,
75
76
  className,
76
77
  ...props
77
78
  }: SelectPositionerProps) => (
@@ -86,7 +87,8 @@ export const SelectPositioner = ({
86
87
  export const SelectPopup = ({ className, ...props }: SelectPopupProps) => (
87
88
  <BaseSelect.Popup
88
89
  className={cn(
89
- "z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden rounded-md border border-border-primary bg-surface shadow-md outline-none",
90
+ "z-50 max-h-[min(24rem,var(--available-height))] w-[var(--anchor-width)] overflow-hidden",
91
+ popupSurface,
90
92
  motion.popupAnchor,
91
93
  className,
92
94
  )}
@@ -96,7 +98,7 @@ export const SelectPopup = ({ className, ...props }: SelectPopupProps) => (
96
98
 
97
99
  export const SelectList = ({ className, ...props }: SelectListProps) => (
98
100
  <BaseSelect.List
99
- className={cn("max-h-[inherit] overflow-y-auto p-1 outline-none", className)}
101
+ className={cn("max-h-[inherit] overflow-y-auto outline-none", popupInset, className)}
100
102
  {...props}
101
103
  />
102
104
  )
@@ -104,7 +106,7 @@ export const SelectList = ({ className, ...props }: SelectListProps) => (
104
106
  export const SelectItem = ({ className, ...props }: SelectItemProps) => (
105
107
  <BaseSelect.Item
106
108
  className={cn(
107
- "flex min-h-8 cursor-default items-center gap-2 rounded-xs px-2.5 py-1.5 text-sm text-fg-primary outline-none select-none",
109
+ popupItem,
108
110
  motion.colors,
109
111
  "data-disabled:cursor-not-allowed data-disabled:opacity-50 data-highlighted:bg-background-tertiary data-selected:text-fg-primary",
110
112
  className,
@@ -145,7 +147,7 @@ export const SelectGroupLabel = ({
145
147
  ...props
146
148
  }: SelectGroupLabelProps) => (
147
149
  <BaseSelect.GroupLabel
148
- className={cn("px-2.5 py-1.5 text-xs text-fg-tertiary", className)}
150
+ className={cn(popupLabel, className)}
149
151
  {...props}
150
152
  />
151
153
  )
package/src/sidebar.tsx CHANGED
@@ -1,56 +1,115 @@
1
- import type { ComponentProps } from "react";
2
- import { cn } from "./lib/cn";
3
- import { motion } from "./lib/motion";
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type { ComponentProps } from "react"
3
+ import { cn } from "./lib/cn"
4
+ import { motion } from "./lib/motion"
4
5
 
5
- export type SidebarProps = ComponentProps<"aside">;
6
- export type SidebarHeaderProps = ComponentProps<"div">;
7
- export type SidebarContentProps = ComponentProps<"div">;
8
- export type SidebarFooterProps = ComponentProps<"div">;
9
- export type SidebarNavProps = ComponentProps<"nav">;
10
- export type SidebarNavItemProps = ComponentProps<"button"> & {
11
- active?: boolean;
12
- };
13
- export type SidebarGroupProps = ComponentProps<"div">;
14
- export type SidebarGroupLabelProps = ComponentProps<"h3">;
6
+ const sidebarVariants = cva(
7
+ "flex h-full shrink-0 flex-col overflow-hidden text-fg-primary",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-r border-border-primary bg-surface",
12
+ inset: "rounded-lg bg-background-secondary",
13
+ floating:
14
+ "rounded-xl border border-border-primary bg-surface-raised shadow-sm",
15
+ },
16
+ size: {
17
+ sm: "w-52",
18
+ md: "w-60",
19
+ lg: "w-72",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "md",
25
+ },
26
+ },
27
+ )
15
28
 
16
- export const Sidebar = ({ className, ...props }: SidebarProps) => (
29
+ const sidebarNavItemVariants = cva(
30
+ "group/nav-item relative flex w-full cursor-pointer items-center gap-2 border border-transparent text-left text-fg-secondary outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
31
+ {
32
+ variants: {
33
+ variant: {
34
+ default:
35
+ "rounded-md hover:bg-background-tertiary hover:text-fg-primary data-[active]:bg-background-tertiary data-[active]:font-medium data-[active]:text-fg-primary",
36
+ pill: "rounded-full hover:bg-background-tertiary hover:text-fg-primary data-[active]:border-brand-primary-border data-[active]:bg-brand-primary data-[active]:font-medium data-[active]:text-brand-foreground",
37
+ indicator:
38
+ "rounded-md before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-transparent before:content-[''] hover:bg-background-tertiary hover:text-fg-primary data-[active]:bg-background-tertiary data-[active]:font-medium data-[active]:text-fg-primary data-[active]:before:bg-brand-primary",
39
+ },
40
+ size: {
41
+ sm: "h-8 px-2.5 text-xs",
42
+ md: "h-9 px-3 text-sm",
43
+ lg: "h-10 px-3.5 text-sm",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ variant: "default",
48
+ size: "md",
49
+ },
50
+ },
51
+ )
52
+
53
+ export type SidebarProps = Omit<ComponentProps<"aside">, "size"> &
54
+ VariantProps<typeof sidebarVariants>
55
+ export type SidebarHeaderProps = ComponentProps<"div">
56
+ export type SidebarContentProps = ComponentProps<"div">
57
+ export type SidebarFooterProps = ComponentProps<"div">
58
+ export type SidebarNavProps = ComponentProps<"nav">
59
+ export type SidebarNavItemProps = Omit<ComponentProps<"button">, "size"> &
60
+ VariantProps<typeof sidebarNavItemVariants> & {
61
+ active?: boolean
62
+ }
63
+ export type SidebarNavItemLabelProps = ComponentProps<"span">
64
+ export type SidebarNavBadgeProps = ComponentProps<"span">
65
+ export type SidebarGroupProps = ComponentProps<"div">
66
+ export type SidebarGroupLabelProps = ComponentProps<"h3">
67
+
68
+ export const Sidebar = ({
69
+ className,
70
+ variant,
71
+ size,
72
+ ...props
73
+ }: SidebarProps) => (
17
74
  <aside
18
- className={cn(
19
- "flex h-full w-60 shrink-0 flex-col border-r border-border-primary bg-surface",
20
- className,
21
- )}
75
+ className={cn(sidebarVariants({ variant, size }), className)}
22
76
  {...props}
23
77
  />
24
- );
78
+ )
25
79
 
26
80
  export const SidebarHeader = ({ className, ...props }: SidebarHeaderProps) => (
27
81
  <div
28
82
  className={cn("border-b border-border-primary p-4", className)}
29
83
  {...props}
30
84
  />
31
- );
85
+ )
32
86
 
33
87
  export const SidebarContent = ({
34
88
  className,
35
89
  ...props
36
90
  }: SidebarContentProps) => (
37
- <div className={cn("flex-1 overflow-y-auto p-3", className)} {...props} />
38
- );
91
+ <div
92
+ className={cn("min-h-0 flex-1 overflow-y-auto p-3", className)}
93
+ {...props}
94
+ />
95
+ )
39
96
 
40
97
  export const SidebarFooter = ({ className, ...props }: SidebarFooterProps) => (
41
98
  <div
42
99
  className={cn("border-t border-border-primary p-3", className)}
43
100
  {...props}
44
101
  />
45
- );
102
+ )
46
103
 
47
104
  export const SidebarNav = ({ className, ...props }: SidebarNavProps) => (
48
105
  <nav className={cn("flex flex-col gap-1", className)} {...props} />
49
- );
106
+ )
50
107
 
51
108
  export const SidebarNavItem = ({
52
109
  className,
53
110
  active = false,
111
+ variant,
112
+ size,
54
113
  type = "button",
55
114
  ...props
56
115
  }: SidebarNavItemProps) => (
@@ -59,17 +118,37 @@ export const SidebarNavItem = ({
59
118
  data-active={active || undefined}
60
119
  aria-current={active ? "page" : undefined}
61
120
  className={cn(
62
- "flex h-9 w-full cursor-pointer items-center gap-2 rounded-md border border-transparent px-3 text-left text-sm text-fg-secondary outline-none hover:bg-background-tertiary hover:text-fg-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-[active]:bg-background-tertiary data-[active]:font-medium data-[active]:text-fg-primary [&_svg]:size-4 [&_svg]:shrink-0",
121
+ sidebarNavItemVariants({ variant, size }),
63
122
  motion.colors,
64
123
  className,
65
124
  )}
66
125
  {...props}
67
126
  />
68
- );
127
+ )
128
+
129
+ export const SidebarNavItemLabel = ({
130
+ className,
131
+ ...props
132
+ }: SidebarNavItemLabelProps) => (
133
+ <span className={cn("min-w-0 flex-1 truncate", className)} {...props} />
134
+ )
135
+
136
+ export const SidebarNavBadge = ({
137
+ className,
138
+ ...props
139
+ }: SidebarNavBadgeProps) => (
140
+ <span
141
+ className={cn(
142
+ "text-2xs-strong ml-auto inline-flex min-w-5 shrink-0 items-center justify-center rounded-full border border-border-primary bg-surface px-1.5 py-0.5 text-fg-secondary",
143
+ className,
144
+ )}
145
+ {...props}
146
+ />
147
+ )
69
148
 
70
149
  export const SidebarGroup = ({ className, ...props }: SidebarGroupProps) => (
71
150
  <div className={cn("flex flex-col gap-1 py-2", className)} {...props} />
72
- );
151
+ )
73
152
 
74
153
  export const SidebarGroupLabel = ({
75
154
  className,
@@ -79,4 +158,6 @@ export const SidebarGroupLabel = ({
79
158
  className={cn("px-3 py-1.5 text-xs-strong text-fg-tertiary", className)}
80
159
  {...props}
81
160
  />
82
- );
161
+ )
162
+
163
+ export { sidebarNavItemVariants, sidebarVariants }
package/src/slider.tsx CHANGED
@@ -1,21 +1,51 @@
1
1
  "use client"
2
2
 
3
3
  import { Slider as BaseSlider } from "@base-ui/react/slider"
4
+ import { cva, type VariantProps } from "class-variance-authority"
4
5
  import type { ComponentProps } from "react"
5
6
  import { cn } from "./lib/cn"
6
7
 
7
- export type SliderProps = ComponentProps<typeof BaseSlider.Root>
8
+ const sliderVariants = cva(
9
+ "group/slider relative flex w-full data-disabled:opacity-50 data-[orientation=vertical]:h-48 data-[orientation=vertical]:w-fit",
10
+ {
11
+ variants: {
12
+ size: {
13
+ sm: "[--slider-thumb-size:0.875rem] [--slider-track-size:0.25rem]",
14
+ md: "[--slider-thumb-size:1rem] [--slider-track-size:0.375rem]",
15
+ lg: "[--slider-thumb-size:1.25rem] [--slider-track-size:0.5rem]",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ size: "md",
20
+ },
21
+ },
22
+ )
23
+
24
+ export type SliderProps = Omit<
25
+ ComponentProps<typeof BaseSlider.Root>,
26
+ "size"
27
+ > &
28
+ VariantProps<typeof sliderVariants>
8
29
  export type SliderControlProps = ComponentProps<typeof BaseSlider.Control>
9
30
  export type SliderTrackProps = ComponentProps<typeof BaseSlider.Track>
10
31
  export type SliderIndicatorProps = ComponentProps<typeof BaseSlider.Indicator>
11
32
  export type SliderThumbProps = ComponentProps<typeof BaseSlider.Thumb>
33
+ export type SliderTicksProps = ComponentProps<"div"> & {
34
+ /** Number of evenly spaced marks, including both ends. */
35
+ count?: number
36
+ }
12
37
 
13
- export const Slider = (props: SliderProps) => <BaseSlider.Root {...props} />
38
+ export const Slider = ({ className, size, ...props }: SliderProps) => (
39
+ <BaseSlider.Root
40
+ className={cn(sliderVariants({ size }), className)}
41
+ {...props}
42
+ />
43
+ )
14
44
 
15
45
  export const SliderControl = ({ className, ...props }: SliderControlProps) => (
16
46
  <BaseSlider.Control
17
47
  className={cn(
18
- "flex w-full touch-none items-center py-3 select-none",
48
+ "flex w-full touch-none items-center py-3 select-none data-disabled:cursor-not-allowed data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:px-3 data-[orientation=vertical]:py-0",
19
49
  className,
20
50
  )}
21
51
  {...props}
@@ -25,7 +55,7 @@ export const SliderControl = ({ className, ...props }: SliderControlProps) => (
25
55
  export const SliderTrack = ({ className, ...props }: SliderTrackProps) => (
26
56
  <BaseSlider.Track
27
57
  className={cn(
28
- "relative h-1.5 w-full rounded-full bg-background-quaternary",
58
+ "relative h-[var(--slider-track-size)] w-full rounded-full bg-background-quaternary transition-colors duration-[var(--duration-sm)] group-hover/slider:bg-background-active data-[orientation=vertical]:h-full data-[orientation=vertical]:w-[var(--slider-track-size)]",
29
59
  className,
30
60
  )}
31
61
  {...props}
@@ -38,7 +68,7 @@ export const SliderIndicator = ({
38
68
  }: SliderIndicatorProps) => (
39
69
  <BaseSlider.Indicator
40
70
  className={cn(
41
- "absolute h-full rounded-full bg-brand-primary transition-[width] duration-150 ease-out motion-reduce:transition-none",
71
+ "absolute h-full rounded-full bg-brand-primary data-[orientation=vertical]:h-auto data-[orientation=vertical]:w-full",
42
72
  className,
43
73
  )}
44
74
  {...props}
@@ -48,9 +78,37 @@ export const SliderIndicator = ({
48
78
  export const SliderThumb = ({ className, ...props }: SliderThumbProps) => (
49
79
  <BaseSlider.Thumb
50
80
  className={cn(
51
- "size-4 cursor-pointer rounded-full border border-brand-primary-border bg-surface shadow-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-disabled:cursor-not-allowed data-disabled:opacity-50",
81
+ "flex size-[var(--slider-thumb-size)] cursor-grab items-center justify-center rounded-full border border-brand-primary-border bg-surface shadow-sm outline-none transition-[scale,box-shadow,background-color,border-color] duration-[var(--duration-sm)] ease-enter after:size-1 after:rounded-full after:bg-brand-primary after:content-[''] hover:scale-110 hover:bg-background-secondary active:scale-105 data-dragging:cursor-grabbing data-dragging:scale-110 data-disabled:cursor-not-allowed data-disabled:hover:scale-100 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20",
52
82
  className,
53
83
  )}
54
84
  {...props}
55
85
  />
56
86
  )
87
+
88
+ export const SliderTicks = ({
89
+ count = 5,
90
+ className,
91
+ ...props
92
+ }: SliderTicksProps) => {
93
+ const tickCount = Number.isFinite(count) ? Math.max(2, Math.floor(count)) : 2
94
+
95
+ return (
96
+ <div
97
+ aria-hidden
98
+ className={cn(
99
+ "pointer-events-none absolute inset-0 flex items-center justify-between group-data-[orientation=vertical]/slider:flex-col",
100
+ className,
101
+ )}
102
+ {...props}
103
+ >
104
+ {Array.from({ length: tickCount }, (_, index) => (
105
+ <span
106
+ key={index}
107
+ className="h-2 w-px rounded-full bg-surface/70 group-data-[orientation=vertical]/slider:h-px group-data-[orientation=vertical]/slider:w-2"
108
+ />
109
+ ))}
110
+ </div>
111
+ )
112
+ }
113
+
114
+ export { sliderVariants }
package/src/sounds.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client"
2
2
 
3
+ import { IconPlay } from "@central-icons-react/round-filled-radius-2-stroke-2/IconPlay"
3
4
  import {
4
5
  createContext,
5
6
  useCallback,
@@ -28,7 +29,7 @@ const SOUND_PRESETS: SoundPreset[] = [
28
29
  {
29
30
  id: "click",
30
31
  label: "Click",
31
- description: "Light confirmation for toggles and secondary actions.",
32
+ description: "Confirmation for toggles and secondary actions.",
32
33
  play: (ctx, when, destination) => {
33
34
  const osc = ctx.createOscillator()
34
35
  const gain = ctx.createGain()
@@ -219,14 +220,15 @@ export const Sound = ({
219
220
  >
220
221
  <Button
221
222
  type="button"
222
- variant="outline"
223
- size="sm"
223
+ variant="secondary"
224
+ size="md"
224
225
  iconOnly
225
226
  rounded
227
+ className="shrink-0 hover:scale-[1.04] active:scale-95"
226
228
  aria-label={`Play ${resolvedLabel}`}
227
229
  onClick={handlePlay}
228
230
  >
229
- <PlayIcon />
231
+ <IconPlay size={12} className="size-3" aria-hidden />
230
232
  </Button>
231
233
  <div className="min-w-0 flex-1">
232
234
  <p className="text-sm-strong text-fg-primary">{resolvedLabel}</p>
@@ -262,9 +264,3 @@ export const SoundToggle = ({
262
264
  </Button>
263
265
  )
264
266
  }
265
-
266
- const PlayIcon = () => (
267
- <svg viewBox="0 0 24 24" fill="none" className="size-4" aria-hidden>
268
- <path d="M9 7.5v9l8-4.5-8-4.5Z" fill="currentColor" />
269
- </svg>
270
- )
package/src/spinner.tsx CHANGED
@@ -2,44 +2,117 @@ import { cva, type VariantProps } from "class-variance-authority";
2
2
  import type { ComponentProps } from "react";
3
3
  import { cn } from "./lib/cn";
4
4
 
5
+ const SPINNER_SPOKES = Array.from({ length: 12 }, (_, index) => index);
6
+
5
7
  const spinnerVariants = cva(
6
- "animate-spin text-current motion-reduce:animate-none",
7
- { variants: {
8
- size: {
9
- sm: "size-3.5",
10
- md: "size-4",
11
- lg: "size-5",
8
+ "text-current motion-reduce:animate-none",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ ring: "animate-spin",
13
+ arc: "animate-spin",
14
+ dots: "",
15
+ spokes: "",
16
+ },
17
+ size: {
18
+ sm: "size-3.5",
19
+ md: "size-4",
20
+ lg: "size-5",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "ring",
25
+ size: "md",
12
26
  },
13
27
  },
14
- defaultVariants: {
15
- size: "md",
16
- },
17
- });
28
+ );
18
29
 
19
30
  export type SpinnerProps = ComponentProps<"svg"> &
20
31
  VariantProps<typeof spinnerVariants>;
21
32
 
22
- export const Spinner = ({ className, size, ...props }: SpinnerProps) => (
23
- <svg
24
- role="status"
25
- aria-label="Loading"
26
- className={cn(spinnerVariants({ size }), className)}
27
- viewBox="0 0 24 24"
28
- fill="none"
29
- {...props}
30
- >
31
- <path
32
- d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
33
- stroke="currentColor"
34
- strokeOpacity="0.3"
35
- strokeWidth="2"
36
- />
37
- <path
38
- d="M21 12C21 16.9706 16.9706 21 12 21"
39
- stroke="currentColor"
40
- strokeWidth="2"
41
- />
42
- </svg>
43
- );
33
+ export const Spinner = ({
34
+ className,
35
+ variant,
36
+ size,
37
+ ...props
38
+ }: SpinnerProps) => {
39
+ const resolvedVariant = variant ?? "ring";
40
+
41
+ return (
42
+ <svg
43
+ role="status"
44
+ aria-label="Loading"
45
+ className={cn(
46
+ spinnerVariants({ variant: resolvedVariant, size }),
47
+ className,
48
+ )}
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ {...props}
52
+ >
53
+ {resolvedVariant === "spokes" ? (
54
+ <g stroke="currentColor" strokeWidth="2.25" strokeLinecap="round">
55
+ {SPINNER_SPOKES.map((spoke) => (
56
+ <line
57
+ key={spoke}
58
+ x1="12"
59
+ y1="3"
60
+ x2="12"
61
+ y2="6.25"
62
+ transform={`rotate(${spoke * 30} 12 12)`}
63
+ className="animate-[spinner-spoke_1.2s_linear_infinite] motion-reduce:animate-none motion-reduce:opacity-40"
64
+ style={{ animationDelay: `${spoke * -0.1}s` }}
65
+ />
66
+ ))}
67
+ </g>
68
+ ) : resolvedVariant === "dots" ? (
69
+ <g fill="currentColor">
70
+ <circle
71
+ cx="5"
72
+ cy="12"
73
+ r="2.25"
74
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] [animation-delay:-0.32s] motion-reduce:animate-none"
75
+ />
76
+ <circle
77
+ cx="12"
78
+ cy="12"
79
+ r="2.25"
80
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] [animation-delay:-0.16s] motion-reduce:animate-none"
81
+ />
82
+ <circle
83
+ cx="19"
84
+ cy="12"
85
+ r="2.25"
86
+ className="animate-[spinner-dot_1.2s_var(--ease-passive)_infinite] motion-reduce:animate-none"
87
+ />
88
+ </g>
89
+ ) : resolvedVariant === "arc" ? (
90
+ <circle
91
+ cx="12"
92
+ cy="12"
93
+ r="9"
94
+ stroke="currentColor"
95
+ strokeWidth="2.5"
96
+ strokeLinecap="round"
97
+ strokeDasharray="42 15"
98
+ />
99
+ ) : (
100
+ <>
101
+ <path
102
+ d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
103
+ stroke="currentColor"
104
+ strokeOpacity="0.3"
105
+ strokeWidth="2"
106
+ />
107
+ <path
108
+ d="M21 12C21 16.9706 16.9706 21 12 21"
109
+ stroke="currentColor"
110
+ strokeWidth="2"
111
+ />
112
+ </>
113
+ )}
114
+ </svg>
115
+ );
116
+ };
44
117
 
45
118
  export { spinnerVariants };
package/src/switch.tsx CHANGED
@@ -9,11 +9,11 @@ export type SwitchProps = ComponentProps<typeof BaseSwitch.Root>
9
9
  export const Switch = ({ className, ...props }: SwitchProps) => (
10
10
  <BaseSwitch.Root
11
11
  className={cn(
12
- "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-150 ease-out motion-reduce:transition-none outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
12
+ "relative inline-flex h-6 w-10 shrink-0 items-center rounded-full bg-background-quaternary transition-colors duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 data-checked:bg-brand-primary data-disabled:cursor-not-allowed data-disabled:opacity-50",
13
13
  className,
14
14
  )}
15
15
  {...props}
16
16
  >
17
- <BaseSwitch.Thumb className="pointer-events-none block size-5 translate-x-0.5 rounded-full bg-surface shadow-sm transition-transform duration-150 ease-out motion-reduce:transition-none data-checked:translate-x-[1.125rem]" />
17
+ <BaseSwitch.Thumb className="pointer-events-none block size-5 translate-x-0.5 rounded-full bg-surface shadow-sm transition-transform duration-[var(--duration-sm)] ease-enter motion-reduce:transition-none data-checked:translate-x-[1.125rem]" />
18
18
  </BaseSwitch.Root>
19
19
  )