@peerbots/core 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.
Files changed (81) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/.github/workflows/storybook.yml +46 -0
  5. package/.storybook/main.ts +28 -0
  6. package/.storybook/preview.ts +22 -0
  7. package/README.md +9 -0
  8. package/dist/index.css +1 -0
  9. package/dist/index.d.mts +704 -0
  10. package/dist/index.d.ts +704 -0
  11. package/dist/index.js +5 -0
  12. package/dist/index.mjs +5 -0
  13. package/package.json +60 -0
  14. package/src/charts/DistributionBarChart.stories.tsx +41 -0
  15. package/src/charts/DistributionBarChart.tsx +170 -0
  16. package/src/charts/DistributionHistogram.stories.tsx +56 -0
  17. package/src/charts/DistributionHistogram.tsx +193 -0
  18. package/src/charts/index.ts +10 -0
  19. package/src/global.d.ts +1 -0
  20. package/src/helpers/SEO.tsx +41 -0
  21. package/src/index.ts +6 -0
  22. package/src/styles/theme.css +60 -0
  23. package/src/ui/Alert.stories.tsx +41 -0
  24. package/src/ui/Alert.tsx +72 -0
  25. package/src/ui/Anchor.stories.tsx +25 -0
  26. package/src/ui/Anchor.tsx +32 -0
  27. package/src/ui/AuthFormUI.stories.tsx +67 -0
  28. package/src/ui/AuthFormUI.tsx +217 -0
  29. package/src/ui/BasePanel.stories.tsx +36 -0
  30. package/src/ui/BasePanel.tsx +59 -0
  31. package/src/ui/Button.stories.tsx +108 -0
  32. package/src/ui/Button.tsx +121 -0
  33. package/src/ui/Checkbox.stories.tsx +61 -0
  34. package/src/ui/Checkbox.tsx +45 -0
  35. package/src/ui/Collapsible.stories.tsx +91 -0
  36. package/src/ui/Collapsible.tsx +52 -0
  37. package/src/ui/Colors.stories.tsx +67 -0
  38. package/src/ui/Dialog.stories.tsx +29 -0
  39. package/src/ui/Dialog.tsx +56 -0
  40. package/src/ui/Dropdown.tsx +66 -0
  41. package/src/ui/Field.stories.tsx +181 -0
  42. package/src/ui/Field.tsx +108 -0
  43. package/src/ui/Icon.stories.tsx +192 -0
  44. package/src/ui/Icon.tsx +42 -0
  45. package/src/ui/IconRegistry.tsx +189 -0
  46. package/src/ui/Input.stories.tsx +67 -0
  47. package/src/ui/Input.tsx +43 -0
  48. package/src/ui/Label.stories.tsx +42 -0
  49. package/src/ui/Label.tsx +26 -0
  50. package/src/ui/NumberField.stories.tsx +86 -0
  51. package/src/ui/NumberField.tsx +116 -0
  52. package/src/ui/Popover.tsx +42 -0
  53. package/src/ui/Select.stories.tsx +74 -0
  54. package/src/ui/Select.tsx +122 -0
  55. package/src/ui/Separator.stories.tsx +61 -0
  56. package/src/ui/Separator.tsx +28 -0
  57. package/src/ui/SettingsPanel.stories.tsx +83 -0
  58. package/src/ui/SettingsPanel.tsx +81 -0
  59. package/src/ui/Skeleton.stories.tsx +43 -0
  60. package/src/ui/Skeleton.tsx +15 -0
  61. package/src/ui/Slider.stories.tsx +140 -0
  62. package/src/ui/Slider.tsx +95 -0
  63. package/src/ui/SliderWithNumberField.stories.tsx +101 -0
  64. package/src/ui/SliderWithNumberField.tsx +88 -0
  65. package/src/ui/Switch.stories.tsx +81 -0
  66. package/src/ui/Switch.tsx +60 -0
  67. package/src/ui/TabRadio.stories.tsx +153 -0
  68. package/src/ui/TabRadio.tsx +68 -0
  69. package/src/ui/TabSelection.stories.tsx +44 -0
  70. package/src/ui/TabSelection.tsx +91 -0
  71. package/src/ui/TextArea.stories.tsx +64 -0
  72. package/src/ui/TextArea.tsx +24 -0
  73. package/src/ui/Tooltip.stories.tsx +84 -0
  74. package/src/ui/Tooltip.tsx +61 -0
  75. package/src/ui/Typography.stories.tsx +87 -0
  76. package/src/ui/Typography.tsx +80 -0
  77. package/src/ui/index.ts +28 -0
  78. package/src/ui/utils.ts +6 -0
  79. package/tsconfig.json +12 -0
  80. package/vitest.config.ts +36 -0
  81. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,217 @@
