@ponti-studios/ui 0.1.2 → 0.4.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 (116) 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 +988 -640
  5. package/dist/index.js.map +1 -1
  6. package/package.json +41 -41
  7. package/src/animations.css +42 -383
  8. package/src/components/data-display/accordion.tsx +22 -10
  9. package/src/components/data-display/filter-chip.stories.tsx +38 -0
  10. package/src/components/data-display/filter-chip.tsx +10 -4
  11. package/src/components/data-display/index.ts +5 -0
  12. package/src/components/data-display/metric-card.stories.tsx +48 -0
  13. package/src/components/data-display/metric-card.tsx +20 -0
  14. package/src/components/data-display/sort-controls.stories.tsx +45 -0
  15. package/src/components/data-display/sort-controls.tsx +99 -0
  16. package/src/components/data-display/sort-row.tsx +74 -0
  17. package/src/components/data-display/table.tsx +5 -9
  18. package/src/components/feedback/alert.tsx +4 -4
  19. package/src/components/feedback/empty-state.tsx +16 -6
  20. package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
  21. package/src/components/feedback/file-upload-status-badge.tsx +33 -0
  22. package/src/components/feedback/file-upload-status.stories.tsx +57 -0
  23. package/src/components/feedback/file-upload-status.tsx +152 -0
  24. package/src/components/feedback/index.ts +9 -0
  25. package/src/components/feedback/progress-bar.stories.tsx +39 -0
  26. package/src/components/feedback/progress-bar.tsx +21 -0
  27. package/src/components/feedback/progress.stories.tsx +8 -6
  28. package/src/components/feedback/progress.tsx +3 -3
  29. package/src/components/feedback/skeleton.stories.tsx +36 -0
  30. package/src/components/feedback/skeleton.tsx +7 -0
  31. package/src/components/feedback/spinner.tsx +2 -2
  32. package/src/components/feedback/update-guard.stories.tsx +32 -0
  33. package/src/components/feedback/update-guard.tsx +280 -0
  34. package/src/components/forms/calendar.tsx +14 -11
  35. package/src/components/forms/date-month-select.stories.tsx +26 -0
  36. package/src/components/forms/date-month-select.tsx +70 -0
  37. package/src/components/forms/date-picker.tsx +3 -3
  38. package/src/components/forms/drop-zone.stories.tsx +56 -0
  39. package/src/components/forms/drop-zone.tsx +146 -0
  40. package/src/components/forms/entity-select.stories.tsx +65 -0
  41. package/src/components/forms/entity-select.tsx +79 -0
  42. package/src/components/forms/field.tsx +3 -3
  43. package/src/components/forms/group-by-select.stories.tsx +26 -0
  44. package/src/components/forms/group-by-select.tsx +47 -0
  45. package/src/components/forms/index.ts +10 -0
  46. package/src/components/forms/input.stories.tsx +4 -4
  47. package/src/components/forms/input.tsx +1 -1
  48. package/src/components/forms/passkey-management.stories.tsx +81 -0
  49. package/src/components/forms/passkey-management.tsx +138 -0
  50. package/src/components/forms/radio-group.tsx +14 -13
  51. package/src/components/forms/select.stories.tsx +4 -4
  52. package/src/components/forms/select.tsx +47 -48
  53. package/src/components/forms/slider.stories.tsx +5 -2
  54. package/src/components/forms/slider.tsx +39 -17
  55. package/src/components/forms/stepper.tsx +4 -4
  56. package/src/components/forms/switch.stories.tsx +1 -1
  57. package/src/components/forms/switch.tsx +2 -2
  58. package/src/components/forms/textarea.stories.tsx +2 -2
  59. package/src/components/forms/textarea.tsx +1 -1
  60. package/src/components/layout/index.ts +3 -0
  61. package/src/components/layout/scroll-area.stories.tsx +4 -4
  62. package/src/components/layout/scroll-area.tsx +23 -21
  63. package/src/components/layout/section-intro.stories.tsx +29 -0
  64. package/src/components/layout/section-intro.tsx +31 -0
  65. package/src/components/layout/surface-panel.stories.tsx +32 -0
  66. package/src/components/layout/surface-panel.tsx +15 -0
  67. package/src/components/navigation/app-navigation.stories.tsx +2 -2
  68. package/src/components/navigation/app-navigation.tsx +13 -13
  69. package/src/components/navigation/index.ts +4 -0
  70. package/src/components/navigation/pagination-controls.stories.tsx +41 -0
  71. package/src/components/navigation/pagination-controls.tsx +50 -0
  72. package/src/components/navigation/route-link.stories.tsx +30 -0
  73. package/src/components/navigation/route-link.tsx +42 -0
  74. package/src/components/navigation/tabs.stories.tsx +5 -5
  75. package/src/components/navigation/tabs.tsx +8 -8
  76. package/src/components/overlays/alert-dialog.tsx +22 -25
  77. package/src/components/overlays/command.tsx +98 -26
  78. package/src/components/overlays/dialog.tsx +5 -5
  79. package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
  80. package/src/components/overlays/dropdown-menu.tsx +49 -31
  81. package/src/components/overlays/popover.stories.tsx +2 -2
  82. package/src/components/overlays/popover.tsx +43 -14
  83. package/src/components/overlays/sheet.stories.tsx +2 -2
  84. package/src/components/overlays/sheet.tsx +51 -17
  85. package/src/components/primitives/avatar.tsx +9 -5
  86. package/src/components/primitives/badge.tsx +11 -12
  87. package/src/components/primitives/button.tsx +49 -23
  88. package/src/components/primitives/card.stories.tsx +2 -2
  89. package/src/components/primitives/card.tsx +2 -5
  90. package/src/components/primitives/index.ts +2 -0
  91. package/src/components/primitives/label.stories.tsx +1 -1
  92. package/src/components/primitives/label.tsx +5 -6
  93. package/src/components/primitives/status-badge.stories.tsx +52 -0
  94. package/src/components/primitives/status-badge.tsx +33 -0
  95. package/src/constants/chart-colors.ts +7 -7
  96. package/src/hooks/index.ts +1 -0
  97. package/src/hooks/use-sort.ts +20 -0
  98. package/src/index.ts +30 -11
  99. package/src/styles.css +100 -974
  100. package/src/tokens/chart-colors.stories.tsx +192 -0
  101. package/src/tokens/color-systems.stories.tsx +20 -112
  102. package/src/tokens/foundations.stories.tsx +58 -100
  103. package/src/tokens/generated/dark.colors.generated.ts +29 -0
  104. package/src/tokens/generated/dark.css +29 -0
  105. package/src/tokens/generated/foundations.css +148 -0
  106. package/src/tokens/generated/foundations.generated.ts +451 -0
  107. package/src/tokens/generated/light.colors.generated.ts +29 -0
  108. package/src/tokens/generated/light.css +29 -0
  109. package/src/tokens/index.ts +79 -16
  110. package/src/tokens/layout.stories.tsx +23 -91
  111. package/src/tokens/colors.ts +0 -156
  112. package/src/tokens/motion.ts +0 -51
  113. package/src/tokens/radii.ts +0 -10
  114. package/src/tokens/shadows.ts +0 -34
  115. package/src/tokens/spacing.ts +0 -30
  116. package/src/tokens/typography.ts +0 -42
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+
4
+ import { PasskeyManagement, type PasskeyRecord } from "./passkey-management";
5
+
6
+ const meta = {
7
+ title: "Forms/PasskeyManagement",
8
+ component: PasskeyManagement,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof PasskeyManagement>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ const initialPasskeys: PasskeyRecord[] = [
16
+ { id: "1", name: "MacBook Pro", createdAt: "2025-01-15T00:00:00.000Z" },
17
+ { id: "2", name: "iPhone", createdAt: "2025-03-02T00:00:00.000Z" },
18
+ ];
19
+
20
+ function PasskeyManagementPreview({ withError = false }: { withError?: boolean }) {
21
+ const [passkeys, setPasskeys] = useState(initialPasskeys);
22
+
23
+ return (
24
+ <div className="w-96">
25
+ <PasskeyManagement
26
+ passkeys={passkeys}
27
+ error={withError ? "Could not delete passkey. Please try again." : null}
28
+ onAdd={async () => {
29
+ setPasskeys((prev) => [
30
+ ...prev,
31
+ {
32
+ id: String(prev.length + 1),
33
+ name: "New device",
34
+ createdAt: new Date().toISOString(),
35
+ },
36
+ ]);
37
+ return true;
38
+ }}
39
+ onDelete={async (id) => {
40
+ setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
41
+ return true;
42
+ }}
43
+ />
44
+ </div>
45
+ );
46
+ }
47
+
48
+ export const Default: Story = {
49
+ args: {
50
+ passkeys: initialPasskeys,
51
+ onAdd: async () => true,
52
+ onDelete: async () => true,
53
+ },
54
+ render: () => <PasskeyManagementPreview />,
55
+ };
56
+
57
+ export const Empty: Story = {
58
+ args: {
59
+ passkeys: [],
60
+ onAdd: async () => true,
61
+ onDelete: async () => true,
62
+ },
63
+ };
64
+
65
+ export const Loading: Story = {
66
+ args: {
67
+ isLoading: true,
68
+ onAdd: async () => true,
69
+ onDelete: async () => true,
70
+ },
71
+ };
72
+
73
+ export const WithError: Story = {
74
+ args: {
75
+ passkeys: initialPasskeys,
76
+ error: "Could not delete passkey. Please try again.",
77
+ onAdd: async () => true,
78
+ onDelete: async () => true,
79
+ },
80
+ render: () => <PasskeyManagementPreview withError />,
81
+ };
@@ -0,0 +1,138 @@
1
+ import { KeyRound, Plus, Trash2 } from "lucide-react";
2
+ import { useCallback, useState } from "react";
3
+
4
+ import { Button } from "../primitives/button";
5
+
6
+ export interface PasskeyRecord {
7
+ id: string;
8
+ name?: string;
9
+ createdAt?: string;
10
+ }
11
+
12
+ export interface PasskeyManagementProps {
13
+ passkeys?: PasskeyRecord[];
14
+ isLoading?: boolean;
15
+ error?: string | null;
16
+ onAdd: () => Promise<boolean>;
17
+ onDelete: (id: string) => Promise<boolean>;
18
+ }
19
+
20
+ /** List, add, and remove passkeys. The host app owns the WebAuthn ceremony via `onAdd`/`onDelete`. */
21
+ export function PasskeyManagement({
22
+ passkeys: passkeysProp,
23
+ isLoading = false,
24
+ error: externalError,
25
+ onAdd,
26
+ onDelete,
27
+ }: PasskeyManagementProps) {
28
+ const [adding, setAdding] = useState(false);
29
+ const [deletingId, setDeletingId] = useState<string | null>(null);
30
+ const [actionError, setActionError] = useState<string | null>(null);
31
+
32
+ const passkeys = passkeysProp ?? [];
33
+ const error = externalError ?? actionError;
34
+
35
+ const handleAdd = useCallback(async () => {
36
+ setAdding(true);
37
+ setActionError(null);
38
+ try {
39
+ const success = await onAdd();
40
+ if (!success) {
41
+ setActionError("Passkey registration was cancelled or failed.");
42
+ }
43
+ } catch {
44
+ setActionError("An error occurred during passkey registration.");
45
+ } finally {
46
+ setAdding(false);
47
+ }
48
+ }, [onAdd]);
49
+
50
+ const handleDelete = useCallback(
51
+ async (id: string) => {
52
+ setDeletingId(id);
53
+ setActionError(null);
54
+ try {
55
+ const success = await onDelete(id);
56
+ if (!success) {
57
+ throw new Error("Failed to delete passkey");
58
+ }
59
+ } catch {
60
+ setActionError("Could not delete passkey. Please try again.");
61
+ } finally {
62
+ setDeletingId(null);
63
+ }
64
+ },
65
+ [onDelete],
66
+ );
67
+
68
+ return (
69
+ <section aria-labelledby="passkey-heading" className="space-y-4">
70
+ <div className="flex items-center justify-between">
71
+ <div>
72
+ <h2 id="passkey-heading" className="text-foreground text-sm font-medium">
73
+ Passkeys
74
+ </h2>
75
+ <p className="text-muted-foreground text-sm">
76
+ Sign in without a password using biometrics or a security key.
77
+ </p>
78
+ </div>
79
+ <Button
80
+ type="button"
81
+ variant="outline"
82
+ size="sm"
83
+ onClick={() => void handleAdd()}
84
+ disabled={adding}
85
+ aria-label="Add a passkey"
86
+ >
87
+ <Plus className="size-4" aria-hidden />
88
+ {adding ? "Adding..." : "Add passkey"}
89
+ </Button>
90
+ </div>
91
+
92
+ {error ? (
93
+ <p role="alert" className="text-destructive text-sm">
94
+ {error}
95
+ </p>
96
+ ) : null}
97
+
98
+ {isLoading ? (
99
+ <p className="text-muted-foreground text-sm">Loading passkeys...</p>
100
+ ) : passkeys.length === 0 ? (
101
+ <div className="border-border text-muted-foreground flex items-center gap-3 border border-dashed p-4 text-sm">
102
+ <KeyRound className="size-4 shrink-0" aria-hidden />
103
+ <span>No passkeys registered. Add one to sign in faster.</span>
104
+ </div>
105
+ ) : (
106
+ <ul className="space-y-2" aria-label="Registered passkeys">
107
+ {passkeys.map((pk) => (
108
+ <li
109
+ key={pk.id}
110
+ className="border-border flex items-center justify-between border px-4 py-3 text-sm"
111
+ >
112
+ <div className="flex items-center gap-3">
113
+ <KeyRound className="text-muted-foreground size-4 shrink-0" aria-hidden />
114
+ <div>
115
+ <span className="font-medium">{pk.name ?? "Passkey"}</span>
116
+ {pk.createdAt ? (
117
+ <span className="text-muted-foreground ml-2 text-xs">
118
+ Added {new Date(pk.createdAt).toLocaleDateString()}
119
+ </span>
120
+ ) : null}
121
+ </div>
122
+ </div>
123
+ <button
124
+ type="button"
125
+ onClick={() => void handleDelete(pk.id)}
126
+ disabled={deletingId === pk.id}
127
+ aria-label={`Remove passkey ${pk.name ?? pk.id}`}
128
+ className="hover:text-destructive text-muted-foreground focus-visible:ring-ring rounded-sm focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
129
+ >
130
+ <Trash2 className="size-4" aria-hidden />
131
+ </button>
132
+ </li>
133
+ ))}
134
+ </ul>
135
+ )}
136
+ </section>
137
+ );
138
+ }
@@ -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-muted/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,28 @@
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<
8
+ React.ComponentProps<typeof SelectPrimitive.Root<string>>,
9
+ "onValueChange" | "value" | "defaultValue"
10
+ > & {
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onValueChange?: (value: string) => void;
14
+ };
15
+
16
+ function Select({ onValueChange, ...props }: SelectProps) {
17
+ return (
18
+ <SelectPrimitive.Root
19
+ data-slot="select"
20
+ {...props}
21
+ onValueChange={(value) => {
22
+ if (value !== null) onValueChange?.(value);
23
+ }}
24
+ />
25
+ );
9
26
  }
10
27
 
11
28
  function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
@@ -21,21 +38,19 @@ function SelectTrigger({
21
38
  size = "default",
22
39
  children,
23
40
  ...props
24
- }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
25
- size?: "sm" | "default";
26
- }) {
41
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & { size?: "sm" | "default" }) {
27
42
  return (
28
43
  <SelectPrimitive.Trigger
29
44
  data-slot="select-trigger"
30
45
  data-size={size}
31
46
  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",
47
+ "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 border-input 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",
33
48
  className,
34
49
  )}
