@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
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aleph-front/ds",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/",
|
|
8
|
+
"!src/**/*.test.tsx",
|
|
9
|
+
"!src/**/*.test.ts"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/aleph-im/aleph-cloud-ds.git",
|
|
15
|
+
"directory": "packages/ds"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
"./button": "./src/components/button/button.tsx",
|
|
19
|
+
"./checkbox": "./src/components/checkbox/checkbox.tsx",
|
|
20
|
+
"./input": "./src/components/input/input.tsx",
|
|
21
|
+
"./textarea": "./src/components/textarea/textarea.tsx",
|
|
22
|
+
"./select": "./src/components/select/select.tsx",
|
|
23
|
+
"./switch": "./src/components/switch/switch.tsx",
|
|
24
|
+
"./form-field": "./src/components/form-field/form-field.tsx",
|
|
25
|
+
"./badge": "./src/components/badge/badge.tsx",
|
|
26
|
+
"./card": "./src/components/card/card.tsx",
|
|
27
|
+
"./status-dot": "./src/components/status-dot/status-dot.tsx",
|
|
28
|
+
"./table": "./src/components/table/table.tsx",
|
|
29
|
+
"./tooltip": "./src/components/tooltip/tooltip.tsx",
|
|
30
|
+
"./radio-group": "./src/components/radio-group/radio-group.tsx",
|
|
31
|
+
"./ui/skeleton": "./src/components/ui/skeleton.tsx",
|
|
32
|
+
"./ui/spinner": "./src/components/ui/spinner.tsx",
|
|
33
|
+
"./lib/cn": "./src/lib/cn.ts",
|
|
34
|
+
"./styles/tokens.css": "./src/styles/tokens.css"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"lint": "oxlint --import-plugin src/",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"check": "pnpm lint && pnpm typecheck && pnpm test"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"react-dom": "^19.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"class-variance-authority": "0.7.1",
|
|
48
|
+
"clsx": "2.1.1",
|
|
49
|
+
"radix-ui": "1.4.3",
|
|
50
|
+
"tailwind-merge": "3.5.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@testing-library/jest-dom": "6.6.3",
|
|
54
|
+
"@testing-library/react": "16.3.2",
|
|
55
|
+
"@testing-library/user-event": "14.6.1",
|
|
56
|
+
"@types/jsdom": "28.0.0",
|
|
57
|
+
"@types/react": "19.2.14",
|
|
58
|
+
"@types/react-dom": "19.2.3",
|
|
59
|
+
"@vitejs/plugin-react": "5.1.4",
|
|
60
|
+
"jsdom": "28.1.0",
|
|
61
|
+
"oxlint": "1.50.0",
|
|
62
|
+
"react": "19.2.4",
|
|
63
|
+
"react-dom": "19.2.4",
|
|
64
|
+
"typescript": "5.9.3",
|
|
65
|
+
"vitest": "4.0.18"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 badgeVariants = cva(
|
|
6
|
+
[
|
|
7
|
+
"inline-flex items-center justify-center",
|
|
8
|
+
"rounded font-sans font-semibold",
|
|
9
|
+
"whitespace-nowrap select-none",
|
|
10
|
+
].join(" "),
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default: [
|
|
15
|
+
"bg-primary-100 text-primary-700",
|
|
16
|
+
"dark:bg-primary-900 dark:text-primary-300",
|
|
17
|
+
].join(" "),
|
|
18
|
+
success: [
|
|
19
|
+
"bg-success-100 text-success-700",
|
|
20
|
+
"dark:bg-success-900 dark:text-success-300",
|
|
21
|
+
].join(" "),
|
|
22
|
+
warning: [
|
|
23
|
+
"bg-warning-100 text-warning-800",
|
|
24
|
+
"dark:bg-warning-900 dark:text-warning-200",
|
|
25
|
+
].join(" "),
|
|
26
|
+
error: [
|
|
27
|
+
"bg-error-100 text-error-700",
|
|
28
|
+
"dark:bg-error-900 dark:text-error-300",
|
|
29
|
+
].join(" "),
|
|
30
|
+
info: [
|
|
31
|
+
"bg-neutral-100 text-neutral-700",
|
|
32
|
+
"dark:bg-neutral-800 dark:text-neutral-300",
|
|
33
|
+
].join(" "),
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
sm: "px-2 py-0.5 text-xs",
|
|
37
|
+
md: "px-2.5 py-0.5 text-sm",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
defaultVariants: {
|
|
41
|
+
variant: "default",
|
|
42
|
+
size: "md",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
type BadgeProps = HTMLAttributes<HTMLSpanElement> &
|
|
48
|
+
VariantProps<typeof badgeVariants>;
|
|
49
|
+
|
|
50
|
+
const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
51
|
+
({ variant, size, className, ...rest }, ref) => {
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn(badgeVariants({ variant, size }), className)}
|
|
56
|
+
{...rest}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
Badge.displayName = "Badge";
|
|
63
|
+
|
|
64
|
+
export { Badge, badgeVariants, type BadgeProps };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
forwardRef,
|
|
4
|
+
isValidElement,
|
|
5
|
+
type ButtonHTMLAttributes,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
10
|
+
import { cn } from "@ac/lib/cn";
|
|
11
|
+
import { Spinner } from "@ac/components/ui/spinner";
|
|
12
|
+
|
|
13
|
+
const buttonVariants = cva(
|
|
14
|
+
[
|
|
15
|
+
"inline-flex items-center justify-center",
|
|
16
|
+
"font-heading font-bold",
|
|
17
|
+
"rounded-full border-3 transition-colors",
|
|
18
|
+
"focus-visible:outline-none focus-visible:ring-2",
|
|
19
|
+
"focus-visible:ring-primary-400 focus-visible:ring-offset-2",
|
|
20
|
+
"disabled:pointer-events-none",
|
|
21
|
+
].join(" "),
|
|
22
|
+
{
|
|
23
|
+
variants: {
|
|
24
|
+
variant: {
|
|
25
|
+
primary: [
|
|
26
|
+
"gradient-fill-main text-white border-transparent",
|
|
27
|
+
"disabled:opacity-50",
|
|
28
|
+
].join(" "),
|
|
29
|
+
secondary: [
|
|
30
|
+
"gradient-fill-lime text-neutral-950 border-neutral-950",
|
|
31
|
+
"disabled:opacity-50",
|
|
32
|
+
].join(" "),
|
|
33
|
+
outline: [
|
|
34
|
+
"border-gradient-main text-primary-700",
|
|
35
|
+
"hover:text-primary-800",
|
|
36
|
+
"active:text-primary-800",
|
|
37
|
+
"disabled:opacity-50",
|
|
38
|
+
].join(" "),
|
|
39
|
+
text: [
|
|
40
|
+
"bg-transparent text-primary-600 dark:text-primary-300 border-transparent",
|
|
41
|
+
"hover:bg-primary-100 hover:text-primary-700",
|
|
42
|
+
"dark:hover:bg-primary-800 dark:hover:text-primary-200",
|
|
43
|
+
"active:bg-primary-200 active:text-primary-800",
|
|
44
|
+
"dark:active:bg-primary-700 dark:active:text-primary-100",
|
|
45
|
+
"disabled:bg-transparent disabled:text-primary-600/50",
|
|
46
|
+
"dark:disabled:text-primary-300/50",
|
|
47
|
+
].join(" "),
|
|
48
|
+
destructive: [
|
|
49
|
+
"bg-error-600/20 text-error-700 dark:text-error-300 border-error-600",
|
|
50
|
+
"hover:bg-error-600/30 hover:border-error-700",
|
|
51
|
+
"active:bg-error-600/40 active:border-error-800",
|
|
52
|
+
"disabled:bg-error-600/10 disabled:text-error-700/50 disabled:border-error-600/50",
|
|
53
|
+
].join(" "),
|
|
54
|
+
warning: [
|
|
55
|
+
"bg-warning-500/20 text-warning-800 dark:text-warning-200 border-warning-500",
|
|
56
|
+
"hover:bg-warning-500/30 hover:border-warning-600",
|
|
57
|
+
"active:bg-warning-500/40 active:border-warning-700",
|
|
58
|
+
"disabled:bg-warning-500/10 disabled:text-warning-800/50 disabled:border-warning-500/50",
|
|
59
|
+
].join(" "),
|
|
60
|
+
},
|
|
61
|
+
size: {
|
|
62
|
+
xs: "py-1 px-4 text-sm gap-1",
|
|
63
|
+
sm: "py-1.5 px-5 text-base gap-1.5",
|
|
64
|
+
md: "py-2 px-6 text-base gap-2",
|
|
65
|
+
lg: "py-2.5 px-8 text-lg gap-2",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
defaultVariants: {
|
|
69
|
+
variant: "primary",
|
|
70
|
+
size: "md",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const iconSize = {
|
|
76
|
+
xs: "size-3.5",
|
|
77
|
+
sm: "size-4",
|
|
78
|
+
md: "size-4",
|
|
79
|
+
lg: "size-5",
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
|
83
|
+
VariantProps<typeof buttonVariants> & {
|
|
84
|
+
iconLeft?: ReactNode;
|
|
85
|
+
iconRight?: ReactNode;
|
|
86
|
+
loading?: boolean;
|
|
87
|
+
asChild?: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
91
|
+
(
|
|
92
|
+
{
|
|
93
|
+
variant,
|
|
94
|
+
size,
|
|
95
|
+
iconLeft,
|
|
96
|
+
iconRight,
|
|
97
|
+
loading = false,
|
|
98
|
+
disabled = false,
|
|
99
|
+
asChild = false,
|
|
100
|
+
className,
|
|
101
|
+
children,
|
|
102
|
+
...rest
|
|
103
|
+
},
|
|
104
|
+
ref,
|
|
105
|
+
) => {
|
|
106
|
+
const sizeKey = size ?? "md";
|
|
107
|
+
const classes = cn(
|
|
108
|
+
buttonVariants({ variant, size }),
|
|
109
|
+
loading && "pointer-events-none",
|
|
110
|
+
className,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const iconClass = cn("shrink-0", iconSize[sizeKey], "[&>svg]:size-full");
|
|
114
|
+
|
|
115
|
+
const content = (
|
|
116
|
+
<>
|
|
117
|
+
{loading ? (
|
|
118
|
+
<Spinner className={cn("shrink-0", iconSize[sizeKey])} />
|
|
119
|
+
) : iconLeft ? (
|
|
120
|
+
<span className={iconClass}>{iconLeft}</span>
|
|
121
|
+
) : null}
|
|
122
|
+
<span>{children}</span>
|
|
123
|
+
{!loading && iconRight ? (
|
|
124
|
+
<span className={iconClass}>{iconRight}</span>
|
|
125
|
+
) : null}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (asChild && isValidElement(children)) {
|
|
130
|
+
return cloneElement(children as ReactElement<Record<string, unknown>>, {
|
|
131
|
+
className: classes,
|
|
132
|
+
ref,
|
|
133
|
+
...rest,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<button
|
|
139
|
+
ref={ref}
|
|
140
|
+
className={classes}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
aria-busy={loading || undefined}
|
|
143
|
+
{...rest}
|
|
144
|
+
>
|
|
145
|
+
{content}
|
|
146
|
+
</button>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
Button.displayName = "Button";
|
|
152
|
+
|
|
153
|
+
export { Button, buttonVariants, type ButtonProps };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const cardVariants = cva("rounded-xl", {
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default: "bg-surface text-surface-foreground border border-edge",
|
|
9
|
+
noise: "card-noise text-surface-foreground",
|
|
10
|
+
ghost: "bg-transparent",
|
|
11
|
+
},
|
|
12
|
+
padding: {
|
|
13
|
+
sm: "p-4",
|
|
14
|
+
md: "p-6",
|
|
15
|
+
lg: "p-8",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default",
|
|
20
|
+
padding: "md",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type CardProps = HTMLAttributes<HTMLDivElement> &
|
|
25
|
+
VariantProps<typeof cardVariants> & {
|
|
26
|
+
title?: ReactNode;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
30
|
+
({ variant, padding, title, className, children, ...rest }, ref) => {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(cardVariants({ variant, padding }), className)}
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
{title ? (
|
|
38
|
+
<h3 className="mb-4 text-lg font-heading font-bold">{title}</h3>
|
|
39
|
+
) : null}
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
Card.displayName = "Card";
|
|
47
|
+
|
|
48
|
+
export { Card, cardVariants, type CardProps };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { cn } from "@ac/lib/cn";
|
|
5
|
+
|
|
6
|
+
const checkboxVariants = cva(
|
|
7
|
+
[
|
|
8
|
+
"peer shrink-0 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]:bg-primary data-[state=checked]:border-primary",
|
|
15
|
+
"data-[state=checked]:text-primary-foreground",
|
|
16
|
+
"transition-colors",
|
|
17
|
+
].join(" "),
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
xs: "size-4 rounded",
|
|
22
|
+
sm: "size-6 rounded-md",
|
|
23
|
+
md: "size-7 rounded-md",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
size: "md",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
type CheckboxProps = Omit<
|
|
33
|
+
ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
|
34
|
+
"size"
|
|
35
|
+
> &
|
|
36
|
+
VariantProps<typeof checkboxVariants> & {
|
|
37
|
+
error?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
41
|
+
({ size, error = false, className, ...rest }, ref) => {
|
|
42
|
+
return (
|
|
43
|
+
<CheckboxPrimitive.Root
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
checkboxVariants({ size }),
|
|
47
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
aria-invalid={error || undefined}
|
|
51
|
+
{...rest}
|
|
52
|
+
>
|
|
53
|
+
<CheckboxPrimitive.Indicator
|
|
54
|
+
forceMount
|
|
55
|
+
className={cn(
|
|
56
|
+
"flex size-full items-center justify-center text-current",
|
|
57
|
+
"[clip-path:circle(0%_at_0%_75%)]",
|
|
58
|
+
"data-[state=checked]:[clip-path:circle(100%_at_50%_50%)]",
|
|
59
|
+
"transition-[clip-path] duration-200 ease-in-out motion-reduce:transition-none",
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<svg
|
|
63
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
64
|
+
viewBox="0 0 24 24"
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth={3}
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
className="size-[65%]"
|
|
71
|
+
>
|
|
72
|
+
<polyline points="20 6 9 17 4 12" />
|
|
73
|
+
</svg>
|
|
74
|
+
</CheckboxPrimitive.Indicator>
|
|
75
|
+
</CheckboxPrimitive.Root>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
Checkbox.displayName = "Checkbox";
|
|
81
|
+
|
|
82
|
+
export { Checkbox, checkboxVariants, type CheckboxProps };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
isValidElement,
|
|
4
|
+
useId,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { cn } from "@ac/lib/cn";
|
|
9
|
+
|
|
10
|
+
type FormFieldProps = {
|
|
11
|
+
label: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
helperText?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function FormField({
|
|
20
|
+
label,
|
|
21
|
+
required = false,
|
|
22
|
+
helperText,
|
|
23
|
+
error,
|
|
24
|
+
children,
|
|
25
|
+
className,
|
|
26
|
+
}: FormFieldProps) {
|
|
27
|
+
const id = useId();
|
|
28
|
+
const inputId = `${id}-input`;
|
|
29
|
+
const messageId = `${id}-message`;
|
|
30
|
+
const hasMessage = Boolean(error ?? helperText);
|
|
31
|
+
|
|
32
|
+
const hasError = Boolean(error);
|
|
33
|
+
|
|
34
|
+
const child = isValidElement(children)
|
|
35
|
+
? cloneElement(children as ReactElement<Record<string, unknown>>, {
|
|
36
|
+
id: inputId,
|
|
37
|
+
...(hasMessage ? { "aria-describedby": messageId } : {}),
|
|
38
|
+
...(hasError ? { error: true, "aria-invalid": true } : {}),
|
|
39
|
+
})
|
|
40
|
+
: children;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={cn("flex flex-col gap-1.5", className)}>
|
|
44
|
+
<label
|
|
45
|
+
htmlFor={inputId}
|
|
46
|
+
className="text-sm font-medium text-foreground"
|
|
47
|
+
>
|
|
48
|
+
{label}
|
|
49
|
+
{required && (
|
|
50
|
+
<span className="text-error-600 ml-0.5" aria-hidden="true">
|
|
51
|
+
*
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</label>
|
|
55
|
+
{child}
|
|
56
|
+
{error ? (
|
|
57
|
+
<p id={messageId} role="alert" className="text-xs text-error-600">
|
|
58
|
+
{error}
|
|
59
|
+
</p>
|
|
60
|
+
) : helperText ? (
|
|
61
|
+
<p id={messageId} className="text-xs text-muted-foreground">
|
|
62
|
+
{helperText}
|
|
63
|
+
</p>
|
|
64
|
+
) : null}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
FormField.displayName = "FormField";
|
|
70
|
+
|
|
71
|
+
export { FormField, type FormFieldProps };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { forwardRef, type InputHTMLAttributes } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "@ac/lib/cn";
|
|
4
|
+
|
|
5
|
+
const inputVariants = cva(
|
|
6
|
+
[
|
|
7
|
+
"w-full font-sans text-foreground bg-surface dark:bg-base-800",
|
|
8
|
+
"border-0 shadow-brand rounded-full",
|
|
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 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 InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> &
|
|
29
|
+
VariantProps<typeof inputVariants> & {
|
|
30
|
+
error?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
34
|
+
({ size, error = false, className, ...rest }, ref) => {
|
|
35
|
+
return (
|
|
36
|
+
<input
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
inputVariants({ size }),
|
|
40
|
+
error && "border-3 border-error-400 hover:border-error-500",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
aria-invalid={error || undefined}
|
|
44
|
+
{...rest}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Input.displayName = "Input";
|
|
51
|
+
|
|
52
|
+
export { Input, inputVariants, type InputProps };
|
|
@@ -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-6",
|
|
22
|
+
md: "size-7",
|
|
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-[65%] 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
|
+
};
|