@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/publish.yml +42 -0
- package/.github/workflows/storybook.yml +46 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +22 -0
- package/README.md +9 -0
- package/dist/index.css +1 -0
- package/dist/index.d.mts +704 -0
- package/dist/index.d.ts +704 -0
- package/dist/index.js +5 -0
- package/dist/index.mjs +5 -0
- package/package.json +60 -0
- package/src/charts/DistributionBarChart.stories.tsx +41 -0
- package/src/charts/DistributionBarChart.tsx +170 -0
- package/src/charts/DistributionHistogram.stories.tsx +56 -0
- package/src/charts/DistributionHistogram.tsx +193 -0
- package/src/charts/index.ts +10 -0
- package/src/global.d.ts +1 -0
- package/src/helpers/SEO.tsx +41 -0
- package/src/index.ts +6 -0
- package/src/styles/theme.css +60 -0
- package/src/ui/Alert.stories.tsx +41 -0
- package/src/ui/Alert.tsx +72 -0
- package/src/ui/Anchor.stories.tsx +25 -0
- package/src/ui/Anchor.tsx +32 -0
- package/src/ui/AuthFormUI.stories.tsx +67 -0
- package/src/ui/AuthFormUI.tsx +217 -0
- package/src/ui/BasePanel.stories.tsx +36 -0
- package/src/ui/BasePanel.tsx +59 -0
- package/src/ui/Button.stories.tsx +108 -0
- package/src/ui/Button.tsx +121 -0
- package/src/ui/Checkbox.stories.tsx +61 -0
- package/src/ui/Checkbox.tsx +45 -0
- package/src/ui/Collapsible.stories.tsx +91 -0
- package/src/ui/Collapsible.tsx +52 -0
- package/src/ui/Colors.stories.tsx +67 -0
- package/src/ui/Dialog.stories.tsx +29 -0
- package/src/ui/Dialog.tsx +56 -0
- package/src/ui/Dropdown.tsx +66 -0
- package/src/ui/Field.stories.tsx +181 -0
- package/src/ui/Field.tsx +108 -0
- package/src/ui/Icon.stories.tsx +192 -0
- package/src/ui/Icon.tsx +42 -0
- package/src/ui/IconRegistry.tsx +189 -0
- package/src/ui/Input.stories.tsx +67 -0
- package/src/ui/Input.tsx +43 -0
- package/src/ui/Label.stories.tsx +42 -0
- package/src/ui/Label.tsx +26 -0
- package/src/ui/NumberField.stories.tsx +86 -0
- package/src/ui/NumberField.tsx +116 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Select.stories.tsx +74 -0
- package/src/ui/Select.tsx +122 -0
- package/src/ui/Separator.stories.tsx +61 -0
- package/src/ui/Separator.tsx +28 -0
- package/src/ui/SettingsPanel.stories.tsx +83 -0
- package/src/ui/SettingsPanel.tsx +81 -0
- package/src/ui/Skeleton.stories.tsx +43 -0
- package/src/ui/Skeleton.tsx +15 -0
- package/src/ui/Slider.stories.tsx +140 -0
- package/src/ui/Slider.tsx +95 -0
- package/src/ui/SliderWithNumberField.stories.tsx +101 -0
- package/src/ui/SliderWithNumberField.tsx +88 -0
- package/src/ui/Switch.stories.tsx +81 -0
- package/src/ui/Switch.tsx +60 -0
- package/src/ui/TabRadio.stories.tsx +153 -0
- package/src/ui/TabRadio.tsx +68 -0
- package/src/ui/TabSelection.stories.tsx +44 -0
- package/src/ui/TabSelection.tsx +91 -0
- package/src/ui/TextArea.stories.tsx +64 -0
- package/src/ui/TextArea.tsx +24 -0
- package/src/ui/Tooltip.stories.tsx +84 -0
- package/src/ui/Tooltip.tsx +61 -0
- package/src/ui/Typography.stories.tsx +87 -0
- package/src/ui/Typography.tsx +80 -0
- package/src/ui/index.ts +28 -0
- package/src/ui/utils.ts +6 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +36 -0
- 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'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'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";
|