@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,1011 @@
|
|
|
1
|
+
// src/presets/ui/input.tsx
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { InputMask } from "../ui/input-mask";
|
|
7
|
+
|
|
8
|
+
type MaskMode = "raw" | "masked";
|
|
9
|
+
|
|
10
|
+
// Mask-related props (UI-level only; value semantics are up to callers)
|
|
11
|
+
export interface InputMaskProps {
|
|
12
|
+
mask?: string;
|
|
13
|
+
maskDefinitions?: Record<string, RegExp>; // reserved for future engine
|
|
14
|
+
slotChar?: string;
|
|
15
|
+
autoClear?: boolean;
|
|
16
|
+
unmask?: MaskMode | boolean;
|
|
17
|
+
maskInsertMode?: "stream" | "caret";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Prefix / suffix (value-level, NOT icons)
|
|
21
|
+
export interface InputAffixProps {
|
|
22
|
+
prefix?: string;
|
|
23
|
+
suffix?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* If true (default), we assume the model value does NOT contain the prefix
|
|
27
|
+
* and we only add it visually at render time.
|
|
28
|
+
*/
|
|
29
|
+
stripPrefix?: boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* If true (default), we assume the model value does NOT contain the suffix
|
|
33
|
+
* and we only add it visually at render time.
|
|
34
|
+
*/
|
|
35
|
+
stripSuffix?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Icons & controls (pure overlays, like in ShadcnTextVariant)
|
|
39
|
+
export interface InputIconControlProps {
|
|
40
|
+
leadingIcons?: React.ReactNode[];
|
|
41
|
+
trailingIcons?: React.ReactNode[];
|
|
42
|
+
icon?: React.ReactNode;
|
|
43
|
+
|
|
44
|
+
iconGap?: number;
|
|
45
|
+
leadingIconSpacing?: number;
|
|
46
|
+
trailingIconSpacing?: number;
|
|
47
|
+
|
|
48
|
+
leadingControl?: React.ReactNode;
|
|
49
|
+
trailingControl?: React.ReactNode;
|
|
50
|
+
leadingControlClassName?: string;
|
|
51
|
+
trailingControlClassName?: string;
|
|
52
|
+
|
|
53
|
+
joinControls?: boolean;
|
|
54
|
+
extendBoxToControls?: boolean;
|
|
55
|
+
|
|
56
|
+
px?: number;
|
|
57
|
+
py?: number;
|
|
58
|
+
ps?: number;
|
|
59
|
+
pe?: number;
|
|
60
|
+
pb?: number;
|
|
61
|
+
|
|
62
|
+
inputClassName?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InputSizeProps {
|
|
66
|
+
size?: "sm" | "md" | "lg" | (string & {});
|
|
67
|
+
density?: "compact" | "normal" | "relaxed" | "dense" | "loose" | (string & {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────
|
|
71
|
+
// KeyFilter support (PrimeReact-style)
|
|
72
|
+
// ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export type InputKeyFilter =
|
|
75
|
+
| string
|
|
76
|
+
| RegExp
|
|
77
|
+
| ((
|
|
78
|
+
nextValue: string,
|
|
79
|
+
ctx: {
|
|
80
|
+
event: any;
|
|
81
|
+
currentValue: string;
|
|
82
|
+
input: HTMLInputElement;
|
|
83
|
+
}
|
|
84
|
+
) => boolean);
|
|
85
|
+
|
|
86
|
+
export interface InputKeyFilterProps {
|
|
87
|
+
/**
|
|
88
|
+
* Filter that constrains what can be typed / pasted.
|
|
89
|
+
*
|
|
90
|
+
* - string preset: "int" | "num" | "money" | "hex" | "alpha" | "alphanum" | "email"
|
|
91
|
+
* - string pattern: converted to new RegExp(pattern)
|
|
92
|
+
* - RegExp: used directly
|
|
93
|
+
* - function: custom validator
|
|
94
|
+
*/
|
|
95
|
+
keyFilter?: InputKeyFilter;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Which keyboard event to hook for filtering:
|
|
99
|
+
* - "keydown"
|
|
100
|
+
* - "keypress" (closest to PrimeReact default)
|
|
101
|
+
* - "beforeinput"
|
|
102
|
+
*
|
|
103
|
+
* Default: "keypress"
|
|
104
|
+
*/
|
|
105
|
+
keyFilterOn?: "keydown" | "keypress" | "beforeinput";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether to apply keyFilter to paste events.
|
|
109
|
+
* Default: true
|
|
110
|
+
*/
|
|
111
|
+
keyFilterOnPaste?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cx(...parts: any[]) {
|
|
115
|
+
return cn(...parts);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveKeyFilterPattern(filter: string | RegExp | undefined): RegExp | null {
|
|
119
|
+
if (!filter) return null;
|
|
120
|
+
|
|
121
|
+
if (filter instanceof RegExp) {
|
|
122
|
+
// remove stateful flags for safety
|
|
123
|
+
const flags = filter.flags.replace("g", "").replace("y", "");
|
|
124
|
+
return new RegExp(filter.source, flags);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const presets: Record<string, RegExp> = {
|
|
128
|
+
int: /^[+-]?\d*$/,
|
|
129
|
+
num: /^-?\d*(\.\d*)?$/,
|
|
130
|
+
money: /^-?\d*(\.\d{0,2})?$/,
|
|
131
|
+
hex: /^[0-9a-f]*$/i,
|
|
132
|
+
alpha: /^[A-Za-z]*$/,
|
|
133
|
+
alphanum: /^[A-Za-z0-9]*$/,
|
|
134
|
+
email: /^[^\s@]*@?[^\s@]*$/, // lenient while typing
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const preset = presets[filter];
|
|
138
|
+
if (preset) return preset;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return new RegExp(filter);
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runKeyFilter(
|
|
148
|
+
filter: InputKeyFilter | undefined,
|
|
149
|
+
nextValue: string,
|
|
150
|
+
input: HTMLInputElement,
|
|
151
|
+
event: any
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!filter) return true;
|
|
154
|
+
// Always allow empty so users can clear the field
|
|
155
|
+
if (nextValue === "") return true;
|
|
156
|
+
|
|
157
|
+
if (typeof filter === "function") {
|
|
158
|
+
return filter(nextValue, {
|
|
159
|
+
event,
|
|
160
|
+
currentValue: input.value,
|
|
161
|
+
input,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const pattern = resolveKeyFilterPattern(filter as any);
|
|
166
|
+
if (!pattern) return true;
|
|
167
|
+
return pattern.test(nextValue);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function computeNextFromInsertion(
|
|
171
|
+
input: HTMLInputElement,
|
|
172
|
+
inserted: string
|
|
173
|
+
): string {
|
|
174
|
+
const value = input.value ?? "";
|
|
175
|
+
const start = input.selectionStart ?? value.length;
|
|
176
|
+
const end = input.selectionEnd ?? start;
|
|
177
|
+
return value.slice(0, start) + inserted + value.slice(end);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Same logic as in ShadcnTextVariant
|
|
181
|
+
function resolveBasePadding(size: unknown, density: unknown) {
|
|
182
|
+
let px = 12;
|
|
183
|
+
let py = 4;
|
|
184
|
+
|
|
185
|
+
const s = (size as string | undefined) ?? "md";
|
|
186
|
+
const d = (density as string | undefined) ?? "normal";
|
|
187
|
+
|
|
188
|
+
if (s === "sm") {
|
|
189
|
+
px = 10;
|
|
190
|
+
py = 3;
|
|
191
|
+
} else if (s === "lg") {
|
|
192
|
+
px = 14;
|
|
193
|
+
py = 5;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (d === "dense" || d === "compact") {
|
|
197
|
+
py = Math.max(2, py - 1);
|
|
198
|
+
} else if (d === "relaxed" || d === "loose") {
|
|
199
|
+
py = py + 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { px, py };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Same logic as in ShadcnTextVariant
|
|
206
|
+
function resolveSizeDensityClasses(size: unknown, density: unknown) {
|
|
207
|
+
const s = (size as string | undefined) ?? "md";
|
|
208
|
+
const d = (density as string | undefined) ?? "normal";
|
|
209
|
+
|
|
210
|
+
let heightCls = "h-9";
|
|
211
|
+
let textCls = "text-base md:text-sm";
|
|
212
|
+
|
|
213
|
+
if (s === "sm") {
|
|
214
|
+
heightCls = "h-8";
|
|
215
|
+
textCls = "text-sm";
|
|
216
|
+
} else if (s === "lg") {
|
|
217
|
+
heightCls = "h-10";
|
|
218
|
+
textCls = "text-base";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let densityCls = "";
|
|
222
|
+
if (d === "dense" || d === "compact") {
|
|
223
|
+
densityCls = "leading-tight";
|
|
224
|
+
} else if (d === "relaxed" || d === "loose") {
|
|
225
|
+
densityCls = "leading-relaxed";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { heightCls, textCls, densityCls };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface InputProps
|
|
232
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
|
|
233
|
+
InputMaskProps,
|
|
234
|
+
InputAffixProps,
|
|
235
|
+
InputIconControlProps,
|
|
236
|
+
InputSizeProps,
|
|
237
|
+
InputKeyFilterProps { }
|
|
238
|
+
|
|
239
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
240
|
+
function Input(rawProps, forwardedRef) {
|
|
241
|
+
const {
|
|
242
|
+
// base
|
|
243
|
+
className,
|
|
244
|
+
style,
|
|
245
|
+
type,
|
|
246
|
+
disabled,
|
|
247
|
+
readOnly,
|
|
248
|
+
required,
|
|
249
|
+
|
|
250
|
+
// size / density
|
|
251
|
+
size = "md",
|
|
252
|
+
density = "normal",
|
|
253
|
+
|
|
254
|
+
// mask
|
|
255
|
+
mask,
|
|
256
|
+
maskDefinitions, // reserved
|
|
257
|
+
slotChar,
|
|
258
|
+
autoClear,
|
|
259
|
+
unmask,
|
|
260
|
+
maskInsertMode,
|
|
261
|
+
|
|
262
|
+
// affixes (value-level)
|
|
263
|
+
prefix,
|
|
264
|
+
suffix,
|
|
265
|
+
stripPrefix = true,
|
|
266
|
+
stripSuffix = true,
|
|
267
|
+
|
|
268
|
+
// icons / controls
|
|
269
|
+
leadingIcons,
|
|
270
|
+
trailingIcons,
|
|
271
|
+
icon,
|
|
272
|
+
iconGap,
|
|
273
|
+
leadingIconSpacing,
|
|
274
|
+
trailingIconSpacing,
|
|
275
|
+
leadingControl,
|
|
276
|
+
trailingControl,
|
|
277
|
+
leadingControlClassName,
|
|
278
|
+
trailingControlClassName,
|
|
279
|
+
joinControls = true,
|
|
280
|
+
extendBoxToControls = true,
|
|
281
|
+
px,
|
|
282
|
+
py,
|
|
283
|
+
ps,
|
|
284
|
+
pe,
|
|
285
|
+
pb,
|
|
286
|
+
inputClassName,
|
|
287
|
+
|
|
288
|
+
// key filter
|
|
289
|
+
keyFilter,
|
|
290
|
+
keyFilterOn = "keypress",
|
|
291
|
+
keyFilterOnPaste = true,
|
|
292
|
+
|
|
293
|
+
// events
|
|
294
|
+
onChange,
|
|
295
|
+
onFocus,
|
|
296
|
+
onBlur,
|
|
297
|
+
onKeyDown,
|
|
298
|
+
onKeyPress,
|
|
299
|
+
onBeforeInput,
|
|
300
|
+
onPaste,
|
|
301
|
+
|
|
302
|
+
// rest of native props (value, defaultValue, placeholder, etc.)
|
|
303
|
+
...rest
|
|
304
|
+
} = rawProps as InputProps;
|
|
305
|
+
|
|
306
|
+
const sizeKey = (size as string | undefined) ?? "md";
|
|
307
|
+
const densityKey = (density as string | undefined) ?? "normal";
|
|
308
|
+
const isMasked = Boolean(mask);
|
|
309
|
+
|
|
310
|
+
const innerRef = React.useRef<HTMLInputElement | null>(null);
|
|
311
|
+
React.useImperativeHandle(
|
|
312
|
+
forwardedRef,
|
|
313
|
+
() => innerRef.current as any,
|
|
314
|
+
[]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Icons ONLY (prefix/suffix are NOT treated as icons)
|
|
318
|
+
const resolvedLeadingIcons: React.ReactNode[] = (() => {
|
|
319
|
+
if (leadingIcons && leadingIcons.length) return leadingIcons;
|
|
320
|
+
if (icon) return [icon];
|
|
321
|
+
return [];
|
|
322
|
+
})();
|
|
323
|
+
|
|
324
|
+
const resolvedTrailingIcons: React.ReactNode[] = trailingIcons ?? [];
|
|
325
|
+
|
|
326
|
+
const hasLeadingIcons = resolvedLeadingIcons.length > 0;
|
|
327
|
+
const hasTrailingIcons = resolvedTrailingIcons.length > 0;
|
|
328
|
+
|
|
329
|
+
const hasLeadingControl = !!leadingControl;
|
|
330
|
+
const hasTrailingControl = !!trailingControl;
|
|
331
|
+
const hasControls = hasLeadingControl || hasTrailingControl;
|
|
332
|
+
const hasIcons = hasLeadingIcons || hasTrailingIcons;
|
|
333
|
+
const hasExtras = hasControls || hasIcons;
|
|
334
|
+
|
|
335
|
+
const baseIconGap = iconGap ?? 4;
|
|
336
|
+
const leadingGap = leadingIconSpacing ?? baseIconGap;
|
|
337
|
+
const trailingGap = trailingIconSpacing ?? baseIconGap;
|
|
338
|
+
|
|
339
|
+
// Measure icon widths (for padding vars)
|
|
340
|
+
const leadingIconsRef = React.useRef<HTMLDivElement | null>(null);
|
|
341
|
+
const trailingIconsRef = React.useRef<HTMLDivElement | null>(null);
|
|
342
|
+
|
|
343
|
+
const [leadingIconsWidth, setLeadingIconsWidth] =
|
|
344
|
+
React.useState<number>(0);
|
|
345
|
+
const [trailingIconsWidth, setTrailingIconsWidth] =
|
|
346
|
+
React.useState<number>(0);
|
|
347
|
+
|
|
348
|
+
React.useLayoutEffect(() => {
|
|
349
|
+
if (typeof window === "undefined") return;
|
|
350
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
351
|
+
|
|
352
|
+
const leadingEl = leadingIconsRef.current;
|
|
353
|
+
const trailingEl = trailingIconsRef.current;
|
|
354
|
+
if (!leadingEl && !trailingEl) return;
|
|
355
|
+
|
|
356
|
+
const observer = new ResizeObserver((entries) => {
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
const width = entry.contentRect.width;
|
|
359
|
+
if (entry.target === leadingIconsRef.current) {
|
|
360
|
+
setLeadingIconsWidth(width);
|
|
361
|
+
} else if (entry.target === trailingIconsRef.current) {
|
|
362
|
+
setTrailingIconsWidth(width);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (leadingEl) observer.observe(leadingEl);
|
|
368
|
+
if (trailingEl) observer.observe(trailingEl);
|
|
369
|
+
|
|
370
|
+
return () => observer.disconnect();
|
|
371
|
+
}, [hasLeadingIcons, hasTrailingIcons]);
|
|
372
|
+
|
|
373
|
+
// Padding vars (same idea as ShadcnTextVariant, feeding into Tailwind
|
|
374
|
+
// utilities on the actual “box” via CSS variables)
|
|
375
|
+
const { px: pxDefault, py: pyDefault } = resolveBasePadding(
|
|
376
|
+
size,
|
|
377
|
+
density
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const extraPx = typeof px === "number" ? px : 0;
|
|
381
|
+
const extraPy = typeof py === "number" ? py : 0;
|
|
382
|
+
const extraPs = typeof ps === "number" ? ps : 0;
|
|
383
|
+
const extraPe = typeof pe === "number" ? pe : 0;
|
|
384
|
+
const extraPb = typeof pb === "number" ? pb : 0;
|
|
385
|
+
|
|
386
|
+
let paddingStart = pxDefault + extraPx + extraPs;
|
|
387
|
+
let paddingEnd = pxDefault + extraPx + extraPe;
|
|
388
|
+
const paddingTop = pyDefault + extraPy;
|
|
389
|
+
const paddingBottom = pyDefault + extraPy + extraPb;
|
|
390
|
+
|
|
391
|
+
const textGap = baseIconGap;
|
|
392
|
+
|
|
393
|
+
if (hasLeadingIcons && leadingIconsWidth > 0) {
|
|
394
|
+
paddingStart += leadingIconsWidth + textGap;
|
|
395
|
+
}
|
|
396
|
+
if (hasTrailingIcons && trailingIconsWidth > 0) {
|
|
397
|
+
paddingEnd += trailingIconsWidth + textGap;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const varsStyle: React.CSSProperties = {
|
|
401
|
+
...(style ?? {}),
|
|
402
|
+
"--fp-pl": `${paddingStart}px`,
|
|
403
|
+
"--fp-pr": `${paddingEnd}px`,
|
|
404
|
+
"--fp-pt": `${paddingTop}px`,
|
|
405
|
+
"--fp-pb": `${paddingBottom}px`,
|
|
406
|
+
} as React.CSSProperties;
|
|
407
|
+
|
|
408
|
+
const { heightCls, textCls, densityCls } = resolveSizeDensityClasses(
|
|
409
|
+
size,
|
|
410
|
+
density
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Core “box” classes (border, radius, focus, size/density),
|
|
414
|
+
// WITHOUT padding – padding is applied only on the actual box element.
|
|
415
|
+
const baseBoxClasses = cx(
|
|
416
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30",
|
|
417
|
+
"border-input w-full min-w-0 rounded-md border bg-transparent shadow-xs",
|
|
418
|
+
"transition-[color,box-shadow] outline-none",
|
|
419
|
+
"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
|
420
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
421
|
+
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
|
|
422
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
423
|
+
heightCls,
|
|
424
|
+
textCls,
|
|
425
|
+
densityCls
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Padding classes driven by CSS vars
|
|
429
|
+
const boxPaddingClasses = cx(
|
|
430
|
+
"px-(--fp-pl,--spacing(3)) pr-(--fp-pr,--spacing(3))",
|
|
431
|
+
"pt-(--fp-pt,--spacing(1)) pb-(--fp-pb,--spacing(1))"
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// Inner neutral input (used when the *wrapper* carries the box)
|
|
435
|
+
const innerInputNeutral = cx(
|
|
436
|
+
"w-full min-w-0 bg-transparent border-none shadow-none outline-none",
|
|
437
|
+
"px-0 py-0",
|
|
438
|
+
"focus-visible:outline-none focus-visible:ring-0 focus-visible:border-transparent",
|
|
439
|
+
"placeholder:text-muted-foreground",
|
|
440
|
+
inputClassName
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const maskMode: MaskMode =
|
|
444
|
+
unmask === true || unmask === "raw" ? "raw" : "masked";
|
|
445
|
+
|
|
446
|
+
// Focus handler with prefix/suffix selection logic
|
|
447
|
+
const handleFocus = React.useCallback(
|
|
448
|
+
(event: React.FocusEvent<HTMLInputElement>) => {
|
|
449
|
+
onFocus?.(event);
|
|
450
|
+
|
|
451
|
+
if (!prefix && !suffix) return;
|
|
452
|
+
|
|
453
|
+
const inputEl = event.currentTarget;
|
|
454
|
+
const inputValue = inputEl.value;
|
|
455
|
+
const prefixLength = (prefix || "").length;
|
|
456
|
+
const suffixLength = (suffix || "").length;
|
|
457
|
+
const end =
|
|
458
|
+
inputValue.length === 0
|
|
459
|
+
? 0
|
|
460
|
+
: inputValue.length - suffixLength;
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
inputEl.setSelectionRange(prefixLength, end);
|
|
464
|
+
} catch {
|
|
465
|
+
// ignore if unsupported
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
[onFocus, prefix, suffix]
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const focusInput = () => {
|
|
472
|
+
if (innerRef.current) {
|
|
473
|
+
innerRef.current.focus();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleIconMouseDown = (e: React.MouseEvent) => {
|
|
478
|
+
e.preventDefault();
|
|
479
|
+
focusInput();
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const placeholder =
|
|
483
|
+
typeof mask === "string" && mask
|
|
484
|
+
? mask
|
|
485
|
+
: (rest as any).placeholder;
|
|
486
|
+
|
|
487
|
+
const hasCustomPadding =
|
|
488
|
+
typeof px === "number" ||
|
|
489
|
+
typeof py === "number" ||
|
|
490
|
+
typeof ps === "number" ||
|
|
491
|
+
typeof pe === "number" ||
|
|
492
|
+
typeof pb === "number";
|
|
493
|
+
|
|
494
|
+
const hasKeyFilter = !!keyFilter;
|
|
495
|
+
|
|
496
|
+
// Key filter wrappers
|
|
497
|
+
const handleKeyDownWrapped = React.useCallback(
|
|
498
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
499
|
+
if (
|
|
500
|
+
hasKeyFilter &&
|
|
501
|
+
keyFilterOn === "keydown" &&
|
|
502
|
+
!event.ctrlKey &&
|
|
503
|
+
!event.metaKey &&
|
|
504
|
+
!event.altKey &&
|
|
505
|
+
event.key &&
|
|
506
|
+
event.key.length === 1
|
|
507
|
+
) {
|
|
508
|
+
const inputEl = event.currentTarget;
|
|
509
|
+
const nextValue = computeNextFromInsertion(
|
|
510
|
+
inputEl,
|
|
511
|
+
event.key
|
|
512
|
+
);
|
|
513
|
+
if (!runKeyFilter(keyFilter, nextValue, inputEl, event)) {
|
|
514
|
+
event.preventDefault();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
onKeyDown?.(event);
|
|
520
|
+
},
|
|
521
|
+
[hasKeyFilter, keyFilterOn, keyFilter, onKeyDown]
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const handleKeyPressWrapped = React.useCallback(
|
|
525
|
+
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
526
|
+
if (
|
|
527
|
+
hasKeyFilter &&
|
|
528
|
+
keyFilterOn === "keypress" &&
|
|
529
|
+
!event.ctrlKey &&
|
|
530
|
+
!event.metaKey &&
|
|
531
|
+
!event.altKey &&
|
|
532
|
+
event.key &&
|
|
533
|
+
event.key.length === 1
|
|
534
|
+
) {
|
|
535
|
+
const inputEl = event.currentTarget;
|
|
536
|
+
const nextValue = computeNextFromInsertion(
|
|
537
|
+
inputEl,
|
|
538
|
+
event.key
|
|
539
|
+
);
|
|
540
|
+
if (!runKeyFilter(keyFilter, nextValue, inputEl, event)) {
|
|
541
|
+
event.preventDefault();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
onKeyPress?.(event);
|
|
547
|
+
},
|
|
548
|
+
[hasKeyFilter, keyFilterOn, keyFilter, onKeyPress]
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
const handleBeforeInputWrapped = React.useCallback(
|
|
552
|
+
(event: any) => {
|
|
553
|
+
if (
|
|
554
|
+
hasKeyFilter &&
|
|
555
|
+
keyFilterOn === "beforeinput" &&
|
|
556
|
+
event?.nativeEvent
|
|
557
|
+
) {
|
|
558
|
+
const inputEl = event.currentTarget as HTMLInputElement;
|
|
559
|
+
const data = event.nativeEvent.data as string | null;
|
|
560
|
+
const inputType = event.nativeEvent.inputType as string | null;
|
|
561
|
+
|
|
562
|
+
// We only care about text insertions; deletions/etc. pass through.
|
|
563
|
+
if (data && inputType && inputType.startsWith("insert")) {
|
|
564
|
+
const nextValue = computeNextFromInsertion(
|
|
565
|
+
inputEl,
|
|
566
|
+
data
|
|
567
|
+
);
|
|
568
|
+
if (!runKeyFilter(keyFilter, nextValue, inputEl, event)) {
|
|
569
|
+
event.preventDefault();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
onBeforeInput?.(event);
|
|
576
|
+
},
|
|
577
|
+
[hasKeyFilter, keyFilterOn, keyFilter, onBeforeInput]
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
const handlePasteWrapped = React.useCallback(
|
|
581
|
+
(event: React.ClipboardEvent<HTMLInputElement>) => {
|
|
582
|
+
if (hasKeyFilter && keyFilterOnPaste) {
|
|
583
|
+
const pasted =
|
|
584
|
+
event.clipboardData?.getData("text") ?? "";
|
|
585
|
+
if (pasted) {
|
|
586
|
+
const inputEl = event.currentTarget;
|
|
587
|
+
const nextValue = computeNextFromInsertion(
|
|
588
|
+
inputEl,
|
|
589
|
+
pasted
|
|
590
|
+
);
|
|
591
|
+
if (!runKeyFilter(keyFilter, nextValue, inputEl, event)) {
|
|
592
|
+
event.preventDefault();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
onPaste?.(event);
|
|
599
|
+
},
|
|
600
|
+
[hasKeyFilter, keyFilterOnPaste, keyFilter, onPaste]
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
// Core renderer (mask vs plain)
|
|
604
|
+
const renderBaseInput = (extra: {
|
|
605
|
+
className?: string;
|
|
606
|
+
style?: React.CSSProperties;
|
|
607
|
+
inner?: boolean; // false → input is the box; true/undefined → input is inner neutral
|
|
608
|
+
}) => {
|
|
609
|
+
const useInnerNeutral = extra.inner !== false;
|
|
610
|
+
|
|
611
|
+
// MASKED: we delegate value semantics to caller.
|
|
612
|
+
if (isMasked && mask) {
|
|
613
|
+
let maskWithAffixes = mask;
|
|
614
|
+
if (prefix) {
|
|
615
|
+
maskWithAffixes = `${prefix}${maskWithAffixes}`;
|
|
616
|
+
}
|
|
617
|
+
if (suffix) {
|
|
618
|
+
maskWithAffixes = `${maskWithAffixes}${suffix}`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return (
|
|
622
|
+
//@ts-ignore
|
|
623
|
+
<InputMask
|
|
624
|
+
ref={innerRef as any}
|
|
625
|
+
mask={maskWithAffixes}
|
|
626
|
+
slotChar={slotChar ?? "_"}
|
|
627
|
+
unmask={maskMode === "raw"}
|
|
628
|
+
disabled={disabled}
|
|
629
|
+
readOnly={readOnly}
|
|
630
|
+
onChange={onChange as any}
|
|
631
|
+
onBlur={onBlur as any}
|
|
632
|
+
onFocus={handleFocus as any}
|
|
633
|
+
onKeyDown={handleKeyDownWrapped as any}
|
|
634
|
+
onKeyPress={handleKeyPressWrapped as any}
|
|
635
|
+
onBeforeInput={handleBeforeInputWrapped as any}
|
|
636
|
+
onPaste={handlePasteWrapped as any}
|
|
637
|
+
aria-required={required ? "true" : undefined}
|
|
638
|
+
data-size={sizeKey}
|
|
639
|
+
data-density={densityKey}
|
|
640
|
+
placeholder={placeholder}
|
|
641
|
+
className={cx(
|
|
642
|
+
useInnerNeutral ? innerInputNeutral : "",
|
|
643
|
+
extra.className
|
|
644
|
+
)}
|
|
645
|
+
style={extra.style}
|
|
646
|
+
data-slot="input"
|
|
647
|
+
{...rest}
|
|
648
|
+
/>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// PLAIN: value-level prefix/suffix
|
|
653
|
+
const modelValue = (rest.value ??
|
|
654
|
+
rest.defaultValue ??
|
|
655
|
+
"") as string | number | readonly string[];
|
|
656
|
+
|
|
657
|
+
let displayValue =
|
|
658
|
+
typeof modelValue === "string"
|
|
659
|
+
? modelValue
|
|
660
|
+
: Array.isArray(modelValue)
|
|
661
|
+
? modelValue.join(",")
|
|
662
|
+
: String(modelValue ?? "");
|
|
663
|
+
|
|
664
|
+
if (prefix) {
|
|
665
|
+
const hasPrefix = displayValue.startsWith(prefix);
|
|
666
|
+
|
|
667
|
+
if (stripPrefix) {
|
|
668
|
+
const withoutPrefix = hasPrefix
|
|
669
|
+
? displayValue.slice(prefix.length)
|
|
670
|
+
: displayValue;
|
|
671
|
+
displayValue = prefix + withoutPrefix;
|
|
672
|
+
} else {
|
|
673
|
+
displayValue = hasPrefix
|
|
674
|
+
? displayValue
|
|
675
|
+
: prefix + displayValue;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (suffix) {
|
|
680
|
+
const hasSuffix = displayValue.endsWith(suffix);
|
|
681
|
+
|
|
682
|
+
if (stripSuffix) {
|
|
683
|
+
const withoutSuffix = hasSuffix
|
|
684
|
+
? displayValue.slice(
|
|
685
|
+
0,
|
|
686
|
+
displayValue.length - suffix.length
|
|
687
|
+
)
|
|
688
|
+
: displayValue;
|
|
689
|
+
displayValue = withoutSuffix + suffix;
|
|
690
|
+
} else {
|
|
691
|
+
displayValue = hasSuffix
|
|
692
|
+
? displayValue
|
|
693
|
+
: displayValue + suffix;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return (
|
|
698
|
+
//@ts-ignore
|
|
699
|
+
<input
|
|
700
|
+
ref={innerRef}
|
|
701
|
+
type={type}
|
|
702
|
+
data-slot="input"
|
|
703
|
+
className={cx(
|
|
704
|
+
useInnerNeutral ? innerInputNeutral : "",
|
|
705
|
+
extra.className
|
|
706
|
+
)}
|
|
707
|
+
style={extra.style}
|
|
708
|
+
disabled={disabled}
|
|
709
|
+
readOnly={readOnly}
|
|
710
|
+
aria-required={required ? "true" : undefined}
|
|
711
|
+
data-size={sizeKey}
|
|
712
|
+
data-density={densityKey}
|
|
713
|
+
placeholder={placeholder}
|
|
714
|
+
value={displayValue}
|
|
715
|
+
onChange={onChange as any}
|
|
716
|
+
onBlur={onBlur as any}
|
|
717
|
+
onFocus={handleFocus}
|
|
718
|
+
onKeyDown={handleKeyDownWrapped as any}
|
|
719
|
+
onKeyPress={handleKeyPressWrapped as any}
|
|
720
|
+
onBeforeInput={handleBeforeInputWrapped as any}
|
|
721
|
+
onPaste={handlePasteWrapped as any}
|
|
722
|
+
{...rest}
|
|
723
|
+
/>
|
|
724
|
+
);
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// RENDER MODES
|
|
728
|
+
// 1. No controls, no icons → simple input (input is the box)
|
|
729
|
+
if (!hasControls && !hasIcons && !hasCustomPadding) {
|
|
730
|
+
return renderBaseInput({
|
|
731
|
+
inner: false,
|
|
732
|
+
className: cx(baseBoxClasses, boxPaddingClasses, className),
|
|
733
|
+
style: varsStyle,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 2. No controls, but icons and/or custom padding → wrapper + box input
|
|
738
|
+
if (!hasControls) {
|
|
739
|
+
return (
|
|
740
|
+
<div
|
|
741
|
+
className={cx("relative w-full")}
|
|
742
|
+
style={style}
|
|
743
|
+
data-slot="input-wrapper"
|
|
744
|
+
data-has-icons={hasIcons ? "true" : "false"}
|
|
745
|
+
>
|
|
746
|
+
{renderBaseInput({
|
|
747
|
+
inner: false,
|
|
748
|
+
className: cx(baseBoxClasses, boxPaddingClasses, className),
|
|
749
|
+
style: varsStyle,
|
|
750
|
+
})}
|
|
751
|
+
|
|
752
|
+
{hasLeadingIcons && (
|
|
753
|
+
<div
|
|
754
|
+
ref={leadingIconsRef}
|
|
755
|
+
className="pointer-events-auto absolute inset-y-0 left-0 flex items-center cursor-pointer"
|
|
756
|
+
style={{
|
|
757
|
+
gap: leadingGap,
|
|
758
|
+
paddingLeft: `${pxDefault}px`,
|
|
759
|
+
}}
|
|
760
|
+
data-slot="leading-icons"
|
|
761
|
+
onMouseDown={handleIconMouseDown}
|
|
762
|
+
>
|
|
763
|
+
{resolvedLeadingIcons.map((node, idx) => (
|
|
764
|
+
<span
|
|
765
|
+
key={idx}
|
|
766
|
+
className="flex items-center justify-center"
|
|
767
|
+
>
|
|
768
|
+
{node}
|
|
769
|
+
</span>
|
|
770
|
+
))}
|
|
771
|
+
</div>
|
|
772
|
+
)}
|
|
773
|
+
|
|
774
|
+
{hasTrailingIcons && (
|
|
775
|
+
<div
|
|
776
|
+
ref={trailingIconsRef}
|
|
777
|
+
className="pointer-events-auto absolute inset-y-0 right-0 flex items-center cursor-pointer"
|
|
778
|
+
style={{
|
|
779
|
+
gap: trailingGap,
|
|
780
|
+
paddingRight: `${pxDefault}px`,
|
|
781
|
+
}}
|
|
782
|
+
data-slot="trailing-icons"
|
|
783
|
+
onMouseDown={handleIconMouseDown}
|
|
784
|
+
>
|
|
785
|
+
{resolvedTrailingIcons.map((node, idx) => (
|
|
786
|
+
<span
|
|
787
|
+
key={idx}
|
|
788
|
+
className="flex items-center justify-center"
|
|
789
|
+
>
|
|
790
|
+
{node}
|
|
791
|
+
</span>
|
|
792
|
+
))}
|
|
793
|
+
</div>
|
|
794
|
+
)}
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// From here: we have controls → we take over the box.
|
|
800
|
+
// data-slot="input-group" NEVER carries padding; padding is on input-region / input-box.
|
|
801
|
+
|
|
802
|
+
const innerInputClassJoined = innerInputNeutral;
|
|
803
|
+
|
|
804
|
+
// 3. Joined mode: controls + input share one visual box
|
|
805
|
+
if (hasControls && joinControls) {
|
|
806
|
+
const groupClassName = cx(
|
|
807
|
+
"flex items-stretch w-full overflow-hidden",
|
|
808
|
+
extendBoxToControls && cx("relative", baseBoxClasses), // box is the group
|
|
809
|
+
!extendBoxToControls &&
|
|
810
|
+
"relative border-none shadow-none bg-transparent",
|
|
811
|
+
className
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
const inputRegionClassName = cx(
|
|
815
|
+
"relative flex-1 flex items-center min-w-0",
|
|
816
|
+
// When the group isn't the box, the region becomes the box.
|
|
817
|
+
!extendBoxToControls && baseBoxClasses,
|
|
818
|
+
"pl-[var(--fp-pl)] pr-[var(--fp-pr)] pt-[var(--fp-pt)] pb-[var(--fp-pb)]"
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
return (
|
|
822
|
+
<div
|
|
823
|
+
className={groupClassName}
|
|
824
|
+
style={varsStyle}
|
|
825
|
+
data-slot="input-group"
|
|
826
|
+
data-has-extras={hasExtras ? "true" : "false"}
|
|
827
|
+
data-disabled={disabled ? "true" : "false"}
|
|
828
|
+
data-size={sizeKey}
|
|
829
|
+
data-density={densityKey}
|
|
830
|
+
>
|
|
831
|
+
{hasLeadingControl && (
|
|
832
|
+
<div
|
|
833
|
+
className={cx(
|
|
834
|
+
"flex items-center",
|
|
835
|
+
leadingControlClassName
|
|
836
|
+
)}
|
|
837
|
+
data-slot="leading-control"
|
|
838
|
+
>
|
|
839
|
+
{leadingControl}
|
|
840
|
+
</div>
|
|
841
|
+
)}
|
|
842
|
+
|
|
843
|
+
<div
|
|
844
|
+
className={inputRegionClassName}
|
|
845
|
+
data-slot="input-region"
|
|
846
|
+
>
|
|
847
|
+
{renderBaseInput({
|
|
848
|
+
inner: true,
|
|
849
|
+
className: innerInputClassJoined,
|
|
850
|
+
style: undefined,
|
|
851
|
+
})}
|
|
852
|
+
|
|
853
|
+
{hasLeadingIcons && (
|
|
854
|
+
<div
|
|
855
|
+
ref={leadingIconsRef}
|
|
856
|
+
className="absolute inset-y-0 left-0 flex items-center cursor-pointer"
|
|
857
|
+
style={{
|
|
858
|
+
gap: leadingGap,
|
|
859
|
+
paddingLeft: `${pxDefault}px`,
|
|
860
|
+
}}
|
|
861
|
+
data-slot="leading-icons"
|
|
862
|
+
onMouseDown={handleIconMouseDown}
|
|
863
|
+
>
|
|
864
|
+
{resolvedLeadingIcons.map((node, idx) => (
|
|
865
|
+
<span
|
|
866
|
+
key={idx}
|
|
867
|
+
className="flex items-center justify-center"
|
|
868
|
+
>
|
|
869
|
+
{node}
|
|
870
|
+
</span>
|
|
871
|
+
))}
|
|
872
|
+
</div>
|
|
873
|
+
)}
|
|
874
|
+
|
|
875
|
+
{hasTrailingIcons && (
|
|
876
|
+
<div
|
|
877
|
+
ref={trailingIconsRef}
|
|
878
|
+
className="absolute inset-y-0 right-0 flex items-center cursor-pointer"
|
|
879
|
+
style={{
|
|
880
|
+
gap: trailingGap,
|
|
881
|
+
paddingRight: `${pxDefault}px`,
|
|
882
|
+
}}
|
|
883
|
+
data-slot="trailing-icons"
|
|
884
|
+
onMouseDown={handleIconMouseDown}
|
|
885
|
+
>
|
|
886
|
+
{resolvedTrailingIcons.map((node, idx) => (
|
|
887
|
+
<span
|
|
888
|
+
key={idx}
|
|
889
|
+
className="flex items-center justify-center"
|
|
890
|
+
>
|
|
891
|
+
{node}
|
|
892
|
+
</span>
|
|
893
|
+
))}
|
|
894
|
+
</div>
|
|
895
|
+
)}
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
{hasTrailingControl && (
|
|
899
|
+
<div
|
|
900
|
+
className={cx(
|
|
901
|
+
"flex items-center",
|
|
902
|
+
trailingControlClassName
|
|
903
|
+
)}
|
|
904
|
+
data-slot="trailing-control"
|
|
905
|
+
>
|
|
906
|
+
{trailingControl}
|
|
907
|
+
</div>
|
|
908
|
+
)}
|
|
909
|
+
</div>
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 4. Separate mode: input box + separate neighbour controls
|
|
914
|
+
const standaloneBoxClassName = cx(
|
|
915
|
+
"relative",
|
|
916
|
+
baseBoxClasses,
|
|
917
|
+
"pl-[var(--fp-pl)] pr-[var(--fp-pr)] pt-[var(--fp-pt)] pb-[var(--fp-pb)]",
|
|
918
|
+
className
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
return (
|
|
922
|
+
<div className="flex items-stretch gap-1 w-full">
|
|
923
|
+
{hasLeadingControl && (
|
|
924
|
+
<div
|
|
925
|
+
className={cx(
|
|
926
|
+
"flex items-center",
|
|
927
|
+
leadingControlClassName
|
|
928
|
+
)}
|
|
929
|
+
data-slot="leading-control"
|
|
930
|
+
>
|
|
931
|
+
{leadingControl}
|
|
932
|
+
</div>
|
|
933
|
+
)}
|
|
934
|
+
|
|
935
|
+
<div className="flex-1 min-w-0">
|
|
936
|
+
<div
|
|
937
|
+
className={standaloneBoxClassName}
|
|
938
|
+
style={varsStyle}
|
|
939
|
+
data-slot="input-box"
|
|
940
|
+
data-has-extras={hasExtras ? "true" : "false"}
|
|
941
|
+
data-disabled={disabled ? "true" : "false"}
|
|
942
|
+
data-size={sizeKey}
|
|
943
|
+
data-density={densityKey}
|
|
944
|
+
>
|
|
945
|
+
{renderBaseInput({
|
|
946
|
+
inner: true,
|
|
947
|
+
className: innerInputNeutral,
|
|
948
|
+
style: undefined,
|
|
949
|
+
})}
|
|
950
|
+
|
|
951
|
+
{hasLeadingIcons && (
|
|
952
|
+
<div
|
|
953
|
+
ref={leadingIconsRef}
|
|
954
|
+
className="absolute inset-y-0 left-0 flex items-center cursor-pointer"
|
|
955
|
+
style={{
|
|
956
|
+
gap: leadingGap,
|
|
957
|
+
paddingLeft: `${pxDefault}px`,
|
|
958
|
+
}}
|
|
959
|
+
data-slot="leading-icons"
|
|
960
|
+
onMouseDown={handleIconMouseDown}
|
|
961
|
+
>
|
|
962
|
+
{resolvedLeadingIcons.map((node, idx) => (
|
|
963
|
+
<span
|
|
964
|
+
key={idx}
|
|
965
|
+
className="flex items-center justify-center"
|
|
966
|
+
>
|
|
967
|
+
{node}
|
|
968
|
+
</span>
|
|
969
|
+
))}
|
|
970
|
+
</div>
|
|
971
|
+
)}
|
|
972
|
+
|
|
973
|
+
{hasTrailingIcons && (
|
|
974
|
+
<div
|
|
975
|
+
ref={trailingIconsRef}
|
|
976
|
+
className="absolute inset-y-0 right-0 flex items-center cursor-pointer"
|
|
977
|
+
style={{
|
|
978
|
+
gap: trailingGap,
|
|
979
|
+
paddingRight: `${pxDefault}px`,
|
|
980
|
+
}}
|
|
981
|
+
data-slot="trailing-icons"
|
|
982
|
+
onMouseDown={handleIconMouseDown}
|
|
983
|
+
>
|
|
984
|
+
{resolvedTrailingIcons.map((node, idx) => (
|
|
985
|
+
<span
|
|
986
|
+
key={idx}
|
|
987
|
+
className="flex items-center justify-center"
|
|
988
|
+
>
|
|
989
|
+
{node}
|
|
990
|
+
</span>
|
|
991
|
+
))}
|
|
992
|
+
</div>
|
|
993
|
+
)}
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
{hasTrailingControl && (
|
|
998
|
+
<div
|
|
999
|
+
className={cx(
|
|
1000
|
+
"flex items-center",
|
|
1001
|
+
trailingControlClassName
|
|
1002
|
+
)}
|
|
1003
|
+
data-slot="trailing-control"
|
|
1004
|
+
>
|
|
1005
|
+
{trailingControl}
|
|
1006
|
+
</div>
|
|
1007
|
+
)}
|
|
1008
|
+
</div>
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
);
|