@timeax/form-palette 0.0.1
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/.scaffold-cache.json +537 -0
- package/package.json +42 -0
- package/src/.scaffold-cache.json +544 -0
- package/src/adapters/axios.ts +117 -0
- package/src/adapters/index.ts +91 -0
- package/src/adapters/inertia.ts +187 -0
- package/src/core/adapter-registry.ts +87 -0
- package/src/core/bound/bind-host.ts +14 -0
- package/src/core/bound/observe-bound-field.ts +172 -0
- package/src/core/bound/wait-for-bound-field.ts +57 -0
- package/src/core/context.ts +23 -0
- package/src/core/core-provider.tsx +818 -0
- package/src/core/core-root.tsx +72 -0
- package/src/core/core-shell.tsx +44 -0
- package/src/core/errors/error-strip.tsx +71 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/map-error-bag.ts +51 -0
- package/src/core/errors/map-zod.ts +39 -0
- package/src/core/hooks/use-button.ts +220 -0
- package/src/core/hooks/use-core-context.ts +20 -0
- package/src/core/hooks/use-core-utility.ts +0 -0
- package/src/core/hooks/use-core.ts +13 -0
- package/src/core/hooks/use-field.ts +497 -0
- package/src/core/hooks/use-optional-field.ts +28 -0
- package/src/core/index.ts +0 -0
- package/src/core/registry/binder-registry.ts +82 -0
- package/src/core/registry/field-registry.ts +187 -0
- package/src/core/test.tsx +17 -0
- package/src/global.d.ts +14 -0
- package/src/index.ts +68 -0
- package/src/input/index.ts +4 -0
- package/src/input/input-field.tsx +854 -0
- package/src/input/input-layout-graph.ts +230 -0
- package/src/input/input-props.ts +190 -0
- package/src/lib/get-global-countries.ts +87 -0
- package/src/lib/utils.ts +6 -0
- package/src/presets/index.ts +0 -0
- package/src/presets/shadcn-preset.ts +0 -0
- package/src/presets/shadcn-variants/checkbox.tsx +849 -0
- package/src/presets/shadcn-variants/chips.tsx +756 -0
- package/src/presets/shadcn-variants/color.tsx +284 -0
- package/src/presets/shadcn-variants/custom.tsx +227 -0
- package/src/presets/shadcn-variants/date.tsx +796 -0
- package/src/presets/shadcn-variants/file.tsx +764 -0
- package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
- package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
- package/src/presets/shadcn-variants/number.tsx +176 -0
- package/src/presets/shadcn-variants/password.tsx +737 -0
- package/src/presets/shadcn-variants/phone.tsx +628 -0
- package/src/presets/shadcn-variants/radio.tsx +578 -0
- package/src/presets/shadcn-variants/select.tsx +956 -0
- package/src/presets/shadcn-variants/slider.tsx +622 -0
- package/src/presets/shadcn-variants/text.tsx +343 -0
- package/src/presets/shadcn-variants/textarea.tsx +66 -0
- package/src/presets/shadcn-variants/toggle.tsx +218 -0
- package/src/presets/shadcn-variants/treeselect.tsx +784 -0
- package/src/presets/ui/badge.tsx +46 -0
- package/src/presets/ui/button.tsx +60 -0
- package/src/presets/ui/calendar.tsx +214 -0
- package/src/presets/ui/checkbox.tsx +115 -0
- package/src/presets/ui/custom.tsx +0 -0
- package/src/presets/ui/dialog.tsx +141 -0
- package/src/presets/ui/field.tsx +246 -0
- package/src/presets/ui/input-mask.tsx +739 -0
- package/src/presets/ui/input-otp.tsx +77 -0
- package/src/presets/ui/input.tsx +1011 -0
- package/src/presets/ui/label.tsx +22 -0
- package/src/presets/ui/number.tsx +1370 -0
- package/src/presets/ui/popover.tsx +46 -0
- package/src/presets/ui/radio-group.tsx +43 -0
- package/src/presets/ui/scroll-area.tsx +56 -0
- package/src/presets/ui/select.tsx +190 -0
- package/src/presets/ui/separator.tsx +28 -0
- package/src/presets/ui/slider.tsx +61 -0
- package/src/presets/ui/switch.tsx +32 -0
- package/src/presets/ui/textarea.tsx +634 -0
- package/src/presets/ui/time-dropdowns.tsx +350 -0
- package/src/schema/adapter.ts +217 -0
- package/src/schema/core.ts +429 -0
- package/src/schema/field-map.ts +0 -0
- package/src/schema/field.ts +224 -0
- package/src/schema/index.ts +0 -0
- package/src/schema/input-field.ts +260 -0
- package/src/schema/presets.ts +0 -0
- package/src/schema/variant.ts +216 -0
- package/src/variants/core/checkbox.tsx +54 -0
- package/src/variants/core/chips.tsx +22 -0
- package/src/variants/core/color.tsx +16 -0
- package/src/variants/core/custom.tsx +18 -0
- package/src/variants/core/date.tsx +25 -0
- package/src/variants/core/file.tsx +9 -0
- package/src/variants/core/keyvalue.tsx +12 -0
- package/src/variants/core/multiselect.tsx +28 -0
- package/src/variants/core/number.tsx +115 -0
- package/src/variants/core/password.tsx +35 -0
- package/src/variants/core/phone.tsx +16 -0
- package/src/variants/core/radio.tsx +38 -0
- package/src/variants/core/select.tsx +15 -0
- package/src/variants/core/slider.tsx +55 -0
- package/src/variants/core/text.tsx +114 -0
- package/src/variants/core/textarea.tsx +22 -0
- package/src/variants/core/toggle.tsx +50 -0
- package/src/variants/core/treeselect.tsx +11 -0
- package/src/variants/helpers/selection-summary.tsx +236 -0
- package/src/variants/index.ts +75 -0
- package/src/variants/registry.ts +38 -0
- package/src/variants/select-shared.ts +0 -0
- package/src/variants/shared.ts +126 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Checkbox } from "@/presets/ui/checkbox";
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
// Types
|
|
8
|
+
// ─────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type CheckboxSize = "sm" | "md" | "lg";
|
|
11
|
+
export type CheckboxDensity = "compact" | "comfortable" | "loose";
|
|
12
|
+
export type CheckboxLayoutMode = "list" | "grid";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal state we store in the value list.
|
|
16
|
+
* "none" never goes into the external value.
|
|
17
|
+
*/
|
|
18
|
+
export type CheckboxTriStateValue = true | false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Internal state we pass to the Shadcn checkbox.
|
|
22
|
+
* "none" is used to represent "no stance yet".
|
|
23
|
+
*/
|
|
24
|
+
export type CheckboxInternalState = true | false | "none";
|
|
25
|
+
|
|
26
|
+
export interface CheckboxGroupEntry<TValue> {
|
|
27
|
+
value: TValue;
|
|
28
|
+
state: CheckboxTriStateValue; // true or false only
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CheckboxGroupValue<TValue> =
|
|
32
|
+
| readonly CheckboxGroupEntry<TValue>[]
|
|
33
|
+
| undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Single checkbox value.
|
|
37
|
+
* undefined → "none"
|
|
38
|
+
*/
|
|
39
|
+
export type CheckboxSingleValue = boolean | undefined;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Public union type for the variant's value.
|
|
43
|
+
*
|
|
44
|
+
* - In single mode: we expect CheckboxSingleValue
|
|
45
|
+
* - In group mode: we expect CheckboxGroupValue<TValue>
|
|
46
|
+
*
|
|
47
|
+
* At the type level this is a union; at runtime we branch using `single`.
|
|
48
|
+
*/
|
|
49
|
+
export type CheckboxVariantValue<TValue> =
|
|
50
|
+
| CheckboxSingleValue
|
|
51
|
+
| CheckboxGroupValue<TValue>;
|
|
52
|
+
|
|
53
|
+
export interface CheckboxItem<TValue> {
|
|
54
|
+
value: TValue;
|
|
55
|
+
label: React.ReactNode;
|
|
56
|
+
description?: React.ReactNode;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
key?: React.Key;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Override tri-state behaviour for this item.
|
|
62
|
+
* If undefined, variant-level `tristate` is used.
|
|
63
|
+
*/
|
|
64
|
+
tristate?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CheckboxMappers<TItem, TValue> {
|
|
68
|
+
getValue: (item: TItem, index: number) => TValue;
|
|
69
|
+
getLabel: (item: TItem, index: number) => React.ReactNode;
|
|
70
|
+
getDescription?: (item: TItem, index: number) => React.ReactNode;
|
|
71
|
+
isDisabled?: (item: TItem, index: number) => boolean;
|
|
72
|
+
getKey?: (item: TItem, index: number) => React.Key;
|
|
73
|
+
getTristate?: (item: TItem, index: number) => boolean | undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CheckboxRenderOptionContext<TValue> {
|
|
77
|
+
item: CheckboxItem<TValue>;
|
|
78
|
+
index: number;
|
|
79
|
+
state: CheckboxInternalState;
|
|
80
|
+
effectiveTristate: boolean;
|
|
81
|
+
disabled: boolean;
|
|
82
|
+
size: CheckboxSize;
|
|
83
|
+
density: CheckboxDensity;
|
|
84
|
+
checkboxId?: string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Prebuilt Shadcn checkbox node.
|
|
88
|
+
*/
|
|
89
|
+
checkbox: React.ReactNode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* UI props for both single and group modes.
|
|
94
|
+
*/
|
|
95
|
+
export interface ShadcnCheckboxUiProps<TItem, TValue> {
|
|
96
|
+
/**
|
|
97
|
+
* Group mode:
|
|
98
|
+
* - Required when `single` is not true.
|
|
99
|
+
*
|
|
100
|
+
* Single mode:
|
|
101
|
+
* - Optional; if provided, `items[0]` can supply label/description.
|
|
102
|
+
*/
|
|
103
|
+
items?: readonly TItem[];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mapping functions for arbitrary item shapes.
|
|
107
|
+
* Takes precedence over optionValue/optionLabel.
|
|
108
|
+
*/
|
|
109
|
+
mappers?: CheckboxMappers<TItem, TValue>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Property name that holds the value on each item.
|
|
113
|
+
*
|
|
114
|
+
* Example:
|
|
115
|
+
* items = [{ id: "read", label: "Read" }]
|
|
116
|
+
* optionValue = "id"
|
|
117
|
+
*/
|
|
118
|
+
optionValue?: keyof TItem;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Property name that holds the label on each item.
|
|
122
|
+
*
|
|
123
|
+
* Example:
|
|
124
|
+
* items = [{ id: "read", title: "Read" }]
|
|
125
|
+
* optionLabel = "title"
|
|
126
|
+
*/
|
|
127
|
+
optionLabel?: keyof TItem;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Custom renderer for each option row.
|
|
131
|
+
*/
|
|
132
|
+
renderOption?: (
|
|
133
|
+
ctx: CheckboxRenderOptionContext<TValue>
|
|
134
|
+
) => React.ReactNode;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* If true, treat this variant as a single checkbox instead of a group.
|
|
138
|
+
*
|
|
139
|
+
* Value is then CheckboxSingleValue (boolean | undefined).
|
|
140
|
+
*/
|
|
141
|
+
single?: boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Variant-level default tri-state behaviour.
|
|
145
|
+
*
|
|
146
|
+
* - In single mode: directly controls tri-state for the single checkbox.
|
|
147
|
+
* - In group mode: default for all items, unless item.tristate overrides.
|
|
148
|
+
*/
|
|
149
|
+
tristate?: boolean;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Layout mode in group mode: vertical list or CSS grid.
|
|
153
|
+
*/
|
|
154
|
+
layout?: CheckboxLayoutMode;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Number of columns in grid mode.
|
|
158
|
+
* Default: 2.
|
|
159
|
+
*/
|
|
160
|
+
columns?: number;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Gap between items in px.
|
|
164
|
+
*/
|
|
165
|
+
itemGapPx?: number;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Visual size of the checkbox / text.
|
|
169
|
+
* Default: "md".
|
|
170
|
+
*/
|
|
171
|
+
size?: CheckboxSize;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Vertical density of each row.
|
|
175
|
+
* Default: "comfortable".
|
|
176
|
+
*/
|
|
177
|
+
density?: CheckboxDensity;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* ARIA attributes for the group wrapper.
|
|
181
|
+
*/
|
|
182
|
+
"aria-label"?: string;
|
|
183
|
+
"aria-labelledby"?: string;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Wrapper class for the entire group (or single field).
|
|
187
|
+
*/
|
|
188
|
+
groupClassName?: string;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extra classes for each option row (group mode).
|
|
192
|
+
*/
|
|
193
|
+
optionClassName?: string;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extra classes for the option label text.
|
|
197
|
+
*/
|
|
198
|
+
labelClassName?: string;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extra classes for the option description text.
|
|
202
|
+
*/
|
|
203
|
+
descriptionClassName?: string;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Single-mode inline label (if you want variant-level text).
|
|
207
|
+
* Usually you'll rely on InputField's label instead.
|
|
208
|
+
*/
|
|
209
|
+
singleLabel?: React.ReactNode;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Single-mode description text under the label.
|
|
213
|
+
*/
|
|
214
|
+
singleDescription?: React.ReactNode;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Full props for the Shadcn-based checkbox variant.
|
|
219
|
+
*
|
|
220
|
+
* TValue: primitive or object key
|
|
221
|
+
* TItem: item shape used to build checkbox items
|
|
222
|
+
*/
|
|
223
|
+
export type ShadcnCheckboxVariantProps<
|
|
224
|
+
TValue,
|
|
225
|
+
TItem = CheckboxItem<TValue>
|
|
226
|
+
> = ShadcnCheckboxUiProps<TItem, TValue> &
|
|
227
|
+
Pick<
|
|
228
|
+
VariantBaseProps<CheckboxVariantValue<TValue>>,
|
|
229
|
+
"value" | "onValue" | "error" | "disabled" | "required"
|
|
230
|
+
> & {
|
|
231
|
+
id?: string;
|
|
232
|
+
className?: string; // alias for groupClassName
|
|
233
|
+
name?: string; // optional: name for native form post in group mode
|
|
234
|
+
"aria-describedby"?: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────
|
|
238
|
+
// Helpers
|
|
239
|
+
// ─────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function paddingForDensity(density: CheckboxDensity): string {
|
|
242
|
+
switch (density) {
|
|
243
|
+
case "compact":
|
|
244
|
+
// return "py-1.5";
|
|
245
|
+
case "loose":
|
|
246
|
+
return "py-2";
|
|
247
|
+
case "comfortable":
|
|
248
|
+
default:
|
|
249
|
+
return "py-0";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function labelTextSize(size: CheckboxSize): string {
|
|
254
|
+
switch (size) {
|
|
255
|
+
case "sm":
|
|
256
|
+
return "text-xs";
|
|
257
|
+
case "lg":
|
|
258
|
+
return "text-base";
|
|
259
|
+
case "md":
|
|
260
|
+
default:
|
|
261
|
+
return "text-sm";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function descriptionTextSize(size: CheckboxSize): string {
|
|
266
|
+
switch (size) {
|
|
267
|
+
case "sm":
|
|
268
|
+
return "text-[0.7rem]";
|
|
269
|
+
case "lg":
|
|
270
|
+
return "text-sm";
|
|
271
|
+
case "md":
|
|
272
|
+
default:
|
|
273
|
+
return "text-xs";
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Normalize arbitrary items to CheckboxItem<TValue>[] using:
|
|
279
|
+
* 1) mappers,
|
|
280
|
+
* 2) optionValue/optionLabel,
|
|
281
|
+
* 3) native CheckboxItem fields.
|
|
282
|
+
*/
|
|
283
|
+
function normalizeItems<TItem, TValue>(
|
|
284
|
+
items: readonly TItem[] | undefined,
|
|
285
|
+
mappers?: CheckboxMappers<TItem, TValue>,
|
|
286
|
+
optionValueKey?: keyof TItem,
|
|
287
|
+
optionLabelKey?: keyof TItem
|
|
288
|
+
): CheckboxItem<TValue>[] {
|
|
289
|
+
if (!items || !items.length) return [];
|
|
290
|
+
|
|
291
|
+
// 1) Explicit mappers win
|
|
292
|
+
if (mappers) {
|
|
293
|
+
return items.map((item, index) => ({
|
|
294
|
+
value: mappers.getValue(item, index),
|
|
295
|
+
label: mappers.getLabel(item, index),
|
|
296
|
+
description: mappers.getDescription
|
|
297
|
+
? mappers.getDescription(item, index)
|
|
298
|
+
: undefined,
|
|
299
|
+
disabled: mappers.isDisabled
|
|
300
|
+
? mappers.isDisabled(item, index)
|
|
301
|
+
: false,
|
|
302
|
+
key: mappers.getKey ? mappers.getKey(item, index) : index,
|
|
303
|
+
tristate: mappers.getTristate
|
|
304
|
+
? mappers.getTristate(item, index)
|
|
305
|
+
: undefined,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 2) optionValue / optionLabel
|
|
310
|
+
if (optionValueKey || optionLabelKey) {
|
|
311
|
+
return items.map((item, index) => {
|
|
312
|
+
const anyItem = item as any;
|
|
313
|
+
|
|
314
|
+
const rawValue =
|
|
315
|
+
optionValueKey != null
|
|
316
|
+
? anyItem[optionValueKey as string]
|
|
317
|
+
: anyItem.value;
|
|
318
|
+
|
|
319
|
+
const value = rawValue as TValue;
|
|
320
|
+
|
|
321
|
+
const rawLabel =
|
|
322
|
+
optionLabelKey != null
|
|
323
|
+
? anyItem[optionLabelKey as string]
|
|
324
|
+
: anyItem.label ?? String(rawValue ?? index);
|
|
325
|
+
|
|
326
|
+
const description = anyItem.description;
|
|
327
|
+
const disabled = !!anyItem.disabled;
|
|
328
|
+
const key: React.Key = anyItem.key ?? index;
|
|
329
|
+
const tristate = anyItem.tristate as boolean | undefined;
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
value,
|
|
333
|
+
label: rawLabel,
|
|
334
|
+
description,
|
|
335
|
+
disabled,
|
|
336
|
+
key,
|
|
337
|
+
tristate,
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 3) Assume already CheckboxItem<TValue>
|
|
343
|
+
return items as unknown as CheckboxItem<TValue>[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isEqualValue(a: unknown, b: unknown): boolean {
|
|
347
|
+
return Object.is(a, b);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Extract group value from the union.
|
|
352
|
+
*/
|
|
353
|
+
function asGroupValue<TValue>(
|
|
354
|
+
value: CheckboxVariantValue<TValue>
|
|
355
|
+
): CheckboxGroupValue<TValue> {
|
|
356
|
+
if (!value) return undefined;
|
|
357
|
+
if (Array.isArray(value)) return value;
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Extract single value from the union.
|
|
363
|
+
*/
|
|
364
|
+
function asSingleValue(
|
|
365
|
+
value: CheckboxVariantValue<unknown>
|
|
366
|
+
): CheckboxSingleValue {
|
|
367
|
+
if (Array.isArray(value)) return undefined;
|
|
368
|
+
if (typeof value === "boolean" || value === undefined) return value;
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─────────────────────────────────────────────
|
|
373
|
+
// Component
|
|
374
|
+
// ─────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
const InnerShadcnCheckboxVariant = <
|
|
377
|
+
TValue,
|
|
378
|
+
TItem = CheckboxItem<TValue>
|
|
379
|
+
>(
|
|
380
|
+
props: ShadcnCheckboxVariantProps<TValue, TItem>,
|
|
381
|
+
ref: React.Ref<HTMLDivElement>
|
|
382
|
+
) => {
|
|
383
|
+
const {
|
|
384
|
+
// variant base
|
|
385
|
+
value,
|
|
386
|
+
onValue,
|
|
387
|
+
error,
|
|
388
|
+
disabled,
|
|
389
|
+
required,
|
|
390
|
+
|
|
391
|
+
// UI / behaviour
|
|
392
|
+
items,
|
|
393
|
+
mappers,
|
|
394
|
+
optionValue,
|
|
395
|
+
optionLabel,
|
|
396
|
+
renderOption,
|
|
397
|
+
single,
|
|
398
|
+
tristate: tristateDefault,
|
|
399
|
+
layout = "list",
|
|
400
|
+
columns = 2,
|
|
401
|
+
itemGapPx,
|
|
402
|
+
size = "md",
|
|
403
|
+
density = "comfortable",
|
|
404
|
+
|
|
405
|
+
"aria-label": ariaLabel,
|
|
406
|
+
"aria-labelledby": ariaLabelledBy,
|
|
407
|
+
"aria-describedby": ariaDescribedBy,
|
|
408
|
+
name,
|
|
409
|
+
|
|
410
|
+
groupClassName,
|
|
411
|
+
optionClassName,
|
|
412
|
+
labelClassName,
|
|
413
|
+
descriptionClassName,
|
|
414
|
+
|
|
415
|
+
className, // alias for groupClassName
|
|
416
|
+
|
|
417
|
+
singleLabel,
|
|
418
|
+
singleDescription,
|
|
419
|
+
|
|
420
|
+
id,
|
|
421
|
+
...restProps
|
|
422
|
+
} = props;
|
|
423
|
+
|
|
424
|
+
const hasError = !!error;
|
|
425
|
+
const isSingle = !!single;
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────
|
|
428
|
+
// Single mode
|
|
429
|
+
// ─────────────────────────────────────────
|
|
430
|
+
if (isSingle) {
|
|
431
|
+
const singleVal = asSingleValue(value);
|
|
432
|
+
const effectiveTristate = !!tristateDefault;
|
|
433
|
+
|
|
434
|
+
const internalState: CheckboxInternalState = effectiveTristate
|
|
435
|
+
? (singleVal ?? "none")
|
|
436
|
+
: !!singleVal;
|
|
437
|
+
|
|
438
|
+
const handleSingleChange = (next: CheckboxInternalState) => {
|
|
439
|
+
if (!onValue || disabled) return;
|
|
440
|
+
|
|
441
|
+
let nextPublic: CheckboxSingleValue;
|
|
442
|
+
|
|
443
|
+
if (effectiveTristate) {
|
|
444
|
+
// tri-state single:
|
|
445
|
+
// "none" → undefined
|
|
446
|
+
// true/false → same
|
|
447
|
+
nextPublic = next === "none" ? undefined : !!next;
|
|
448
|
+
} else {
|
|
449
|
+
// non-tristate: behave like normal checkbox
|
|
450
|
+
nextPublic = next === true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const detail: ChangeDetail = {
|
|
454
|
+
source: "variant",
|
|
455
|
+
raw: nextPublic,
|
|
456
|
+
nativeEvent: undefined,
|
|
457
|
+
meta: undefined,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
onValue(nextPublic, detail);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const labelText = singleLabel ?? undefined;
|
|
464
|
+
const descriptionText = singleDescription ?? undefined;
|
|
465
|
+
|
|
466
|
+
const labelCls = cn(
|
|
467
|
+
"text-foreground",
|
|
468
|
+
labelTextSize(size),
|
|
469
|
+
labelClassName
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const descriptionCls = cn(
|
|
473
|
+
"mt-0.5 text-muted-foreground",
|
|
474
|
+
descriptionTextSize(size),
|
|
475
|
+
descriptionClassName
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<div
|
|
480
|
+
ref={ref}
|
|
481
|
+
role="group"
|
|
482
|
+
aria-label={ariaLabel}
|
|
483
|
+
aria-labelledby={ariaLabelledBy}
|
|
484
|
+
aria-describedby={ariaDescribedBy}
|
|
485
|
+
aria-invalid={hasError || undefined}
|
|
486
|
+
aria-required={required || undefined}
|
|
487
|
+
data-slot="checkbox-single"
|
|
488
|
+
className={cn(
|
|
489
|
+
"flex items-start gap-3",
|
|
490
|
+
paddingForDensity(density),
|
|
491
|
+
groupClassName ?? className
|
|
492
|
+
)}
|
|
493
|
+
{...restProps}
|
|
494
|
+
>
|
|
495
|
+
<Checkbox
|
|
496
|
+
id={id}
|
|
497
|
+
checked={internalState}
|
|
498
|
+
tristate={effectiveTristate}
|
|
499
|
+
disabled={disabled}
|
|
500
|
+
onCheckedChange={handleSingleChange}
|
|
501
|
+
className="mt-0.5"
|
|
502
|
+
/>
|
|
503
|
+
|
|
504
|
+
{(labelText || descriptionText) && (
|
|
505
|
+
<div className="flex min-w-0 flex-col">
|
|
506
|
+
{labelText && (
|
|
507
|
+
<span className={labelCls}>{labelText}</span>
|
|
508
|
+
)}
|
|
509
|
+
{descriptionText && (
|
|
510
|
+
<span className={descriptionCls}>
|
|
511
|
+
{descriptionText}
|
|
512
|
+
</span>
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─────────────────────────────────────────
|
|
521
|
+
// Group mode
|
|
522
|
+
// ─────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
const groupValue = asGroupValue<TValue>(value);
|
|
525
|
+
const normalized = React.useMemo(
|
|
526
|
+
() =>
|
|
527
|
+
normalizeItems<TItem, TValue>(
|
|
528
|
+
items,
|
|
529
|
+
mappers,
|
|
530
|
+
optionValue,
|
|
531
|
+
optionLabel
|
|
532
|
+
),
|
|
533
|
+
[items, mappers, optionValue, optionLabel]
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const groupStyle: React.CSSProperties | undefined = React.useMemo(() => {
|
|
537
|
+
if (!itemGapPx) {
|
|
538
|
+
if (layout === "grid") {
|
|
539
|
+
return {
|
|
540
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (layout === "list") {
|
|
547
|
+
return { rowGap: itemGapPx };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
552
|
+
gap: itemGapPx,
|
|
553
|
+
};
|
|
554
|
+
}, [layout, columns, itemGapPx]);
|
|
555
|
+
|
|
556
|
+
const groupClasses = cn(
|
|
557
|
+
layout === "grid" ? "grid" : "flex flex-col",
|
|
558
|
+
groupClassName ?? className
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const baseOptionClass = cn(
|
|
562
|
+
"relative flex items-start",
|
|
563
|
+
"data-[disabled=true]:opacity-60 data-[disabled=true]:cursor-not-allowed",
|
|
564
|
+
paddingForDensity(density),
|
|
565
|
+
optionClassName
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
const labelClassesBase = cn(
|
|
569
|
+
"font-medium text-foreground",
|
|
570
|
+
labelTextSize(size),
|
|
571
|
+
labelClassName
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const descriptionClassesBase = cn(
|
|
575
|
+
"mt-0.5 text-muted-foreground",
|
|
576
|
+
descriptionTextSize(size),
|
|
577
|
+
descriptionClassName
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
const findEntryIndex = React.useCallback(
|
|
581
|
+
(val: TValue): number => {
|
|
582
|
+
if (!groupValue) return -1;
|
|
583
|
+
return groupValue.findIndex((e) => isEqualValue(e.value, val));
|
|
584
|
+
},
|
|
585
|
+
[groupValue]
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const getEntryState = React.useCallback(
|
|
589
|
+
(val: TValue): CheckboxTriStateValue | "none" => {
|
|
590
|
+
const idx = findEntryIndex(val);
|
|
591
|
+
if (!groupValue || idx === -1) return "none";
|
|
592
|
+
return groupValue[idx].state;
|
|
593
|
+
},
|
|
594
|
+
[groupValue, findEntryIndex]
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
const updateGroupValue = React.useCallback(
|
|
598
|
+
(
|
|
599
|
+
itemValue: TValue,
|
|
600
|
+
nextInternal: CheckboxInternalState,
|
|
601
|
+
effectiveTristate: boolean
|
|
602
|
+
) => {
|
|
603
|
+
if (!onValue || disabled) return;
|
|
604
|
+
|
|
605
|
+
const currentList = groupValue ? [...groupValue] : [];
|
|
606
|
+
const idx = currentList.findIndex((e) =>
|
|
607
|
+
isEqualValue(e.value, itemValue)
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
let nextList: CheckboxGroupEntry<TValue>[] = currentList;
|
|
611
|
+
|
|
612
|
+
if (effectiveTristate) {
|
|
613
|
+
// Tri-state:
|
|
614
|
+
// "none" → remove
|
|
615
|
+
// true/false → ensure entry is present with state
|
|
616
|
+
if (nextInternal === "none") {
|
|
617
|
+
if (idx !== -1) {
|
|
618
|
+
nextList = [
|
|
619
|
+
...currentList.slice(0, idx),
|
|
620
|
+
...currentList.slice(idx + 1),
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
const nextState: CheckboxTriStateValue =
|
|
625
|
+
nextInternal === true;
|
|
626
|
+
if (idx === -1) {
|
|
627
|
+
nextList = [
|
|
628
|
+
...currentList,
|
|
629
|
+
{ value: itemValue, state: nextState },
|
|
630
|
+
];
|
|
631
|
+
} else {
|
|
632
|
+
nextList = [...currentList];
|
|
633
|
+
nextList[idx] = {
|
|
634
|
+
...nextList[idx],
|
|
635
|
+
state: nextState,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
// Non tri-state:
|
|
641
|
+
// true → ensure present
|
|
642
|
+
// false/"none" → remove entry (false acts as none)
|
|
643
|
+
if (nextInternal === true) {
|
|
644
|
+
if (idx === -1) {
|
|
645
|
+
nextList = [
|
|
646
|
+
...currentList,
|
|
647
|
+
{ value: itemValue, state: true },
|
|
648
|
+
];
|
|
649
|
+
} else {
|
|
650
|
+
nextList = [...currentList];
|
|
651
|
+
nextList[idx] = {
|
|
652
|
+
...nextList[idx],
|
|
653
|
+
state: true,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
// false / "none": remove
|
|
658
|
+
if (idx !== -1) {
|
|
659
|
+
nextList = [
|
|
660
|
+
...currentList.slice(0, idx),
|
|
661
|
+
...currentList.slice(idx + 1),
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const detail: ChangeDetail = {
|
|
668
|
+
source: "variant",
|
|
669
|
+
raw: nextList,
|
|
670
|
+
nativeEvent: undefined,
|
|
671
|
+
meta: undefined,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
onValue(nextList, detail);
|
|
675
|
+
},
|
|
676
|
+
[onValue, disabled, groupValue]
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div
|
|
681
|
+
ref={ref}
|
|
682
|
+
id={id}
|
|
683
|
+
role="group"
|
|
684
|
+
aria-label={ariaLabel}
|
|
685
|
+
aria-labelledby={ariaLabelledBy}
|
|
686
|
+
aria-describedby={ariaDescribedBy}
|
|
687
|
+
aria-invalid={hasError || undefined}
|
|
688
|
+
aria-required={required || undefined}
|
|
689
|
+
data-slot="checkbox-group"
|
|
690
|
+
className={groupClasses}
|
|
691
|
+
style={groupStyle}
|
|
692
|
+
{...restProps}
|
|
693
|
+
>
|
|
694
|
+
{normalized.map((item, index) => {
|
|
695
|
+
const effectiveTristate =
|
|
696
|
+
item.tristate ?? tristateDefault ?? false;
|
|
697
|
+
|
|
698
|
+
const currentState = getEntryState(item.value);
|
|
699
|
+
const internalState: CheckboxInternalState =
|
|
700
|
+
effectiveTristate
|
|
701
|
+
? currentState // "none" | true | false
|
|
702
|
+
: currentState === "none"
|
|
703
|
+
? false
|
|
704
|
+
: currentState;
|
|
705
|
+
|
|
706
|
+
const optionDisabled = !!disabled || !!item.disabled;
|
|
707
|
+
const optionKey = item.key ?? index;
|
|
708
|
+
const checkboxId = id
|
|
709
|
+
? `${id}-option-${optionKey}`
|
|
710
|
+
: undefined;
|
|
711
|
+
|
|
712
|
+
const checkboxNode = (
|
|
713
|
+
<Checkbox
|
|
714
|
+
id={checkboxId}
|
|
715
|
+
checked={internalState}
|
|
716
|
+
disabled={optionDisabled}
|
|
717
|
+
tristate={effectiveTristate}
|
|
718
|
+
onCheckedChange={(next) =>
|
|
719
|
+
updateGroupValue(
|
|
720
|
+
item.value,
|
|
721
|
+
next as CheckboxInternalState,
|
|
722
|
+
effectiveTristate
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
className="mt-1"
|
|
726
|
+
/>
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
const hiddenInput =
|
|
730
|
+
name != null ? (
|
|
731
|
+
<input
|
|
732
|
+
type="hidden"
|
|
733
|
+
name={name}
|
|
734
|
+
value={String(item.value)}
|
|
735
|
+
// Only send if in list; tri-state false still "has standing"
|
|
736
|
+
// in code, but native form post is simple and you can
|
|
737
|
+
// derive negative states server-side if you want.
|
|
738
|
+
disabled={
|
|
739
|
+
getEntryState(item.value) === "none"
|
|
740
|
+
}
|
|
741
|
+
/>
|
|
742
|
+
) : null;
|
|
743
|
+
|
|
744
|
+
if (renderOption) {
|
|
745
|
+
return (
|
|
746
|
+
<div
|
|
747
|
+
key={optionKey}
|
|
748
|
+
data-slot="checkbox-option"
|
|
749
|
+
data-disabled={optionDisabled ? "true" : "false"}
|
|
750
|
+
className={baseOptionClass}
|
|
751
|
+
>
|
|
752
|
+
{renderOption({
|
|
753
|
+
item,
|
|
754
|
+
index,
|
|
755
|
+
state: internalState,
|
|
756
|
+
effectiveTristate,
|
|
757
|
+
disabled: optionDisabled,
|
|
758
|
+
size,
|
|
759
|
+
density,
|
|
760
|
+
checkboxId,
|
|
761
|
+
checkbox: checkboxNode,
|
|
762
|
+
})}
|
|
763
|
+
{hiddenInput}
|
|
764
|
+
</div>
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Default row layout: checkbox + label + description
|
|
769
|
+
return (
|
|
770
|
+
<div
|
|
771
|
+
key={optionKey}
|
|
772
|
+
data-slot="checkbox-option"
|
|
773
|
+
data-disabled={optionDisabled ? "true" : "false"}
|
|
774
|
+
className={baseOptionClass}
|
|
775
|
+
>
|
|
776
|
+
<label
|
|
777
|
+
htmlFor={checkboxId}
|
|
778
|
+
className="flex w-full cursor-pointer items-start gap-3 select-none"
|
|
779
|
+
>
|
|
780
|
+
{checkboxNode}
|
|
781
|
+
|
|
782
|
+
<div className="flex min-w-0 flex-col">
|
|
783
|
+
<span className={labelClassesBase}>
|
|
784
|
+
{item.label}
|
|
785
|
+
</span>
|
|
786
|
+
{item.description != null && (
|
|
787
|
+
<span
|
|
788
|
+
className={descriptionClassesBase}
|
|
789
|
+
>
|
|
790
|
+
{item.description}
|
|
791
|
+
</span>
|
|
792
|
+
)}
|
|
793
|
+
</div>
|
|
794
|
+
</label>
|
|
795
|
+
|
|
796
|
+
{hiddenInput}
|
|
797
|
+
</div>
|
|
798
|
+
);
|
|
799
|
+
})}
|
|
800
|
+
</div>
|
|
801
|
+
);
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
export const ShadcnCheckboxVariant =
|
|
805
|
+
React.forwardRef(InnerShadcnCheckboxVariant) as unknown as <
|
|
806
|
+
TValue,
|
|
807
|
+
TItem = CheckboxItem<TValue>
|
|
808
|
+
>(
|
|
809
|
+
props: ShadcnCheckboxVariantProps<TValue, TItem> & {
|
|
810
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
811
|
+
}
|
|
812
|
+
) => React.ReactElement | null;
|
|
813
|
+
|
|
814
|
+
// ShadcnCheckboxVariant.displayName = "ShadcnCheckboxVariant";
|
|
815
|
+
|
|
816
|
+
export default ShadcnCheckboxVariant;
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
// ─────────────────────────────────────────────
|
|
820
|
+
// Public aliases for the registry
|
|
821
|
+
// ─────────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Default item value type for the checkbox variant.
|
|
825
|
+
*
|
|
826
|
+
* You can still use the generic ShadcnCheckboxVariantProps<TValue, TItem>
|
|
827
|
+
* directly if you need a different TValue; the registry uses this alias.
|
|
828
|
+
*/
|
|
829
|
+
export type DefaultCheckboxItemValue = string | number;
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Public "value" type for the checkbox variant used by the registry:
|
|
833
|
+
*
|
|
834
|
+
* - Single mode: boolean | undefined
|
|
835
|
+
* - Group mode: CheckboxGroupEntry<DefaultCheckboxItemValue>[] | undefined
|
|
836
|
+
*
|
|
837
|
+
* In tri-state group mode, both `true` and `false` entries are present;
|
|
838
|
+
* `"none"` never appears in this type.
|
|
839
|
+
*/
|
|
840
|
+
export type CheckboxVariantPublicValue =
|
|
841
|
+
CheckboxVariantValue<DefaultCheckboxItemValue>;
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Public props type for the checkbox variant used by the registry.
|
|
845
|
+
*
|
|
846
|
+
* This is ShadcnCheckboxVariantProps with TValue fixed to DefaultCheckboxItemValue.
|
|
847
|
+
*/
|
|
848
|
+
export type ShadcnCheckboxVariantPublicProps =
|
|
849
|
+
ShadcnCheckboxVariantProps<DefaultCheckboxItemValue>;
|