35
50
  {...props}
36
51
  >
37
52
  {children}
38
- <SelectPrimitive.Icon asChild>
53
+ <SelectPrimitive.Icon>
39
54
  <ChevronDownIcon className="size-4 opacity-50" />
40
55
  </SelectPrimitive.Icon>
41
56
  </SelectPrimitive.Trigger>
@@ -45,34 +60,27 @@ function SelectTrigger({
45
60
  function SelectContent({
46
61
  className,
47
62
  children,
48
- position = "item-aligned",
63
+ position: _position = "item-aligned",
49
64
  ...props
50
- }: React.ComponentProps<typeof SelectPrimitive.Content>) {
65
+ }: React.ComponentProps<typeof SelectPrimitive.Popup> & {
66
+ position?: "item-aligned" | "popper";
67
+ }) {
51
68
  return (
52
69
  <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
70
+ <SelectPrimitive.Positioner sideOffset={4}>
71
+ <SelectPrimitive.Popup
72
+ data-slot="select-content"
66
73
  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",
74
+ "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",
75
+ className,
70
76
  )}
77
+ {...props}
71
78
  >
72
- {children}
73
- </SelectPrimitive.Viewport>
74
- <SelectScrollDownButton />
75
- </SelectPrimitive.Content>
79
+ <SelectScrollUpButton />
80
+ <SelectPrimitive.List>{children}</SelectPrimitive.List>
81
+ <SelectScrollDownButton />
82
+ </SelectPrimitive.Popup>
83
+ </SelectPrimitive.Positioner>
76
84
  </SelectPrimitive.Portal>
77
85
  );
