@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.
Files changed (109) hide show
  1. package/.scaffold-cache.json +537 -0
  2. package/package.json +42 -0
  3. package/src/.scaffold-cache.json +544 -0
  4. package/src/adapters/axios.ts +117 -0
  5. package/src/adapters/index.ts +91 -0
  6. package/src/adapters/inertia.ts +187 -0
  7. package/src/core/adapter-registry.ts +87 -0
  8. package/src/core/bound/bind-host.ts +14 -0
  9. package/src/core/bound/observe-bound-field.ts +172 -0
  10. package/src/core/bound/wait-for-bound-field.ts +57 -0
  11. package/src/core/context.ts +23 -0
  12. package/src/core/core-provider.tsx +818 -0
  13. package/src/core/core-root.tsx +72 -0
  14. package/src/core/core-shell.tsx +44 -0
  15. package/src/core/errors/error-strip.tsx +71 -0
  16. package/src/core/errors/index.ts +2 -0
  17. package/src/core/errors/map-error-bag.ts +51 -0
  18. package/src/core/errors/map-zod.ts +39 -0
  19. package/src/core/hooks/use-button.ts +220 -0
  20. package/src/core/hooks/use-core-context.ts +20 -0
  21. package/src/core/hooks/use-core-utility.ts +0 -0
  22. package/src/core/hooks/use-core.ts +13 -0
  23. package/src/core/hooks/use-field.ts +497 -0
  24. package/src/core/hooks/use-optional-field.ts +28 -0
  25. package/src/core/index.ts +0 -0
  26. package/src/core/registry/binder-registry.ts +82 -0
  27. package/src/core/registry/field-registry.ts +187 -0
  28. package/src/core/test.tsx +17 -0
  29. package/src/global.d.ts +14 -0
  30. package/src/index.ts +68 -0
  31. package/src/input/index.ts +4 -0
  32. package/src/input/input-field.tsx +854 -0
  33. package/src/input/input-layout-graph.ts +230 -0
  34. package/src/input/input-props.ts +190 -0
  35. package/src/lib/get-global-countries.ts +87 -0
  36. package/src/lib/utils.ts +6 -0
  37. package/src/presets/index.ts +0 -0
  38. package/src/presets/shadcn-preset.ts +0 -0
  39. package/src/presets/shadcn-variants/checkbox.tsx +849 -0
  40. package/src/presets/shadcn-variants/chips.tsx +756 -0
  41. package/src/presets/shadcn-variants/color.tsx +284 -0
  42. package/src/presets/shadcn-variants/custom.tsx +227 -0
  43. package/src/presets/shadcn-variants/date.tsx +796 -0
  44. package/src/presets/shadcn-variants/file.tsx +764 -0
  45. package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
  46. package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
  47. package/src/presets/shadcn-variants/number.tsx +176 -0
  48. package/src/presets/shadcn-variants/password.tsx +737 -0
  49. package/src/presets/shadcn-variants/phone.tsx +628 -0
  50. package/src/presets/shadcn-variants/radio.tsx +578 -0
  51. package/src/presets/shadcn-variants/select.tsx +956 -0
  52. package/src/presets/shadcn-variants/slider.tsx +622 -0
  53. package/src/presets/shadcn-variants/text.tsx +343 -0
  54. package/src/presets/shadcn-variants/textarea.tsx +66 -0
  55. package/src/presets/shadcn-variants/toggle.tsx +218 -0
  56. package/src/presets/shadcn-variants/treeselect.tsx +784 -0
  57. package/src/presets/ui/badge.tsx +46 -0
  58. package/src/presets/ui/button.tsx +60 -0
  59. package/src/presets/ui/calendar.tsx +214 -0
  60. package/src/presets/ui/checkbox.tsx +115 -0
  61. package/src/presets/ui/custom.tsx +0 -0
  62. package/src/presets/ui/dialog.tsx +141 -0
  63. package/src/presets/ui/field.tsx +246 -0
  64. package/src/presets/ui/input-mask.tsx +739 -0
  65. package/src/presets/ui/input-otp.tsx +77 -0
  66. package/src/presets/ui/input.tsx +1011 -0
  67. package/src/presets/ui/label.tsx +22 -0
  68. package/src/presets/ui/number.tsx +1370 -0
  69. package/src/presets/ui/popover.tsx +46 -0
  70. package/src/presets/ui/radio-group.tsx +43 -0
  71. package/src/presets/ui/scroll-area.tsx +56 -0
  72. package/src/presets/ui/select.tsx +190 -0
  73. package/src/presets/ui/separator.tsx +28 -0
  74. package/src/presets/ui/slider.tsx +61 -0
  75. package/src/presets/ui/switch.tsx +32 -0
  76. package/src/presets/ui/textarea.tsx +634 -0
  77. package/src/presets/ui/time-dropdowns.tsx +350 -0
  78. package/src/schema/adapter.ts +217 -0
  79. package/src/schema/core.ts +429 -0
  80. package/src/schema/field-map.ts +0 -0
  81. package/src/schema/field.ts +224 -0
  82. package/src/schema/index.ts +0 -0
  83. package/src/schema/input-field.ts +260 -0
  84. package/src/schema/presets.ts +0 -0
  85. package/src/schema/variant.ts +216 -0
  86. package/src/variants/core/checkbox.tsx +54 -0
  87. package/src/variants/core/chips.tsx +22 -0
  88. package/src/variants/core/color.tsx +16 -0
  89. package/src/variants/core/custom.tsx +18 -0
  90. package/src/variants/core/date.tsx +25 -0
  91. package/src/variants/core/file.tsx +9 -0
  92. package/src/variants/core/keyvalue.tsx +12 -0
  93. package/src/variants/core/multiselect.tsx +28 -0
  94. package/src/variants/core/number.tsx +115 -0
  95. package/src/variants/core/password.tsx +35 -0
  96. package/src/variants/core/phone.tsx +16 -0
  97. package/src/variants/core/radio.tsx +38 -0
  98. package/src/variants/core/select.tsx +15 -0
  99. package/src/variants/core/slider.tsx +55 -0
  100. package/src/variants/core/text.tsx +114 -0
  101. package/src/variants/core/textarea.tsx +22 -0
  102. package/src/variants/core/toggle.tsx +50 -0
  103. package/src/variants/core/treeselect.tsx +11 -0
  104. package/src/variants/helpers/selection-summary.tsx +236 -0
  105. package/src/variants/index.ts +75 -0
  106. package/src/variants/registry.ts +38 -0
  107. package/src/variants/select-shared.ts +0 -0
  108. package/src/variants/shared.ts +126 -0
  109. package/tsconfig.json +14 -0
