@ponti-studios/ui 0.1.2 → 0.3.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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -7
  3. package/THIRD_PARTY_NOTICES.md +17 -0
  4. package/dist/index.js +1028 -638
  5. package/dist/index.js.map +1 -1
  6. package/package.json +30 -32
  7. package/src/animations.css +16 -434
  8. package/src/components/data-display/accordion.tsx +10 -7
  9. package/src/components/data-display/filter-chip.tsx +2 -2
  10. package/src/components/data-display/index.ts +5 -0
  11. package/src/components/data-display/metric-card.stories.tsx +48 -0
  12. package/src/components/data-display/metric-card.tsx +20 -0
  13. package/src/components/data-display/sort-controls.stories.tsx +45 -0
  14. package/src/components/data-display/sort-controls.tsx +99 -0
  15. package/src/components/data-display/sort-row.tsx +74 -0
  16. package/src/components/data-display/table.tsx +5 -5
  17. package/src/components/feedback/alert.tsx +4 -4
  18. package/src/components/feedback/empty-state.tsx +6 -6
  19. package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
  20. package/src/components/feedback/file-upload-status-badge.tsx +33 -0
  21. package/src/components/feedback/file-upload-status.stories.tsx +57 -0
  22. package/src/components/feedback/file-upload-status.tsx +152 -0
  23. package/src/components/feedback/index.ts +9 -0
  24. package/src/components/feedback/progress-bar.stories.tsx +39 -0
  25. package/src/components/feedback/progress-bar.tsx +21 -0
  26. package/src/components/feedback/progress.stories.tsx +8 -6
  27. package/src/components/feedback/progress.tsx +1 -1
  28. package/src/components/feedback/skeleton.stories.tsx +36 -0
  29. package/src/components/feedback/skeleton.tsx +7 -0
  30. package/src/components/feedback/spinner.tsx +2 -2
  31. package/src/components/feedback/update-guard.stories.tsx +32 -0
  32. package/src/components/feedback/update-guard.tsx +280 -0
  33. package/src/components/forms/calendar.tsx +11 -11
  34. package/src/components/forms/date-month-select.stories.tsx +26 -0
  35. package/src/components/forms/date-month-select.tsx +70 -0
  36. package/src/components/forms/date-picker.tsx +3 -3
  37. package/src/components/forms/drop-zone.stories.tsx +56 -0
  38. package/src/components/forms/drop-zone.tsx +146 -0
  39. package/src/components/forms/entity-select.stories.tsx +65 -0
  40. package/src/components/forms/entity-select.tsx +79 -0
  41. package/src/components/forms/field.tsx +3 -3
  42. package/src/components/forms/group-by-select.stories.tsx +26 -0
  43. package/src/components/forms/group-by-select.tsx +47 -0
  44. package/src/components/forms/index.ts +10 -0
  45. package/src/components/forms/input.stories.tsx +4 -4
  46. package/src/components/forms/input.tsx +1 -1
  47. package/src/components/forms/passkey-management.stories.tsx +81 -0
  48. package/src/components/forms/passkey-management.tsx +138 -0
  49. package/src/components/forms/radio-group.tsx +14 -13
  50. package/src/components/forms/select.stories.tsx +4 -4
  51. package/src/components/forms/select.tsx +45 -53
  52. package/src/components/forms/slider.stories.tsx +5 -2
  53. package/src/components/forms/slider.tsx +27 -17
  54. package/src/components/forms/stepper.tsx +4 -4
  55. package/src/components/forms/switch.stories.tsx +1 -1
  56. package/src/components/forms/switch.tsx +2 -2
  57. package/src/components/forms/textarea.stories.tsx +2 -2
  58. package/src/components/forms/textarea.tsx +1 -1
  59. package/src/components/layout/index.ts +3 -0
  60. package/src/components/layout/scroll-area.stories.tsx +4 -4
  61. package/src/components/layout/scroll-area.tsx +23 -21
  62. package/src/components/layout/section-intro.stories.tsx +29 -0
  63. package/src/components/layout/section-intro.tsx +31 -0
  64. package/src/components/layout/surface-panel.stories.tsx +32 -0
  65. package/src/components/layout/surface-panel.tsx +15 -0
  66. package/src/components/navigation/app-navigation.stories.tsx +2 -2
  67. package/src/components/navigation/app-navigation.tsx +13 -13
  68. package/src/components/navigation/index.ts +4 -0
  69. package/src/components/navigation/pagination-controls.stories.tsx +41 -0
  70. package/src/components/navigation/pagination-controls.tsx +50 -0
  71. package/src/components/navigation/route-link.stories.tsx +30 -0
  72. package/src/components/navigation/route-link.tsx +42 -0
  73. package/src/components/navigation/tabs.stories.tsx +5 -5
  74. package/src/components/navigation/tabs.tsx +7 -7
  75. package/src/components/overlays/alert-dialog.tsx +21 -46
  76. package/src/components/overlays/command.tsx +51 -82
  77. package/src/components/overlays/dialog.tsx +5 -5
  78. package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
  79. package/src/components/overlays/dropdown-menu.tsx +44 -142
  80. package/src/components/overlays/popover.stories.tsx +2 -2
  81. package/src/components/overlays/popover.tsx +34 -16
  82. package/src/components/overlays/sheet.stories.tsx +2 -2
  83. package/src/components/overlays/sheet.tsx +35 -17
  84. package/src/components/primitives/avatar.tsx +3 -3
  85. package/src/components/primitives/badge.tsx +7 -11
  86. package/src/components/primitives/button.tsx +41 -13
  87. package/src/components/primitives/card.stories.tsx +2 -2
  88. package/src/components/primitives/card.tsx +2 -2
  89. package/src/components/primitives/index.ts +2 -0
  90. package/src/components/primitives/label.stories.tsx +1 -1
  91. package/src/components/primitives/label.tsx +5 -6
  92. package/src/components/primitives/status-badge.stories.tsx +52 -0
  93. package/src/components/primitives/status-badge.tsx +33 -0
  94. package/src/constants/chart-colors.ts +5 -5
  95. package/src/hooks/index.ts +1 -0
  96. package/src/hooks/use-sort.ts +20 -0
  97. package/src/index.ts +30 -11
  98. package/src/styles.css +76 -1040
  99. package/src/tokens/color-systems.stories.tsx +9 -119
  100. package/src/tokens/foundations.stories.tsx +21 -113
  101. package/src/tokens/generated/dark.colors.generated.ts +56 -0
  102. package/src/tokens/generated/dark.css +54 -0
  103. package/src/tokens/generated/foundations.css +148 -0
  104. package/src/tokens/generated/foundations.generated.ts +443 -0
  105. package/src/tokens/generated/light.colors.generated.ts +56 -0
  106. package/src/tokens/generated/light.css +54 -0
  107. package/src/tokens/index.ts +74 -16
  108. package/src/tokens/layout.stories.tsx +3 -108
  109. package/src/tokens/colors.ts +0 -156
  110. package/src/tokens/motion.ts +0 -51
  111. package/src/tokens/radii.ts +0 -10
  112. package/src/tokens/shadows.ts +0 -34
  113. package/src/tokens/spacing.ts +0 -30
  114. package/src/tokens/typography.ts +0 -42
