Skip to content

Commit f322432

Browse files
committed
Add base checkbox and rename Component Library -> Components inside Storybook
1 parent 3f15fad commit f322432

File tree

7 files changed

+369
-7
lines changed

7 files changed

+369
-7
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { useState } from "react";
3+
4+
import { Checkbox, type CheckboxProps } from "./checkbox";
5+
6+
const meta: Meta<CheckboxProps> = {
7+
title: "Components/Checkbox",
8+
component: Checkbox,
9+
parameters: {
10+
layout: "centered",
11+
docs: {
12+
description: {
13+
component: `
14+
# Checkbox Component
15+
16+
A simple checkbox component built with @ark-ui/react and styled with PandaCSS.
17+
No refraction effects - just clean, accessible checkboxes.
18+
19+
## States
20+
21+
- **Unchecked**: Default empty state
22+
- **Checked**: Selected state with checkmark
23+
- **Indeterminate**: Partial selection state with minus icon
24+
- **Disabled**: Non-interactive state
25+
26+
## Interactions
27+
28+
- **Hover**: Slightly darker border and background
29+
- **Focus**: Blue outline ring
30+
- **Disabled**: Reduced opacity and muted colors
31+
`,
32+
},
33+
},
34+
},
35+
argTypes: {
36+
disabled: {
37+
control: "boolean",
38+
description: "Whether the checkbox is disabled",
39+
},
40+
label: {
41+
control: "text",
42+
description: "Label text for the checkbox",
43+
},
44+
},
45+
args: {
46+
disabled: false,
47+
label: "Label",
48+
},
49+
};
50+
51+
export default meta;
52+
53+
type Story = StoryObj<typeof meta>;
54+
55+
/**
56+
* Default checkbox in unchecked state
57+
*/
58+
export const Default: Story = {
59+
args: {
60+
checked: false,
61+
},
62+
};
63+
64+
/**
65+
* Checkbox in checked state
66+
*/
67+
export const Checked: Story = {
68+
args: {
69+
checked: true,
70+
},
71+
};
72+
73+
/**
74+
* Checkbox in indeterminate state (partial selection)
75+
*/
76+
export const Indeterminate: Story = {
77+
args: {
78+
checked: "indeterminate",
79+
},
80+
};
81+
82+
/**
83+
* Disabled checkbox states
84+
*/
85+
export const Disabled: Story = {
86+
render: () => (
87+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
88+
<Checkbox label="Unchecked Disabled" disabled defaultChecked={false} />
89+
<Checkbox label="Checked Disabled" disabled defaultChecked />
90+
<Checkbox
91+
label="Indeterminate Disabled"
92+
disabled
93+
defaultChecked="indeterminate"
94+
/>
95+
</div>
96+
),
97+
};
98+
99+
/**
100+
* Interactive checkbox with controlled state
101+
*/
102+
export const Interactive: Story = {
103+
render: () => {
104+
const [checked, setChecked] = useState<boolean | "indeterminate">(false);
105+
106+
return (
107+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
108+
<Checkbox
109+
label="Interactive Checkbox"
110+
checked={checked}
111+
onCheckedChange={setChecked}
112+
/>
113+
<div style={{ fontSize: "14px", color: "#666" }}>
114+
Current state:{" "}
115+
<strong>
116+
{checked === "indeterminate"
117+
? "Indeterminate"
118+
: checked
119+
? "Checked"
120+
: "Unchecked"}
121+
</strong>
122+
</div>
123+
<div style={{ display: "flex", gap: "8px" }}>
124+
<button
125+
type="button"
126+
onClick={() => setChecked(false)}
127+
style={{
128+
padding: "4px 12px",
129+
borderRadius: "4px",
130+
border: "1px solid #ccc",
131+
background: "white",
132+
cursor: "pointer",
133+
}}
134+
>
135+
Uncheck
136+
</button>
137+
<button
138+
type="button"
139+
onClick={() => setChecked(true)}
140+
style={{
141+
padding: "4px 12px",
142+
borderRadius: "4px",
143+
border: "1px solid #ccc",
144+
background: "white",
145+
cursor: "pointer",
146+
}}
147+
>
148+
Check
149+
</button>
150+
<button
151+
type="button"
152+
onClick={() => setChecked("indeterminate")}
153+
style={{
154+
padding: "4px 12px",
155+
borderRadius: "4px",
156+
border: "1px solid #ccc",
157+
background: "white",
158+
cursor: "pointer",
159+
}}
160+
>
161+
Set Indeterminate
162+
</button>
163+
</div>
164+
</div>
165+
);
166+
},
167+
};
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { Checkbox as BaseCheckbox } from "@ark-ui/react/checkbox";
2+
import { css } from "@hashintel/ds-helpers/css";
3+
4+
const CHECK_ICON = (
5+
<svg
6+
width="12"
7+
height="12"
8+
viewBox="0 0 12 12"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M10 3L4.5 8.5L2 6"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
/>
19+
</svg>
20+
);
21+
22+
const INDETERMINATE_ICON = (
23+
<svg
24+
width="14"
25+
height="14"
26+
viewBox="0 0 14 14"
27+
fill="none"
28+
xmlns="http://www.w3.org/2000/svg"
29+
>
30+
<path
31+
d="M4 7H10"
32+
stroke="currentColor"
33+
strokeWidth="1.5"
34+
strokeLinecap="round"
35+
/>
36+
</svg>
37+
);
38+
39+
export interface CheckboxProps {
40+
checked?: boolean | "indeterminate";
41+
defaultChecked?: boolean | "indeterminate";
42+
disabled?: boolean;
43+
invalid?: boolean;
44+
readOnly?: boolean;
45+
required?: boolean;
46+
name?: string;
47+
value?: string;
48+
form?: string;
49+
onCheckedChange?: (checked: boolean | "indeterminate") => void;
50+
label?: string;
51+
id?: string;
52+
}
53+
54+
export const Checkbox: React.FC<CheckboxProps> = ({
55+
checked,
56+
defaultChecked,
57+
disabled = false,
58+
invalid = false,
59+
readOnly = false,
60+
required = false,
61+
name,
62+
value,
63+
form,
64+
onCheckedChange,
65+
label,
66+
id,
67+
}) => {
68+
return (
69+
<BaseCheckbox.Root
70+
{...(checked !== undefined ? { checked } : { defaultChecked })}
71+
disabled={disabled}
72+
invalid={invalid}
73+
readOnly={readOnly}
74+
required={required}
75+
name={name}
76+
value={value}
77+
form={form}
78+
onCheckedChange={(details) => {
79+
onCheckedChange?.(details.checked);
80+
}}
81+
id={id}
82+
className={css({
83+
display: "inline-flex",
84+
alignItems: "center",
85+
gap: "[8px]",
86+
cursor: disabled ? "not-allowed" : "pointer",
87+
})}
88+
>
89+
<BaseCheckbox.Control
90+
className={css({
91+
position: "relative",
92+
display: "inline-flex",
93+
alignItems: "center",
94+
justifyContent: "center",
95+
width: "[16px]",
96+
height: "[16px]",
97+
borderRadius: "[4px]",
98+
border: "1px solid",
99+
borderColor: "gray.20",
100+
backgroundColor: "neutral.white",
101+
transition: "[all 0.2s ease]",
102+
flexShrink: "0",
103+
104+
// Hover state (unchecked)
105+
"&[data-state='unchecked']:hover:not([data-disabled])": {
106+
borderColor: "gray.35",
107+
},
108+
109+
// Focus state
110+
"&[data-focus-visible]": {
111+
boxShadow: "[0px 0px 0px 2px {colors.grayAlpha.30}]",
112+
},
113+
114+
// Checked and indeterminate states
115+
"&[data-state='checked'], &[data-state='indeterminate']": {
116+
borderColor: "gray.80",
117+
backgroundColor: "gray.80",
118+
color: "neutral.white",
119+
},
120+
121+
// Hover on checked/indeterminate states
122+
"&[data-state='checked']:hover:not([data-disabled]), &[data-state='indeterminate']:hover:not([data-disabled])":
123+
{
124+
backgroundColor: "gray.70",
125+
borderColor: "gray.70",
126+
},
127+
128+
// Disabled state
129+
"&[data-disabled]": {
130+
cursor: "not-allowed",
131+
opacity: "[0.4]",
132+
},
133+
134+
// Invalid state
135+
"&[data-invalid]": {
136+
borderColor: "red.50",
137+
},
138+
})}
139+
>
140+
{/* Checked indicator */}
141+
<BaseCheckbox.Indicator
142+
className={css({
143+
display: "flex",
144+
alignItems: "center",
145+
justifyContent: "center",
146+
width: "[100%]",
147+
height: "[100%]",
148+
})}
149+
>
150+
{CHECK_ICON}
151+
</BaseCheckbox.Indicator>
152+
153+
{/* Indeterminate indicator */}
154+
<BaseCheckbox.Indicator
155+
indeterminate
156+
className={css({
157+
display: "flex",
158+
alignItems: "center",
159+
justifyContent: "center",
160+
width: "[100%]",
161+
height: "[100%]",
162+
})}
163+
>
164+
{INDETERMINATE_ICON}
165+
</BaseCheckbox.Indicator>
166+
</BaseCheckbox.Control>
167+
168+
{label && (
169+
<BaseCheckbox.Label
170+
className={css({
171+
fontSize: "[14px]",
172+
fontWeight: "medium",
173+
lineHeight: "[14px]",
174+
color: "gray.90",
175+
cursor: disabled ? "not-allowed" : "pointer",
176+
userSelect: "none",
177+
whiteSpace: "nowrap",
178+
179+
"&[data-disabled]": {
180+
opacity: "[0.4]",
181+
},
182+
})}
183+
>
184+
{label}
185+
</BaseCheckbox.Label>
186+
)}
187+
188+
<BaseCheckbox.HiddenInput />
189+
</BaseCheckbox.Root>
190+
);
191+
};