78
86
  }
@@ -81,7 +89,7 @@ function SelectLabel({ className, ...props }: React.ComponentProps<typeof Select
81
89
  return (
82
90
  <SelectPrimitive.Label
83
91
  data-slot="select-label"
84
- className={cn("text-secondary px-2 py-1.5 text-xs", className)}
92
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
85
93
  {...props}
86
94
  />
87
95
  );
@@ -96,15 +104,12 @@ function SelectItem({
96
104
  <SelectPrimitive.Item
97
105
  data-slot="select-item"
98
106
  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",
107
+ "data-highlighted:bg-muted data-highlighted:text-muted-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
108
  className,
101
109
  )}
102
110
  {...props}
103
111
  >
104
- <span
105
- data-slot="select-item-indicator"
106
- className="absolute right-2 flex size-3.5 items-center justify-center"
107
- >
112
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
108
113
  <SelectPrimitive.ItemIndicator>
109
114
  <CheckIcon className="size-4" />
110
115
  </SelectPrimitive.ItemIndicator>
@@ -114,33 +119,27 @@ function SelectItem({
114
119
  );
115
120
  }
116
121
 
117
- function SelectScrollUpButton({
118
- className,
119
- ...props
120
- }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
122
+ function SelectScrollUpButton({ className, ...props }: React.ComponentProps<"div">) {
121
123
  return (
122
- <SelectPrimitive.ScrollUpButton
124
+ <SelectPrimitive.ScrollUpArrow
123
125
  data-slot="select-scroll-up-button"
124
126
  className={cn("flex cursor-default items-center justify-center py-1", className)}
125
127
  {...props}
126
128
  >
127
129
  <ChevronUpIcon className="size-4" />
128
- </SelectPrimitive.ScrollUpButton>
130
+ </SelectPrimitive.ScrollUpArrow>
129
131
  );
130
132
  }
131
133
 
132
- function SelectScrollDownButton({
133
- className,
134
- ...props
135
- }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
134
+ function SelectScrollDownButton({ className, ...props }: React.ComponentProps<"div">) {
136
135
  return (
137
- <SelectPrimitive.ScrollDownButton
136
+ <SelectPrimitive.ScrollDownArrow
138
137
  data-slot="select-scroll-down-button"
139
138
  className={cn("flex cursor-default items-center justify-center py-1", className)}
140
139
  {...props}
141
140
  >
142
141
  <ChevronDownIcon className="size-4" />
143
- </SelectPrimitive.ScrollDownButton>
142
+ </SelectPrimitive.ScrollDownArrow>
144
143
  );
145
144
  }
146
145
 
@@ -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,47 @@
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<
9
+ React.ComponentProps<typeof SliderPrimitive.Root>,
10
+ "onValueChange" | "value" | "defaultValue"
11
+ > & {
12
+ value?: number[];
13
+ defaultValue?: number[];
14
+ onValueChange?: (value: number[]) => void;
15
+ };
16
+
17
+ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
18
+ (
19
+ {
20
+ className,
21
+ onValueChange,
22
+ "aria-label": ariaLabel,
23
+ "aria-labelledby": ariaLabelledby,
24
+ ...props
25
+ },
26
+ ref,
27
+ ) => (
28
+ <SliderPrimitive.Root
29
+ ref={ref}
30
+ className={cn("relative flex w-full touch-none items-center select-none", className)}
31
+ {...props}
32
+ onValueChange={(value) => onValueChange?.(Array.isArray(value) ? [...value] : [value])}
33
+ >
34
+ <SliderPrimitive.Track className="bg-muted relative h-2 w-full grow overflow-hidden rounded-full">
35
+ <SliderPrimitive.Indicator className="bg-primary absolute h-full" />
36
+ </SliderPrimitive.Track>
37
+ <SliderPrimitive.Thumb
38
+ aria-label={ariaLabel ?? "Slider"}
39
+ aria-labelledby={ariaLabelledby}
40
+ className="border-border focus-visible:border-ring focus-visible:bg-primary focus-visible:ring-ring bg-background block h-6 w-6 rounded-full border 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"
41
+ />
42
+ </SliderPrimitive.Root>
43
+ ),
44
+ );
45
+ Slider.displayName = "Slider";
24
46
 
25
47
  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("border-input bg-muted flex items-center rounded-lg border", 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}