@stasho/ds 0.1.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/package.json +88 -0
- package/src/components/alert/alert.tsx +204 -0
- package/src/components/badge/badge.tsx +139 -0
- package/src/components/breadcrumb/breadcrumb.tsx +130 -0
- package/src/components/button/button.tsx +153 -0
- package/src/components/card/card.tsx +48 -0
- package/src/components/checkbox/checkbox.tsx +82 -0
- package/src/components/combobox/combobox.tsx +166 -0
- package/src/components/copyable-text/copyable-text.tsx +194 -0
- package/src/components/dialog/dialog.tsx +145 -0
- package/src/components/form-field/form-field.tsx +71 -0
- package/src/components/input/input.tsx +52 -0
- package/src/components/logo/logo.tsx +68 -0
- package/src/components/multi-select/multi-select.tsx +312 -0
- package/src/components/pagination/pagination.tsx +218 -0
- package/src/components/progress-bar/progress-bar.tsx +134 -0
- package/src/components/radio-group/radio-group.tsx +87 -0
- package/src/components/select/select.tsx +124 -0
- package/src/components/slider/slider.tsx +127 -0
- package/src/components/status-dot/status-dot.tsx +53 -0
- package/src/components/stepper/stepper.tsx +229 -0
- package/src/components/switch/switch.tsx +74 -0
- package/src/components/table/table.tsx +260 -0
- package/src/components/tabs/tabs.tsx +493 -0
- package/src/components/textarea/textarea.tsx +56 -0
- package/src/components/tooltip/tooltip.tsx +35 -0
- package/src/components/ui/skeleton.tsx +21 -0
- package/src/components/ui/spinner.tsx +27 -0
- package/src/lib/cn.ts +6 -0
- package/src/styles/tokens.css +459 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CaretDoubleLeft,
|
|
4
|
+
CaretDoubleRight,
|
|
5
|
+
CaretLeft,
|
|
6
|
+
CaretRight,
|
|
7
|
+
} from "@phosphor-icons/react";
|
|
8
|
+
import { cn } from "@ac/lib/cn";
|
|
9
|
+
|
|
10
|
+
type PageItem = number | "ellipsis";
|
|
11
|
+
|
|
12
|
+
type BuildPageRangeArgs = {
|
|
13
|
+
page: number;
|
|
14
|
+
totalPages: number;
|
|
15
|
+
siblingCount: number;
|
|
16
|
+
showFirstLast: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function buildPageRange({
|
|
20
|
+
page,
|
|
21
|
+
totalPages,
|
|
22
|
+
siblingCount,
|
|
23
|
+
showFirstLast,
|
|
24
|
+
}: BuildPageRangeArgs): PageItem[] {
|
|
25
|
+
if (!showFirstLast) {
|
|
26
|
+
const left = Math.max(page - siblingCount, 1);
|
|
27
|
+
const right = Math.min(page + siblingCount, totalPages);
|
|
28
|
+
const items: PageItem[] = [];
|
|
29
|
+
for (let i = left; i <= right; i++) {
|
|
30
|
+
items.push(i);
|
|
31
|
+
}
|
|
32
|
+
return items;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const maxSlots = 2 * siblingCount + 5;
|
|
36
|
+
|
|
37
|
+
if (totalPages <= maxSlots) {
|
|
38
|
+
const items: PageItem[] = [];
|
|
39
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
40
|
+
items.push(i);
|
|
41
|
+
}
|
|
42
|
+
return items;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nearStart = page <= siblingCount + 3;
|
|
46
|
+
const nearEnd = page >= totalPages - siblingCount - 2;
|
|
47
|
+
|
|
48
|
+
if (nearStart) {
|
|
49
|
+
const items: PageItem[] = [];
|
|
50
|
+
for (let i = 1; i <= maxSlots - 2; i++) {
|
|
51
|
+
items.push(i);
|
|
52
|
+
}
|
|
53
|
+
items.push("ellipsis", totalPages);
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (nearEnd) {
|
|
58
|
+
const items: PageItem[] = [1, "ellipsis"];
|
|
59
|
+
for (let i = totalPages - (maxSlots - 3); i <= totalPages; i++) {
|
|
60
|
+
items.push(i);
|
|
61
|
+
}
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const items: PageItem[] = [1, "ellipsis"];
|
|
66
|
+
for (let i = page - siblingCount; i <= page + siblingCount; i++) {
|
|
67
|
+
items.push(i);
|
|
68
|
+
}
|
|
69
|
+
items.push("ellipsis", totalPages);
|
|
70
|
+
return items;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type PaginationProps = Omit<HTMLAttributes<HTMLElement>, "onChange"> & {
|
|
74
|
+
page: number;
|
|
75
|
+
totalPages: number;
|
|
76
|
+
onPageChange: (page: number) => void;
|
|
77
|
+
siblingCount?: number;
|
|
78
|
+
showFirstLast?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const NAV_BUTTON = [
|
|
82
|
+
"inline-flex items-center justify-center",
|
|
83
|
+
"size-8 rounded-full",
|
|
84
|
+
"text-primary-600 dark:text-primary-400",
|
|
85
|
+
"hover:bg-primary-100 dark:hover:bg-primary-200/10",
|
|
86
|
+
"transition-colors cursor-pointer",
|
|
87
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500",
|
|
88
|
+
].join(" ");
|
|
89
|
+
|
|
90
|
+
const NAV_DISABLED = "opacity-50 pointer-events-none";
|
|
91
|
+
|
|
92
|
+
const PAGE_BUTTON = [
|
|
93
|
+
"inline-flex items-center justify-center",
|
|
94
|
+
"size-8 rounded-full",
|
|
95
|
+
"font-heading font-bold text-lg",
|
|
96
|
+
"text-primary-600 dark:text-primary-400",
|
|
97
|
+
"hover:bg-primary-100 dark:hover:bg-primary-200/10",
|
|
98
|
+
"transition-colors cursor-pointer",
|
|
99
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500",
|
|
100
|
+
].join(" ");
|
|
101
|
+
|
|
102
|
+
const PAGE_ACTIVE = [
|
|
103
|
+
"bg-primary-400 text-white dark:bg-primary-600 dark:text-white",
|
|
104
|
+
"hover:bg-primary-400 dark:hover:bg-primary-600",
|
|
105
|
+
].join(" ");
|
|
106
|
+
|
|
107
|
+
const Pagination = forwardRef<HTMLElement, PaginationProps>(
|
|
108
|
+
(
|
|
109
|
+
{
|
|
110
|
+
page,
|
|
111
|
+
totalPages,
|
|
112
|
+
onPageChange,
|
|
113
|
+
siblingCount = 1,
|
|
114
|
+
showFirstLast = true,
|
|
115
|
+
className,
|
|
116
|
+
...rest
|
|
117
|
+
},
|
|
118
|
+
ref,
|
|
119
|
+
) => {
|
|
120
|
+
const items = buildPageRange({
|
|
121
|
+
page,
|
|
122
|
+
totalPages,
|
|
123
|
+
siblingCount,
|
|
124
|
+
showFirstLast,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const isFirst = page <= 1;
|
|
128
|
+
const isLast = page >= totalPages;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<nav
|
|
132
|
+
ref={ref}
|
|
133
|
+
aria-label="Pagination"
|
|
134
|
+
className={cn("flex items-center gap-4", className)}
|
|
135
|
+
{...rest}
|
|
136
|
+
>
|
|
137
|
+
{showFirstLast && (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className={cn(NAV_BUTTON, isFirst && NAV_DISABLED)}
|
|
141
|
+
aria-label="First page"
|
|
142
|
+
aria-disabled={isFirst || undefined}
|
|
143
|
+
onClick={isFirst ? undefined : () => onPageChange(1)}
|
|
144
|
+
>
|
|
145
|
+
<CaretDoubleLeft
|
|
146
|
+
weight="bold"
|
|
147
|
+
className="size-4"
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
/>
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className={cn(NAV_BUTTON, isFirst && NAV_DISABLED)}
|
|
156
|
+
aria-label="Previous page"
|
|
157
|
+
aria-disabled={isFirst || undefined}
|
|
158
|
+
onClick={isFirst ? undefined : () => onPageChange(page - 1)}
|
|
159
|
+
>
|
|
160
|
+
<CaretLeft weight="bold" className="size-4" aria-hidden="true" />
|
|
161
|
+
</button>
|
|
162
|
+
|
|
163
|
+
{items.map((item, i) =>
|
|
164
|
+
item === "ellipsis" ? (
|
|
165
|
+
<span
|
|
166
|
+
key={i}
|
|
167
|
+
className="inline-flex items-center justify-center size-8 font-heading font-bold text-lg text-primary-600 dark:text-primary-400 select-none"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
>
|
|
170
|
+
{"\u2026"}
|
|
171
|
+
</span>
|
|
172
|
+
) : (
|
|
173
|
+
<button
|
|
174
|
+
key={i}
|
|
175
|
+
type="button"
|
|
176
|
+
className={cn(PAGE_BUTTON, item === page && PAGE_ACTIVE)}
|
|
177
|
+
aria-label={`Page ${item}`}
|
|
178
|
+
aria-current={item === page ? "page" : undefined}
|
|
179
|
+
onClick={item === page ? undefined : () => onPageChange(item)}
|
|
180
|
+
>
|
|
181
|
+
{item}
|
|
182
|
+
</button>
|
|
183
|
+
),
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
className={cn(NAV_BUTTON, isLast && NAV_DISABLED)}
|
|
189
|
+
aria-label="Next page"
|
|
190
|
+
aria-disabled={isLast || undefined}
|
|
191
|
+
onClick={isLast ? undefined : () => onPageChange(page + 1)}
|
|
192
|
+
>
|
|
193
|
+
<CaretRight weight="bold" className="size-4" aria-hidden="true" />
|
|
194
|
+
</button>
|
|
195
|
+
|
|
196
|
+
{showFirstLast && (
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
className={cn(NAV_BUTTON, isLast && NAV_DISABLED)}
|
|
200
|
+
aria-label="Last page"
|
|
201
|
+
aria-disabled={isLast || undefined}
|
|
202
|
+
onClick={isLast ? undefined : () => onPageChange(totalPages)}
|
|
203
|
+
>
|
|
204
|
+
<CaretDoubleRight
|
|
205
|
+
weight="bold"
|
|
206
|
+
className="size-4"
|
|
207
|
+
aria-hidden="true"
|
|
208
|
+
/>
|
|
209
|
+
</button>
|
|
210
|
+
)}
|
|
211
|
+
</nav>
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
Pagination.displayName = "Pagination";
|
|
217
|
+
|
|
218
|
+
export { buildPageRange, Pagination, type PageItem, type PaginationProps };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
cloneElement,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useId,
|
|
7
|
+
type HTMLAttributes,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
11
|
+
import { cn } from "@ac/lib/cn";
|
|
12
|
+
|
|
13
|
+
/* ── Variants ──────────────────────────────────── */
|
|
14
|
+
|
|
15
|
+
const progressBarVariants = cva(
|
|
16
|
+
"relative rounded-full bg-surface overflow-hidden",
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: "h-1",
|
|
21
|
+
md: "h-1.5",
|
|
22
|
+
lg: "h-2.5",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: { size: "md" },
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/* ── ProgressBarDescription ────────────────────── */
|
|
30
|
+
|
|
31
|
+
type ProgressBarDescriptionProps = HTMLAttributes<HTMLSpanElement>;
|
|
32
|
+
|
|
33
|
+
const ProgressBarDescription = forwardRef<
|
|
34
|
+
HTMLSpanElement,
|
|
35
|
+
ProgressBarDescriptionProps
|
|
36
|
+
>(({ className, ...rest }, ref) => (
|
|
37
|
+
<span
|
|
38
|
+
ref={ref}
|
|
39
|
+
data-description=""
|
|
40
|
+
className={cn("text-xs text-muted-foreground", className)}
|
|
41
|
+
{...rest}
|
|
42
|
+
/>
|
|
43
|
+
));
|
|
44
|
+
|
|
45
|
+
ProgressBarDescription.displayName = "ProgressBarDescription";
|
|
46
|
+
|
|
47
|
+
/* ── ProgressBar ───────────────────────────────── */
|
|
48
|
+
|
|
49
|
+
type ProgressBarProps = HTMLAttributes<HTMLDivElement> &
|
|
50
|
+
VariantProps<typeof progressBarVariants> & {
|
|
51
|
+
/** 0–max. Omit for indeterminate mode. */
|
|
52
|
+
value?: number;
|
|
53
|
+
/** Upper bound. Default 100. */
|
|
54
|
+
max?: number;
|
|
55
|
+
/** Accessible label (required). Becomes aria-label. */
|
|
56
|
+
label: string;
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
|
61
|
+
({ value, max = 100, label, size, className, children, ...rest }, ref) => {
|
|
62
|
+
const descId = useId();
|
|
63
|
+
const indeterminate = value === undefined;
|
|
64
|
+
|
|
65
|
+
const clampedPercent = indeterminate
|
|
66
|
+
? 0
|
|
67
|
+
: (Math.min(Math.max(value, 0), max) / max) * 100;
|
|
68
|
+
|
|
69
|
+
let hasDescription = false;
|
|
70
|
+
Children.forEach(children, (child) => {
|
|
71
|
+
if (isValidElement(child) && child.type === ProgressBarDescription) {
|
|
72
|
+
hasDescription = true;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const track = (
|
|
77
|
+
<div
|
|
78
|
+
ref={hasDescription ? undefined : ref}
|
|
79
|
+
role="progressbar"
|
|
80
|
+
aria-label={label}
|
|
81
|
+
aria-valuemin={indeterminate ? undefined : 0}
|
|
82
|
+
aria-valuemax={indeterminate ? undefined : max}
|
|
83
|
+
aria-valuenow={indeterminate ? undefined : Math.round(clampedPercent)}
|
|
84
|
+
aria-describedby={hasDescription ? descId : undefined}
|
|
85
|
+
className={cn(progressBarVariants({ size }), className)}
|
|
86
|
+
{...(hasDescription ? {} : rest)}
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
data-fill=""
|
|
90
|
+
data-indeterminate={indeterminate ? "" : undefined}
|
|
91
|
+
className={cn(
|
|
92
|
+
"h-full rounded-full bg-primary",
|
|
93
|
+
indeterminate
|
|
94
|
+
? "animate-progress-indeterminate"
|
|
95
|
+
: "transition-all",
|
|
96
|
+
"motion-reduce:animate-none",
|
|
97
|
+
)}
|
|
98
|
+
style={indeterminate ? undefined : { width: `${clampedPercent}%` }}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (!hasDescription) return track;
|
|
104
|
+
|
|
105
|
+
const descChildren = Children.map(children, (child) => {
|
|
106
|
+
if (
|
|
107
|
+
isValidElement<ProgressBarDescriptionProps>(child) &&
|
|
108
|
+
child.type === ProgressBarDescription
|
|
109
|
+
) {
|
|
110
|
+
return cloneElement(child, { id: descId });
|
|
111
|
+
}
|
|
112
|
+
return child;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div ref={ref} className="flex flex-col gap-1.5" {...rest}>
|
|
117
|
+
{track}
|
|
118
|
+
{descChildren}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
ProgressBar.displayName = "ProgressBar";
|
|
125
|
+
|
|
126
|
+
/* ── Exports ───────────────────────────────────── */
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
ProgressBar,
|
|
130
|
+
ProgressBarDescription,
|
|
131
|
+
progressBarVariants,
|
|
132
|
+
type ProgressBarProps,
|
|
133
|
+
type ProgressBarDescriptionProps,
|
|
134
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
const radioItemVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"peer shrink-0 rounded-full bg-surface",
|
|
9
|
+
"border-3 border-edge",
|
|
10
|
+
"hover:border-edge-hover",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
12
|
+
"focus-visible:ring-primary-500",
|
|
13
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
14
|
+
"data-[state=checked]:border-primary",
|
|
15
|
+
"transition-colors",
|
|
16
|
+
].join(" "),
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
xs: "size-4",
|
|
21
|
+
sm: "size-5",
|
|
22
|
+
md: "size-6",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
size: "md",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
type RadioGroupProps = ComponentPropsWithoutRef<
|
|
32
|
+
typeof RadioGroupPrimitive.Root
|
|
33
|
+
> & {
|
|
34
|
+
size?: "xs" | "sm" | "md";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
38
|
+
({ className, ...rest }, ref) => {
|
|
39
|
+
return (
|
|
40
|
+
<RadioGroupPrimitive.Root
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cn("flex flex-col gap-2", className)}
|
|
43
|
+
{...rest}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
RadioGroup.displayName = "RadioGroup";
|
|
49
|
+
|
|
50
|
+
type RadioGroupItemProps = Omit<
|
|
51
|
+
ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
|
|
52
|
+
"size"
|
|
53
|
+
> &
|
|
54
|
+
VariantProps<typeof radioItemVariants>;
|
|
55
|
+
|
|
56
|
+
const RadioGroupItem = forwardRef<HTMLButtonElement, RadioGroupItemProps>(
|
|
57
|
+
({ size, className, ...rest }, ref) => {
|
|
58
|
+
return (
|
|
59
|
+
<RadioGroupPrimitive.Item
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={cn(radioItemVariants({ size }), className)}
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
<RadioGroupPrimitive.Indicator
|
|
65
|
+
forceMount
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex size-full items-center justify-center",
|
|
68
|
+
"[clip-path:circle(0%_at_50%_50%)]",
|
|
69
|
+
"data-[state=checked]:[clip-path:circle(100%_at_50%_50%)]",
|
|
70
|
+
"transition-[clip-path] duration-200 ease-in-out motion-reduce:transition-none",
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span className="block size-[80%] rounded-full bg-primary" />
|
|
74
|
+
</RadioGroupPrimitive.Indicator>
|
|
75
|
+
</RadioGroupPrimitive.Item>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
RadioGroupItem.displayName = "RadioGroupItem";
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
RadioGroup,
|
|
83
|
+
RadioGroupItem,
|
|
84
|
+
radioItemVariants,
|
|
85
|
+
type RadioGroupProps,
|
|
86
|
+
type RadioGroupItemProps,
|
|
87
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Select as SelectPrimitive } from "radix-ui";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { CaretDown, Check } from "@phosphor-icons/react";
|
|
5
|
+
import { cn } from "@ac/lib/cn";
|
|
6
|
+
|
|
7
|
+
const triggerVariants = cva(
|
|
8
|
+
[
|
|
9
|
+
"inline-flex items-center justify-between",
|
|
10
|
+
"w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
|
|
11
|
+
"border-0 rounded-full",
|
|
12
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
13
|
+
"focus-visible:ring-primary-500",
|
|
14
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
15
|
+
"ring-0 transition-colors",
|
|
16
|
+
"data-[placeholder]:text-muted-foreground",
|
|
17
|
+
].join(" "),
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
sm: "py-1.5 px-4 text-sm",
|
|
22
|
+
md: "py-2 px-5 text-base",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
size: "md",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
type SelectOption = {
|
|
32
|
+
value: string;
|
|
33
|
+
label: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type SelectProps = Omit<
|
|
38
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Root>,
|
|
39
|
+
"children"
|
|
40
|
+
> &
|
|
41
|
+
VariantProps<typeof triggerVariants> & {
|
|
42
|
+
options: SelectOption[];
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
error?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
"aria-describedby"?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
options,
|
|
54
|
+
placeholder,
|
|
55
|
+
size,
|
|
56
|
+
error = false,
|
|
57
|
+
className,
|
|
58
|
+
id,
|
|
59
|
+
"aria-describedby": ariaDescribedBy,
|
|
60
|
+
...rest
|
|
61
|
+
},
|
|
62
|
+
ref,
|
|
63
|
+
) => {
|
|
64
|
+
return (
|
|
65
|
+
<SelectPrimitive.Root {...rest}>
|
|
66
|
+
<SelectPrimitive.Trigger
|
|
67
|
+
ref={ref}
|
|
68
|
+
id={id}
|
|
69
|
+
aria-describedby={ariaDescribedBy}
|
|
70
|
+
aria-invalid={error || undefined}
|
|
71
|
+
className={cn(
|
|
72
|
+
triggerVariants({ size }),
|
|
73
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<SelectPrimitive.Value placeholder={placeholder} />
|
|
78
|
+
<SelectPrimitive.Icon className="ml-2 shrink-0 text-muted-foreground">
|
|
79
|
+
<CaretDown weight="bold" className="size-4" />
|
|
80
|
+
</SelectPrimitive.Icon>
|
|
81
|
+
</SelectPrimitive.Trigger>
|
|
82
|
+
<SelectPrimitive.Portal>
|
|
83
|
+
<SelectPrimitive.Content
|
|
84
|
+
className={cn(
|
|
85
|
+
"z-50 overflow-hidden rounded-2xl",
|
|
86
|
+
"bg-surface border border-edge shadow-brand",
|
|
87
|
+
)}
|
|
88
|
+
position="popper"
|
|
89
|
+
sideOffset={4}
|
|
90
|
+
>
|
|
91
|
+
<SelectPrimitive.Viewport className="p-1 min-w-[var(--radix-select-trigger-width)]">
|
|
92
|
+
{options.map((option) => (
|
|
93
|
+
<SelectPrimitive.Item
|
|
94
|
+
key={option.value}
|
|
95
|
+
value={option.value}
|
|
96
|
+
disabled={option.disabled ?? false}
|
|
97
|
+
className={cn(
|
|
98
|
+
"relative flex items-center rounded-xl px-4 py-2",
|
|
99
|
+
"text-sm text-foreground cursor-pointer select-none",
|
|
100
|
+
"outline-none",
|
|
101
|
+
"data-[highlighted]:bg-muted",
|
|
102
|
+
"data-[disabled]:opacity-50",
|
|
103
|
+
"data-[disabled]:pointer-events-none",
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<SelectPrimitive.ItemText>
|
|
107
|
+
{option.label}
|
|
108
|
+
</SelectPrimitive.ItemText>
|
|
109
|
+
<SelectPrimitive.ItemIndicator className="ml-auto pl-4">
|
|
110
|
+
<Check weight="bold" className="size-4" />
|
|
111
|
+
</SelectPrimitive.ItemIndicator>
|
|
112
|
+
</SelectPrimitive.Item>
|
|
113
|
+
))}
|
|
114
|
+
</SelectPrimitive.Viewport>
|
|
115
|
+
</SelectPrimitive.Content>
|
|
116
|
+
</SelectPrimitive.Portal>
|
|
117
|
+
</SelectPrimitive.Root>
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
Select.displayName = "Select";
|
|
123
|
+
|
|
124
|
+
export { Select, triggerVariants, type SelectProps, type SelectOption };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { forwardRef, useState, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Slider as SliderPrimitive } from "radix-ui";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
const trackVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"relative w-full grow overflow-hidden rounded-full",
|
|
9
|
+
"bg-neutral-200 dark:bg-base-700",
|
|
10
|
+
].join(" "),
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
sm: "h-1.5",
|
|
15
|
+
md: "h-2",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
size: "md",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const thumbVariants = cva(
|
|
25
|
+
[
|
|
26
|
+
"block rounded-full bg-white",
|
|
27
|
+
"border-2 border-primary-500",
|
|
28
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
29
|
+
"focus-visible:ring-primary-500",
|
|
30
|
+
"disabled:pointer-events-none",
|
|
31
|
+
].join(" "),
|
|
32
|
+
{
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
sm: "size-4",
|
|
36
|
+
md: "size-5",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
size: "md",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
type SliderProps = Omit<
|
|
46
|
+
ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
|
|
47
|
+
"size"
|
|
48
|
+
> &
|
|
49
|
+
VariantProps<typeof trackVariants> & {
|
|
50
|
+
error?: boolean;
|
|
51
|
+
showTooltip?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const Slider = forwardRef<
|
|
55
|
+
React.ComponentRef<typeof SliderPrimitive.Root>,
|
|
56
|
+
SliderProps
|
|
57
|
+
>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
size,
|
|
61
|
+
error = false,
|
|
62
|
+
showTooltip = false,
|
|
63
|
+
className,
|
|
64
|
+
disabled,
|
|
65
|
+
onValueChange: onValueChangeProp,
|
|
66
|
+
...rootProps
|
|
67
|
+
},
|
|
68
|
+
ref,
|
|
69
|
+
) => {
|
|
70
|
+
const [hovering, setHovering] = useState(false);
|
|
71
|
+
const [internalValue, setInternalValue] = useState(
|
|
72
|
+
rootProps.defaultValue ?? rootProps.value ?? [0],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const displayValue = rootProps.value ?? internalValue;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<SliderPrimitive.Root
|
|
79
|
+
ref={ref}
|
|
80
|
+
{...(disabled ? { disabled: true } : {})}
|
|
81
|
+
className={cn(
|
|
82
|
+
"relative flex w-full touch-none select-none items-center",
|
|
83
|
+
disabled && "opacity-50 pointer-events-none",
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
onValueChange={(val) => {
|
|
87
|
+
setInternalValue(val);
|
|
88
|
+
onValueChangeProp?.(val);
|
|
89
|
+
}}
|
|
90
|
+
onPointerEnter={() => setHovering(true)}
|
|
91
|
+
onPointerLeave={() => setHovering(false)}
|
|
92
|
+
{...rootProps}
|
|
93
|
+
>
|
|
94
|
+
<SliderPrimitive.Track
|
|
95
|
+
className={cn(
|
|
96
|
+
trackVariants({ size }),
|
|
97
|
+
error && "ring-2 ring-error-400",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<SliderPrimitive.Range className="absolute h-full bg-primary-500 rounded-full" />
|
|
101
|
+
</SliderPrimitive.Track>
|
|
102
|
+
{displayValue.map((val, i) => (
|
|
103
|
+
<SliderPrimitive.Thumb
|
|
104
|
+
key={i}
|
|
105
|
+
className={cn(thumbVariants({ size }), "relative")}
|
|
106
|
+
>
|
|
107
|
+
{showTooltip && hovering && (
|
|
108
|
+
<span
|
|
109
|
+
className={cn(
|
|
110
|
+
"absolute bottom-full left-1/2 -translate-x-1/2 mb-2",
|
|
111
|
+
"rounded-md bg-neutral-900 dark:bg-base-700 px-2 py-1",
|
|
112
|
+
"text-xs text-white whitespace-nowrap pointer-events-none",
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{val}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</SliderPrimitive.Thumb>
|
|
119
|
+
))}
|
|
120
|
+
</SliderPrimitive.Root>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
Slider.displayName = "Slider";
|
|
126
|
+
|
|
127
|
+
export { Slider, trackVariants, thumbVariants, type SliderProps };
|