@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,284 @@
|
|
|
1
|
+
// src/presets/shadcn-variants/color.tsx
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import type { VariantModule } from "@/schema/variant";
|
|
6
|
+
import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
|
|
7
|
+
import type { ShadcnTextVariantProps } from "@/presets/shadcn-variants/text";
|
|
8
|
+
import { Input } from "@/presets/ui/input";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
import { Palette } from "lucide-react";
|
|
11
|
+
|
|
12
|
+
type BaseProps = VariantBaseProps<string | undefined>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extra options specific to the color variant.
|
|
16
|
+
*/
|
|
17
|
+
export interface ColorSpecificProps {
|
|
18
|
+
/**
|
|
19
|
+
* If false, we hide the colour preview box.
|
|
20
|
+
* Default: true
|
|
21
|
+
*/
|
|
22
|
+
showPreview?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* If false, we hide the picker toggle control/icon.
|
|
26
|
+
* Default: true
|
|
27
|
+
*/
|
|
28
|
+
showPickerToggle?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Size of the colour swatch in pixels.
|
|
32
|
+
* Default: 18
|
|
33
|
+
*/
|
|
34
|
+
previewSize?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Optional className for the outer wrapper that hosts
|
|
38
|
+
* the Input + hidden color input.
|
|
39
|
+
*/
|
|
40
|
+
wrapperClassName?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional className for the preview button itself (around the swatch).
|
|
44
|
+
*/
|
|
45
|
+
previewButtonClassName?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional className for the swatch box inside the preview button.
|
|
49
|
+
*/
|
|
50
|
+
previewSwatchClassName?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional className for the hidden `<input type="color">`.
|
|
54
|
+
*
|
|
55
|
+
* By default this input is visually hidden and only used
|
|
56
|
+
* to invoke the browser/OS colour picker, but you can override
|
|
57
|
+
* this class to make it visible and style it.
|
|
58
|
+
*/
|
|
59
|
+
pickerInputClassName?: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Custom icon shown in the trailing control as the picker toggle.
|
|
63
|
+
* If omitted, a tiny ▾ triangle is used.
|
|
64
|
+
*/
|
|
65
|
+
pickerToggleIcon?: React.ReactNode;
|
|
66
|
+
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* We inherit the *visual/behavioural* props from ShadcnTextVariant,
|
|
72
|
+
* but control value / onValue / type / inputMode / leadingControl / trailingControl ourselves.
|
|
73
|
+
*/
|
|
74
|
+
type TextUiProps = Omit<
|
|
75
|
+
ShadcnTextVariantProps,
|
|
76
|
+
"type" | "inputMode" | "leadingControl" | "trailingControl" | "value" | "onValue"
|
|
77
|
+
>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Full props for the color variant as seen by the form runtime.
|
|
81
|
+
*/
|
|
82
|
+
export type ShadcnColorVariantProps = TextUiProps &
|
|
83
|
+
ColorSpecificProps &
|
|
84
|
+
Pick<BaseProps, "value" | "onValue">;
|
|
85
|
+
|
|
86
|
+
function normalizeColorForPicker(value: string | undefined): string {
|
|
87
|
+
// Very light sanity: accept #rgb or #rrggbb; otherwise fall back.
|
|
88
|
+
if (typeof value === "string" && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)) {
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
return "#000000";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const ShadcnColorVariant = React.forwardRef<
|
|
95
|
+
HTMLInputElement,
|
|
96
|
+
ShadcnColorVariantProps
|
|
97
|
+
>(function ShadcnColorVariant(props, ref) {
|
|
98
|
+
const {
|
|
99
|
+
// variant contract
|
|
100
|
+
value,
|
|
101
|
+
onValue,
|
|
102
|
+
|
|
103
|
+
// colour-specific
|
|
104
|
+
showPreview = true,
|
|
105
|
+
showPickerToggle = true,
|
|
106
|
+
previewSize = 18,
|
|
107
|
+
wrapperClassName,
|
|
108
|
+
previewButtonClassName,
|
|
109
|
+
previewSwatchClassName,
|
|
110
|
+
pickerInputClassName,
|
|
111
|
+
pickerToggleIcon,
|
|
112
|
+
|
|
113
|
+
// from text variant UI
|
|
114
|
+
error,
|
|
115
|
+
joinControls = true,
|
|
116
|
+
extendBoxToControls = true,
|
|
117
|
+
|
|
118
|
+
// everything else → Input (size, density, className, icons, etc.)
|
|
119
|
+
...restTextProps
|
|
120
|
+
} = props;
|
|
121
|
+
|
|
122
|
+
const [local, setLocal] = React.useState<string>(value ?? "");
|
|
123
|
+
const [pickerOpen, setPickerOpen] = React.useState(false);
|
|
124
|
+
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
setLocal(value ?? "");
|
|
127
|
+
}, [value]);
|
|
128
|
+
|
|
129
|
+
const pickerRef = React.useRef<HTMLInputElement | null>(null);
|
|
130
|
+
|
|
131
|
+
const effectiveColor = normalizeColorForPicker(local || value);
|
|
132
|
+
const showError = Boolean(error);
|
|
133
|
+
|
|
134
|
+
const openSystemPicker = React.useCallback(() => {
|
|
135
|
+
setPickerOpen(true);
|
|
136
|
+
// Small timeout so state flushes before click; not strictly required but safe.
|
|
137
|
+
window.setTimeout(() => {
|
|
138
|
+
pickerRef.current?.click();
|
|
139
|
+
}, 0);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const handleTextChange = React.useCallback(
|
|
143
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
144
|
+
const next = event.target.value ?? "";
|
|
145
|
+
setLocal(next);
|
|
146
|
+
|
|
147
|
+
if (onValue) {
|
|
148
|
+
const detail: ChangeDetail<{ source: "input" }> = {
|
|
149
|
+
source: "variant",
|
|
150
|
+
raw: next,
|
|
151
|
+
nativeEvent: event,
|
|
152
|
+
meta: { source: "input" },
|
|
153
|
+
};
|
|
154
|
+
onValue(next || undefined, detail);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
[onValue]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const handlePickerChange = React.useCallback(
|
|
161
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
162
|
+
const next = event.target.value ?? "";
|
|
163
|
+
setLocal(next);
|
|
164
|
+
|
|
165
|
+
if (onValue) {
|
|
166
|
+
const detail: ChangeDetail<{ source: "picker" }> = {
|
|
167
|
+
source: "variant",
|
|
168
|
+
raw: next,
|
|
169
|
+
nativeEvent: event,
|
|
170
|
+
meta: { source: "picker" },
|
|
171
|
+
};
|
|
172
|
+
onValue(next || undefined, detail);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Once the user picks a colour, the OS picker closes.
|
|
176
|
+
setPickerOpen(false);
|
|
177
|
+
},
|
|
178
|
+
[onValue]
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const handlePickerBlur = React.useCallback(() => {
|
|
182
|
+
// If the user cancels the picker, blur fires and we can clear state.
|
|
183
|
+
setPickerOpen(false);
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
// ———————————————————————————————
|
|
187
|
+
// Leading control: colour preview
|
|
188
|
+
// ———————————————————————————————
|
|
189
|
+
|
|
190
|
+
const leadingControl = showPreview ? (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={openSystemPicker}
|
|
194
|
+
className={cn(
|
|
195
|
+
"flex h-full items-center px-3 border-r border-border/50",
|
|
196
|
+
"hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:bg-muted/50",
|
|
197
|
+
previewButtonClassName
|
|
198
|
+
)}
|
|
199
|
+
tabIndex={-1}
|
|
200
|
+
aria-label="Open colour picker"
|
|
201
|
+
>
|
|
202
|
+
<span
|
|
203
|
+
className={cn(
|
|
204
|
+
"inline-flex rounded-sm shadow-sm ring-1 ring-inset ring-foreground/10",
|
|
205
|
+
previewSwatchClassName
|
|
206
|
+
)}
|
|
207
|
+
style={{
|
|
208
|
+
width: previewSize,
|
|
209
|
+
height: previewSize,
|
|
210
|
+
backgroundColor: effectiveColor,
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
</button>
|
|
214
|
+
) : undefined;
|
|
215
|
+
|
|
216
|
+
// ———————————————————————————————
|
|
217
|
+
// Trailing control: picker toggle icon
|
|
218
|
+
// ———————————————————————————————
|
|
219
|
+
|
|
220
|
+
const toggleNode =
|
|
221
|
+
pickerToggleIcon !== undefined ? (
|
|
222
|
+
pickerToggleIcon
|
|
223
|
+
) : (
|
|
224
|
+
// Swapped the text caret for a Lucide Palette icon
|
|
225
|
+
<Palette className="h-4 w-4 opacity-50" />
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const trailingControl = showPickerToggle ? (
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={openSystemPicker}
|
|
232
|
+
className="flex h-full items-center px-3 text-muted-foreground hover:text-foreground transition-colors"
|
|
233
|
+
tabIndex={-1}
|
|
234
|
+
aria-label={pickerOpen ? "Close colour picker" : "Open colour picker"}
|
|
235
|
+
data-open={pickerOpen ? "true" : "false"}
|
|
236
|
+
>
|
|
237
|
+
{toggleNode}
|
|
238
|
+
</button>
|
|
239
|
+
) : undefined;
|
|
240
|
+
|
|
241
|
+
// ———————————————————————————————
|
|
242
|
+
// Render
|
|
243
|
+
// ———————————————————————————————
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div className={cn("relative group/color", wrapperClassName)}>
|
|
247
|
+
<Input
|
|
248
|
+
ref={ref}
|
|
249
|
+
{...restTextProps}
|
|
250
|
+
type="text"
|
|
251
|
+
value={local}
|
|
252
|
+
onChange={handleTextChange}
|
|
253
|
+
leadingControl={leadingControl}
|
|
254
|
+
trailingControl={trailingControl}
|
|
255
|
+
joinControls={joinControls}
|
|
256
|
+
extendBoxToControls={extendBoxToControls}
|
|
257
|
+
aria-invalid={showError ? "true" : undefined}
|
|
258
|
+
// Added mono font and uppercase for cleaner hex code display
|
|
259
|
+
className={cn("font-mono uppercase", restTextProps.className)}
|
|
260
|
+
maxLength={9}
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
{/* Native color input – used to show the real browser/OS picker.
|
|
264
|
+
By default it's visually hidden; override pickerInputClassName
|
|
265
|
+
if you ever want to show/style it directly. */}
|
|
266
|
+
<input
|
|
267
|
+
ref={pickerRef}
|
|
268
|
+
type="color"
|
|
269
|
+
// hidden
|
|
270
|
+
className={cn(
|
|
271
|
+
"absolute h-0 w-0 opacity-0 pointer-events-none",
|
|
272
|
+
pickerInputClassName
|
|
273
|
+
)}
|
|
274
|
+
value={effectiveColor}
|
|
275
|
+
onChange={handlePickerChange}
|
|
276
|
+
onBlur={handlePickerBlur}
|
|
277
|
+
tabIndex={-1}
|
|
278
|
+
aria-hidden="true"
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
ShadcnColorVariant.displayName = "ShadcnColorVariant";
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// src/presets/shadcn-variants/custom.tsx
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props for the generic "custom" variant.
|
|
8
|
+
*
|
|
9
|
+
* - The only special props we define are:
|
|
10
|
+
* - component: the React component to render
|
|
11
|
+
* - valueProp / changeProp / disabledProp / readOnlyProp / errorProp
|
|
12
|
+
* - idProp / nameProp / placeholderProp
|
|
13
|
+
* - mapValue / mapDetail (optional hooks)
|
|
14
|
+
*
|
|
15
|
+
* - All other props are treated as "component props" and forwarded
|
|
16
|
+
* directly to the underlying component.
|
|
17
|
+
*
|
|
18
|
+
* The underlying component is expected to:
|
|
19
|
+
* - accept the mapped `valueProp`
|
|
20
|
+
* - call the mapped `changeProp` with the next value (first argument)
|
|
21
|
+
* - optionally use disabled/readOnly/error/id/name/placeholder via the mapped names
|
|
22
|
+
*/
|
|
23
|
+
export interface ShadcnCustomVariantProps<TValue = unknown>
|
|
24
|
+
extends VariantBaseProps<TValue> {
|
|
25
|
+
/**
|
|
26
|
+
* The actual React component to render.
|
|
27
|
+
*
|
|
28
|
+
* Example:
|
|
29
|
+
* component={MyToggle}
|
|
30
|
+
*/
|
|
31
|
+
component: React.ComponentType<any>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prop name that carries the current value for the component.
|
|
35
|
+
* Default: "value".
|
|
36
|
+
*/
|
|
37
|
+
valueProp?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Prop name for the change handler that the component will call.
|
|
41
|
+
* Default: "onChange".
|
|
42
|
+
*
|
|
43
|
+
* The component is expected to call:
|
|
44
|
+
* props[changeProp](nextValue, ...otherArgs?)
|
|
45
|
+
*
|
|
46
|
+
* The first argument is taken as the new value.
|
|
47
|
+
*/
|
|
48
|
+
changeProp?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Prop name for disabled state.
|
|
52
|
+
* Default: "disabled".
|
|
53
|
+
*/
|
|
54
|
+
disabledProp?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prop name for read-only state.
|
|
58
|
+
* Default: "readOnly".
|
|
59
|
+
*/
|
|
60
|
+
readOnlyProp?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prop name for passing error to the component (if it cares).
|
|
64
|
+
* If provided, we pass the `error` field as-is.
|
|
65
|
+
* Example values: "error", "isInvalid", "status".
|
|
66
|
+
*/
|
|
67
|
+
errorProp?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prop name for the id attribute.
|
|
71
|
+
* Default: "id".
|
|
72
|
+
*/
|
|
73
|
+
idProp?: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prop name for the name attribute.
|
|
77
|
+
* Default: "name".
|
|
78
|
+
*/
|
|
79
|
+
nameProp?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Prop name for the placeholder attribute.
|
|
83
|
+
* Default: "placeholder".
|
|
84
|
+
*/
|
|
85
|
+
placeholderProp?: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional transform for the raw next value before it hits the field.
|
|
89
|
+
*
|
|
90
|
+
* Receives the first argument that the component passes to the change
|
|
91
|
+
* handler, plus the full argument list for flexibility.
|
|
92
|
+
*/
|
|
93
|
+
mapValue?: (raw: any, ...args: any[]) => TValue;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optional builder for ChangeDetail, given the raw next value.
|
|
97
|
+
*
|
|
98
|
+
* If omitted, a default { source: "variant", raw } detail is used.
|
|
99
|
+
*/
|
|
100
|
+
mapDetail?: (raw: any, ...args: any[]) => ChangeDetail;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Any other props are assumed to belong to the custom component.
|
|
104
|
+
*/
|
|
105
|
+
[key: string]: unknown;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const ShadcnCustomVariant = React.forwardRef<
|
|
109
|
+
any,
|
|
110
|
+
ShadcnCustomVariantProps<any>
|
|
111
|
+
>(function ShadcnCustomVariant(props, ref) {
|
|
112
|
+
const {
|
|
113
|
+
// Variant base props we care about:
|
|
114
|
+
value,
|
|
115
|
+
onValue,
|
|
116
|
+
error,
|
|
117
|
+
disabled,
|
|
118
|
+
readOnly,
|
|
119
|
+
id,
|
|
120
|
+
name,
|
|
121
|
+
placeholder,
|
|
122
|
+
|
|
123
|
+
// Mapping props:
|
|
124
|
+
component: Component,
|
|
125
|
+
valueProp = "value",
|
|
126
|
+
changeProp = "onChange",
|
|
127
|
+
disabledProp = "disabled",
|
|
128
|
+
readOnlyProp = "readOnly",
|
|
129
|
+
errorProp,
|
|
130
|
+
idProp = "id",
|
|
131
|
+
nameProp = "name",
|
|
132
|
+
placeholderProp = "placeholder",
|
|
133
|
+
|
|
134
|
+
mapValue,
|
|
135
|
+
mapDetail,
|
|
136
|
+
|
|
137
|
+
// Everything else goes straight to the component:
|
|
138
|
+
...rest
|
|
139
|
+
} = props as ShadcnCustomVariantProps<any>;
|
|
140
|
+
|
|
141
|
+
// If there is no component, bail out (dev-time safety).
|
|
142
|
+
if (!Component) {
|
|
143
|
+
if (process.env.NODE_ENV !== "production") {
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.warn(
|
|
146
|
+
"[form-palette] ShadcnCustomVariant: `component` prop is required.",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isDisabled = !!disabled;
|
|
153
|
+
const isReadOnly = !!readOnly;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Bridge from the component's change callback to the variant contract.
|
|
157
|
+
*
|
|
158
|
+
* We assume the custom component calls the mapped change prop
|
|
159
|
+
* with the **next value as its first argument**:
|
|
160
|
+
*
|
|
161
|
+
* props[changeProp](nextValue, ...rest)
|
|
162
|
+
*/
|
|
163
|
+
const handleChange = React.useCallback(
|
|
164
|
+
(...args: any[]) => {
|
|
165
|
+
if (!onValue) return;
|
|
166
|
+
if (isDisabled || isReadOnly) return;
|
|
167
|
+
|
|
168
|
+
const raw = args[0];
|
|
169
|
+
|
|
170
|
+
const next = mapValue
|
|
171
|
+
? mapValue(raw, ...args)
|
|
172
|
+
: (raw as any);
|
|
173
|
+
|
|
174
|
+
const detail: ChangeDetail =
|
|
175
|
+
mapDetail?.(raw, ...args) ?? {
|
|
176
|
+
source: "variant",
|
|
177
|
+
raw,
|
|
178
|
+
nativeEvent: undefined,
|
|
179
|
+
meta: undefined,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
(onValue as any)(next, detail);
|
|
183
|
+
},
|
|
184
|
+
[onValue, isDisabled, isReadOnly, mapValue, mapDetail],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Build the props for the custom component.
|
|
188
|
+
const innerProps: Record<string, unknown> = {
|
|
189
|
+
...rest, // ← all non-special props from InputField go directly to the component
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Map value → component[valueProp]
|
|
193
|
+
innerProps[valueProp] = value;
|
|
194
|
+
|
|
195
|
+
// Map handler → component[changeProp]
|
|
196
|
+
innerProps[changeProp] = handleChange;
|
|
197
|
+
|
|
198
|
+
// Map disabled / readOnly
|
|
199
|
+
if (disabledProp) {
|
|
200
|
+
innerProps[disabledProp] = isDisabled;
|
|
201
|
+
}
|
|
202
|
+
if (readOnlyProp) {
|
|
203
|
+
innerProps[readOnlyProp] = isReadOnly;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Map error if a mapping key is provided
|
|
207
|
+
if (errorProp && error !== undefined) {
|
|
208
|
+
innerProps[errorProp] = error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Map id/name/placeholder if present
|
|
212
|
+
if (id !== undefined && idProp) {
|
|
213
|
+
innerProps[idProp] = id;
|
|
214
|
+
}
|
|
215
|
+
if (name !== undefined && nameProp) {
|
|
216
|
+
innerProps[nameProp] = name;
|
|
217
|
+
}
|
|
218
|
+
if (placeholder !== undefined && placeholderProp) {
|
|
219
|
+
innerProps[placeholderProp] = placeholder;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return <Component ref={ref} {...innerProps} />;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
ShadcnCustomVariant.displayName = "ShadcnCustomVariant";
|
|
226
|
+
|
|
227
|
+
export default ShadcnCustomVariant;
|