@@ -0,0 +1,343 @@
1
+ // src/presets/ui/shadcn-variants/text.tsx
2
+ // noinspection GrazieInspection
3
+
4
+ import * as React from "react";
5
+
6
+ import { Input } from "@/presets/ui/input";
7
+ import type {
8
+ ChangeDetail,
9
+ ExtraFieldProps,
10
+ VariantBaseProps,
11
+ } from "@/variants/shared";
12
+ import type { InputMaskChangeEvent } from "../ui/input-mask";
13
+
14
+ type MaskMode = "raw" | "masked";
15
+
16
+ /**
17
+ * Mask-related props for the Shadcn text variant.
18
+ *
19
+ * These are forwarded to the underlying <Input>, which in turn wires
20
+ * them into the InputMask implementation.
21
+ */
22
+ export interface ShadcnTextMaskProps {
23
+ /**
24
+ * Mask pattern – Primereact style.
25
+ * Example: "99/99/9999", "(999) 999-9999"
26
+ */
27
+ mask?: string;
28
+
29
+ /**
30
+ * Per-symbol definitions for slots.
31
+ * Kept for future custom engine; not used by the current
32
+ * react-input-mask implementation.
33
+ */
34
+ maskDefinitions?: Record<string, RegExp>;
35
+
36
+ /**
37
+ * Character used to visually represent an empty slot.
38
+ * Default: "_".
39
+ */
40
+ slotChar?: string;
41
+
42
+ /**
43
+ * If true, when the value is effectively "empty" (no unmasked chars),
44
+ * we emit an empty string "" instead of a fully-masked placeholder.
45
+ *
46
+ * NOTE: This behaviour is implemented in the variant, not Input,
47
+ * so we preserve your existing semantics.
48
+ */
49
+ autoClear?: boolean;
50
+
51
+ /**
52
+ * Whether the *model* value is raw or masked.
53
+ *
54
+ * - "raw" or true → onValue receives unmasked value
55
+ * - "masked" or false/undefined → onValue receives full masked string
56
+ *
57
+ * NOTE: detail.raw is **always** the masked string.
58
+ */
59
+ unmask?: MaskMode | boolean;
60
+
61
+ /**
62
+ * Placeholder for future caret-mode logic when we go back
63
+ * to a custom engine. Currently unused, kept for API compatibility.
64
+ */
65
+ maskInsertMode?: "stream" | "caret";
66
+ }
67
+
68
+ /**
69
+ * Extra UI props for the Shadcn text input (pure HTML-level).
70
+ *
71
+ * These are forwarded straight to the underlying <Input />.
72
+ */
73
+ export type ShadcnTextUiProps = Omit<
74
+ React.InputHTMLAttributes<HTMLInputElement>,
75
+ "value" | "defaultValue" | "onChange" | "size"
76
+ > & {
77
+ /**
78
+ * Extra classes applied only to the *inner* input element
79
+ * (the actual <input>, not the wrapper box).
80
+ */
81
+ inputClassName?: string;
82
+
83
+ /**
84
+ * Fixed prefix rendered as part of the input value, NOT as an icon.
85
+ * E.g. "₦", "ID: ".
86
+ *
87
+ * The underlying <Input> will:
88
+ * - take the model value (without prefix),
89
+ * - render prefix + value,
90
+ * - expose the full visible string in event.target.value.
91
+ */
92
+ prefix?: string;
93
+
94
+ /**
95
+ * Fixed suffix rendered as part of the input value, NOT as an icon.
96
+ * E.g. "%", "kg".
97
+ */
98
+ suffix?: string;
99
+
100
+ /**
101
+ * If true (default), we strip the prefix from the value
102
+ * before emitting it via `onValue`.
103
+ */
104
+ stripPrefix?: boolean;
105
+
106
+ /**
107
+ * If true (default), we strip the suffix from the value
108
+ * before emitting it via `onValue`.
109
+ */
110
+ stripSuffix?: boolean;
111
+ } & ShadcnTextMaskProps;
112
+
113
+ /**
114
+ * Props for the Shadcn-based text variant.
115
+ *
116
+ * This is a *form* wrapper around the base <Input />:
117
+ * - Handles value ↔ ChangeDetail mapping.
118
+ * - Delegates all visual concerns (masking, affixes, icons, controls,
119
+ * size, density) to the Input component.
120
+ */
121
+ export type ShadcnTextVariantProps = ExtraFieldProps<
122
+ VariantBaseProps<string | undefined>
123
+ > & {
124
+ /**
125
+ * If true and there are controls, the input + controls share one box
126
+ * (borders, radius, focus states).
127
+ *
128
+ * Delegated to the underlying <Input />.
129
+ */
130
+ joinControls?: boolean;
131
+
132
+ /**
133
+ * When joinControls is true, whether the box styling extends over controls
134
+ * (true) or controls are visually separate (false).
135
+ */
136
+ extendBoxToControls?: boolean;
137
+ };
138
+
139
+ export const ShadcnTextVariant = React.forwardRef<
140
+ HTMLInputElement,
141
+ ShadcnTextVariantProps & ShadcnTextUiProps
142
+ >(function ShadcnTextVariant(props, forwardedRef) {
143
+ const {
144
+ // form-level props
145
+ value,
146
+ onValue,
147
+ disabled,
148
+ readOnly,
149
+ required,
150
+ error,
151
+ size,
152
+ density,
153
+
154
+ // extras from VariantBaseProps / ExtraFieldProps
155
+ leadingIcons,
156
+ trailingIcons,
157
+ icon,
158
+ iconGap,
159
+ leadingIconSpacing,
160
+ trailingIconSpacing,
161
+ leadingControl,
162
+ trailingControl,
163
+ leadingControlClassName,
164
+ trailingControlClassName,
165
+ px,
166
+ py,
167
+ ps,
168
+ pe,
169
+ pb,
170
+
171
+ joinControls = true,
172
+ extendBoxToControls = true,
173
+
174
+ // masking
175
+ mask,
176
+ maskDefinitions,
177
+ slotChar,
178
+ autoClear,
179
+ unmask,
180
+ maskInsertMode,
181
+
182
+ // affixes
183
+ prefix,
184
+ suffix,
185
+ stripPrefix = true,
186
+ stripSuffix = true,
187
+
188
+ // visual props
189
+ inputClassName,
190
+ className,
191
+ style,
192
+ ...rest
193
+ } = props;
194
+
195
+ const isMasked = Boolean(mask);
196
+
197
+ // ─────────────────────────────────────────────
198
+ // Plain change handler (unmasked <Input />)
199
+ // ─────────────────────────────────────────────
200
+
201
+ const handlePlainChange = React.useCallback(
202
+ (e: React.ChangeEvent<HTMLInputElement>) => {
203
+ const displayed = e.target.value ?? "";
204
+
205
+ let modelValue = displayed;
206
+
207
+ // strip prefix if configured
208
+ if (prefix && stripPrefix && modelValue.startsWith(prefix)) {
209
+ modelValue = modelValue.slice(prefix.length);
210
+ }
211
+
212
+ // strip suffix if configured
213
+ if (suffix && stripSuffix && modelValue.endsWith(suffix)) {
214
+ modelValue = modelValue.slice(
215
+ 0,
216
+ modelValue.length - suffix.length
217
+ );
218
+ }
219
+
220
+ const detail: ChangeDetail = {
221
+ source: "variant",
222
+ raw: displayed, // actual visible value (with affixes)
223
+ nativeEvent: e,
224
+ meta: {
225
+ prefix,
226
+ suffix,
227
+ stripPrefix,
228
+ stripSuffix,
229
+ model: modelValue,
230
+ },
231
+ };
232
+
233
+ onValue?.(modelValue, detail);
234
+ },
235
+ [onValue, prefix, suffix, stripPrefix, stripSuffix]
236
+ );
237
+
238
+ // ─────────────────────────────────────────────
239
+ // Masked change handler (InputMask under <Input />)
240
+ // ─────────────────────────────────────────────
241
+
242
+ const handleMaskedChange = React.useCallback(
243
+ (e: InputMaskChangeEvent) => {
244
+ const maskedValue = e.value ?? "";
245
+
246
+ // Same heuristic as your original variant:
247
+ // "Unmasked" = characters that would normally be accepted by masks.
248
+ const unmaskedInner =
249
+ maskedValue.match(/[0-9A-Za-z]/g)?.join("") ?? "";
250
+
251
+ const mode: MaskMode =
252
+ unmask === true || unmask === "raw" ? "raw" : "masked";
253
+
254
+ // IMPORTANT: detail.raw is ALWAYS the masked value.
255
+ const detail: ChangeDetail = {
256
+ source: "variant",
257
+ raw: maskedValue,
258
+ nativeEvent: e.originalEvent as any,
259
+ meta: {
260
+ masked: maskedValue,
261
+ unmasked: unmaskedInner,
262
+ mode,
263
+ prefix,
264
+ suffix,
265
+ },
266
+ };
267
+
268
+ let emitValue = mode === "raw" ? unmaskedInner : maskedValue;
269
+
270
+ // autoClear: if nothing "real" was typed, treat as empty.
271
+ if (autoClear && unmaskedInner.length === 0) {
272
+ emitValue = "";
273
+ }
274
+
275
+ onValue?.(emitValue, detail);
276
+ },
277
+ [onValue, unmask, autoClear, prefix, suffix]
278
+ );
279
+
280
+ // Variant-level "model" is always the raw value you store.
281
+ // The underlying <Input> is responsible for visually applying prefix/suffix
282
+ // or mask literals on top of this model.
283
+ const modelValue = value ?? "";
284
+
285
+ return (
286
+ <Input
287
+ ref={forwardedRef}
288
+ // visual & sizing
289
+ className={className}
290
+ style={style}
291
+ size={size as any}
292
+ density={density as any}
293
+ inputClassName={inputClassName}
294
+ // flags
295
+ disabled={disabled}
296
+ readOnly={readOnly}
297
+ required={required}
298
+ aria-invalid={error ? "true" : undefined}
299
+ // masking
300
+ mask={mask}
301
+ maskDefinitions={maskDefinitions}
302
+ slotChar={slotChar}
303
+ autoClear={autoClear}
304
+ unmask={unmask}
305
+ maskInsertMode={maskInsertMode}
306
+ // affixes (value-level, not icons)
307
+ prefix={prefix}
308
+ suffix={suffix}
309
+ stripPrefix={stripPrefix}
310
+ stripSuffix={stripSuffix}
311
+ // icons & controls
312
+ leadingIcons={leadingIcons}
313
+ trailingIcons={trailingIcons}
314
+ icon={icon}
315
+ iconGap={iconGap}
316
+ leadingIconSpacing={leadingIconSpacing}
317
+ trailingIconSpacing={trailingIconSpacing}
318
+ leadingControl={leadingControl}
319
+ trailingControl={trailingControl}
320
+ leadingControlClassName={leadingControlClassName}
321
+ trailingControlClassName={trailingControlClassName}
322
+ joinControls={joinControls}
323
+ extendBoxToControls={extendBoxToControls}
324
+ px={px}
325
+ py={py}
326
+ ps={ps}
327
+ pe={pe}
328
+ pb={pb}
329
+ // value & event mapping
330
+ value={modelValue}
331
+ onChange={
332
+ isMasked
333
+ ? (handleMaskedChange as any)
334
+ : (handlePlainChange as any)
335
+ }
336
+ {...rest}
337
+ />
338
+ );
339
+ });
340
+
341
+ ShadcnTextVariant.displayName = "ShadcnTextVariant";
342
+
343
+ export default ShadcnTextVariant;
@@ -0,0 +1,66 @@
1
+ // src/presets/shadcn-variants/textarea.tsx
2
+
3
+ import * as React from "react";
4
+
5
+ import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
6
+ import { Textarea } from "@/presets/ui/textarea";
7
+ import type { TextareaProps as UiTextareaProps } from "@/presets/ui/textarea";
8
+
9
+ type TextareaValue = string | undefined;
10
+ type BaseProps = VariantBaseProps<TextareaValue>;
11
+
12
+ /**
13
+ * Full props for the Shadcn-based textarea variant.
14
+ *
15
+ * - Reuses all UI-level behaviour from `Textarea` (autoResize, upperControl,
16
+ * leading/trailing controls, icons, size/density, padding knobs, etc.).
17
+ * - Takes over `value` / `onChange` so it can emit through `onValue` with
18
+ * a `ChangeDetail`.
19
+ */
20
+ export interface ShadcnTextareaVariantProps
21
+ extends Omit<UiTextareaProps, "value" | "defaultValue" | "onChange">,
22
+ Pick<BaseProps, "value" | "onValue" | "error"> { }
23
+
24
+ export const ShadcnTextareaVariant = React.forwardRef<
25
+ HTMLTextAreaElement,
26
+ ShadcnTextareaVariantProps
27
+ >(function ShadcnTextareaVariant(props, ref) {
28
+ const {
29
+ value,
30
+ onValue,
31
+ error,
32
+ // everything else goes straight to the UI Textarea
33
+ ...rest
34
+ } = props;
35
+
36
+ const handleChange = React.useCallback(
37
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
38
+ const next = event.target.value ?? "";
39
+
40
+ const detail: ChangeDetail = {
41
+ source: "variant",
42
+ raw: next,
43
+ nativeEvent: event,
44
+ meta: undefined,
45
+ };
46
+
47
+ // empty string → undefined, same convention as text/chips
48
+ onValue?.(next.length ? next : undefined, detail);
49
+ },
50
+ [onValue],
51
+ );
52
+
53
+ return (
54
+ <Textarea
55
+ ref={ref}
56
+ {...rest}
57
+ value={value ?? ""}
58
+ onChange={handleChange}
59
+ aria-invalid={error ? "true" : undefined}
60
+ />
61
+ );
62
+ });
63
+
64
+ ShadcnTextareaVariant.displayName = "ShadcnTextareaVariant";
65
+
66
+ export default ShadcnTextareaVariant;
@@ -0,0 +1,218 @@
1
+ // src/presets/shadcn-variants/toggle.tsx
2
+
3
+ import * as React from "react";
4
+
5
+ import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
6
+ import { cn } from "@/lib/utils";
7
+ import { Switch } from "@/presets/ui/switch"; // adjust path if your Switch lives elsewhere
8
+
9
+ type ToggleValue = boolean | undefined;
10
+ type BaseProps = VariantBaseProps<ToggleValue>;
11
+
12
+ type Size = "sm" | "md" | "lg";
13
+ type Density = "default" | "dense";
14
+
15
+ /**
16
+ * UI props specific to the Shadcn-based toggle.
17
+ *
18
+ * This uses Switch as the underlying control, but we keep
19
+ * the API surface small and focused.
20
+ */
21
+ export interface ShadcnToggleUiProps
22
+ extends Omit<
23
+ React.ComponentProps<typeof Switch>,
24
+ "checked" | "onCheckedChange" | "className"
25
+ > {
26
+ /**
27
+ * Visual size of the switch / text.
28
+ * Default: "md".
29
+ */
30
+ size?: Size;
31
+
32
+ /**
33
+ * Row density (vertical padding & gap).
34
+ * Default: "default".
35
+ */
36
+ density?: Density;
37
+
38
+ /**
39
+ * Place the switch on the left or right of the state text.
40
+ * Default: "left".
41
+ */
42
+ controlPlacement?: "left" | "right";
43
+
44
+ /**
45
+ * Optional state text shown next to the control when ON.
46
+ */
47
+ onText?: React.ReactNode;
48
+
49
+ /**
50
+ * Optional state text shown next to the control when OFF.
51
+ */
52
+ offText?: React.ReactNode;
53
+
54
+ /**
55
+ * Wrapper class for the whole toggle row.
56
+ */
57
+ className?: string;
58
+
59
+ /**
60
+ * Extra classes for the Switch root.
61
+ */
62
+ switchClassName?: string;
63
+
64
+ /**
65
+ * Extra classes for the Switch thumb.
66
+ * (Your patched Switch should support thumbClassName.)
67
+ */
68
+ switchThumbClassName?: string;
69
+ }
70
+
71
+ /**
72
+ * Full props for the Shadcn-based toggle variant.
73
+ *
74
+ * We only pick value/onValue/error from the variant base props;
75
+ * everything else (id, disabled, aria-*) flows via Switch props.
76
+ */
77
+ export type ShadcnToggleVariantProps = ShadcnToggleUiProps &
78
+ Pick<BaseProps, "value" | "onValue" | "error">;
79
+
80
+ // ─────────────────────────────────────────────
81
+ // Helpers
82
+ // ─────────────────────────────────────────────
83
+
84
+ function rowGap(density: Density) {
85
+ return density === "dense" ? "gap-2" : "gap-3";
86
+ }
87
+
88
+ function rowPadding(density: Density) {
89
+ return density === "dense" ? "py-0.5" : "py-1";
90
+ }
91
+
92
+ function textSize(size: Size) {
93
+ if (size === "sm") return "text-sm";
94
+ if (size === "lg") return "text-base";
95
+ return "text-sm";
96
+ }
97
+
98
+ // Map size → Switch track + thumb sizing
99
+ function switchRootSize(size: Size) {
100
+ if (size === "sm") return "h-5 w-9";
101
+ if (size === "lg") return "h-7 w-12";
102
+ // default shadcn-ish base
103
+ return "h-[1.15rem] w-8";
104
+ }
105
+
106
+ function switchThumbSize(size: Size) {
107
+ if (size === "sm") return "size-3.5";
108
+ if (size === "lg") return "size-5";
109
+ return "size-4";
110
+ }
111
+
112
+ // ─────────────────────────────────────────────
113
+ // Component
114
+ // ─────────────────────────────────────────────
115
+
116
+ export const ShadcnToggleVariant = React.forwardRef<
117
+ HTMLButtonElement,
118
+ ShadcnToggleVariantProps
119
+ >(function ShadcnToggleVariant(props, _ref) {
120
+ const {
121
+ // variant bits
122
+ value,
123
+ onValue,
124
+ error,
125
+
126
+ // UI config
127
+ size = "md",
128
+ density = "default",
129
+ controlPlacement = "left",
130
+ onText,
131
+ offText,
132
+ className,
133
+ switchClassName,
134
+ switchThumbClassName,
135
+
136
+ // Switch passthroughs
137
+ disabled,
138
+ id,
139
+ "aria-describedby": describedBy,
140
+ ...restSwitchProps
141
+ } = props;
142
+
143
+ const checked = !!value;
144
+
145
+ const handleToggle = React.useCallback(
146
+ (next: boolean) => {
147
+ const nextVal = !!next;
148
+ const detail: ChangeDetail = {
149
+ source: "variant",
150
+ raw: nextVal,
151
+ nativeEvent: undefined,
152
+ meta: undefined,
153
+ };
154
+ onValue?.(nextVal, detail);
155
+ },
156
+ [onValue],
157
+ );
158
+
159
+ const rowCls = cn(
160
+ "flex w-fit items-center",
161
+ rowGap(density),
162
+ rowPadding(density),
163
+ );
164
+
165
+ const stateText =
166
+ onText != null || offText != null ? (
167
+ <span
168
+ className={cn("select-none text-muted-foreground", textSize(size))}
169
+ >
170
+ {checked ? onText : offText}
171
+ </span>
172
+ ) : null;
173
+
174
+ const switchEl = (
175
+ <Switch
176
+ id={id}
177
+ checked={checked}
178
+ onCheckedChange={handleToggle}
179
+ disabled={disabled}
180
+ aria-describedby={describedBy}
181
+ aria-checked={checked}
182
+ className={cn(switchRootSize(size), switchClassName)}
183
+ thumbClassName={cn(switchThumbSize(size), switchThumbClassName)}
184
+ {...restSwitchProps}
185
+ />
186
+ );
187
+
188
+ return (
189
+ <div
190
+ data-slot="toggle-field"
191
+ className={cn(
192
+ "w-fit",
193
+ disabled && "opacity-50 cursor-not-allowed",
194
+ className,
195
+ )}
196
+ aria-disabled={disabled || undefined}
197
+ aria-invalid={error ? "true" : undefined}
198
+ >
199
+ <div className={rowCls}>
200
+ {controlPlacement === "left" ? (
201
+ <>
202
+ {switchEl}
203
+ {stateText}
204
+ </>
205
+ ) : (
206
+ <>
207
+ {stateText}
208
+ {switchEl}
209
+ </>
210
+ )}
211
+ </div>
212
+ </div>
213
+ );
214
+ });
215
+
216
+ ShadcnToggleVariant.displayName = "ShadcnToggleVariant";
217
+
218
+ export default ShadcnToggleVariant;