@choice-ui/checkbox 0.0.4

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/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # Checkbox
2
+
3
+ A flexible checkbox component with support for labels, mixed state, and multiple visual variants. It uses a compound component pattern for maximum composability.
4
+
5
+ ## Import
6
+
7
+ ```tsx
8
+ import { Checkbox } from "@choice-ui/react"
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Three visual variants (default, accent, outline) for different UI contexts
14
+ - Support for indeterminate/mixed state
15
+ - Compound component pattern with `Checkbox.Label`
16
+ - Full keyboard navigation (Space/Enter to toggle)
17
+ - Proper ARIA attributes for accessibility
18
+ - Focus state management
19
+ - Disabled state styling
20
+ - Auto-wrapping of string children as labels
21
+ - Context-based communication between sub-components
22
+
23
+ ## Usage
24
+
25
+ ### Basic
26
+
27
+ ```tsx
28
+ <Checkbox
29
+ value={checked}
30
+ onChange={setChecked}
31
+ >
32
+ Accept terms and conditions
33
+ </Checkbox>
34
+ ```
35
+
36
+ ### With separate label
37
+
38
+ ```tsx
39
+ <Checkbox
40
+ value={checked}
41
+ onChange={setChecked}
42
+ >
43
+ <Checkbox.Label>Enable notifications</Checkbox.Label>
44
+ </Checkbox>
45
+ ```
46
+
47
+ ### Variants
48
+
49
+ ```tsx
50
+ <Checkbox variant="default" value={checked} onChange={setChecked}>
51
+ Default style
52
+ </Checkbox>
53
+
54
+ <Checkbox variant="accent" value={checked} onChange={setChecked}>
55
+ Accent style
56
+ </Checkbox>
57
+
58
+ <Checkbox variant="outline" value={checked} onChange={setChecked}>
59
+ Outline style
60
+ </Checkbox>
61
+ ```
62
+
63
+ ### Mixed/Indeterminate state
64
+
65
+ ```tsx
66
+ <Checkbox
67
+ value={isPartiallySelected}
68
+ mixed={true}
69
+ onChange={handleSelectAll}
70
+ >
71
+ Select all
72
+ </Checkbox>
73
+ ```
74
+
75
+ ### Disabled state
76
+
77
+ ```tsx
78
+ <Checkbox
79
+ disabled
80
+ value={checked}
81
+ onChange={setChecked}
82
+ >
83
+ This option is disabled
84
+ </Checkbox>
85
+ ```
86
+
87
+ ### With custom label styling
88
+
89
+ ```tsx
90
+ <Checkbox
91
+ value={checked}
92
+ onChange={setChecked}
93
+ >
94
+ <Checkbox.Label className="text-body-small-strong">Custom styled label</Checkbox.Label>
95
+ </Checkbox>
96
+ ```
97
+
98
+ ### Controlled with focus
99
+
100
+ ```tsx
101
+ <Checkbox
102
+ value={checked}
103
+ onChange={setChecked}
104
+ focused={isFocused}
105
+ >
106
+ Controlled focus state
107
+ </Checkbox>
108
+ ```
109
+
110
+ ## Props
111
+
112
+ ### Checkbox
113
+
114
+ ```ts
115
+ interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
116
+ /** Child elements (strings auto-wrapped in Checkbox.Label) */
117
+ children?: ReactNode
118
+
119
+ /** Additional CSS class names */
120
+ className?: string
121
+
122
+ /** Whether the checkbox appears focused */
123
+ focused?: boolean
124
+
125
+ /** Whether to show mixed/indeterminate state */
126
+ mixed?: boolean
127
+
128
+ /** Callback when checked state changes */
129
+ onChange?: (value: boolean) => void
130
+
131
+ /** Whether the checkbox is checked */
132
+ value?: boolean
133
+
134
+ /** Visual style variant */
135
+ variant?: "default" | "accent" | "outline"
136
+ }
137
+ ```
138
+
139
+ ### Checkbox.Label
140
+
141
+ ```ts
142
+ interface CheckboxLabelProps extends Omit<
143
+ HTMLProps<HTMLLabelElement>,
144
+ "htmlFor" | "id" | "disabled"
145
+ > {
146
+ /** Label content */
147
+ children: ReactNode
148
+ }
149
+ ```
150
+
151
+ - Defaults:
152
+ - `variant`: "default"
153
+ - `value`: `false`
154
+ - `mixed`: `false`
155
+ - `focused`: `false`
156
+
157
+ - Accessibility:
158
+ - Proper `aria-checked` attribute (including "mixed" state)
159
+ - Automatic label association via context
160
+ - Keyboard support for Space and Enter keys
161
+ - Screen reader announcements
162
+
163
+ ## Styling
164
+
165
+ - Uses Tailwind CSS via `tailwind-variants` with slot-based styling
166
+ - Slots available: `root`, `box`, `input`, `label`
167
+ - Each variant has distinct visual treatments
168
+ - Focus states include border color changes and shadow effects
169
+ - Disabled state applies to both checkbox and label
170
+
171
+ ## Best practices
172
+
173
+ - Always provide a label for accessibility (either as children or via aria-label)
174
+ - Use the accent variant for primary actions
175
+ - Use mixed state for "select all" scenarios with partial selection
176
+ - Keep labels concise and action-oriented
177
+ - Group related checkboxes together visually
178
+ - Consider using Checkbox with forms and validation
179
+
180
+ ## Examples
181
+
182
+ ### Select all with mixed state
183
+
184
+ ```tsx
185
+ function SelectAllExample() {
186
+ const [items, setItems] = useState([
187
+ { id: 1, checked: false },
188
+ { id: 2, checked: true },
189
+ { id: 3, checked: false },
190
+ ])
191
+
192
+ const checkedCount = items.filter((item) => item.checked).length
193
+ const allChecked = checkedCount === items.length
194
+ const someChecked = checkedCount > 0 && checkedCount < items.length
195
+
196
+ const handleSelectAll = (checked: boolean) => {
197
+ setItems(items.map((item) => ({ ...item, checked })))
198
+ }
199
+
200
+ return (
201
+ <Checkbox
202
+ value={allChecked}
203
+ mixed={someChecked}
204
+ onChange={handleSelectAll}
205
+ >
206
+ Select all ({checkedCount}/{items.length})
207
+ </Checkbox>
208
+ )
209
+ }
210
+ ```
211
+
212
+ ### Form with validation
213
+
214
+ ```tsx
215
+ <form onSubmit={handleSubmit}>
216
+ <Checkbox
217
+ value={agreedToTerms}
218
+ onChange={setAgreedToTerms}
219
+ variant="accent"
220
+ required
221
+ >
222
+ I agree to the{" "}
223
+ <a
224
+ href="/terms"
225
+ className="underline"
226
+ >
227
+ terms and conditions
228
+ </a>
229
+ </Checkbox>
230
+
231
+ <Button
232
+ type="submit"
233
+ disabled={!agreedToTerms}
234
+ >
235
+ Continue
236
+ </Button>
237
+ </form>
238
+ ```
239
+
240
+ ### Settings panel
241
+
242
+ ```tsx
243
+ <div className="space-y-2">
244
+ <Checkbox
245
+ value={emailNotifications}
246
+ onChange={setEmailNotifications}
247
+ >
248
+ <Checkbox.Label>
249
+ <div>Email notifications</div>
250
+ <div className="text-secondary-foreground text-body-small">
251
+ Receive updates about your account
252
+ </div>
253
+ </Checkbox.Label>
254
+ </Checkbox>
255
+
256
+ <Checkbox
257
+ value={marketingEmails}
258
+ onChange={setMarketingEmails}
259
+ disabled={!emailNotifications}
260
+ >
261
+ Marketing emails
262
+ </Checkbox>
263
+ </div>
264
+ ```
265
+
266
+ ## Notes
267
+
268
+ - The component uses a context provider for communication between Checkbox and Label
269
+ - String children are automatically wrapped in `Checkbox.Label` for convenience
270
+ - The mixed state displays an indeterminate icon instead of a checkmark
271
+ - Focus management can be controlled externally via the `focused` prop
272
+ - The component is fully controlled - you must manage the `value` state
package/dist/index.cjs ADDED
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ var shared = require('@choice-ui/shared');
4
+ var iconsReact = require('@choiceform/icons-react');
5
+ var react = require('react');
6
+ var usehooksTs = require('usehooks-ts');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ // src/checkbox.tsx
10
+ var CheckboxContext = react.createContext(null);
11
+ function useCheckboxContext() {
12
+ const context = react.useContext(CheckboxContext);
13
+ if (!context) {
14
+ throw new Error("Checkbox components must be used within a Checkbox component");
15
+ }
16
+ return context;
17
+ }
18
+ var checkboxTv = shared.tcv({
19
+ slots: {
20
+ root: "flex items-center select-none",
21
+ box: ["relative flex size-4 items-center justify-center", "border border-solid"],
22
+ input: "peer pointer-events-auto absolute inset-0 appearance-none opacity-0",
23
+ label: "pl-2"
24
+ },
25
+ variants: {
26
+ type: {
27
+ checkbox: {
28
+ box: "rounded-md"
29
+ },
30
+ radio: {
31
+ box: "rounded-full"
32
+ }
33
+ },
34
+ variant: {
35
+ default: {},
36
+ accent: {},
37
+ outline: {}
38
+ },
39
+ checked: {
40
+ true: {},
41
+ false: {}
42
+ },
43
+ disabled: {
44
+ true: {},
45
+ false: {}
46
+ },
47
+ focused: {
48
+ true: {},
49
+ false: {}
50
+ }
51
+ },
52
+ compoundVariants: [
53
+ // 未选中状态
54
+ {
55
+ variant: ["default", "accent"],
56
+ checked: false,
57
+ disabled: false,
58
+ focused: false,
59
+ class: {
60
+ box: "bg-secondary-background border-default-boundary"
61
+ }
62
+ },
63
+ {
64
+ variant: "outline",
65
+ checked: false,
66
+ disabled: false,
67
+ focused: false,
68
+ class: {
69
+ box: ["border-default-foreground", "peer-focus-visible:border-selected-boundary"]
70
+ }
71
+ },
72
+ // 选中状态 - default
73
+ {
74
+ variant: "default",
75
+ checked: true,
76
+ disabled: false,
77
+ focused: false,
78
+ class: {
79
+ box: [
80
+ "bg-secondary-background border-default-boundary",
81
+ "peer-focus-visible:border-selected-strong-boundary"
82
+ ]
83
+ }
84
+ },
85
+ // 选中状态 - accent & outline
86
+ {
87
+ variant: ["accent", "outline"],
88
+ checked: true,
89
+ disabled: false,
90
+ focused: false,
91
+ class: {
92
+ box: [
93
+ "bg-accent-background border-selected-strong-boundary text-on-accent-foreground",
94
+ "peer-focus-visible:border-selected-strong-boundary",
95
+ "peer-focus-visible:text-on-accent-foreground",
96
+ "peer-focus-visible:shadow-checked-focused"
97
+ ]
98
+ }
99
+ },
100
+ {
101
+ variant: ["default", "accent", "outline"],
102
+ checked: false,
103
+ disabled: false,
104
+ focused: true,
105
+ class: {
106
+ box: "border-selected-boundary"
107
+ }
108
+ },
109
+ {
110
+ variant: "default",
111
+ checked: true,
112
+ disabled: false,
113
+ focused: true,
114
+ class: {
115
+ box: "border-selected-strong-boundary"
116
+ }
117
+ },
118
+ {
119
+ variant: ["accent", "outline"],
120
+ checked: true,
121
+ disabled: false,
122
+ focused: true,
123
+ class: {
124
+ box: "text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused"
125
+ }
126
+ },
127
+ {
128
+ variant: ["accent", "outline", "default"],
129
+ disabled: true,
130
+ class: {
131
+ root: "text-default-background",
132
+ box: "border-disabled-background bg-disabled-background",
133
+ label: "text-disabled-foreground"
134
+ }
135
+ }
136
+ ],
137
+ defaultVariants: {
138
+ variant: "default",
139
+ checked: false,
140
+ disabled: false,
141
+ focused: false
142
+ }
143
+ });
144
+ var CheckboxLabel = react.memo(
145
+ react.forwardRef(function CheckboxLabel2(props, ref) {
146
+ const { children, className, ...rest } = props;
147
+ const { id, descriptionId, disabled } = useCheckboxContext();
148
+ const styles = checkboxTv({ disabled });
149
+ return /* @__PURE__ */ jsxRuntime.jsx(
150
+ "label",
151
+ {
152
+ ref,
153
+ id: descriptionId,
154
+ htmlFor: id,
155
+ className: shared.tcx(styles.label(), className),
156
+ ...rest,
157
+ children
158
+ }
159
+ );
160
+ })
161
+ );
162
+ CheckboxLabel.displayName = "Checkbox.Label";
163
+ var CheckboxBase = react.forwardRef(function Checkbox(props, ref) {
164
+ const {
165
+ value,
166
+ onChange,
167
+ disabled,
168
+ readOnly = false,
169
+ variant = "default",
170
+ className,
171
+ focused,
172
+ mixed,
173
+ children,
174
+ id,
175
+ "aria-label": ariaLabel,
176
+ "aria-describedby": ariaDescribedby,
177
+ onKeyDown,
178
+ ...rest
179
+ } = props;
180
+ const internalId = react.useId();
181
+ const descriptionId = react.useId();
182
+ const styles = checkboxTv({
183
+ type: "checkbox",
184
+ variant,
185
+ disabled,
186
+ checked: value,
187
+ focused
188
+ });
189
+ const handleChange = usehooksTs.useEventCallback((e) => {
190
+ if (readOnly) return;
191
+ onChange?.(e.target.checked);
192
+ });
193
+ const handleKeyDown = usehooksTs.useEventCallback((e) => {
194
+ if (readOnly) return;
195
+ if (e.key === " " || e.key === "Enter") {
196
+ e.preventDefault();
197
+ onChange?.(!value);
198
+ }
199
+ onKeyDown?.(e);
200
+ });
201
+ const renderChildren = () => {
202
+ if (typeof children === "string" || typeof children === "number") {
203
+ return /* @__PURE__ */ jsxRuntime.jsx(CheckboxLabel, { children });
204
+ }
205
+ return children;
206
+ };
207
+ return /* @__PURE__ */ jsxRuntime.jsx(
208
+ CheckboxContext.Provider,
209
+ {
210
+ value: {
211
+ value,
212
+ onChange: (val) => onChange?.(val),
213
+ disabled,
214
+ id: id || internalId,
215
+ descriptionId,
216
+ variant,
217
+ mixed
218
+ },
219
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: shared.tcx(styles.root(), className), children: [
220
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pointer-events-none relative", children: [
221
+ /* @__PURE__ */ jsxRuntime.jsx(
222
+ "input",
223
+ {
224
+ ref,
225
+ className: styles.input(),
226
+ type: "checkbox",
227
+ id: id || internalId,
228
+ checked: value,
229
+ disabled: disabled || readOnly,
230
+ onChange: handleChange,
231
+ "aria-label": ariaLabel,
232
+ "aria-describedby": ariaDescribedby || descriptionId,
233
+ "aria-checked": mixed ? "mixed" : value,
234
+ "aria-disabled": disabled || readOnly,
235
+ role: "checkbox",
236
+ onKeyDown: handleKeyDown,
237
+ ...rest
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.box(), children: value && (mixed ? /* @__PURE__ */ jsxRuntime.jsx(iconsReact.Indeterminate, {}) : /* @__PURE__ */ jsxRuntime.jsx(iconsReact.Check, {})) })
241
+ ] }),
242
+ renderChildren()
243
+ ] })
244
+ }
245
+ );
246
+ });
247
+ var MemoizedCheckbox = react.memo(CheckboxBase);
248
+ var Checkbox2 = MemoizedCheckbox;
249
+ Checkbox2.Label = CheckboxLabel;
250
+ Checkbox2.displayName = "Checkbox";
251
+
252
+ exports.Checkbox = Checkbox2;
253
+ exports.useCheckboxContext = useCheckboxContext;
254
+ //# sourceMappingURL=index.cjs.map
255
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/tv.ts","../src/checkbox-label.tsx","../src/checkbox.tsx"],"names":["createContext","useContext","tcv","memo","forwardRef","CheckboxLabel","jsx","tcx","useId","useEventCallback","jsxs","Indeterminate","Check","Checkbox"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext, useContext } from \"react\"\n\nexport interface CheckboxContextValue {\n descriptionId?: string\n disabled?: boolean\n id: string\n mixed?: boolean\n onChange: (value: boolean) => void\n value?: boolean\n variant?: \"default\" | \"accent\" | \"outline\"\n}\n\nexport const CheckboxContext = createContext<CheckboxContextValue | null>(null)\n\nexport function useCheckboxContext() {\n const context = useContext(CheckboxContext)\n if (!context) {\n throw new Error(\"Checkbox components must be used within a Checkbox component\")\n }\n return context\n}\n","import { tcv } from \"@choice-ui/shared\"\n\nexport const checkboxTv = tcv({\n slots: {\n root: \"flex items-center select-none\",\n box: [\"relative flex size-4 items-center justify-center\", \"border border-solid\"],\n input: \"peer pointer-events-auto absolute inset-0 appearance-none opacity-0\",\n label: \"pl-2\",\n },\n variants: {\n type: {\n checkbox: {\n box: \"rounded-md\",\n },\n radio: {\n box: \"rounded-full\",\n },\n },\n variant: {\n default: {},\n accent: {},\n outline: {},\n },\n checked: {\n true: {},\n false: {},\n },\n disabled: {\n true: {},\n false: {},\n },\n focused: {\n true: {},\n false: {},\n },\n },\n compoundVariants: [\n // 未选中状态\n {\n variant: [\"default\", \"accent\"],\n checked: false,\n disabled: false,\n focused: false,\n class: {\n box: \"bg-secondary-background border-default-boundary\",\n },\n },\n {\n variant: \"outline\",\n checked: false,\n disabled: false,\n focused: false,\n class: {\n box: [\"border-default-foreground\", \"peer-focus-visible:border-selected-boundary\"],\n },\n },\n // 选中状态 - default\n {\n variant: \"default\",\n checked: true,\n disabled: false,\n focused: false,\n class: {\n box: [\n \"bg-secondary-background border-default-boundary\",\n \"peer-focus-visible:border-selected-strong-boundary\",\n ],\n },\n },\n // 选中状态 - accent & outline\n {\n variant: [\"accent\", \"outline\"],\n checked: true,\n disabled: false,\n focused: false,\n class: {\n box: [\n \"bg-accent-background border-selected-strong-boundary text-on-accent-foreground\",\n \"peer-focus-visible:border-selected-strong-boundary\",\n \"peer-focus-visible:text-on-accent-foreground\",\n \"peer-focus-visible:shadow-checked-focused\",\n ],\n },\n },\n {\n variant: [\"default\", \"accent\", \"outline\"],\n checked: false,\n disabled: false,\n focused: true,\n class: {\n box: \"border-selected-boundary\",\n },\n },\n {\n variant: \"default\",\n checked: true,\n disabled: false,\n focused: true,\n class: {\n box: \"border-selected-strong-boundary\",\n },\n },\n {\n variant: [\"accent\", \"outline\"],\n checked: true,\n disabled: false,\n focused: true,\n class: {\n box: \"text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused\",\n },\n },\n {\n variant: [\"accent\", \"outline\", \"default\"],\n disabled: true,\n class: {\n root: \"text-default-background\",\n box: \"border-disabled-background bg-disabled-background\",\n label: \"text-disabled-foreground\",\n },\n },\n ],\n defaultVariants: {\n variant: \"default\",\n checked: false,\n disabled: false,\n focused: false,\n },\n})\n","import { tcx } from \"@choice-ui/shared\"\nimport { forwardRef, HTMLProps, memo, ReactNode } from \"react\"\nimport { useCheckboxContext } from \"./context\"\nimport { checkboxTv } from \"./tv\"\n\nexport interface CheckboxLabelProps extends Omit<\n HTMLProps<HTMLLabelElement>,\n \"htmlFor\" | \"id\" | \"disabled\"\n> {\n children: ReactNode\n}\n\nexport const CheckboxLabel = memo(\n forwardRef<HTMLLabelElement, CheckboxLabelProps>(function CheckboxLabel(props, ref) {\n const { children, className, ...rest } = props\n const { id, descriptionId, disabled } = useCheckboxContext()\n const styles = checkboxTv({ disabled })\n\n return (\n <label\n ref={ref}\n id={descriptionId}\n htmlFor={id}\n className={tcx(styles.label(), className)}\n {...rest}\n >\n {children}\n </label>\n )\n }),\n)\n\nCheckboxLabel.displayName = \"Checkbox.Label\"\n","import { tcx } from \"@choice-ui/shared\"\nimport { Check, Indeterminate } from \"@choiceform/icons-react\"\nimport { forwardRef, HTMLProps, memo, ReactNode, useId } from \"react\"\nimport { useEventCallback } from \"usehooks-ts\"\nimport { CheckboxLabel } from \"./checkbox-label\"\nimport { CheckboxContext } from \"./context\"\nimport { checkboxTv } from \"./tv\"\n\nexport interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, \"value\" | \"onChange\"> {\n children?: ReactNode\n className?: string\n focused?: boolean\n mixed?: boolean\n onChange?: (value: boolean) => void\n readOnly?: boolean\n value?: boolean\n variant?: \"default\" | \"accent\" | \"outline\"\n}\n\nconst CheckboxBase = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(props, ref) {\n const {\n value,\n onChange,\n disabled,\n readOnly = false,\n variant = \"default\",\n className,\n focused,\n mixed,\n children,\n id,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedby,\n onKeyDown,\n ...rest\n } = props\n const internalId = useId()\n const descriptionId = useId()\n\n const styles = checkboxTv({\n type: \"checkbox\",\n variant,\n disabled,\n checked: value,\n focused: focused,\n })\n\n const handleChange = useEventCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n if (readOnly) return\n onChange?.(e.target.checked)\n })\n\n const handleKeyDown = useEventCallback((e: React.KeyboardEvent<HTMLInputElement>) => {\n if (readOnly) return\n if (e.key === \" \" || e.key === \"Enter\") {\n e.preventDefault()\n onChange?.(!value)\n }\n onKeyDown?.(e)\n })\n\n // 自动将字符串类型的 children 包装成 CheckboxLabel\n const renderChildren = () => {\n if (typeof children === \"string\" || typeof children === \"number\") {\n return <CheckboxLabel>{children}</CheckboxLabel>\n }\n return children\n }\n\n return (\n <CheckboxContext.Provider\n value={{\n value,\n onChange: (val) => onChange?.(val),\n disabled,\n id: id || internalId,\n descriptionId,\n variant,\n mixed,\n }}\n >\n <div className={tcx(styles.root(), className)}>\n <div className=\"pointer-events-none relative\">\n <input\n ref={ref}\n className={styles.input()}\n type=\"checkbox\"\n id={id || internalId}\n checked={value}\n disabled={disabled || readOnly}\n onChange={handleChange}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedby || descriptionId}\n aria-checked={mixed ? \"mixed\" : value}\n aria-disabled={disabled || readOnly}\n role=\"checkbox\"\n onKeyDown={handleKeyDown}\n {...rest}\n />\n\n <div className={styles.box()}>{value && (mixed ? <Indeterminate /> : <Check />)}</div>\n </div>\n\n {renderChildren()}\n </div>\n </CheckboxContext.Provider>\n )\n})\n\nconst MemoizedCheckbox = memo(CheckboxBase) as unknown as CheckboxType\n\ninterface CheckboxType {\n (props: CheckboxProps & { ref?: React.Ref<HTMLInputElement> }): JSX.Element\n Label: typeof CheckboxLabel\n displayName?: string\n}\n\nexport const Checkbox = MemoizedCheckbox as CheckboxType\nCheckbox.Label = CheckboxLabel\nCheckbox.displayName = \"Checkbox\"\n"]}
@@ -0,0 +1,39 @@
1
+ import * as react from 'react';
2
+ import { HTMLProps, ReactNode } from 'react';
3
+
4
+ interface CheckboxLabelProps extends Omit<HTMLProps<HTMLLabelElement>, "htmlFor" | "id" | "disabled"> {
5
+ children: ReactNode;
6
+ }
7
+ declare const CheckboxLabel: react.MemoExoticComponent<react.ForwardRefExoticComponent<Omit<CheckboxLabelProps, "ref"> & react.RefAttributes<HTMLLabelElement>>>;
8
+
9
+ interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
10
+ children?: ReactNode;
11
+ className?: string;
12
+ focused?: boolean;
13
+ mixed?: boolean;
14
+ onChange?: (value: boolean) => void;
15
+ readOnly?: boolean;
16
+ value?: boolean;
17
+ variant?: "default" | "accent" | "outline";
18
+ }
19
+ interface CheckboxType {
20
+ (props: CheckboxProps & {
21
+ ref?: React.Ref<HTMLInputElement>;
22
+ }): JSX.Element;
23
+ Label: typeof CheckboxLabel;
24
+ displayName?: string;
25
+ }
26
+ declare const Checkbox: CheckboxType;
27
+
28
+ interface CheckboxContextValue {
29
+ descriptionId?: string;
30
+ disabled?: boolean;
31
+ id: string;
32
+ mixed?: boolean;
33
+ onChange: (value: boolean) => void;
34
+ value?: boolean;
35
+ variant?: "default" | "accent" | "outline";
36
+ }
37
+ declare function useCheckboxContext(): CheckboxContextValue;
38
+
39
+ export { Checkbox, type CheckboxContextValue, type CheckboxLabelProps, type CheckboxProps, useCheckboxContext };
@@ -0,0 +1,39 @@
1
+ import * as react from 'react';
2
+ import { HTMLProps, ReactNode } from 'react';
3
+
4
+ interface CheckboxLabelProps extends Omit<HTMLProps<HTMLLabelElement>, "htmlFor" | "id" | "disabled"> {
5
+ children: ReactNode;
6
+ }
7
+ declare const CheckboxLabel: react.MemoExoticComponent<react.ForwardRefExoticComponent<Omit<CheckboxLabelProps, "ref"> & react.RefAttributes<HTMLLabelElement>>>;
8
+
9
+ interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
10
+ children?: ReactNode;
11
+ className?: string;
12
+ focused?: boolean;
13
+ mixed?: boolean;
14
+ onChange?: (value: boolean) => void;
15
+ readOnly?: boolean;
16
+ value?: boolean;
17
+ variant?: "default" | "accent" | "outline";
18
+ }
19
+ interface CheckboxType {
20
+ (props: CheckboxProps & {
21
+ ref?: React.Ref<HTMLInputElement>;
22
+ }): JSX.Element;
23
+ Label: typeof CheckboxLabel;
24
+ displayName?: string;
25
+ }
26
+ declare const Checkbox: CheckboxType;
27
+
28
+ interface CheckboxContextValue {
29
+ descriptionId?: string;
30
+ disabled?: boolean;
31
+ id: string;
32
+ mixed?: boolean;
33
+ onChange: (value: boolean) => void;
34
+ value?: boolean;
35
+ variant?: "default" | "accent" | "outline";
36
+ }
37
+ declare function useCheckboxContext(): CheckboxContextValue;
38
+
39
+ export { Checkbox, type CheckboxContextValue, type CheckboxLabelProps, type CheckboxProps, useCheckboxContext };
package/dist/index.js ADDED
@@ -0,0 +1,252 @@
1
+ import { tcv, tcx } from '@choice-ui/shared';
2
+ import { Indeterminate, Check } from '@choiceform/icons-react';
3
+ import { createContext, memo, forwardRef, useId, useContext } from 'react';
4
+ import { useEventCallback } from 'usehooks-ts';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ // src/checkbox.tsx
8
+ var CheckboxContext = createContext(null);
9
+ function useCheckboxContext() {
10
+ const context = useContext(CheckboxContext);
11
+ if (!context) {
12
+ throw new Error("Checkbox components must be used within a Checkbox component");
13
+ }
14
+ return context;
15
+ }
16
+ var checkboxTv = tcv({
17
+ slots: {
18
+ root: "flex items-center select-none",
19
+ box: ["relative flex size-4 items-center justify-center", "border border-solid"],
20
+ input: "peer pointer-events-auto absolute inset-0 appearance-none opacity-0",
21
+ label: "pl-2"
22
+ },
23
+ variants: {
24
+ type: {
25
+ checkbox: {
26
+ box: "rounded-md"
27
+ },
28
+ radio: {
29
+ box: "rounded-full"
30
+ }
31
+ },
32
+ variant: {
33
+ default: {},
34
+ accent: {},
35
+ outline: {}
36
+ },
37
+ checked: {
38
+ true: {},
39
+ false: {}
40
+ },
41
+ disabled: {
42
+ true: {},
43
+ false: {}
44
+ },
45
+ focused: {
46
+ true: {},
47
+ false: {}
48
+ }
49
+ },
50
+ compoundVariants: [
51
+ // 未选中状态
52
+ {
53
+ variant: ["default", "accent"],
54
+ checked: false,
55
+ disabled: false,
56
+ focused: false,
57
+ class: {
58
+ box: "bg-secondary-background border-default-boundary"
59
+ }
60
+ },
61
+ {
62
+ variant: "outline",
63
+ checked: false,
64
+ disabled: false,
65
+ focused: false,
66
+ class: {
67
+ box: ["border-default-foreground", "peer-focus-visible:border-selected-boundary"]
68
+ }
69
+ },
70
+ // 选中状态 - default
71
+ {
72
+ variant: "default",
73
+ checked: true,
74
+ disabled: false,
75
+ focused: false,
76
+ class: {
77
+ box: [
78
+ "bg-secondary-background border-default-boundary",
79
+ "peer-focus-visible:border-selected-strong-boundary"
80
+ ]
81
+ }
82
+ },
83
+ // 选中状态 - accent & outline
84
+ {
85
+ variant: ["accent", "outline"],
86
+ checked: true,
87
+ disabled: false,
88
+ focused: false,
89
+ class: {
90
+ box: [
91
+ "bg-accent-background border-selected-strong-boundary text-on-accent-foreground",
92
+ "peer-focus-visible:border-selected-strong-boundary",
93
+ "peer-focus-visible:text-on-accent-foreground",
94
+ "peer-focus-visible:shadow-checked-focused"
95
+ ]
96
+ }
97
+ },
98
+ {
99
+ variant: ["default", "accent", "outline"],
100
+ checked: false,
101
+ disabled: false,
102
+ focused: true,
103
+ class: {
104
+ box: "border-selected-boundary"
105
+ }
106
+ },
107
+ {
108
+ variant: "default",
109
+ checked: true,
110
+ disabled: false,
111
+ focused: true,
112
+ class: {
113
+ box: "border-selected-strong-boundary"
114
+ }
115
+ },
116
+ {
117
+ variant: ["accent", "outline"],
118
+ checked: true,
119
+ disabled: false,
120
+ focused: true,
121
+ class: {
122
+ box: "text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused"
123
+ }
124
+ },
125
+ {
126
+ variant: ["accent", "outline", "default"],
127
+ disabled: true,
128
+ class: {
129
+ root: "text-default-background",
130
+ box: "border-disabled-background bg-disabled-background",
131
+ label: "text-disabled-foreground"
132
+ }
133
+ }
134
+ ],
135
+ defaultVariants: {
136
+ variant: "default",
137
+ checked: false,
138
+ disabled: false,
139
+ focused: false
140
+ }
141
+ });
142
+ var CheckboxLabel = memo(
143
+ forwardRef(function CheckboxLabel2(props, ref) {
144
+ const { children, className, ...rest } = props;
145
+ const { id, descriptionId, disabled } = useCheckboxContext();
146
+ const styles = checkboxTv({ disabled });
147
+ return /* @__PURE__ */ jsx(
148
+ "label",
149
+ {
150
+ ref,
151
+ id: descriptionId,
152
+ htmlFor: id,
153
+ className: tcx(styles.label(), className),
154
+ ...rest,
155
+ children
156
+ }
157
+ );
158
+ })
159
+ );
160
+ CheckboxLabel.displayName = "Checkbox.Label";
161
+ var CheckboxBase = forwardRef(function Checkbox(props, ref) {
162
+ const {
163
+ value,
164
+ onChange,
165
+ disabled,
166
+ readOnly = false,
167
+ variant = "default",
168
+ className,
169
+ focused,
170
+ mixed,
171
+ children,
172
+ id,
173
+ "aria-label": ariaLabel,
174
+ "aria-describedby": ariaDescribedby,
175
+ onKeyDown,
176
+ ...rest
177
+ } = props;
178
+ const internalId = useId();
179
+ const descriptionId = useId();
180
+ const styles = checkboxTv({
181
+ type: "checkbox",
182
+ variant,
183
+ disabled,
184
+ checked: value,
185
+ focused
186
+ });
187
+ const handleChange = useEventCallback((e) => {
188
+ if (readOnly) return;
189
+ onChange?.(e.target.checked);
190
+ });
191
+ const handleKeyDown = useEventCallback((e) => {
192
+ if (readOnly) return;
193
+ if (e.key === " " || e.key === "Enter") {
194
+ e.preventDefault();
195
+ onChange?.(!value);
196
+ }
197
+ onKeyDown?.(e);
198
+ });
199
+ const renderChildren = () => {
200
+ if (typeof children === "string" || typeof children === "number") {
201
+ return /* @__PURE__ */ jsx(CheckboxLabel, { children });
202
+ }
203
+ return children;
204
+ };
205
+ return /* @__PURE__ */ jsx(
206
+ CheckboxContext.Provider,
207
+ {
208
+ value: {
209
+ value,
210
+ onChange: (val) => onChange?.(val),
211
+ disabled,
212
+ id: id || internalId,
213
+ descriptionId,
214
+ variant,
215
+ mixed
216
+ },
217
+ children: /* @__PURE__ */ jsxs("div", { className: tcx(styles.root(), className), children: [
218
+ /* @__PURE__ */ jsxs("div", { className: "pointer-events-none relative", children: [
219
+ /* @__PURE__ */ jsx(
220
+ "input",
221
+ {
222
+ ref,
223
+ className: styles.input(),
224
+ type: "checkbox",
225
+ id: id || internalId,
226
+ checked: value,
227
+ disabled: disabled || readOnly,
228
+ onChange: handleChange,
229
+ "aria-label": ariaLabel,
230
+ "aria-describedby": ariaDescribedby || descriptionId,
231
+ "aria-checked": mixed ? "mixed" : value,
232
+ "aria-disabled": disabled || readOnly,
233
+ role: "checkbox",
234
+ onKeyDown: handleKeyDown,
235
+ ...rest
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx("div", { className: styles.box(), children: value && (mixed ? /* @__PURE__ */ jsx(Indeterminate, {}) : /* @__PURE__ */ jsx(Check, {})) })
239
+ ] }),
240
+ renderChildren()
241
+ ] })
242
+ }
243
+ );
244
+ });
245
+ var MemoizedCheckbox = memo(CheckboxBase);
246
+ var Checkbox2 = MemoizedCheckbox;
247
+ Checkbox2.Label = CheckboxLabel;
248
+ Checkbox2.displayName = "Checkbox";
249
+
250
+ export { Checkbox2 as Checkbox, useCheckboxContext };
251
+ //# sourceMappingURL=index.js.map
252
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/tv.ts","../src/checkbox-label.tsx","../src/checkbox.tsx"],"names":["CheckboxLabel","forwardRef","jsx","tcx","memo","Checkbox"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext, useContext } from \"react\"\n\nexport interface CheckboxContextValue {\n descriptionId?: string\n disabled?: boolean\n id: string\n mixed?: boolean\n onChange: (value: boolean) => void\n value?: boolean\n variant?: \"default\" | \"accent\" | \"outline\"\n}\n\nexport const CheckboxContext = createContext<CheckboxContextValue | null>(null)\n\nexport function useCheckboxContext() {\n const context = useContext(CheckboxContext)\n if (!context) {\n throw new Error(\"Checkbox components must be used within a Checkbox component\")\n }\n return context\n}\n","import { tcv } from \"@choice-ui/shared\"\n\nexport const checkboxTv = tcv({\n slots: {\n root: \"flex items-center select-none\",\n box: [\"relative flex size-4 items-center justify-center\", \"border border-solid\"],\n input: \"peer pointer-events-auto absolute inset-0 appearance-none opacity-0\",\n label: \"pl-2\",\n },\n variants: {\n type: {\n checkbox: {\n box: \"rounded-md\",\n },\n radio: {\n box: \"rounded-full\",\n },\n },\n variant: {\n default: {},\n accent: {},\n outline: {},\n },\n checked: {\n true: {},\n false: {},\n },\n disabled: {\n true: {},\n false: {},\n },\n focused: {\n true: {},\n false: {},\n },\n },\n compoundVariants: [\n // 未选中状态\n {\n variant: [\"default\", \"accent\"],\n checked: false,\n disabled: false,\n focused: false,\n class: {\n box: \"bg-secondary-background border-default-boundary\",\n },\n },\n {\n variant: \"outline\",\n checked: false,\n disabled: false,\n focused: false,\n class: {\n box: [\"border-default-foreground\", \"peer-focus-visible:border-selected-boundary\"],\n },\n },\n // 选中状态 - default\n {\n variant: \"default\",\n checked: true,\n disabled: false,\n focused: false,\n class: {\n box: [\n \"bg-secondary-background border-default-boundary\",\n \"peer-focus-visible:border-selected-strong-boundary\",\n ],\n },\n },\n // 选中状态 - accent & outline\n {\n variant: [\"accent\", \"outline\"],\n checked: true,\n disabled: false,\n focused: false,\n class: {\n box: [\n \"bg-accent-background border-selected-strong-boundary text-on-accent-foreground\",\n \"peer-focus-visible:border-selected-strong-boundary\",\n \"peer-focus-visible:text-on-accent-foreground\",\n \"peer-focus-visible:shadow-checked-focused\",\n ],\n },\n },\n {\n variant: [\"default\", \"accent\", \"outline\"],\n checked: false,\n disabled: false,\n focused: true,\n class: {\n box: \"border-selected-boundary\",\n },\n },\n {\n variant: \"default\",\n checked: true,\n disabled: false,\n focused: true,\n class: {\n box: \"border-selected-strong-boundary\",\n },\n },\n {\n variant: [\"accent\", \"outline\"],\n checked: true,\n disabled: false,\n focused: true,\n class: {\n box: \"text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused\",\n },\n },\n {\n variant: [\"accent\", \"outline\", \"default\"],\n disabled: true,\n class: {\n root: \"text-default-background\",\n box: \"border-disabled-background bg-disabled-background\",\n label: \"text-disabled-foreground\",\n },\n },\n ],\n defaultVariants: {\n variant: \"default\",\n checked: false,\n disabled: false,\n focused: false,\n },\n})\n","import { tcx } from \"@choice-ui/shared\"\nimport { forwardRef, HTMLProps, memo, ReactNode } from \"react\"\nimport { useCheckboxContext } from \"./context\"\nimport { checkboxTv } from \"./tv\"\n\nexport interface CheckboxLabelProps extends Omit<\n HTMLProps<HTMLLabelElement>,\n \"htmlFor\" | \"id\" | \"disabled\"\n> {\n children: ReactNode\n}\n\nexport const CheckboxLabel = memo(\n forwardRef<HTMLLabelElement, CheckboxLabelProps>(function CheckboxLabel(props, ref) {\n const { children, className, ...rest } = props\n const { id, descriptionId, disabled } = useCheckboxContext()\n const styles = checkboxTv({ disabled })\n\n return (\n <label\n ref={ref}\n id={descriptionId}\n htmlFor={id}\n className={tcx(styles.label(), className)}\n {...rest}\n >\n {children}\n </label>\n )\n }),\n)\n\nCheckboxLabel.displayName = \"Checkbox.Label\"\n","import { tcx } from \"@choice-ui/shared\"\nimport { Check, Indeterminate } from \"@choiceform/icons-react\"\nimport { forwardRef, HTMLProps, memo, ReactNode, useId } from \"react\"\nimport { useEventCallback } from \"usehooks-ts\"\nimport { CheckboxLabel } from \"./checkbox-label\"\nimport { CheckboxContext } from \"./context\"\nimport { checkboxTv } from \"./tv\"\n\nexport interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, \"value\" | \"onChange\"> {\n children?: ReactNode\n className?: string\n focused?: boolean\n mixed?: boolean\n onChange?: (value: boolean) => void\n readOnly?: boolean\n value?: boolean\n variant?: \"default\" | \"accent\" | \"outline\"\n}\n\nconst CheckboxBase = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(props, ref) {\n const {\n value,\n onChange,\n disabled,\n readOnly = false,\n variant = \"default\",\n className,\n focused,\n mixed,\n children,\n id,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedby,\n onKeyDown,\n ...rest\n } = props\n const internalId = useId()\n const descriptionId = useId()\n\n const styles = checkboxTv({\n type: \"checkbox\",\n variant,\n disabled,\n checked: value,\n focused: focused,\n })\n\n const handleChange = useEventCallback((e: React.ChangeEvent<HTMLInputElement>) => {\n if (readOnly) return\n onChange?.(e.target.checked)\n })\n\n const handleKeyDown = useEventCallback((e: React.KeyboardEvent<HTMLInputElement>) => {\n if (readOnly) return\n if (e.key === \" \" || e.key === \"Enter\") {\n e.preventDefault()\n onChange?.(!value)\n }\n onKeyDown?.(e)\n })\n\n // 自动将字符串类型的 children 包装成 CheckboxLabel\n const renderChildren = () => {\n if (typeof children === \"string\" || typeof children === \"number\") {\n return <CheckboxLabel>{children}</CheckboxLabel>\n }\n return children\n }\n\n return (\n <CheckboxContext.Provider\n value={{\n value,\n onChange: (val) => onChange?.(val),\n disabled,\n id: id || internalId,\n descriptionId,\n variant,\n mixed,\n }}\n >\n <div className={tcx(styles.root(), className)}>\n <div className=\"pointer-events-none relative\">\n <input\n ref={ref}\n className={styles.input()}\n type=\"checkbox\"\n id={id || internalId}\n checked={value}\n disabled={disabled || readOnly}\n onChange={handleChange}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedby || descriptionId}\n aria-checked={mixed ? \"mixed\" : value}\n aria-disabled={disabled || readOnly}\n role=\"checkbox\"\n onKeyDown={handleKeyDown}\n {...rest}\n />\n\n <div className={styles.box()}>{value && (mixed ? <Indeterminate /> : <Check />)}</div>\n </div>\n\n {renderChildren()}\n </div>\n </CheckboxContext.Provider>\n )\n})\n\nconst MemoizedCheckbox = memo(CheckboxBase) as unknown as CheckboxType\n\ninterface CheckboxType {\n (props: CheckboxProps & { ref?: React.Ref<HTMLInputElement> }): JSX.Element\n Label: typeof CheckboxLabel\n displayName?: string\n}\n\nexport const Checkbox = MemoizedCheckbox as CheckboxType\nCheckbox.Label = CheckboxLabel\nCheckbox.displayName = \"Checkbox\"\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@choice-ui/checkbox",
3
+ "version": "0.0.4",
4
+ "description": "Checkbox component for Choiceform Design System",
5
+ "sideEffects": false,
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./src/index.ts",
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "import": "./dist/index.js",
18
+ "require": "./dist/index.cjs"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@choiceform/icons-react": "^1.3.8",
26
+ "usehooks-ts": "^3.1.0",
27
+ "@choice-ui/shared": "0.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.3.12",
31
+ "@types/react-dom": "^18.3.1",
32
+ "react": "^18.3.1",
33
+ "react-dom": "^18.3.1",
34
+ "rimraf": "^6.0.1",
35
+ "tsup": "^8.5.0",
36
+ "typescript": "^5.5.3"
37
+ },
38
+ "peerDependencies": {
39
+ "react": ">=18.0.0",
40
+ "react-dom": ">=18.0.0"
41
+ },
42
+ "scripts": {
43
+ "build": "tsup",
44
+ "build:watch": "tsup --watch",
45
+ "clean": "rimraf dist"
46
+ }
47
+ }
@@ -0,0 +1,33 @@
1
+ import { tcx } from "@choice-ui/shared"
2
+ import { forwardRef, HTMLProps, memo, ReactNode } from "react"
3
+ import { useCheckboxContext } from "./context"
4
+ import { checkboxTv } from "./tv"
5
+
6
+ export interface CheckboxLabelProps extends Omit<
7
+ HTMLProps<HTMLLabelElement>,
8
+ "htmlFor" | "id" | "disabled"
9
+ > {
10
+ children: ReactNode
11
+ }
12
+
13
+ export const CheckboxLabel = memo(
14
+ forwardRef<HTMLLabelElement, CheckboxLabelProps>(function CheckboxLabel(props, ref) {
15
+ const { children, className, ...rest } = props
16
+ const { id, descriptionId, disabled } = useCheckboxContext()
17
+ const styles = checkboxTv({ disabled })
18
+
19
+ return (
20
+ <label
21
+ ref={ref}
22
+ id={descriptionId}
23
+ htmlFor={id}
24
+ className={tcx(styles.label(), className)}
25
+ {...rest}
26
+ >
27
+ {children}
28
+ </label>
29
+ )
30
+ }),
31
+ )
32
+
33
+ CheckboxLabel.displayName = "Checkbox.Label"
@@ -0,0 +1,120 @@
1
+ import { tcx } from "@choice-ui/shared"
2
+ import { Check, Indeterminate } from "@choiceform/icons-react"
3
+ import { forwardRef, HTMLProps, memo, ReactNode, useId } from "react"
4
+ import { useEventCallback } from "usehooks-ts"
5
+ import { CheckboxLabel } from "./checkbox-label"
6
+ import { CheckboxContext } from "./context"
7
+ import { checkboxTv } from "./tv"
8
+
9
+ export interface CheckboxProps extends Omit<HTMLProps<HTMLInputElement>, "value" | "onChange"> {
10
+ children?: ReactNode
11
+ className?: string
12
+ focused?: boolean
13
+ mixed?: boolean
14
+ onChange?: (value: boolean) => void
15
+ readOnly?: boolean
16
+ value?: boolean
17
+ variant?: "default" | "accent" | "outline"
18
+ }
19
+
20
+ const CheckboxBase = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(props, ref) {
21
+ const {
22
+ value,
23
+ onChange,
24
+ disabled,
25
+ readOnly = false,
26
+ variant = "default",
27
+ className,
28
+ focused,
29
+ mixed,
30
+ children,
31
+ id,
32
+ "aria-label": ariaLabel,
33
+ "aria-describedby": ariaDescribedby,
34
+ onKeyDown,
35
+ ...rest
36
+ } = props
37
+ const internalId = useId()
38
+ const descriptionId = useId()
39
+
40
+ const styles = checkboxTv({
41
+ type: "checkbox",
42
+ variant,
43
+ disabled,
44
+ checked: value,
45
+ focused: focused,
46
+ })
47
+
48
+ const handleChange = useEventCallback((e: React.ChangeEvent<HTMLInputElement>) => {
49
+ if (readOnly) return
50
+ onChange?.(e.target.checked)
51
+ })
52
+
53
+ const handleKeyDown = useEventCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
54
+ if (readOnly) return
55
+ if (e.key === " " || e.key === "Enter") {
56
+ e.preventDefault()
57
+ onChange?.(!value)
58
+ }
59
+ onKeyDown?.(e)
60
+ })
61
+
62
+ // 自动将字符串类型的 children 包装成 CheckboxLabel
63
+ const renderChildren = () => {
64
+ if (typeof children === "string" || typeof children === "number") {
65
+ return <CheckboxLabel>{children}</CheckboxLabel>
66
+ }
67
+ return children
68
+ }
69
+
70
+ return (
71
+ <CheckboxContext.Provider
72
+ value={{
73
+ value,
74
+ onChange: (val) => onChange?.(val),
75
+ disabled,
76
+ id: id || internalId,
77
+ descriptionId,
78
+ variant,
79
+ mixed,
80
+ }}
81
+ >
82
+ <div className={tcx(styles.root(), className)}>
83
+ <div className="pointer-events-none relative">
84
+ <input
85
+ ref={ref}
86
+ className={styles.input()}
87
+ type="checkbox"
88
+ id={id || internalId}
89
+ checked={value}
90
+ disabled={disabled || readOnly}
91
+ onChange={handleChange}
92
+ aria-label={ariaLabel}
93
+ aria-describedby={ariaDescribedby || descriptionId}
94
+ aria-checked={mixed ? "mixed" : value}
95
+ aria-disabled={disabled || readOnly}
96
+ role="checkbox"
97
+ onKeyDown={handleKeyDown}
98
+ {...rest}
99
+ />
100
+
101
+ <div className={styles.box()}>{value && (mixed ? <Indeterminate /> : <Check />)}</div>
102
+ </div>
103
+
104
+ {renderChildren()}
105
+ </div>
106
+ </CheckboxContext.Provider>
107
+ )
108
+ })
109
+
110
+ const MemoizedCheckbox = memo(CheckboxBase) as unknown as CheckboxType
111
+
112
+ interface CheckboxType {
113
+ (props: CheckboxProps & { ref?: React.Ref<HTMLInputElement> }): JSX.Element
114
+ Label: typeof CheckboxLabel
115
+ displayName?: string
116
+ }
117
+
118
+ export const Checkbox = MemoizedCheckbox as CheckboxType
119
+ Checkbox.Label = CheckboxLabel
120
+ Checkbox.displayName = "Checkbox"
package/src/context.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from "react"
2
+
3
+ export interface CheckboxContextValue {
4
+ descriptionId?: string
5
+ disabled?: boolean
6
+ id: string
7
+ mixed?: boolean
8
+ onChange: (value: boolean) => void
9
+ value?: boolean
10
+ variant?: "default" | "accent" | "outline"
11
+ }
12
+
13
+ export const CheckboxContext = createContext<CheckboxContextValue | null>(null)
14
+
15
+ export function useCheckboxContext() {
16
+ const context = useContext(CheckboxContext)
17
+ if (!context) {
18
+ throw new Error("Checkbox components must be used within a Checkbox component")
19
+ }
20
+ return context
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { Checkbox } from "./checkbox"
2
+ export type { CheckboxProps } from "./checkbox"
3
+ export type { CheckboxLabelProps } from "./checkbox-label"
4
+ export { useCheckboxContext } from "./context"
5
+ export type { CheckboxContextValue } from "./context"
package/src/tv.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { tcv } from "@choice-ui/shared"
2
+
3
+ export const checkboxTv = tcv({
4
+ slots: {
5
+ root: "flex items-center select-none",
6
+ box: ["relative flex size-4 items-center justify-center", "border border-solid"],
7
+ input: "peer pointer-events-auto absolute inset-0 appearance-none opacity-0",
8
+ label: "pl-2",
9
+ },
10
+ variants: {
11
+ type: {
12
+ checkbox: {
13
+ box: "rounded-md",
14
+ },
15
+ radio: {
16
+ box: "rounded-full",
17
+ },
18
+ },
19
+ variant: {
20
+ default: {},
21
+ accent: {},
22
+ outline: {},
23
+ },
24
+ checked: {
25
+ true: {},
26
+ false: {},
27
+ },
28
+ disabled: {
29
+ true: {},
30
+ false: {},
31
+ },
32
+ focused: {
33
+ true: {},
34
+ false: {},
35
+ },
36
+ },
37
+ compoundVariants: [
38
+ // 未选中状态
39
+ {
40
+ variant: ["default", "accent"],
41
+ checked: false,
42
+ disabled: false,
43
+ focused: false,
44
+ class: {
45
+ box: "bg-secondary-background border-default-boundary",
46
+ },
47
+ },
48
+ {
49
+ variant: "outline",
50
+ checked: false,
51
+ disabled: false,
52
+ focused: false,
53
+ class: {
54
+ box: ["border-default-foreground", "peer-focus-visible:border-selected-boundary"],
55
+ },
56
+ },
57
+ // 选中状态 - default
58
+ {
59
+ variant: "default",
60
+ checked: true,
61
+ disabled: false,
62
+ focused: false,
63
+ class: {
64
+ box: [
65
+ "bg-secondary-background border-default-boundary",
66
+ "peer-focus-visible:border-selected-strong-boundary",
67
+ ],
68
+ },
69
+ },
70
+ // 选中状态 - accent & outline
71
+ {
72
+ variant: ["accent", "outline"],
73
+ checked: true,
74
+ disabled: false,
75
+ focused: false,
76
+ class: {
77
+ box: [
78
+ "bg-accent-background border-selected-strong-boundary text-on-accent-foreground",
79
+ "peer-focus-visible:border-selected-strong-boundary",
80
+ "peer-focus-visible:text-on-accent-foreground",
81
+ "peer-focus-visible:shadow-checked-focused",
82
+ ],
83
+ },
84
+ },
85
+ {
86
+ variant: ["default", "accent", "outline"],
87
+ checked: false,
88
+ disabled: false,
89
+ focused: true,
90
+ class: {
91
+ box: "border-selected-boundary",
92
+ },
93
+ },
94
+ {
95
+ variant: "default",
96
+ checked: true,
97
+ disabled: false,
98
+ focused: true,
99
+ class: {
100
+ box: "border-selected-strong-boundary",
101
+ },
102
+ },
103
+ {
104
+ variant: ["accent", "outline"],
105
+ checked: true,
106
+ disabled: false,
107
+ focused: true,
108
+ class: {
109
+ box: "text-on-accent-foreground border-selected-strong-boundary shadow-checked-focused",
110
+ },
111
+ },
112
+ {
113
+ variant: ["accent", "outline", "default"],
114
+ disabled: true,
115
+ class: {
116
+ root: "text-default-background",
117
+ box: "border-disabled-background bg-disabled-background",
118
+ label: "text-disabled-foreground",
119
+ },
120
+ },
121
+ ],
122
+ defaultVariants: {
123
+ variant: "default",
124
+ checked: false,
125
+ disabled: false,
126
+ focused: false,
127
+ },
128
+ })