@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,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;
|