@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.
@@ -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
+ }
package/src/lib/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }