@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.
- package/LICENSE +21 -0
- package/README.md +7 -7
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/dist/index.js +988 -640
- package/dist/index.js.map +1 -1
- package/package.json +41 -41
- package/src/animations.css +42 -383
- package/src/components/data-display/accordion.tsx +22 -10
- package/src/components/data-display/filter-chip.stories.tsx +38 -0
- package/src/components/data-display/filter-chip.tsx +10 -4
- package/src/components/data-display/index.ts +5 -0
- package/src/components/data-display/metric-card.stories.tsx +48 -0
- package/src/components/data-display/metric-card.tsx +20 -0
- package/src/components/data-display/sort-controls.stories.tsx +45 -0
- package/src/components/data-display/sort-controls.tsx +99 -0
- package/src/components/data-display/sort-row.tsx +74 -0
- package/src/components/data-display/table.tsx +5 -9
- package/src/components/feedback/alert.tsx +4 -4
- package/src/components/feedback/empty-state.tsx +16 -6
- package/src/components/feedback/file-upload-status-badge.stories.tsx +28 -0
- package/src/components/feedback/file-upload-status-badge.tsx +33 -0
- package/src/components/feedback/file-upload-status.stories.tsx +57 -0
- package/src/components/feedback/file-upload-status.tsx +152 -0
- package/src/components/feedback/index.ts +9 -0
- package/src/components/feedback/progress-bar.stories.tsx +39 -0
- package/src/components/feedback/progress-bar.tsx +21 -0
- package/src/components/feedback/progress.stories.tsx +8 -6
- package/src/components/feedback/progress.tsx +3 -3
- package/src/components/feedback/skeleton.stories.tsx +36 -0
- package/src/components/feedback/skeleton.tsx +7 -0
- package/src/components/feedback/spinner.tsx +2 -2
- package/src/components/feedback/update-guard.stories.tsx +32 -0
- package/src/components/feedback/update-guard.tsx +280 -0
- package/src/components/forms/calendar.tsx +14 -11
- package/src/components/forms/date-month-select.stories.tsx +26 -0
- package/src/components/forms/date-month-select.tsx +70 -0
- package/src/components/forms/date-picker.tsx +3 -3
- package/src/components/forms/drop-zone.stories.tsx +56 -0
- package/src/components/forms/drop-zone.tsx +146 -0
- package/src/components/forms/entity-select.stories.tsx +65 -0
- package/src/components/forms/entity-select.tsx +79 -0
- package/src/components/forms/field.tsx +3 -3
- package/src/components/forms/group-by-select.stories.tsx +26 -0
- package/src/components/forms/group-by-select.tsx +47 -0
- package/src/components/forms/index.ts +10 -0
- package/src/components/forms/input.stories.tsx +4 -4
- package/src/components/forms/input.tsx +1 -1
- package/src/components/forms/passkey-management.stories.tsx +81 -0
- package/src/components/forms/passkey-management.tsx +138 -0
- package/src/components/forms/radio-group.tsx +14 -13
- package/src/components/forms/select.stories.tsx +4 -4
- package/src/components/forms/select.tsx +47 -48
- package/src/components/forms/slider.stories.tsx +5 -2
- package/src/components/forms/slider.tsx +39 -17
- package/src/components/forms/stepper.tsx +4 -4
- package/src/components/forms/switch.stories.tsx +1 -1
- package/src/components/forms/switch.tsx +2 -2
- package/src/components/forms/textarea.stories.tsx +2 -2
- package/src/components/forms/textarea.tsx +1 -1
- package/src/components/layout/index.ts +3 -0
- package/src/components/layout/scroll-area.stories.tsx +4 -4
- package/src/components/layout/scroll-area.tsx +23 -21
- package/src/components/layout/section-intro.stories.tsx +29 -0
- package/src/components/layout/section-intro.tsx +31 -0
- package/src/components/layout/surface-panel.stories.tsx +32 -0
- package/src/components/layout/surface-panel.tsx +15 -0
- package/src/components/navigation/app-navigation.stories.tsx +2 -2
- package/src/components/navigation/app-navigation.tsx +13 -13
- package/src/components/navigation/index.ts +4 -0
- package/src/components/navigation/pagination-controls.stories.tsx +41 -0
- package/src/components/navigation/pagination-controls.tsx +50 -0
- package/src/components/navigation/route-link.stories.tsx +30 -0
- package/src/components/navigation/route-link.tsx +42 -0
- package/src/components/navigation/tabs.stories.tsx +5 -5
- package/src/components/navigation/tabs.tsx +8 -8
- package/src/components/overlays/alert-dialog.tsx +22 -25
- package/src/components/overlays/command.tsx +98 -26
- package/src/components/overlays/dialog.tsx +5 -5
- package/src/components/overlays/dropdown-menu.stories.tsx +0 -5
- package/src/components/overlays/dropdown-menu.tsx +49 -31
- package/src/components/overlays/popover.stories.tsx +2 -2
- package/src/components/overlays/popover.tsx +43 -14
- package/src/components/overlays/sheet.stories.tsx +2 -2
- package/src/components/overlays/sheet.tsx +51 -17
- package/src/components/primitives/avatar.tsx +9 -5
- package/src/components/primitives/badge.tsx +11 -12
- package/src/components/primitives/button.tsx +49 -23
- package/src/components/primitives/card.stories.tsx +2 -2
- package/src/components/primitives/card.tsx +2 -5
- package/src/components/primitives/index.ts +2 -0
- package/src/components/primitives/label.stories.tsx +1 -1
- package/src/components/primitives/label.tsx +5 -6
- package/src/components/primitives/status-badge.stories.tsx +52 -0
- package/src/components/primitives/status-badge.tsx +33 -0
- package/src/constants/chart-colors.ts +7 -7
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-sort.ts +20 -0
- package/src/index.ts +30 -11
- package/src/styles.css +100 -974
- package/src/tokens/chart-colors.stories.tsx +192 -0
- package/src/tokens/color-systems.stories.tsx +20 -112
- package/src/tokens/foundations.stories.tsx +58 -100
- package/src/tokens/generated/dark.colors.generated.ts +29 -0
- package/src/tokens/generated/dark.css +29 -0
- package/src/tokens/generated/foundations.css +148 -0
- package/src/tokens/generated/foundations.generated.ts +451 -0
- package/src/tokens/generated/light.colors.generated.ts +29 -0
- package/src/tokens/generated/light.css +29 -0
- package/src/tokens/index.ts +79 -16
- package/src/tokens/layout.stories.tsx +23 -91
- package/src/tokens/colors.ts +0 -156
- package/src/tokens/motion.ts +0 -51
- package/src/tokens/radii.ts +0 -10
- package/src/tokens/shadows.ts +0 -34
- package/src/tokens/spacing.ts +0 -30
- 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
|
|
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
|
|
11
|
-
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive
|
|
11
|
+
React.ComponentRef<typeof RadioGroupPrimitive>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive>
|
|
12
13
|
>(({ className, ...props }, ref) => {
|
|
13
|
-
return <RadioGroupPrimitive
|
|
14
|
+
return <RadioGroupPrimitive className={cn("grid gap-2", className)} {...props} ref={ref} />;
|
|
14
15
|
});
|
|
15
|
-
RadioGroup.displayName =
|
|
16
|
+
RadioGroup.displayName = "RadioGroup";
|
|
16
17
|
|
|
17
18
|
const RadioGroupItem = React.forwardRef<
|
|
18
|
-
React.ComponentRef<typeof
|
|
19
|
-
React.ComponentPropsWithoutRef<typeof
|
|
19
|
+
React.ComponentRef<typeof RadioPrimitive.Root>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof RadioPrimitive.Root>
|
|
20
21
|
>(({ className, ...props }, ref) => {
|
|
21
22
|
return (
|
|
22
|
-
<
|
|
23
|
+
<RadioPrimitive.Root
|
|
23
24
|
ref={ref}
|
|
24
25
|
className={cn(
|
|
25
|
-
"border-
|
|
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
|
-
<
|
|
31
|
+
<RadioPrimitive.Indicator className="flex items-center justify-center">
|
|
31
32
|
<Circle className="fill-primary h-4 w-4" />
|
|
32
|
-
</
|
|
33
|
-
</
|
|
33
|
+
</RadioPrimitive.Indicator>
|
|
34
|
+
</RadioPrimitive.Root>
|
|
34
35
|
);
|
|
35
36
|
});
|
|
36
|
-
RadioGroupItem.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
|
|
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
|
-
|
|
8
|
-
|
|
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-
|
|
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
|
|
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.
|
|
65
|
+
}: React.ComponentProps<typeof SelectPrimitive.Popup> & {
|
|
66
|
+
position?: "item-aligned" | "popper";
|
|
67
|
+
}) {
|
|
51
68
|
return (
|
|
52
69
|
<SelectPrimitive.Portal>
|
|
53
|
-
<SelectPrimitive.
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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-
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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
|
|
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
|
-
|
|
9
|
-
React.
|
|
10
|
-
|
|
11
|
-
>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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-
|
|
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="
|
|
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-
|
|
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="
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
35
|
+
<p id={helpId} className="text-muted-foreground text-sm">
|
|
36
36
|
{helpText}
|
|
37
37
|
</p>
|
|
38
38
|
) : null}
|