libs/@hashintel/ds-components/src/components/RefractivePane/refractive-pane.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ExampleArticle } from "../../playground/example-article";
1616
import { RefractivePane } from "./refractive-pane";
1717

1818
const meta = {
19-
title: "Component Library/RefractivePane",
19+
title: "Components/RefractivePane",
2020
component: RefractivePane,
2121
tags: ["docsPage"],
2222
parameters: {

libs/@hashintel/ds-components/src/components/SegmentedControl/segmented-control.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useState } from "react";
44
import { SegmentedControl } from "./segmented-control";
55

66
const meta: Meta<typeof SegmentedControl> = {
7-
title: "Component Library/SegmentedControl",
7+
title: "Components/SegmentedControl",
88
component: SegmentedControl,
99
tags: ["docsPage"],
1010
parameters: {
@@ -128,7 +128,7 @@ export const Interactive: Story = {
128128

129129
render: (args) => {
130130
const [selectedValue, setSelectedValue] = useState(
131-
args.defaultValue ?? "option1",
131+
args.defaultValue ?? "option1"
132132
);
133133

134134
return (

libs/@hashintel/ds-components/src/components/Slider/slider.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
33
import { Slider } from "./slider";
44

55
const meta = {
6-
title: "Component Library/Slider",
6+
title: "Components/Slider",
77
component: Slider,
88
tags: ["docsPage"],
99
parameters: {

0 commit comments

Comments
 (0)