@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,634 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface TextareaIconControlProps {
|
|
5
|
+
// lower icons (overlaid in textarea-field)
|
|
6
|
+
leadingIcons?: React.ReactNode[];
|
|
7
|
+
trailingIcons?: React.ReactNode[];
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
|
|
10
|
+
iconGap?: number;
|
|
11
|
+
leadingIconSpacing?: number;
|
|
12
|
+
trailingIconSpacing?: number;
|
|
13
|
+
|
|
14
|
+
// lower side controls (outside the text area by default)
|
|
15
|
+
leadingControl?: React.ReactNode;
|
|
16
|
+
trailingControl?: React.ReactNode;
|
|
17
|
+
leadingControlClassName?: string;
|
|
18
|
+
trailingControlClassName?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* If true, move the visual box (border, bg, radius, focus) from
|
|
22
|
+
* `textarea-field` to `textarea-inner` so the side controls are
|
|
23
|
+
* inside the same frame.
|
|
24
|
+
*
|
|
25
|
+
* Default: false (controls sit outside the border).
|
|
26
|
+
*/
|
|
27
|
+
extendBoxToControls?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If true, move the visual box all the way up to `textarea-box`,
|
|
31
|
+
* so the upper toolbox and the inner row share a single frame.
|
|
32
|
+
*
|
|
33
|
+
* When this is true, it overrides `extendBoxToControls`.
|
|
34
|
+
*
|
|
35
|
+
* Default: false.
|
|
36
|
+
*/
|
|
37
|
+
extendBoxToToolbox?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extra padding knobs (same semantics as Input).
|
|
41
|
+
*
|
|
42
|
+
* px → symmetric horizontal padding
|
|
43
|
+
* py → symmetric vertical padding
|
|
44
|
+
* ps/pe → logical start/end padding adjustments
|
|
45
|
+
* pb → extra bottom padding (stacked with py)
|
|
46
|
+
*/
|
|
47
|
+
px?: number;
|
|
48
|
+
py?: number;
|
|
49
|
+
ps?: number;
|
|
50
|
+
pe?: number;
|
|
51
|
+
pb?: number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extra classes merged into the raw <textarea>.
|
|
55
|
+
* (The box padding/border live on the wrappers.)
|
|
56
|
+
*/
|
|
57
|
+
inputClassName?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TextareaSizeProps {
|
|
61
|
+
size?: "sm" | "md" | "lg" | (string & {});
|
|
62
|
+
density?:
|
|
63
|
+
| "compact"
|
|
64
|
+
| "normal"
|
|
65
|
+
| "relaxed"
|
|
66
|
+
| "dense"
|
|
67
|
+
| "loose"
|
|
68
|
+
| (string & {});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TextareaProps
|
|
72
|
+
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">,
|
|
73
|
+
TextareaIconControlProps,
|
|
74
|
+
TextareaSizeProps {
|
|
75
|
+
/**
|
|
76
|
+
* Auto-resize based on content.
|
|
77
|
+
* Default: true.
|
|
78
|
+
*/
|
|
79
|
+
autoResize?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Minimum number of visual rows.
|
|
83
|
+
* Default: 1.
|
|
84
|
+
*/
|
|
85
|
+
rows?: number;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Maximum number of visual rows.
|
|
89
|
+
* Undefined → unlimited.
|
|
90
|
+
*/
|
|
91
|
+
maxRows?: number;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Optional upper toolbox area.
|
|
95
|
+
*/
|
|
96
|
+
upperControl?: React.ReactNode;
|
|
97
|
+
upperControlClassName?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────
|
|
101
|
+
// Helpers
|
|
102
|
+
// ─────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function resolveSizeDensityClasses(size: unknown, density: unknown) {
|
|
105
|
+
const s = (size as string | undefined) ?? "md";
|
|
106
|
+
const d = (density as string | undefined) ?? "normal";
|
|
107
|
+
|
|
108
|
+
let textCls = "text-base md:text-sm";
|
|
109
|
+
|
|
110
|
+
if (s === "sm") {
|
|
111
|
+
textCls = "text-sm";
|
|
112
|
+
} else if (s === "lg") {
|
|
113
|
+
textCls = "text-base";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let densityCls = "";
|
|
117
|
+
if (d === "dense" || d === "compact") {
|
|
118
|
+
densityCls = "leading-tight";
|
|
119
|
+
} else if (d === "relaxed" || d === "loose") {
|
|
120
|
+
densityCls = "leading-relaxed";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { textCls, densityCls };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveBasePadding(size: unknown, density: unknown) {
|
|
127
|
+
// mirror Input baseline
|
|
128
|
+
let px = 12;
|
|
129
|
+
let py = 8;
|
|
130
|
+
|
|
131
|
+
const s = (size as string | undefined) ?? "md";
|
|
132
|
+
const d = (density as string | undefined) ?? "normal";
|
|
133
|
+
|
|
134
|
+
if (s === "sm") {
|
|
135
|
+
px = 10;
|
|
136
|
+
py = 6;
|
|
137
|
+
} else if (s === "lg") {
|
|
138
|
+
px = 14;
|
|
139
|
+
py = 10;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (d === "dense" || d === "compact") {
|
|
143
|
+
py = Math.max(2, py - 1);
|
|
144
|
+
} else if (d === "relaxed" || d === "loose") {
|
|
145
|
+
py = py + 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { px, py };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────
|
|
152
|
+
// Component
|
|
153
|
+
// ─────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
156
|
+
function Textarea(rawProps, forwardedRef) {
|
|
157
|
+
const {
|
|
158
|
+
// layout wrapper
|
|
159
|
+
className,
|
|
160
|
+
style,
|
|
161
|
+
|
|
162
|
+
// native textarea bits
|
|
163
|
+
disabled,
|
|
164
|
+
readOnly,
|
|
165
|
+
required,
|
|
166
|
+
onChange,
|
|
167
|
+
onFocus,
|
|
168
|
+
onBlur,
|
|
169
|
+
placeholder,
|
|
170
|
+
|
|
171
|
+
// size / density
|
|
172
|
+
size = "md",
|
|
173
|
+
density = "normal",
|
|
174
|
+
|
|
175
|
+
// auto-resize
|
|
176
|
+
autoResize = true,
|
|
177
|
+
rows: minRowsProp,
|
|
178
|
+
maxRows,
|
|
179
|
+
|
|
180
|
+
// controls / icons
|
|
181
|
+
leadingIcons,
|
|
182
|
+
trailingIcons,
|
|
183
|
+
icon,
|
|
184
|
+
iconGap,
|
|
185
|
+
leadingIconSpacing,
|
|
186
|
+
trailingIconSpacing,
|
|
187
|
+
leadingControl,
|
|
188
|
+
trailingControl,
|
|
189
|
+
leadingControlClassName,
|
|
190
|
+
trailingControlClassName,
|
|
191
|
+
extendBoxToControls = false,
|
|
192
|
+
extendBoxToToolbox = false,
|
|
193
|
+
px,
|
|
194
|
+
py,
|
|
195
|
+
ps,
|
|
196
|
+
pe,
|
|
197
|
+
pb,
|
|
198
|
+
inputClassName,
|
|
199
|
+
|
|
200
|
+
// upper toolbox
|
|
201
|
+
upperControl,
|
|
202
|
+
upperControlClassName,
|
|
203
|
+
|
|
204
|
+
// rest of <textarea> props
|
|
205
|
+
...rest
|
|
206
|
+
} = rawProps;
|
|
207
|
+
|
|
208
|
+
const sizeKey = (size as string | undefined) ?? "md";
|
|
209
|
+
const densityKey = (density as string | undefined) ?? "normal";
|
|
210
|
+
|
|
211
|
+
const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
|
|
212
|
+
React.useImperativeHandle(
|
|
213
|
+
forwardedRef,
|
|
214
|
+
() => innerRef.current as HTMLTextAreaElement,
|
|
215
|
+
[]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// icons
|
|
219
|
+
const resolvedLeadingIcons: React.ReactNode[] = (() => {
|
|
220
|
+
if (leadingIcons && leadingIcons.length) return leadingIcons;
|
|
221
|
+
if (icon) return [icon];
|
|
222
|
+
return [];
|
|
223
|
+
})();
|
|
224
|
+
const resolvedTrailingIcons: React.ReactNode[] = trailingIcons ?? [];
|
|
225
|
+
|
|
226
|
+
const hasLeadingIcons = resolvedLeadingIcons.length > 0;
|
|
227
|
+
const hasTrailingIcons = resolvedTrailingIcons.length > 0;
|
|
228
|
+
const hasLeadingControl = !!leadingControl;
|
|
229
|
+
const hasTrailingControl = !!trailingControl;
|
|
230
|
+
|
|
231
|
+
const hasIcons = hasLeadingIcons || hasTrailingIcons;
|
|
232
|
+
const hasControls = hasLeadingControl || hasTrailingControl;
|
|
233
|
+
const hasExtras = hasIcons || hasControls;
|
|
234
|
+
|
|
235
|
+
const baseIconGap = iconGap ?? 1;
|
|
236
|
+
const leadingGap = leadingIconSpacing ?? baseIconGap;
|
|
237
|
+
const trailingGap = trailingIconSpacing ?? baseIconGap;
|
|
238
|
+
|
|
239
|
+
const leadingIconsRef = React.useRef<HTMLDivElement | null>(null);
|
|
240
|
+
const trailingIconsRef = React.useRef<HTMLDivElement | null>(null);
|
|
241
|
+
const [leadingIconsWidth, setLeadingIconsWidth] = React.useState(0);
|
|
242
|
+
const [trailingIconsWidth, setTrailingIconsWidth] = React.useState(0);
|
|
243
|
+
|
|
244
|
+
const measureIconWidths = React.useCallback(() => {
|
|
245
|
+
if (typeof window === "undefined") return;
|
|
246
|
+
|
|
247
|
+
const lead = leadingIconsRef.current;
|
|
248
|
+
const trail = trailingIconsRef.current;
|
|
249
|
+
|
|
250
|
+
if (lead) {
|
|
251
|
+
const rect = lead.getBoundingClientRect();
|
|
252
|
+
setLeadingIconsWidth(rect.width);
|
|
253
|
+
} else {
|
|
254
|
+
setLeadingIconsWidth(0);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (trail) {
|
|
258
|
+
const rect = trail.getBoundingClientRect();
|
|
259
|
+
setTrailingIconsWidth(rect.width);
|
|
260
|
+
} else {
|
|
261
|
+
setTrailingIconsWidth(0);
|
|
262
|
+
}
|
|
263
|
+
}, []);
|
|
264
|
+
|
|
265
|
+
// MutationObserver → recompute icon widths when content changes
|
|
266
|
+
React.useLayoutEffect(() => {
|
|
267
|
+
if (
|
|
268
|
+
typeof window === "undefined" ||
|
|
269
|
+
typeof MutationObserver === "undefined"
|
|
270
|
+
) {
|
|
271
|
+
measureIconWidths();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const observers: MutationObserver[] = [];
|
|
276
|
+
const lead = leadingIconsRef.current;
|
|
277
|
+
const trail = trailingIconsRef.current;
|
|
278
|
+
|
|
279
|
+
if (lead) {
|
|
280
|
+
const obs = new MutationObserver(() => measureIconWidths());
|
|
281
|
+
obs.observe(lead, {
|
|
282
|
+
childList: true,
|
|
283
|
+
subtree: true,
|
|
284
|
+
attributes: true,
|
|
285
|
+
});
|
|
286
|
+
observers.push(obs);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (trail) {
|
|
290
|
+
const obs = new MutationObserver(() => measureIconWidths());
|
|
291
|
+
obs.observe(trail, {
|
|
292
|
+
childList: true,
|
|
293
|
+
subtree: true,
|
|
294
|
+
attributes: true,
|
|
295
|
+
});
|
|
296
|
+
observers.push(obs);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
measureIconWidths();
|
|
300
|
+
|
|
301
|
+
return () => observers.forEach((o) => o.disconnect());
|
|
302
|
+
}, [measureIconWidths, hasLeadingIcons, hasTrailingIcons]);
|
|
303
|
+
|
|
304
|
+
// row height / rows
|
|
305
|
+
const [rowHeight, setRowHeight] = React.useState<number | null>(null);
|
|
306
|
+
const baseMinRows = Math.max(minRowsProp ?? 1, 1);
|
|
307
|
+
const [rows, setRows] = React.useState<number>(baseMinRows);
|
|
308
|
+
|
|
309
|
+
// measure a single-row height from the textarea itself
|
|
310
|
+
React.useLayoutEffect(() => {
|
|
311
|
+
if (typeof window === "undefined") return;
|
|
312
|
+
const el = innerRef.current;
|
|
313
|
+
if (!el) return;
|
|
314
|
+
|
|
315
|
+
const prevValue = el.value;
|
|
316
|
+
const prevHeight = el.style.height;
|
|
317
|
+
|
|
318
|
+
el.value = "X";
|
|
319
|
+
el.style.height = "0px";
|
|
320
|
+
const singleRowHeight = el.scrollHeight;
|
|
321
|
+
|
|
322
|
+
el.value = prevValue;
|
|
323
|
+
el.style.height = prevHeight;
|
|
324
|
+
|
|
325
|
+
if (singleRowHeight > 0 && Number.isFinite(singleRowHeight)) {
|
|
326
|
+
setRowHeight(singleRowHeight);
|
|
327
|
+
setRows(baseMinRows);
|
|
328
|
+
}
|
|
329
|
+
}, [sizeKey, densityKey, baseMinRows]);
|
|
330
|
+
|
|
331
|
+
// auto-resize helper
|
|
332
|
+
const recomputeHeight = React.useCallback(() => {
|
|
333
|
+
if (!autoResize) return;
|
|
334
|
+
if (!innerRef.current) return;
|
|
335
|
+
if (!rowHeight) return;
|
|
336
|
+
|
|
337
|
+
const el = innerRef.current;
|
|
338
|
+
|
|
339
|
+
el.style.height = "0px";
|
|
340
|
+
const scrollH = el.scrollHeight;
|
|
341
|
+
|
|
342
|
+
// if empty, keep exactly minRows
|
|
343
|
+
if (!el.value || el.value.length === 0) {
|
|
344
|
+
const h = baseMinRows * rowHeight;
|
|
345
|
+
el.style.height = `${h}px`;
|
|
346
|
+
setRows(baseMinRows);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const rawRows = scrollH / rowHeight;
|
|
351
|
+
let nextRows = Math.max(baseMinRows, Math.ceil(rawRows));
|
|
352
|
+
if (typeof maxRows === "number" && maxRows > 0) {
|
|
353
|
+
nextRows = Math.min(nextRows, maxRows);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const nextHeight = nextRows * rowHeight;
|
|
357
|
+
el.style.height = `${nextHeight}px`;
|
|
358
|
+
setRows(nextRows);
|
|
359
|
+
}, [autoResize, rowHeight, baseMinRows, maxRows]);
|
|
360
|
+
|
|
361
|
+
// run when controlled value changes or initial mount
|
|
362
|
+
React.useLayoutEffect(() => {
|
|
363
|
+
recomputeHeight();
|
|
364
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
365
|
+
}, [recomputeHeight, rest.value, rest.defaultValue]);
|
|
366
|
+
|
|
367
|
+
// padding (frame-level)
|
|
368
|
+
const { px: pxDefault, py: pyDefault } = resolveBasePadding(size, density);
|
|
369
|
+
|
|
370
|
+
const extraPx = typeof px === "number" ? px : 0;
|
|
371
|
+
const extraPy = typeof py === "number" ? py : 0;
|
|
372
|
+
const extraPs = typeof ps === "number" ? ps : 0;
|
|
373
|
+
const extraPe = typeof pe === "number" ? pe : 0;
|
|
374
|
+
const extraPb = typeof pb === "number" ? pb : 0;
|
|
375
|
+
|
|
376
|
+
const basePaddingStart = pxDefault + extraPx + extraPs;
|
|
377
|
+
const basePaddingEnd = pxDefault + extraPx + extraPe;
|
|
378
|
+
const paddingTop = pyDefault + extraPy;
|
|
379
|
+
const paddingBottom = pyDefault + extraPy + extraPb;
|
|
380
|
+
|
|
381
|
+
// extra space text needs because of icons
|
|
382
|
+
const iconsLeftExtra =
|
|
383
|
+
hasLeadingIcons && leadingIconsWidth > 0
|
|
384
|
+
? leadingIconsWidth + baseIconGap
|
|
385
|
+
: 0;
|
|
386
|
+
|
|
387
|
+
const iconsRightExtra =
|
|
388
|
+
hasTrailingIcons && trailingIconsWidth > 0
|
|
389
|
+
? trailingIconsWidth + baseIconGap
|
|
390
|
+
: 0;
|
|
391
|
+
|
|
392
|
+
const { textCls, densityCls } = resolveSizeDensityClasses(size, density);
|
|
393
|
+
|
|
394
|
+
// vars for the frame: both base + adjusted
|
|
395
|
+
const vars: React.CSSProperties = {
|
|
396
|
+
"--fp-pl-base": `${basePaddingStart}px`,
|
|
397
|
+
"--fp-pr-base": `${basePaddingEnd}px`,
|
|
398
|
+
"--fp-pl": `${basePaddingStart + iconsLeftExtra}px`,
|
|
399
|
+
"--fp-pr": `${basePaddingEnd + iconsRightExtra}px`,
|
|
400
|
+
"--fp-pt": `${paddingTop}px`,
|
|
401
|
+
"--fp-pb": `${paddingBottom}px`,
|
|
402
|
+
"--fp-row-height": rowHeight ? `${rowHeight}px` : undefined,
|
|
403
|
+
"--fp-rows": rows,
|
|
404
|
+
} as React.CSSProperties;
|
|
405
|
+
|
|
406
|
+
const mergedWrapperStyle: React.CSSProperties = {
|
|
407
|
+
...(style ?? {}),
|
|
408
|
+
...vars,
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// visual frame (border/background/focus)
|
|
412
|
+
const frameClasses = cn(
|
|
413
|
+
"border-input placeholder:text-muted-foreground focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
|
|
414
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
415
|
+
"dark:bg-input/30 rounded-md border bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
|
|
416
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// padding utility using adjusted vars (--fp-pl / --fp-pr)
|
|
420
|
+
const framePaddingClasses = cn(
|
|
421
|
+
"px-(--fp-pl,--spacing(3)) pr-(--fp-pr,--spacing(3))",
|
|
422
|
+
"pt-(--fp-pt,--spacing(1)) pb-(--fp-pb,--spacing(1))"
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// which element owns the frame?
|
|
426
|
+
const boxOnToolbox = extendBoxToToolbox;
|
|
427
|
+
const boxOnInner = !boxOnToolbox && extendBoxToControls;
|
|
428
|
+
const boxOnField = !boxOnToolbox && !boxOnInner;
|
|
429
|
+
|
|
430
|
+
const wrapperClasses = cn("w-full", className);
|
|
431
|
+
|
|
432
|
+
const boxClasses = cn(
|
|
433
|
+
"flex flex-col gap-1",
|
|
434
|
+
boxOnToolbox && frameClasses,
|
|
435
|
+
boxOnToolbox && framePaddingClasses
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const innerRowClasses = cn(
|
|
439
|
+
"flex items-stretch gap-1",
|
|
440
|
+
boxOnInner && frameClasses,
|
|
441
|
+
boxOnInner && framePaddingClasses
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const fieldWrapperClasses = cn(
|
|
445
|
+
"relative flex-1 min-w-0",
|
|
446
|
+
boxOnField && frameClasses,
|
|
447
|
+
boxOnField && framePaddingClasses
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const textareaClasses = cn(
|
|
451
|
+
"block w-full min-h-[1px] resize-none bg-transparent border-none outline-none shadow-none",
|
|
452
|
+
"px-0 py-0",
|
|
453
|
+
"placeholder:text-muted-foreground",
|
|
454
|
+
textCls,
|
|
455
|
+
densityCls,
|
|
456
|
+
inputClassName
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
const focusTextarea = () => {
|
|
460
|
+
if (innerRef.current) innerRef.current.focus();
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const handleFocus = React.useCallback(
|
|
464
|
+
(event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
465
|
+
onFocus?.(event);
|
|
466
|
+
},
|
|
467
|
+
[onFocus]
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const handleBlur = React.useCallback(
|
|
471
|
+
(event: React.FocusEvent<HTMLTextAreaElement>) => {
|
|
472
|
+
onBlur?.(event);
|
|
473
|
+
},
|
|
474
|
+
[onBlur]
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const handleChange = React.useCallback(
|
|
478
|
+
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
479
|
+
onChange?.(event);
|
|
480
|
+
// for uncontrolled usage, recompute on each keystroke
|
|
481
|
+
recomputeHeight();
|
|
482
|
+
},
|
|
483
|
+
[onChange, recomputeHeight]
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const handleIconMouseDown = (e: React.MouseEvent) => {
|
|
487
|
+
e.preventDefault();
|
|
488
|
+
focusTextarea();
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const controlCellStyle: React.CSSProperties | undefined =
|
|
492
|
+
rowHeight != null ? { height: `${rowHeight}px` } : undefined;
|
|
493
|
+
|
|
494
|
+
const lowerControlAlignStyle: React.CSSProperties = {
|
|
495
|
+
marginTop: "auto",
|
|
496
|
+
...controlCellStyle,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const leadingArea = hasLeadingControl ? (
|
|
500
|
+
<div
|
|
501
|
+
data-slot="textarea-leading-area"
|
|
502
|
+
className={cn("flex flex-col h-full", leadingControlClassName)}
|
|
503
|
+
>
|
|
504
|
+
<div
|
|
505
|
+
data-slot="textarea-leading-control"
|
|
506
|
+
className="flex items-center mt-auto"
|
|
507
|
+
style={lowerControlAlignStyle}
|
|
508
|
+
onMouseDown={(e) => {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
focusTextarea();
|
|
511
|
+
}}
|
|
512
|
+
>
|
|
513
|
+
{leadingControl}
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
) : null;
|
|
517
|
+
|
|
518
|
+
const trailingArea = hasTrailingControl ? (
|
|
519
|
+
<div
|
|
520
|
+
data-slot="textarea-trailing-area"
|
|
521
|
+
className={cn("flex flex-col h-full mt-auto", trailingControlClassName)}
|
|
522
|
+
>
|
|
523
|
+
<div
|
|
524
|
+
data-slot="textarea-trailing-control"
|
|
525
|
+
className="flex items-center"
|
|
526
|
+
style={lowerControlAlignStyle}
|
|
527
|
+
onMouseDown={(e) => {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
focusTextarea();
|
|
530
|
+
}}
|
|
531
|
+
>
|
|
532
|
+
{trailingControl}
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
) : null;
|
|
536
|
+
|
|
537
|
+
return (
|
|
538
|
+
<div
|
|
539
|
+
data-slot="textarea-wrapper"
|
|
540
|
+
className={wrapperClasses}
|
|
541
|
+
style={mergedWrapperStyle}
|
|
542
|
+
data-size={sizeKey}
|
|
543
|
+
data-density={densityKey}
|
|
544
|
+
>
|
|
545
|
+
<div
|
|
546
|
+
data-slot="textarea-box"
|
|
547
|
+
className={boxClasses}
|
|
548
|
+
data-has-extras={hasExtras ? "true" : "false"}
|
|
549
|
+
>
|
|
550
|
+
{upperControl && (
|
|
551
|
+
<div
|
|
552
|
+
data-slot="textarea-upper"
|
|
553
|
+
className={cn("flex items-center", upperControlClassName)}
|
|
554
|
+
>
|
|
555
|
+
{upperControl}
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
558
|
+
|
|
559
|
+
<div data-slot="textarea-inner" className={innerRowClasses}>
|
|
560
|
+
{leadingArea}
|
|
561
|
+
|
|
562
|
+
<div data-slot="textarea-field" className={fieldWrapperClasses}>
|
|
563
|
+
<textarea
|
|
564
|
+
ref={innerRef}
|
|
565
|
+
data-slot="textarea"
|
|
566
|
+
className={textareaClasses}
|
|
567
|
+
disabled={disabled}
|
|
568
|
+
readOnly={readOnly}
|
|
569
|
+
aria-required={required ? "true" : undefined}
|
|
570
|
+
rows={autoResize ? undefined : baseMinRows}
|
|
571
|
+
placeholder={placeholder}
|
|
572
|
+
onChange={handleChange}
|
|
573
|
+
onFocus={handleFocus}
|
|
574
|
+
onBlur={handleBlur}
|
|
575
|
+
{...rest}
|
|
576
|
+
/>
|
|
577
|
+
|
|
578
|
+
{hasLeadingIcons && (
|
|
579
|
+
<div
|
|
580
|
+
ref={leadingIconsRef}
|
|
581
|
+
data-slot="textarea-leading-icons"
|
|
582
|
+
className="pointer-events-auto absolute left-0 flex items-end"
|
|
583
|
+
style={{
|
|
584
|
+
gap: leadingGap,
|
|
585
|
+
// anchor from base padding, NOT icon-adjusted padding
|
|
586
|
+
paddingLeft: "var(--fp-pl-base)",
|
|
587
|
+
bottom: "calc(var(--fp-pb, 0px) + 2px)",
|
|
588
|
+
}}
|
|
589
|
+
onMouseDown={handleIconMouseDown}
|
|
590
|
+
>
|
|
591
|
+
{resolvedLeadingIcons.map((node, idx) => (
|
|
592
|
+
<span
|
|
593
|
+
key={idx}
|
|
594
|
+
className="flex items-center justify-center"
|
|
595
|
+
>
|
|
596
|
+
{node}
|
|
597
|
+
</span>
|
|
598
|
+
))}
|
|
599
|
+
</div>
|
|
600
|
+
)}
|
|
601
|
+
|
|
602
|
+
{hasTrailingIcons && (
|
|
603
|
+
<div
|
|
604
|
+
ref={trailingIconsRef}
|
|
605
|
+
data-slot="textarea-trailing-icons"
|
|
606
|
+
className="pointer-events-auto absolute right-0 flex items-end"
|
|
607
|
+
style={{
|
|
608
|
+
gap: trailingGap,
|
|
609
|
+
paddingRight: "var(--fp-pr-base)",
|
|
610
|
+
bottom: "calc(var(--fp-pb, 0px) + 2px)",
|
|
611
|
+
}}
|
|
612
|
+
onMouseDown={handleIconMouseDown}
|
|
613
|
+
>
|
|
614
|
+
{resolvedTrailingIcons.map((node, idx) => (
|
|
615
|
+
<span
|
|
616
|
+
key={idx}
|
|
617
|
+
className="flex items-center justify-center"
|
|
618
|
+
>
|
|
619
|
+
{node}
|
|
620
|
+
</span>
|
|
621
|
+
))}
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{trailingArea}
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
Textarea.displayName = "Textarea";
|