@@ -1,38 +1,39 @@
1
1
  "use client";
2
2
 
3
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
3
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
4
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
4
5
  import { Circle } from "lucide-react";
5
6
  import * as React from "react";
6
7
 
7
8
  import { cn } from "../../lib/utils";
8
9
 
9
10
  const RadioGroup = React.forwardRef<
10
- React.ComponentRef<typeof RadioGroupPrimitive.Root>,
11
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
11
+ React.ComponentRef<typeof RadioGroupPrimitive>,
12
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive>
12
13
  >(({ className, ...props }, ref) => {
13
- return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
14
+ return <RadioGroupPrimitive className={cn("grid gap-2", className)} {...props} ref={ref} />;
14
15
  });
15
- RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
16
+ RadioGroup.displayName = "RadioGroup";
16
17
 
17
18
  const RadioGroupItem = React.forwardRef<
18
- React.ComponentRef<typeof RadioGroupPrimitive.Item>,
19
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
19
+ React.ComponentRef<typeof RadioPrimitive.Root>,
20
+ React.ComponentPropsWithoutRef<typeof RadioPrimitive.Root>
20
21
  >(({ className, ...props }, ref) => {
21
22
  return (
22
- <RadioGroupPrimitive.Item
23
+ <RadioPrimitive.Root
23
24
  ref={ref}
24
25
  className={cn(
25
- "border-accent bg-canvas text-primary focus-visible:border-foreground focus-visible:bg-accent/25 focus-visible:ring-focus aspect-square h-6 w-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
26
+ "border-primary bg-background text-foreground focus-visible:border-ring focus-visible:bg-accent/25 focus-visible:ring-ring aspect-square h-6 w-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
26
27
  className,
27
28
  )}
28
29
  {...props}
29
30
  >
30
- <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
31
+ <RadioPrimitive.Indicator className="flex items-center justify-center">
31
32
  <Circle className="fill-primary h-4 w-4" />
32
- </RadioGroupPrimitive.Indicator>
33
- </RadioGroupPrimitive.Item>
33
+ </RadioPrimitive.Indicator>
34
+ </RadioPrimitive.Root>
34
35
  );
35
36
  });