1
+ import { useFormStatus } from "react-dom";
2
+ import { Button, Input, Heading, Text, Icon, Field } from ".";
3
+
4
+ function SubmitButton({ label }: { label: string }) {
5
+ const { pending } = useFormStatus();
6
+ return (
7
+ <Button
8
+ type="submit"
9
+ variant="primary"
10
+ isLoading={pending}
11
+ disabled={pending}
12
+ >
13
+ {label}
14
+ </Button>
15
+ );
16
+ }
17
+
18
+ export type AuthFormMode = "signing up" | "signing in" | "resetting password";
19
+
20
+ export interface AuthFormUIProps {
21
+ mode: AuthFormMode;
22
+ onModeChange: (mode: AuthFormMode) => void;
23
+ formAction: (payload: FormData) => void;
24
+ actionState: { error: string; message: string };
25
+ onGoogleSignIn?: () => void;
26
+ title?: React.ReactNode;
27
+ description?: React.ReactNode;
28
+ }
29
+
30
+ export function AuthFormUI({
31
+ mode,
32
+ onModeChange,
33
+ formAction,
34
+ actionState,
35
+ onGoogleSignIn,
36
+ title,
37
+ description,
38
+ }: AuthFormUIProps) {
39
+ const defaultTitle =
40
+ mode === "signing up"
41
+ ? "Sign up"
42
+ : mode === "signing in"
43
+ ? "Sign In"
44
+ : "Reset Password";
45
+
46
+ return (
47
+ <div className="text-left overflow-hidden">
48
+ <Heading level={2} className="text-center mb-2">
49
+ {title || defaultTitle}
50
+ </Heading>
51
+
52
+ <form className="md:m-10 sm:m-4 space-y-4" action={formAction}>
53
+ {description && (
54
+ <Text className="text-center mb-6" variant="muted">
55
+ {description}
56
+ </Text>
57
+ )}
58
+
59
+ {mode === "resetting password" && (
60
+ <Text className="text-center font-bold text-dark-primary">
61
+ {actionState.message}
62
+ </Text>
63
+ )}
64
+ <Text className="text-center" variant="error">
65
+ {actionState.error}
66
+ </Text>
67
+
68
+ <div className="space-y-4">
69
+ <Field
70
+ id="email"
71
+ label="Email"
72
+ error={
73
+ actionState.error && actionState.error.includes("email")
74
+ ? actionState.error
75
+ : ""
76
+ }
77
+ >
78
+ <Input
79
+ name="email"
80
+ type="email"
81
+ required
82
+ placeholder="Email"
83
+ leftIcon={<Icon name="envelope" />}
84
+ />
85
+ </Field>
86
+
87
+ {mode !== "resetting password" && (
88
+ <Field
89
+ id="password"
90
+ label="Password"
91
+ error={
92
+ actionState.error && actionState.error.includes("password")
93
+ ? actionState.error
94
+ : ""
95
+ }
96
+ >
97
+ <Input
98
+ name="password"
99
+ type="password"
100
+ required
101
+ placeholder="Password"
102
+ leftIcon={<Icon name="lockClosed" />}
103
+ />
104
+ </Field>
105
+ )}
106
+ </div>
107
+
108
+ <div className="text-center mt-6 space-y-4">
109
+ {mode === "signing up" && <SubmitButton label="Sign Up" />}
110
+ {mode === "signing in" && <SubmitButton label="Sign In" />}
111
+ {mode === "resetting password" && (
112
+ <SubmitButton label="Reset Password" />
113
+ )}
114
+
115
+ <div className="text-center text-sm text-gray-500">
116
+ {mode === "signing up" && (
117
+ <Text>
118
+ Already have an account?{" "}
119
+ <Button
120
+ variant="ghost"
121
+ size="sm"
122
+ onClick={() => onModeChange("signing in")}
123
+ className="underline p-0 h-auto hover:bg-transparent"
124
+ >
125
+ Sign in
126
+ </Button>
127
+ </Text>
128
+ )}
129
+ {mode === "signing in" && (
130
+ <div className="flex flex-col gap-2">
131
+ <Text>
132
+ Forgot your password?{" "}
133
+ <Button
134
+ variant="ghost"
135
+ size="sm"
136
+ onClick={() => onModeChange("resetting password")}
137
+ className="underline p-0 h-auto hover:bg-transparent"
138
+ >
139
+ Reset password.
140
+ </Button>
141
+ </Text>
142
+ <div className="relative">
143
+ <div className="absolute inset-0 flex items-center">
144
+ <div className="w-full border-t border-gray-300"></div>
145
+ </div>
146
+ <div className="relative flex justify-center text-sm">
147
+ <span className="px-2 bg-white text-gray-500">or</span>
148
+ </div>
149
+ </div>
150
+ <Text>
151
+ Don&apos;t have an account?{" "}
152
+ <Button
153
+ variant="ghost"
154
+ size="sm"
155
+ onClick={() => onModeChange("signing up")}
156
+ className="underline p-0 h-auto hover:bg-transparent"
157
+ >
158
+ Sign up
159
+ </Button>
160
+ </Text>
161
+ </div>
162
+ )}
163
+ {mode === "resetting password" && (
164
+ <div className="flex flex-col gap-2">
165
+ <Text>
166
+ Don&apos;t have an account?{" "}
167
+ <Button
168
+ variant="ghost"
169
+ size="sm"
170
+ onClick={() => onModeChange("signing up")}
171
+ className="underline p-0 h-auto hover:bg-transparent"
172
+ >
173
+ Sign up
174
+ </Button>
175
+ </Text>
176
+ <div className="relative">
177
+ <div className="absolute inset-0 flex items-center">
178
+ <div className="w-full border-t border-gray-300"></div>
179
+ </div>
180
+ <div className="relative flex justify-center text-sm">
181
+ <span className="px-2 bg-white text-gray-500">or</span>
182
+ </div>
183
+ </div>
184
+ <Text>
185
+ Remembered your password?
186
+ <Button
187
+ variant="ghost"
188
+ size="sm"
189
+ onClick={() => onModeChange("signing in")}
190
+ className="underline p-0 h-auto hover:bg-transparent"
191
+ >
192
+ Sign In
193
+ </Button>
194
+ </Text>
195
+ </div>
196
+ )}
197
+ </div>
198
+ </div>
199
+
200
+ {mode !== "resetting password" && onGoogleSignIn && (
201
+ <div className="text-center mt-4">
202
+ <Button
203
+ variant="secondary"
204
+ onClick={onGoogleSignIn}
205
+ type="button"
206
+ className="w-full flex items-center justify-center gap-2"
207
+ >
208
+ <Icon name="google" stroke="none" />
209
+ {mode === "signing up" && <span>Sign up with Google</span>}
210
+ {mode === "signing in" && <span>Sign in with Google</span>}
211
+ </Button>
212
+ </div>
213
+ )}
214
+ </form>
215
+ </div>
216
+ );
217
+ }
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { BasePanel } from "./BasePanel";
3
+ import { Text as UIText } from "./Typography";
4
+
5
+ const meta = {
6
+ title: "UI/BasePanel",
7
+ component: BasePanel,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="w-[450px] border border-gray-200 rounded-lg shadow-sm bg-white overflow-hidden p-4">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ tags: ["autodocs"],
19
+ } satisfies Meta<typeof BasePanel>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ title: "Example Panel",
27
+ children: (
28
+ <UIText
29
+ variant="small"
30
+ className="p-4 bg-gray-50 rounded-md border border-gray-100 text-black text-center"
31
+ >
32
+ Panel content goes here.
33
+ </UIText>
34
+ ),
35
+ },
36
+ };
@@ -0,0 +1,59 @@
1
+ import { ReactNode } from "react";
2
+ import { Heading, Button, Icon } from "./";
3
+
4
+ export interface BasePanelProps {
5
+ title: string;
6
+ children: ReactNode;
7
+ onClose?: () => void;
8
+ className?: string;
9
+ headerClassName?: string;
10
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
11
+ setPanel: (panel: string) => void;
12
+ }
13
+
14
+ export function BasePanel({
15
+ title,
16
+ children,
17
+ onClose,
18
+ className = "",
19
+ headerClassName = "",
20
+ headingLevel = 2,
21
+ setPanel = (panel: string) => { }
22
+ }: BasePanelProps) {
23
+
24
+ const handleClose = () => {
25
+ if (onClose) {
26
+ onClose();
27
+ } else {
28
+ setPanel("None");
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className={`space-y-1 p-1 ${className}`}>
34
+ <div
35
+ className={`flex items-center justify-between px-1 mb-1 border-b border-solid border-gray-400 pb-1 ${headerClassName}`}
36
+ >
37
+ <Heading level={headingLevel} className="text-lg">
38
+ {title}
39
+ </Heading>
40
+ <Button
41
+ variant="ghost"
42
+ size="sm"
43
+ onClick={handleClose}
44
+ className="p-1 rounded hover:bg-gray-100 h-auto"
45
+ aria-label={`Close ${title} Panel`}
46
+ >
47
+ <Icon size="md" className="h-5 w-5">
48
+ <path
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ d="M6 18 18 6M6 6l12 12"
52
+ />
53
+ </Icon>
54
+ </Button>
55
+ </div>
56
+ {children}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,108 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Button } from "./Button";
3
+ import { Icon } from "./Icon";
4
+ import { Heading } from "./Typography";
5
+
6
+ const meta: Meta<typeof Button> = {
7
+ title: "UI/Button",
8
+ component: Button,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ argTypes: {
14
+ variant: {
15
+ control: "select",
16
+ options: ["primary", "secondary", "danger", "ghost", "ghostly-danger"],
17
+ },
18
+ size: {
19
+ control: "select",
20
+ options: ["sm", "md", "lg"],
21
+ },
22
+ disabled: { control: "boolean" },
23
+ isLoading: { control: "boolean" },
24
+ onClick: { action: "clicked" },
25
+ },
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof Button>;
30
+
31
+ export const Default: Story = {
32
+ args: {
33
+ children: "Button",
34
+ variant: "primary",
35
+ },
36
+ };
37
+
38
+ export const Variations: Story = {
39
+ render: () => (
40
+ <div className="flex flex-col gap-8">
41
+ <div className="space-y-4">
42
+ <Heading level={4} className="text-sm font-medium text-black italic">
43
+ Variants
44
+ </Heading>
45
+ <div className="flex flex-wrap gap-4 items-center">
46
+ <Button variant="primary">Primary</Button>
47
+ <Button variant="secondary">Secondary</Button>
48
+ <Button variant="danger">Danger</Button>
49
+ <Button variant="ghost">Ghost</Button>
50
+ <Button variant="ghostly-danger">Ghostly Danger</Button>
51
+ </div>
52
+ </div>
53
+
54
+ <div className="space-y-4">
55
+ <Heading level={4} className="text-sm font-medium text-black italic">
56
+ Sizes
57
+ </Heading>
58
+ <div className="flex flex-wrap gap-4 items-end">
59
+ <Button size="sm">Small</Button>
60
+ <Button size="md">Medium</Button>
61
+ <Button size="lg">Large</Button>
62
+ </div>
63
+ </div>
64
+
65
+ <div className="space-y-4">
66
+ <Heading level={4} className="text-sm font-medium text-black italic">
67
+ States
68
+ </Heading>
69
+ <div className="flex flex-wrap gap-4 items-center">
70
+ <Button isLoading>Loading</Button>
71
+ <Button disabled>Disabled</Button>
72
+ </div>
73
+ </div>
74
+
75
+ <div className="space-y-4">
76
+ <Heading level={4} className="text-sm font-medium text-black italic">
77
+ With Icons
78
+ </Heading>
79
+ <div className="flex flex-wrap gap-4 items-center">
80
+ <Button leftIcon={<Icon name="cloudArrowUp" />}>Upload</Button>
81
+ <Button variant="secondary" rightIcon={<Icon name="chevronRight" />}>
82
+ Next
83
+ </Button>
84
+ </div>
85
+ </div>
86
+
87
+ <div className="space-y-4">
88
+ <Heading level={4} className="text-sm font-medium text-black italic">
89
+ Icon Only
90
+ </Heading>
91
+ <div className="flex flex-wrap gap-4 items-center">
92
+ <Button size="sm">
93
+ <Icon name="plus" />
94
+ </Button>
95
+ <Button size="md">
96
+ <Icon name="plus" />
97
+ </Button>
98
+ <Button size="lg">
99
+ <Icon name="plus" />
100
+ </Button>
101
+ <Button variant="ghost" size="sm" aria-label="Close">
102
+ <Icon name="close" />
103
+ </Button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ ),
108
+ };
@@ -0,0 +1,121 @@
1
+ import { Button as BaseButton } from "@base-ui/react";
2
+ import React from "react";
3
+ import { cn } from "./utils";
4
+
5
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
+ variant?: "primary" | "secondary" | "danger" | "ghost" | "ghostly-danger";
7
+ size?: "sm" | "md" | "lg";
8
+ isLoading?: boolean;
9
+ leftIcon?: React.ReactNode;
10
+ rightIcon?: React.ReactNode;
11
+ render?: BaseButton.Props["render"];
12
+ nativeButton?: boolean;
13
+ isIconOnly?: boolean;
14
+ }
15
+
16
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
17
+ (
18
+ {
19
+ className,
20
+ variant = "primary",
21
+ size = "md",
22
+ isLoading = false,
23
+ leftIcon,
24
+ rightIcon,
25
+ children,
26
+ disabled,
27
+ render,
28
+ nativeButton,
29
+ isIconOnly,
30
+ ...props
31
+ },
32
+ ref,
33
+ ) => {
34
+ const isActuallyIconOnly =
35
+ isIconOnly || (!children && (!!leftIcon || !!rightIcon));
36
+
37
+ const variants = {
38
+ primary:
39
+ "bg-primary hover:bg-dark-primary text-gray-900 shadow-sm border border-transparent font-bold disabled:bg-gray-400 disabled:hover:bg-gray-300",
40
+ secondary:
41
+ "bg-gray-100 hover:bg-gray-200 text-gray-800 border-gray-200 border font-normal disabled:bg-gray-400 disabled:hover:bg-gray-300",
42
+ danger:
43
+ "bg-danger hover:opacity-80 text-gray-900 shadow-sm border border-transparent font-bold disabled:bg-gray-400 disabled:hover:bg-gray-300",
44
+ ghost:
45
+ "bg-transparent hover:bg-gray-100 text-gray-700 hover:text-gray-900 font-medium",
46
+ "ghostly-danger":
47
+ "bg-transparent hover:bg-danger/10 text-red-700 border border-red-700 font-medium disabled:border-gray-400 disabled:text-gray-400",
48
+ };
49
+
50
+ const sizes = {
51
+ sm: isActuallyIconOnly ? "p-1 text-xs" : "px-2 py-1 text-xs",
52
+ md: isActuallyIconOnly ? "p-2 text-sm" : "px-4 py-2 text-sm",
53
+ lg: isActuallyIconOnly ? "p-3 text-base" : "px-6 py-3 text-base",
54
+ };
55
+
56
+ return (
57
+ <BaseButton
58
+ ref={ref}
59
+ render={render}
60
+ nativeButton={nativeButton}
61
+ className={cn(
62
+ "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:cursor-not-allowed cursor-pointer",
63
+ variants[variant],
64
+ sizes[size],
65
+ className,
66
+ )}
67
+ disabled={disabled || isLoading}
68
+ {...props}
69
+ >
70
+ {isLoading && (
71
+ <svg
72
+ className={cn(
73
+ "animate-spin h-4 w-4",
74
+ !isActuallyIconOnly && "mr-2 -ml-1",
75
+ )}
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ fill="none"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ <circle
81
+ className="opacity-25"
82
+ cx="12"
83
+ cy="12"
84
+ r="10"
85
+ stroke="currentColor"
86
+ strokeWidth="4"
87
+ ></circle>
88
+ <path
89
+ className="opacity-75"
90
+ fill="currentColor"
91
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
92
+ ></path>
93
+ </svg>
94
+ )}
95
+ {!isLoading && leftIcon && (
96
+ <span
97
+ className={cn(
98
+ "inline-flex items-center justify-center",
99
+ !isActuallyIconOnly && "mr-2 -ml-1",
100
+ )}
101
+ >
102
+ {leftIcon}
103
+ </span>
104
+ )}
105
+ {children}
106
+ {!isLoading && rightIcon && (
107
+ <span
108
+ className={cn(
109
+ "inline-flex items-center justify-center",
110
+ !isActuallyIconOnly && "ml-2 -mr-1",
111
+ )}
112
+ >
113
+ {rightIcon}
114
+ </span>
115
+ )}
116
+ </BaseButton>
117
+ );
118
+ },
119
+ );
120
+
121
+ Button.displayName = "Button";
@@ -0,0 +1,61 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Checkbox } from "./Checkbox";
3
+ import { Heading } from "./Typography";
4
+ import React from "react";
5
+
6
+ const meta: Meta<typeof Checkbox> = {
7
+ title: "UI/Checkbox",
8
+ component: Checkbox,
9
+ tags: ["autodocs"],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof Checkbox>;
14
+
15
+ const StatefulCheckbox = (
16
+ props: React.ComponentProps<typeof Checkbox> & { label?: string },
17
+ ) => {
18
+ const [checked, setChecked] = React.useState(props.checked || false);
19
+ return (
20
+ <div className="flex items-center space-x-2">
21
+ <Checkbox
22
+ {...props}
23
+ id="terms"
24
+ checked={checked}
25
+ onChange={(e) => setChecked(e.target.checked)}
26
+ />
27
+ <label
28
+ htmlFor="terms"
29
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
30
+ >
31
+ {props.label || "Accept terms and conditions"}
32
+ </label>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export const Default: Story = {
38
+ render: () => <StatefulCheckbox />,
39
+ };
40
+
41
+ export const Variations: Story = {
42
+ render: () => (
43
+ <div className="flex flex-col gap-8 p-4">
44
+ <div className="space-y-4">
45
+ <Heading level={4} className="text-sm font-medium text-black uppercase">
46
+ States
47
+ </Heading>
48
+ <div className="flex flex-col gap-4">
49
+ <StatefulCheckbox label="Default Unchecked" />
50
+ <StatefulCheckbox label="Default Checked" checked={true} />
51
+ <StatefulCheckbox label="Disabled Unchecked" disabled={true} />
52
+ <StatefulCheckbox
53
+ label="Disabled Checked"
54
+ disabled={true}
55
+ checked={true}
56
+ />
57
+ </div>
58
+ </div>
59
+ </div>
60
+ ),
61
+ };
@@ -0,0 +1,45 @@
1
+ import { Checkbox as BaseCheckbox } from "@base-ui/react";
2
+ import React, { ComponentPropsWithoutRef } from "react";
3
+ import { cn } from "./utils";
4
+ import { Icon } from "./Icon";
5
+
6
+ export type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement>;
7
+
8
+ export const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
9
+ ({ className, id, checked, onChange, ...props }, ref) => {
10
+ return (
11
+ <BaseCheckbox.Root
12
+ ref={ref}
13
+ id={id}
14
+ checked={checked}
15
+ onCheckedChange={(val) => {
16
+ if (onChange) {
17
+ // Mocking a change event since Base UI uses onCheckedChange
18
+ onChange({
19
+ target: { checked: val === true },
20
+ } as React.ChangeEvent<HTMLInputElement>);
21
+ }
22
+ }}
23
+ className={cn(
24
+ "flex h-5 w-5 shrink-0 items-center justify-center rounded border border-gray-300 bg-white shadow-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
25
+ "hover:border-primary hover:bg-primary/5",
26
+ "data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-white",
27
+ className,
28
+ )}
29
+ {...(props as ComponentPropsWithoutRef<typeof BaseCheckbox.Root>)}
30
+ >
31
+ <BaseCheckbox.Indicator>
32
+ <Icon size="sm" strokeWidth={3} className="text-dark-primary">
33
+ <path
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
+ d="m4.5 12.75 6 6 9-13.5"
37
+ />
38
+ </Icon>
39
+ </BaseCheckbox.Indicator>
40
+ </BaseCheckbox.Root>
41
+ );
42
+ },
43
+ );
44
+
45
+ Checkbox.displayName = "Checkbox";