@aleph-front/ds 0.0.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 +67 -0
- package/src/components/badge/badge.tsx +64 -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/form-field/form-field.tsx +71 -0
- package/src/components/input/input.tsx +52 -0
- package/src/components/radio-group/radio-group.tsx +87 -0
- package/src/components/select/select.tsx +145 -0
- package/src/components/status-dot/status-dot.tsx +53 -0
- package/src/components/switch/switch.tsx +74 -0
- package/src/components/table/table.tsx +208 -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 +267 -0
|
@@ -0,0 +1,145 @@
|
|
|
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 { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
const triggerVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"inline-flex items-center justify-between",
|
|
9
|
+
"w-full font-sans text-foreground bg-surface dark:bg-base-800",
|
|
10
|
+
"border-0 shadow-brand rounded-full",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
12
|
+
"focus-visible:ring-primary-500",
|
|
13
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
14
|
+
"ring-0 transition-[color,box-shadow]",
|
|
15
|
+
"data-[placeholder]:text-muted-foreground",
|
|
16
|
+
].join(" "),
|
|
17
|
+
{
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: "py-1.5 px-4 text-sm",
|
|
21
|
+
md: "py-2 px-5 text-base",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
size: "md",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
type SelectOption = {
|
|
31
|
+
value: string;
|
|
32
|
+
label: string;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type SelectProps = Omit<
|
|
37
|
+
ComponentPropsWithoutRef<typeof SelectPrimitive.Root>,
|
|
38
|
+
"children"
|
|
39
|
+
> &
|
|
40
|
+
VariantProps<typeof triggerVariants> & {
|
|
41
|
+
options: SelectOption[];
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
error?: boolean;
|
|
44
|
+
className?: string;
|
|
45
|
+
id?: string;
|
|
46
|
+
"aria-describedby"?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
options,
|
|
53
|
+
placeholder,
|
|
54
|
+
size,
|
|
55
|
+
error = false,
|
|
56
|
+
className,
|
|
57
|
+
id,
|
|
58
|
+
"aria-describedby": ariaDescribedBy,
|
|
59
|
+
...rest
|
|
60
|
+
},
|
|
61
|
+
ref,
|
|
62
|
+
) => {
|
|
63
|
+
return (
|
|
64
|
+
<SelectPrimitive.Root {...rest}>
|
|
65
|
+
<SelectPrimitive.Trigger
|
|
66
|
+
ref={ref}
|
|
67
|
+
id={id}
|
|
68
|
+
aria-describedby={ariaDescribedBy}
|
|
69
|
+
aria-invalid={error || undefined}
|
|
70
|
+
className={cn(
|
|
71
|
+
triggerVariants({ size }),
|
|
72
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<SelectPrimitive.Value placeholder={placeholder} />
|
|
77
|
+
<SelectPrimitive.Icon className="ml-2 shrink-0 text-muted-foreground">
|
|
78
|
+
<svg
|
|
79
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
strokeWidth={2}
|
|
84
|
+
strokeLinecap="round"
|
|
85
|
+
strokeLinejoin="round"
|
|
86
|
+
className="size-4"
|
|
87
|
+
>
|
|
88
|
+
<polyline points="6 9 12 15 18 9" />
|
|
89
|
+
</svg>
|
|
90
|
+
</SelectPrimitive.Icon>
|
|
91
|
+
</SelectPrimitive.Trigger>
|
|
92
|
+
<SelectPrimitive.Portal>
|
|
93
|
+
<SelectPrimitive.Content
|
|
94
|
+
className={cn(
|
|
95
|
+
"z-50 overflow-hidden rounded-2xl",
|
|
96
|
+
"bg-surface border border-edge shadow-brand",
|
|
97
|
+
)}
|
|
98
|
+
position="popper"
|
|
99
|
+
sideOffset={4}
|
|
100
|
+
>
|
|
101
|
+
<SelectPrimitive.Viewport className="p-1">
|
|
102
|
+
{options.map((option) => (
|
|
103
|
+
<SelectPrimitive.Item
|
|
104
|
+
key={option.value}
|
|
105
|
+
value={option.value}
|
|
106
|
+
disabled={option.disabled ?? false}
|
|
107
|
+
className={cn(
|
|
108
|
+
"relative flex items-center rounded-xl px-4 py-2",
|
|
109
|
+
"text-sm text-foreground cursor-pointer select-none",
|
|
110
|
+
"outline-none",
|
|
111
|
+
"data-[highlighted]:bg-muted",
|
|
112
|
+
"data-[disabled]:opacity-50",
|
|
113
|
+
"data-[disabled]:pointer-events-none",
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<SelectPrimitive.ItemText>
|
|
117
|
+
{option.label}
|
|
118
|
+
</SelectPrimitive.ItemText>
|
|
119
|
+
<SelectPrimitive.ItemIndicator className="ml-auto">
|
|
120
|
+
<svg
|
|
121
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
fill="none"
|
|
124
|
+
stroke="currentColor"
|
|
125
|
+
strokeWidth={2.5}
|
|
126
|
+
strokeLinecap="round"
|
|
127
|
+
strokeLinejoin="round"
|
|
128
|
+
className="size-4"
|
|
129
|
+
>
|
|
130
|
+
<polyline points="20 6 9 17 4 12" />
|
|
131
|
+
</svg>
|
|
132
|
+
</SelectPrimitive.ItemIndicator>
|
|
133
|
+
</SelectPrimitive.Item>
|
|
134
|
+
))}
|
|
135
|
+
</SelectPrimitive.Viewport>
|
|
136
|
+
</SelectPrimitive.Content>
|
|
137
|
+
</SelectPrimitive.Portal>
|
|
138
|
+
</SelectPrimitive.Root>
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
Select.displayName = "Select";
|
|
144
|
+
|
|
145
|
+
export { Select, triggerVariants, type SelectProps, type SelectOption };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const statusDotVariants = cva("inline-block rounded-full shrink-0", {
|
|
6
|
+
variants: {
|
|
7
|
+
status: {
|
|
8
|
+
healthy: "bg-success-500 animate-pulse motion-reduce:animate-none",
|
|
9
|
+
degraded: "bg-warning-500",
|
|
10
|
+
error: "bg-error-500",
|
|
11
|
+
offline: "bg-neutral-400",
|
|
12
|
+
unknown: "bg-neutral-300",
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
sm: "size-2",
|
|
16
|
+
md: "size-3",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
status: "unknown",
|
|
21
|
+
size: "md",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
type StatusDotProps = HTMLAttributes<HTMLSpanElement> &
|
|
26
|
+
VariantProps<typeof statusDotVariants>;
|
|
27
|
+
|
|
28
|
+
const statusLabels: Record<NonNullable<StatusDotProps["status"]>, string> = {
|
|
29
|
+
healthy: "Healthy",
|
|
30
|
+
degraded: "Degraded",
|
|
31
|
+
error: "Error",
|
|
32
|
+
offline: "Offline",
|
|
33
|
+
unknown: "Unknown",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const StatusDot = forwardRef<HTMLSpanElement, StatusDotProps>(
|
|
37
|
+
({ status, size, className, ...rest }, ref) => {
|
|
38
|
+
const resolvedStatus = status ?? "unknown";
|
|
39
|
+
return (
|
|
40
|
+
<span
|
|
41
|
+
ref={ref}
|
|
42
|
+
role="status"
|
|
43
|
+
aria-label={statusLabels[resolvedStatus]}
|
|
44
|
+
className={cn(statusDotVariants({ status, size }), className)}
|
|
45
|
+
{...rest}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
StatusDot.displayName = "StatusDot";
|
|
52
|
+
|
|
53
|
+
export { StatusDot, statusDotVariants, type StatusDotProps };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Switch as SwitchPrimitive } from "radix-ui";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
const switchVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"peer inline-flex shrink-0 cursor-pointer",
|
|
9
|
+
"items-center rounded-full",
|
|
10
|
+
"border-3 border-edge bg-muted",
|
|
11
|
+
"hover:border-edge-hover",
|
|
12
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
13
|
+
"focus-visible:ring-primary-500",
|
|
14
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
15
|
+
"data-[state=checked]:bg-primary data-[state=checked]:border-primary",
|
|
16
|
+
"transition-colors",
|
|
17
|
+
].join(" "),
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
xs: "h-5 w-9",
|
|
22
|
+
sm: "h-[26px] w-12",
|
|
23
|
+
md: "h-8 w-[60px]",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
size: "md",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const thumbVariants = cva(
|
|
33
|
+
[
|
|
34
|
+
"pointer-events-none block rounded-full bg-white",
|
|
35
|
+
"shadow-sm transition-transform motion-reduce:transition-none",
|
|
36
|
+
"data-[state=unchecked]:translate-x-0.5",
|
|
37
|
+
].join(" "),
|
|
38
|
+
{
|
|
39
|
+
variants: {
|
|
40
|
+
size: {
|
|
41
|
+
xs: "size-3 data-[state=checked]:translate-x-[18px]",
|
|
42
|
+
sm: "size-[18px] data-[state=checked]:translate-x-[24px]",
|
|
43
|
+
md: "size-6 data-[state=checked]:translate-x-[30px]",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
size: "md",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
type SwitchProps = Omit<
|
|
53
|
+
ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>,
|
|
54
|
+
"size"
|
|
55
|
+
> &
|
|
56
|
+
VariantProps<typeof switchVariants>;
|
|
57
|
+
|
|
58
|
+
const Switch = forwardRef<HTMLButtonElement, SwitchProps>(
|
|
59
|
+
({ size, className, ...rest }, ref) => {
|
|
60
|
+
return (
|
|
61
|
+
<SwitchPrimitive.Root
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn(switchVariants({ size }), className)}
|
|
64
|
+
{...rest}
|
|
65
|
+
>
|
|
66
|
+
<SwitchPrimitive.Thumb className={thumbVariants({ size })} />
|
|
67
|
+
</SwitchPrimitive.Root>
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
Switch.displayName = "Switch";
|
|
73
|
+
|
|
74
|
+
export { Switch, switchVariants, type SwitchProps };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type KeyboardEvent, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
type SortDirection = "asc" | "desc";
|
|
7
|
+
|
|
8
|
+
export type Column<T> = {
|
|
9
|
+
header: string;
|
|
10
|
+
accessor: (row: T) => ReactNode;
|
|
11
|
+
sortable?: boolean;
|
|
12
|
+
sortValue?: (row: T) => string | number;
|
|
13
|
+
width?: string;
|
|
14
|
+
align?: "left" | "center" | "right";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type TableProps<T> = {
|
|
18
|
+
columns: Column<T>[];
|
|
19
|
+
data: T[];
|
|
20
|
+
keyExtractor: (row: T) => string;
|
|
21
|
+
onRowClick?: (row: T) => void;
|
|
22
|
+
activeKey?: string | undefined;
|
|
23
|
+
emptyState?: ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function ChevronIcon({
|
|
28
|
+
direction,
|
|
29
|
+
}: {
|
|
30
|
+
direction: SortDirection | null;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<svg
|
|
34
|
+
className={cn(
|
|
35
|
+
"ml-1 inline size-3 transition-transform motion-reduce:transition-none",
|
|
36
|
+
direction === "desc" && "rotate-180",
|
|
37
|
+
direction === null && "opacity-0",
|
|
38
|
+
)}
|
|
39
|
+
fill="none"
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
strokeWidth={2.5}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
>
|
|
45
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ariaSortValue(
|
|
51
|
+
colIndex: number,
|
|
52
|
+
sortCol: number | null,
|
|
53
|
+
sortDir: SortDirection,
|
|
54
|
+
): "ascending" | "descending" | "none" {
|
|
55
|
+
if (colIndex !== sortCol) return "none";
|
|
56
|
+
return sortDir === "asc" ? "ascending" : "descending";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function Table<T>({
|
|
60
|
+
columns,
|
|
61
|
+
data,
|
|
62
|
+
keyExtractor,
|
|
63
|
+
onRowClick,
|
|
64
|
+
activeKey,
|
|
65
|
+
emptyState,
|
|
66
|
+
className,
|
|
67
|
+
}: TableProps<T>) {
|
|
68
|
+
const [sortCol, setSortCol] = useState<number | null>(null);
|
|
69
|
+
const [sortDir, setSortDir] = useState<SortDirection>("asc");
|
|
70
|
+
|
|
71
|
+
function handleSort(colIndex: number) {
|
|
72
|
+
if (sortCol === colIndex) {
|
|
73
|
+
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
|
74
|
+
} else {
|
|
75
|
+
setSortCol(colIndex);
|
|
76
|
+
setSortDir("asc");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleHeaderKeyDown(
|
|
81
|
+
e: KeyboardEvent,
|
|
82
|
+
colIndex: number,
|
|
83
|
+
) {
|
|
84
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
handleSort(colIndex);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleRowKeyDown(e: KeyboardEvent, row: T) {
|
|
91
|
+
if (e.key === "Enter") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
onRowClick?.(row);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let sortedData = data;
|
|
98
|
+
const activeCol = sortCol !== null ? columns[sortCol] : undefined;
|
|
99
|
+
if (activeCol?.sortValue) {
|
|
100
|
+
const getValue = activeCol.sortValue;
|
|
101
|
+
const dir = sortDir === "asc" ? 1 : -1;
|
|
102
|
+
sortedData = [...data].sort((a, b) => {
|
|
103
|
+
const aVal = getValue(a);
|
|
104
|
+
const bVal = getValue(b);
|
|
105
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
106
|
+
return (aVal - bVal) * dir;
|
|
107
|
+
}
|
|
108
|
+
return String(aVal).localeCompare(String(bVal)) * dir;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const alignClass = {
|
|
113
|
+
left: "text-left",
|
|
114
|
+
center: "text-center",
|
|
115
|
+
right: "text-right",
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className={cn("w-full overflow-x-auto", className)}>
|
|
120
|
+
<table className="w-full border-collapse">
|
|
121
|
+
<thead>
|
|
122
|
+
<tr className="bg-muted/50">
|
|
123
|
+
{columns.map((col, i) => (
|
|
124
|
+
<th
|
|
125
|
+
key={col.header}
|
|
126
|
+
className={cn(
|
|
127
|
+
"px-4 py-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground",
|
|
128
|
+
alignClass[col.align ?? "left"],
|
|
129
|
+
col.sortable && "cursor-pointer select-none",
|
|
130
|
+
)}
|
|
131
|
+
style={col.width ? { width: col.width } : undefined}
|
|
132
|
+
tabIndex={col.sortable ? 0 : undefined}
|
|
133
|
+
aria-sort={
|
|
134
|
+
col.sortable
|
|
135
|
+
? ariaSortValue(i, sortCol, sortDir)
|
|
136
|
+
: undefined
|
|
137
|
+
}
|
|
138
|
+
onClick={col.sortable ? () => handleSort(i) : undefined}
|
|
139
|
+
onKeyDown={
|
|
140
|
+
col.sortable
|
|
141
|
+
? (e) => handleHeaderKeyDown(e, i)
|
|
142
|
+
: undefined
|
|
143
|
+
}
|
|
144
|
+
>
|
|
145
|
+
{col.header}
|
|
146
|
+
{col.sortable && (
|
|
147
|
+
<ChevronIcon
|
|
148
|
+
direction={sortCol === i ? sortDir : null}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</th>
|
|
152
|
+
))}
|
|
153
|
+
</tr>
|
|
154
|
+
</thead>
|
|
155
|
+
<tbody>
|
|
156
|
+
{sortedData.length === 0 && emptyState ? (
|
|
157
|
+
<tr>
|
|
158
|
+
<td
|
|
159
|
+
colSpan={columns.length}
|
|
160
|
+
className="px-4 py-8 text-center text-sm text-muted-foreground"
|
|
161
|
+
>
|
|
162
|
+
{emptyState}
|
|
163
|
+
</td>
|
|
164
|
+
</tr>
|
|
165
|
+
) : (
|
|
166
|
+
sortedData.map((row) => (
|
|
167
|
+
<tr
|
|
168
|
+
key={keyExtractor(row)}
|
|
169
|
+
className={cn(
|
|
170
|
+
"border-b border-edge transition-all",
|
|
171
|
+
activeKey === keyExtractor(row)
|
|
172
|
+
? "bg-primary-600/10 shadow-[inset_3px_0_0_var(--color-primary-500)]"
|
|
173
|
+
: "even:bg-muted/30",
|
|
174
|
+
"hover:bg-muted/50",
|
|
175
|
+
onRowClick &&
|
|
176
|
+
"cursor-pointer hover:shadow-[inset_3px_0_0_var(--color-primary-500)]",
|
|
177
|
+
)}
|
|
178
|
+
aria-current={
|
|
179
|
+
activeKey === keyExtractor(row) ? "true" : undefined
|
|
180
|
+
}
|
|
181
|
+
style={{ transitionDuration: "var(--duration-fast)" }}
|
|
182
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
183
|
+
onClick={onRowClick ? () => onRowClick(row) : undefined}
|
|
184
|
+
onKeyDown={
|
|
185
|
+
onRowClick
|
|
186
|
+
? (e) => handleRowKeyDown(e, row)
|
|
187
|
+
: undefined
|
|
188
|
+
}
|
|
189
|
+
>
|
|
190
|
+
{columns.map((col) => (
|
|
191
|
+
<td
|
|
192
|
+
key={col.header}
|
|
193
|
+
className={cn(
|
|
194
|
+
"px-4 py-3 text-sm",
|
|
195
|
+
alignClass[col.align ?? "left"],
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{col.accessor(row)}
|
|
199
|
+
</td>
|
|
200
|
+
))}
|
|
201
|
+
</tr>
|
|
202
|
+
))
|
|
203
|
+
)}
|
|
204
|
+
</tbody>
|
|
205
|
+
</table>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { forwardRef, type TextareaHTMLAttributes } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const textareaVariants = cva(
|
|
6
|
+
[
|
|
7
|
+
"w-full font-sans text-foreground bg-surface dark:bg-base-800",
|
|
8
|
+
"border-0 shadow-brand rounded-2xl",
|
|
9
|
+
"placeholder:text-muted-foreground",
|
|
10
|
+
"focus-visible:outline-none focus-visible:ring-3",
|
|
11
|
+
"focus-visible:ring-primary-500",
|
|
12
|
+
"disabled:opacity-50 disabled:pointer-events-none",
|
|
13
|
+
"ring-0 resize-y transition-[color,box-shadow]",
|
|
14
|
+
].join(" "),
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: "py-1.5 px-4 text-sm",
|
|
19
|
+
md: "py-2 px-5 text-base",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
size: "md",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
type TextareaProps = Omit<
|
|
29
|
+
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
30
|
+
"size"
|
|
31
|
+
> &
|
|
32
|
+
VariantProps<typeof textareaVariants> & {
|
|
33
|
+
error?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
37
|
+
({ size, error = false, rows = 4, className, ...rest }, ref) => {
|
|
38
|
+
return (
|
|
39
|
+
<textarea
|
|
40
|
+
ref={ref}
|
|
41
|
+
rows={rows}
|
|
42
|
+
className={cn(
|
|
43
|
+
textareaVariants({ size }),
|
|
44
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
aria-invalid={error || undefined}
|
|
48
|
+
{...rest}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Textarea.displayName = "Textarea";
|
|
55
|
+
|
|
56
|
+
export { Textarea, textareaVariants, type TextareaProps };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
7
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
8
|
+
|
|
9
|
+
const TooltipContent = forwardRef<
|
|
10
|
+
HTMLDivElement,
|
|
11
|
+
ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
12
|
+
>(({ className, sideOffset = 6, ...rest }, ref) => (
|
|
13
|
+
<TooltipPrimitive.Portal>
|
|
14
|
+
<TooltipPrimitive.Content
|
|
15
|
+
ref={ref}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
[
|
|
19
|
+
"z-50 rounded-lg bg-neutral-900 dark:bg-base-700 px-3 py-1.5",
|
|
20
|
+
"text-sm text-white shadow-brand-sm",
|
|
21
|
+
"animate-in fade-in-0 zoom-in-95",
|
|
22
|
+
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
|
|
23
|
+
"data-[state=closed]:zoom-out-95",
|
|
24
|
+
"motion-reduce:animate-none",
|
|
25
|
+
].join(" "),
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...rest}
|
|
29
|
+
/>
|
|
30
|
+
</TooltipPrimitive.Portal>
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
TooltipContent.displayName = "TooltipContent";
|
|
34
|
+
|
|
35
|
+
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@ac/lib/cn";
|
|
3
|
+
|
|
4
|
+
type SkeletonProps = HTMLAttributes<HTMLDivElement>;
|
|
5
|
+
|
|
6
|
+
const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
7
|
+
({ className, ...rest }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
className={cn("animate-pulse motion-reduce:animate-none rounded-md bg-muted", className)}
|
|
13
|
+
{...rest}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
Skeleton.displayName = "Skeleton";
|
|
20
|
+
|
|
21
|
+
export { Skeleton, type SkeletonProps };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cn } from "@ac/lib/cn";
|
|
2
|
+
|
|
3
|
+
export function Spinner({ className }: { className?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
className={cn("animate-spin motion-reduce:animate-none", className)}
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
fill="none"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
aria-hidden="true"
|
|
11
|
+
>
|
|
12
|
+
<circle
|
|
13
|
+
className="opacity-25"
|
|
14
|
+
cx="12"
|
|
15
|
+
cy="12"
|
|
16
|
+
r="10"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="4"
|
|
19
|
+
/>
|
|
20
|
+
<path
|
|
21
|
+
className="opacity-75"
|
|
22
|
+
fill="currentColor"
|
|
23
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
24
|
+
/>
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|