36
- RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
37
+ RadioGroupItem.displayName = "Radio";
37
38
 
38
39
  export { RadioGroup, RadioGroupItem };
@@ -22,7 +22,7 @@ type Story = StoryObj<typeof meta>;
22
22
  export const Default: Story = {
23
23
  render: () => (
24
24
  <Select>
25
- <SelectTrigger className="w-[180px]">
25
+ <SelectTrigger className="w-[180px]" aria-label="Select a fruit">
26
26
  <SelectValue placeholder="Select a fruit" />
27
27
  </SelectTrigger>
28
28
  <SelectContent>
@@ -39,7 +39,7 @@ export const Default: Story = {
39
39
  export const WithGroups: Story = {
40
40
  render: () => (
41
41
  <Select>
42
- <SelectTrigger className="w-[200px]">
42
+ <SelectTrigger className="w-[200px]" aria-label="Select timezone">
43
43
  <SelectValue placeholder="Select timezone" />
44
44
  </SelectTrigger>
45
45
  <SelectContent>
@@ -62,7 +62,7 @@ export const WithGroups: Story = {
62
62
  export const Disabled: Story = {
63
63
  render: () => (
64
64
  <Select disabled>
65
- <SelectTrigger className="w-[180px]">
65
+ <SelectTrigger className="w-[180px]" aria-label="Select a fruit">
66
66
  <SelectValue placeholder="Select a fruit" />
67
67
  </SelectTrigger>
68
68
  <SelectContent>
@@ -75,7 +75,7 @@ export const Disabled: Story = {
75
75
  export const Small: Story = {
76
76
  render: () => (
77
77
  <Select>
78
- <SelectTrigger size="sm" className="w-[160px]">
78
+ <SelectTrigger size="sm" className="w-[160px]" aria-label="Select an option">
79
79
  <SelectValue placeholder="Select" />
80
80
  </SelectTrigger>
81
81
  <SelectContent>
@@ -1,11 +1,25 @@
1
- import * as SelectPrimitive from "@radix-ui/react-select";
1
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
2
2
  import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from "../../lib/utils";
6
6
 
7
- function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
- return <SelectPrimitive.Root data-slot="select" {...props} />;
7
+ type SelectProps = Omit<React.ComponentProps<typeof SelectPrimitive.Root<string>>, "onValueChange" | "value" | "defaultValue"> & {
8
+ value?: string;
9
+ defaultValue?: string;
10
+ onValueChange?: (value: string) => void;
11
+ };
12
+
13
+ function Select({ onValueChange, ...props }: SelectProps) {
14
+ return (
15
+ <SelectPrimitive.Root
16
+ data-slot="select"
17
+ {...props}
18
+ onValueChange={(value) => {
19
+ if (value !== null) onValueChange?.(value);
20
+ }}
21
+ />
22
+ );
9
23
  }
10
24
 
11
25
  function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
@@ -21,21 +35,19 @@ function SelectTrigger({
21
35
  size = "default",
22
36
  children,
23
37
  ...props
24
- }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
25
- size?: "sm" | "default";
26
- }) {
38
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size?: "sm" | "default" }) {
27
39
  return (
28
40
  <SelectPrimitive.Trigger
29
41
  data-slot="select-trigger"
30
42
  data-size={size}
31
43
  className={cn(
32
- "data-placeholder:text-secondary [&_svg:not([class*='text-'])]:text-secondary focus-visible:border-focus focus-visible:ring-focus/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
44
+ "data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
33
45
  className,
34
46
  )}
35
47
  {...props}
36
48
  >
37
49
  {children}
38
- <SelectPrimitive.Icon asChild>
50
+ <SelectPrimitive.Icon>
39
51
  <ChevronDownIcon className="size-4 opacity-50" />
40
52
  </SelectPrimitive.Icon>
41
53
  </SelectPrimitive.Trigger>
@@ -45,34 +57,27 @@ function SelectTrigger({
45
57
  function SelectContent({
46
58
  className,
47
59
  children,
48
- position = "item-aligned",
60
+ position: _position = "item-aligned",
49
61
  ...props
50
- }: React.ComponentProps<typeof SelectPrimitive.Content>) {
62
+ }: React.ComponentProps<typeof SelectPrimitive.Popup> & {
63
+ position?: "item-aligned" | "popper";
64
+ }) {
51
65
  return (
52
66
  <SelectPrimitive.Portal>
53
- <SelectPrimitive.Content
54
- data-slot="select-content"
55
- className={cn(
56
- "bg-raised text-primary data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border",
57
- position === "popper" &&
58
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
59
- className,
60
- )}
61
- position={position}
62
- {...props}
63
- >
64
- <SelectScrollUpButton />
65
- <SelectPrimitive.Viewport
67
+ <SelectPrimitive.Positioner sideOffset={4}>
68
+ <SelectPrimitive.Popup
69
+ data-slot="select-content"
66
70
  className={cn(
67
- "p-1",
68
- position === "popper" &&
69
- "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
71
+ "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
72
+ className,
70
73
  )}
74
+ {...props}
71
75
  >
72
- {children}
73
- </SelectPrimitive.Viewport>
74
- <SelectScrollDownButton />
75
- </SelectPrimitive.Content>
76
+ <SelectScrollUpButton />
77
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
78
+ <SelectScrollDownButton />
79
+ </SelectPrimitive.Popup>
80
+ </SelectPrimitive.Positioner>
76
81
  </SelectPrimitive.Portal>
77
82
  );
78
83
  }
@@ -81,30 +86,23 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
81
86
  return (
82
87
  <SelectPrimitive.Label
83
88
  data-slot="select-label"
84
- className={cn("text-secondary px-2 py-1.5 text-xs", className)}
89
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
85
90
  {...props}
86
91
  />
87
92
  );
88
93
  }
89
94
 
90
- function SelectItem({
91
- className,
92
- children,
93
- ...props
94
- }: React.ComponentProps<typeof SelectPrimitive.Item>) {
95
+ function SelectItem({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
95
96
  return (
96
97
  <SelectPrimitive.Item
97
98
  data-slot="select-item"
98
99
  className={cn(
99
- "focus:bg-accent focus:text-on-accent [&_svg:not([class*='text-'])]:text-secondary relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
100
+ "data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50",
100
101
  className,
101
102
  )}
102
103
  {...props}
103
104
  >
104
- <span
105
- data-slot="select-item-indicator"
106
- className="absolute right-2 flex size-3.5 items-center justify-center"
107
- >
105
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
108
106
  <SelectPrimitive.ItemIndicator>
109
107
  <CheckIcon className="size-4" />
110
108
  </SelectPrimitive.ItemIndicator>
@@ -114,33 +112,27 @@ function SelectItem({
114
112
  );
115
113
  }
116
114
 
117
- function SelectScrollUpButton({
118
- className,
119
- ...props
120
- }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
115
+ function SelectScrollUpButton({ className, ...props }: React.ComponentProps<"div">) {
121
116
  return (
122
- <SelectPrimitive.ScrollUpButton
117
+ <SelectPrimitive.ScrollUpArrow
123
118
  data-slot="select-scroll-up-button"
124
119
  className={cn("flex cursor-default items-center justify-center py-1", className)}
125
120
  {...props}
126
121
  >
127
122
  <ChevronUpIcon className="size-4" />
128
- </SelectPrimitive.ScrollUpButton>
123
+ </SelectPrimitive.ScrollUpArrow>
129
124
  );
130
125
  }
131
126
 
132
- function SelectScrollDownButton({
133
- className,
134
- ...props
135
- }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
127
+ function SelectScrollDownButton({ className, ...props }: React.ComponentProps<"div">) {
136
128
  return (
137
- <SelectPrimitive.ScrollDownButton
129
+ <SelectPrimitive.ScrollDownArrow
138
130
  data-slot="select-scroll-down-button"
139
131
  className={cn("flex cursor-default items-center justify-center py-1", className)}
140
132
  {...props}
141
133
  >
142
134
  <ChevronDownIcon className="size-4" />
143
- </SelectPrimitive.ScrollDownButton>
135
+ </SelectPrimitive.ScrollDownArrow>
144
136
  );
145
137
  }
146
138
 
@@ -30,6 +30,7 @@ export const Default: Story = {
30
30
  defaultValue: [50],
31
31
  min: 0,
32
32
  max: 100,
33
+ "aria-label": "Volume",
33
34
  },
34
35
  };
35
36
 
@@ -39,6 +40,7 @@ export const WithStep: Story = {
39
40
  min: 0,
40
41
  max: 100,
41
42
  step: 10,
43
+ "aria-label": "Volume",
42
44
  },
43
45
  };
44
46
 
@@ -47,9 +49,9 @@ export const Controlled: Story = {
47
49
  const [value, setValue] = React.useState([33]);
48
50
  return (
49
51
  <div className="w-full max-w-sm space-y-4 px-1">
50
- <Slider value={value} onValueChange={setValue} min={0} max={100} />
52
+ <Slider aria-label="Volume" value={value} onValueChange={setValue} min={0} max={100} />
51
53
  <div className="flex justify-between text-sm">
52
- <span className="text-secondary">Volume</span>
54
+ <span className="text-muted-foreground">Volume</span>
53
55
  <span className="font-medium tabular-nums">{value[0]}%</span>
54
56
  </div>
55
57
  </div>
@@ -63,5 +65,6 @@ export const Disabled: Story = {
63
65
  disabled: true,
64
66
  min: 0,
65
67
  max: 100,
68
+ "aria-label": "Volume",
66
69
  },
67
70
  };
@@ -1,25 +1,35 @@
1
1
  "use client";
2
2
 
3
- import * as SliderPrimitive from "@radix-ui/react-slider";
3
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
4
4
  import * as React from "react";
5
5
 
6
6
  import { cn } from "../../lib/utils";
7
7
 
8
- const Slider = React.forwardRef<
9
- React.ComponentRef<typeof SliderPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
11
- >(({ className, ...props }, ref) => (
12
- <SliderPrimitive.Root
13
- ref={ref}
14
- className={cn("relative flex w-full touch-none items-center select-none", className)}
15
- {...props}
16
- >
17
- <SliderPrimitive.Track className="bg-panel/40 relative h-2 w-full grow overflow-hidden rounded-full">
18
- <SliderPrimitive.Range className="bg-accent absolute h-full" />
19
- </SliderPrimitive.Track>
20
- <SliderPrimitive.Thumb className="border-default focus-visible:border-foreground focus-visible:bg-accent focus-visible:ring-focus block h-6 w-6 rounded-full border bg-white shadow-sm ring-1 ring-black/5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
21
- </SliderPrimitive.Root>
22
- ));
23
- Slider.displayName = SliderPrimitive.Root.displayName;
8
+ type SliderProps = Omit<React.ComponentProps<typeof SliderPrimitive.Root>, "onValueChange" | "value" | "defaultValue"> & {
9
+ value?: number[];
10
+ defaultValue?: number[];
11
+ onValueChange?: (value: number[]) => void;
12
+ };
13
+
14
+ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
15
+ ({ className, onValueChange, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }, ref) => (
16
+ <SliderPrimitive.Root
17
+ ref={ref}
18
+ className={cn("relative flex w-full touch-none items-center select-none", className)}
19
+ {...props}
20
+ onValueChange={(value) => onValueChange?.(Array.isArray(value) ? [...value] : [value])}
21
+ >
22
+ <SliderPrimitive.Track className="bg-muted relative h-2 w-full grow overflow-hidden rounded-full">
23
+ <SliderPrimitive.Indicator className="bg-accent absolute h-full" />
24
+ </SliderPrimitive.Track>
25
+ <SliderPrimitive.Thumb
26
+ aria-label={ariaLabel ?? "Slider"}
27
+ aria-labelledby={ariaLabelledby}
28
+ className="border-border focus-visible:border-ring focus-visible:bg-accent focus-visible:ring-ring block h-6 w-6 rounded-full border bg-background shadow-sm ring-1 ring-black/5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
29
+ />
30
+ </SliderPrimitive.Root>
31
+ ),
32
+ );
33
+ Slider.displayName = "Slider";
24
34
 
25
35
  export { Slider };
@@ -32,19 +32,19 @@ function Stepper({
32
32
  const atMax = value >= max;
33
33
 
34
34
  return (
35
- <div className={cn("border-default bg-inset flex items-center rounded-lg border", className)}>
35
+ <div className={cn("flex items-center rounded-lg border border-input bg-muted", className)}>
36
36
  <Button
37
37
  type="button"
38
38
  variant="ghost"
39
39
  onClick={() => onChange(Math.max(min, value - step))}
40
40
  disabled={disabled || atMin}
41
- className="border-default text-secondary rounded-none rounded-l-lg border-r"
41
+ className="text-muted-foreground rounded-none rounded-l-lg border-r"
42
42
  aria-label={decreaseLabel}
43
43
  >
44
44
  <Minus className="size-3.5" />
45
45
  </Button>
46
46
  <div className="flex min-w-[3ch] flex-1 items-center justify-center px-3">
47
- <span className="text-primary font-['Geist'] text-sm font-semibold tabular-nums">
47
+ <span className="text-foreground text-sm font-semibold tabular-nums">
48
48
  {format ? format(value) : value}
49
49
  </span>
50
50
  </div>
@@ -53,7 +53,7 @@ function Stepper({
53
53
  variant="ghost"
54
54
  onClick={() => onChange(Math.min(max, value + step))}
55
55
  disabled={disabled || atMax}
56
- className="border-default text-secondary rounded-none rounded-r-lg border-l"
56
+ className="text-muted-foreground rounded-none rounded-r-lg border-l"
57
57
  aria-label={increaseLabel}
58
58
  >
59
59
  <Plus className="size-3.5" />
@@ -49,7 +49,7 @@ function SwitchPreview({
49
49
  onCheckedChange={setCurrentChecked}
50
50
  size={size}
51
51
  />
52
- <label htmlFor={id} className="text-primary text-sm leading-none font-medium">
52
+ <label htmlFor={id} className="text-foreground text-sm leading-none font-medium">
53
53
  {label}
54
54
  </label>
55
55
  </div>
@@ -1,4 +1,4 @@
1
- import * as SwitchPrimitive from "@radix-ui/react-switch";
1
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
2
  import * as React from "react";
3
3
 
4
4
  import { cn } from "../../lib/utils";
@@ -15,7 +15,7 @@ function Switch({
15
15
  data-slot="switch"
16
16
  data-size={size}
17
17
  className={cn(
18
- "peer data-[state=checked]:bg-accent data-[state=unchecked]:bg-inset focus-visible:ring-focus inline-flex shrink-0 items-center rounded-full border border-transparent p-px focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:justify-end data-[state=unchecked]:justify-start",
18
+ "peer data-checked:bg-primary data-unchecked:bg-input focus-visible:ring-ring inline-flex shrink-0 items-center rounded-full border border-transparent p-px focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-checked:justify-end data-unchecked:justify-start",
19
19
  size === "default" ? "h-[1.15rem] w-8" : "h-3.5 w-6",
20
20
  className,
21
21
  )}
@@ -22,7 +22,7 @@ function TextareaField({
22
22
 
23
23
  return (
24
24
  <div className="grid gap-2">
25
- <label htmlFor={id} className="text-primary text-sm leading-none font-medium">
25
+ <label htmlFor={id} className="text-foreground text-sm leading-none font-medium">
26
26
  {label}
27
27
  </label>
28
28
  <Textarea
@@ -32,7 +32,7 @@ function TextareaField({
32
32
  {...props}
33
33
  />
34
34
  {helpText ? (
35
- <p id={helpId} className="text-secondary text-sm">
35
+ <p id={helpId} className="text-muted-foreground text-sm">
36
36
  {helpText}
37
37
  </p>
38
38
  ) : null}
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "placeholder:text-secondary aria-invalid:ring-destructive/20 aria-invalid:border-destructive focus-visible:ring-focus flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
10
+ "placeholder:text-muted-foreground aria-invalid:ring-destructive/20 aria-invalid:border-destructive focus-visible:ring-ring flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
11
  className,
12
12
  )}
13
13
  {...props}
@@ -1,2 +1,5 @@
1
1
  export { ScrollArea } from "./scroll-area";
2
2
  export type { ScrollAreaProps } from "./scroll-area";
3
+ export { SectionIntro } from "./section-intro";
4
+ export type { SectionIntroProps } from "./section-intro";
5
+ export { SurfacePanel } from "./surface-panel";
@@ -25,7 +25,7 @@ export const Default: Story = {
25
25
  className="flex size-40 shrink-0 items-center justify-center rounded-lg"
26
26
  style={{ backgroundColor: item.bg }}
27
27
  >
28
- <span className="text-primary font-medium">{item.label}</span>
28
+ <span className="text-foreground font-medium">{item.label}</span>
29
29
  </div>
30
30
  )),
31
31
  },
@@ -41,7 +41,7 @@ export const WithSnap: Story = {
41
41
  className="flex size-48 shrink-0 items-center justify-center rounded-lg"
42
42
  style={{ backgroundColor: item.bg }}
43
43
  >
44
- <span className="text-primary font-medium">{item.label}</span>
44
+ <span className="text-foreground font-medium">{item.label}</span>
45
45
  </div>
46
46
  )),
47
47
  },
@@ -57,7 +57,7 @@ export const Vertical: Story = {
57
57
  className="flex h-24 shrink-0 items-center justify-center rounded-lg"
58
58
  style={{ backgroundColor: item.bg }}
59
59
  >
60
- <span className="text-primary font-medium">{item.label}</span>
60
+ <span className="text-foreground font-medium">{item.label}</span>
61
61
  </div>
62
62
  )),
63
63
  },
@@ -74,7 +74,7 @@ export const ScreenshotsGallery: Story = {
74
74
  className="flex aspect-video w-72 shrink-0 items-center justify-center overflow-hidden rounded-xl border"
75
75
  style={{ backgroundColor: item.bg }}
76
76
  >
77
- <span className="text-primary/70 text-sm">{item.label}</span>
77
+ <span className="text-foreground/70 text-sm">{item.label}</span>
78
78
  </div>
79
79
  ))}
80
80
  </ScrollArea>
@@ -1,7 +1,9 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
1
2
  import * as React from "react";
3
+
2
4
  import { cn } from "../../lib/utils";
3
5
 
4
- interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ interface ScrollAreaProps extends React.ComponentProps<typeof ScrollAreaPrimitive.Root> {
5
7
  orientation?: "horizontal" | "vertical";
6
8
  snap?: "start" | "center" | "none";
7
9
  }
@@ -9,31 +11,31 @@ interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
9
11
  const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
10
12
  ({ className, children, orientation = "horizontal", snap = "none", ...props }, ref) => {
11
13
  return (
12
- <div
13
- ref={ref}
14
- className={cn(
15
- "scrollbar-hide flex overflow-auto",
16
- orientation === "horizontal" && "flex-row",
17
- orientation === "vertical" && "flex-col",
18
- snap !== "none" && orientation === "horizontal" && "snap-x snap-mandatory",
19
- snap !== "none" && orientation === "vertical" && "snap-y snap-mandatory",
20
- className,
21
- )}
22
- {...props}
23
- >
24
- {React.Children.map(children, (child) => {
25
- if (!React.isValidElement(child)) return child;
26
- if (snap !== "none") {
14
+ <ScrollAreaPrimitive.Root ref={ref} className={cn("relative", className)} {...props}>
15
+ <ScrollAreaPrimitive.Viewport
16
+ className={cn(
17
+ "scrollbar-hide flex overflow-auto",
18
+ orientation === "horizontal" ? "flex-row" : "flex-col",
19
+ snap !== "none" && (orientation === "horizontal" ? "snap-x" : "snap-y"),
20
+ snap !== "none" && "snap-mandatory",
21
+ )}
22
+ >
23
+ {React.Children.map(children, (child) => {
24
+ if (!React.isValidElement(child)) return child;
27
25
  return React.cloneElement(child as React.ReactElement<{ className?: string }>, {
28
- className: cn(`snap-${snap}`, (child.props as { className?: string }).className),
26
+ className: cn(
27
+ snap !== "none" && orientation === "horizontal" && `snap-${snap}`,
28
+ snap !== "none" && orientation === "vertical" && `snap-${snap}`,
29
+ (child.props as { className?: string }).className,
30
+ ),
29
31
  });
30
- }
31
- return child;
32
- })}
33
- </div>
32
+ })}
33
+ </ScrollAreaPrimitive.Viewport>
34
+ </ScrollAreaPrimitive.Root>
34
35
  );
35
36
  },
36
37
  );
38
+
37
39
  ScrollArea.displayName = "ScrollArea";
38
40
 
39
41
  export { ScrollArea };
@@ -0,0 +1,29 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { Button } from "../primitives/button";
4
+ import { SectionIntro } from "./section-intro";
5
+
6
+ const meta = {
7
+ title: "Layout/SectionIntro",
8
+ component: SectionIntro,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof SectionIntro>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ title: "Accounts",
18
+ description: "Connect and manage the accounts you track.",
19
+ },
20
+ };
21
+
22
+ export const WithEyebrowAndActions: Story = {
23
+ args: {
24
+ eyebrow: "Settings",
25
+ title: "Security",
26
+ description: "Manage how you sign in and keep your account secure.",
27
+ actions: <Button variant="outline">Add passkey</Button>,
28
+ },
29
+ };
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface SectionIntroProps {
4
+ eyebrow?: ReactNode;
5
+ title: ReactNode;
6
+ description?: ReactNode;
7
+ actions?: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Page-level heading composition: eyebrow label, title, description, and an
12
+ * optional trailing actions slot.
13
+ */
14
+ export function SectionIntro({ eyebrow, title, description, actions }: SectionIntroProps) {
15
+ return (
16
+ <div className="flex items-start justify-between gap-6">
17
+ <div className="min-w-0 flex-1">
18
+ {eyebrow ? (
19
+ <div className="text-muted-foreground mb-3 text-[11px] font-medium tracking-[0.18em] uppercase">
20
+ {eyebrow}
21
+ </div>
22
+ ) : null}
23
+ <div className="space-y-3">
24
+ <h1 className="text-title-2 text-foreground font-semibold tracking-tight">{title}</h1>
25
+ {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
26
+ </div>
27
+ </div>
28
+ {actions ? <div className="shrink-0">{actions}</div> : null}
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { SurfacePanel } from "./surface-panel";
4
+
5
+ const meta = {
6
+ title: "Layout/SurfacePanel",
7
+ component: SurfacePanel,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof SurfacePanel>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <div className="w-80">
17
+ <SurfacePanel>
18
+ <p className="text-muted-foreground text-sm">Panel content goes here.</p>
19
+ </SurfacePanel>
20
+ </div>
21
+ ),
22
+ };
23
+
24
+ export const AsSection: Story = {
25
+ render: () => (
26
+ <div className="w-80">
27
+ <SurfacePanel as="section" aria-label="Example panel">
28
+ <p className="text-muted-foreground text-sm">Rendered as a semantic &lt;section&gt;.</p>
29
+ </SurfacePanel>
30
+ </div>
31
+ ),
32
+ };