@timeax/form-palette 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.scaffold-cache.json +537 -0
  2. package/package.json +42 -0
  3. package/src/.scaffold-cache.json +544 -0
  4. package/src/adapters/axios.ts +117 -0
  5. package/src/adapters/index.ts +91 -0
  6. package/src/adapters/inertia.ts +187 -0
  7. package/src/core/adapter-registry.ts +87 -0
  8. package/src/core/bound/bind-host.ts +14 -0
  9. package/src/core/bound/observe-bound-field.ts +172 -0
  10. package/src/core/bound/wait-for-bound-field.ts +57 -0
  11. package/src/core/context.ts +23 -0
  12. package/src/core/core-provider.tsx +818 -0
  13. package/src/core/core-root.tsx +72 -0
  14. package/src/core/core-shell.tsx +44 -0
  15. package/src/core/errors/error-strip.tsx +71 -0
  16. package/src/core/errors/index.ts +2 -0
  17. package/src/core/errors/map-error-bag.ts +51 -0
  18. package/src/core/errors/map-zod.ts +39 -0
  19. package/src/core/hooks/use-button.ts +220 -0
  20. package/src/core/hooks/use-core-context.ts +20 -0
  21. package/src/core/hooks/use-core-utility.ts +0 -0
  22. package/src/core/hooks/use-core.ts +13 -0
  23. package/src/core/hooks/use-field.ts +497 -0
  24. package/src/core/hooks/use-optional-field.ts +28 -0
  25. package/src/core/index.ts +0 -0
  26. package/src/core/registry/binder-registry.ts +82 -0
  27. package/src/core/registry/field-registry.ts +187 -0
  28. package/src/core/test.tsx +17 -0
  29. package/src/global.d.ts +14 -0
  30. package/src/index.ts +68 -0
  31. package/src/input/index.ts +4 -0
  32. package/src/input/input-field.tsx +854 -0
  33. package/src/input/input-layout-graph.ts +230 -0
  34. package/src/input/input-props.ts +190 -0
  35. package/src/lib/get-global-countries.ts +87 -0
  36. package/src/lib/utils.ts +6 -0
  37. package/src/presets/index.ts +0 -0
  38. package/src/presets/shadcn-preset.ts +0 -0
  39. package/src/presets/shadcn-variants/checkbox.tsx +849 -0
  40. package/src/presets/shadcn-variants/chips.tsx +756 -0
  41. package/src/presets/shadcn-variants/color.tsx +284 -0
  42. package/src/presets/shadcn-variants/custom.tsx +227 -0
  43. package/src/presets/shadcn-variants/date.tsx +796 -0
  44. package/src/presets/shadcn-variants/file.tsx +764 -0
  45. package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
  46. package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
  47. package/src/presets/shadcn-variants/number.tsx +176 -0
  48. package/src/presets/shadcn-variants/password.tsx +737 -0
  49. package/src/presets/shadcn-variants/phone.tsx +628 -0
  50. package/src/presets/shadcn-variants/radio.tsx +578 -0
  51. package/src/presets/shadcn-variants/select.tsx +956 -0
  52. package/src/presets/shadcn-variants/slider.tsx +622 -0
  53. package/src/presets/shadcn-variants/text.tsx +343 -0
  54. package/src/presets/shadcn-variants/textarea.tsx +66 -0
  55. package/src/presets/shadcn-variants/toggle.tsx +218 -0
  56. package/src/presets/shadcn-variants/treeselect.tsx +784 -0
  57. package/src/presets/ui/badge.tsx +46 -0
  58. package/src/presets/ui/button.tsx +60 -0
  59. package/src/presets/ui/calendar.tsx +214 -0
  60. package/src/presets/ui/checkbox.tsx +115 -0
  61. package/src/presets/ui/custom.tsx +0 -0
  62. package/src/presets/ui/dialog.tsx +141 -0
  63. package/src/presets/ui/field.tsx +246 -0
  64. package/src/presets/ui/input-mask.tsx +739 -0
  65. package/src/presets/ui/input-otp.tsx +77 -0
  66. package/src/presets/ui/input.tsx +1011 -0
  67. package/src/presets/ui/label.tsx +22 -0
  68. package/src/presets/ui/number.tsx +1370 -0
  69. package/src/presets/ui/popover.tsx +46 -0
  70. package/src/presets/ui/radio-group.tsx +43 -0
  71. package/src/presets/ui/scroll-area.tsx +56 -0
  72. package/src/presets/ui/select.tsx +190 -0
  73. package/src/presets/ui/separator.tsx +28 -0
  74. package/src/presets/ui/slider.tsx +61 -0
  75. package/src/presets/ui/switch.tsx +32 -0
  76. package/src/presets/ui/textarea.tsx +634 -0
  77. package/src/presets/ui/time-dropdowns.tsx +350 -0
  78. package/src/schema/adapter.ts +217 -0
  79. package/src/schema/core.ts +429 -0
  80. package/src/schema/field-map.ts +0 -0
  81. package/src/schema/field.ts +224 -0
  82. package/src/schema/index.ts +0 -0
  83. package/src/schema/input-field.ts +260 -0
  84. package/src/schema/presets.ts +0 -0
  85. package/src/schema/variant.ts +216 -0
  86. package/src/variants/core/checkbox.tsx +54 -0
  87. package/src/variants/core/chips.tsx +22 -0
  88. package/src/variants/core/color.tsx +16 -0
  89. package/src/variants/core/custom.tsx +18 -0
  90. package/src/variants/core/date.tsx +25 -0
  91. package/src/variants/core/file.tsx +9 -0
  92. package/src/variants/core/keyvalue.tsx +12 -0
  93. package/src/variants/core/multiselect.tsx +28 -0
  94. package/src/variants/core/number.tsx +115 -0
  95. package/src/variants/core/password.tsx +35 -0
  96. package/src/variants/core/phone.tsx +16 -0
  97. package/src/variants/core/radio.tsx +38 -0
  98. package/src/variants/core/select.tsx +15 -0
  99. package/src/variants/core/slider.tsx +55 -0
  100. package/src/variants/core/text.tsx +114 -0
  101. package/src/variants/core/textarea.tsx +22 -0
  102. package/src/variants/core/toggle.tsx +50 -0
  103. package/src/variants/core/treeselect.tsx +11 -0
  104. package/src/variants/helpers/selection-summary.tsx +236 -0
  105. package/src/variants/index.ts +75 -0
  106. package/src/variants/registry.ts +38 -0
  107. package/src/variants/select-shared.ts +0 -0
  108. package/src/variants/shared.ts +126 -0
  109. package/tsconfig.json +14 -0
@@ -0,0 +1,854 @@
1
+ // src/input/input-field.tsx
2
+ // noinspection JSUnusedLocalSymbols,SpellCheckingInspection,DuplicatedCode
3
+
4
+ import * as React from "react";
5
+
6
+ import { useField } from "@/core/hooks/use-field";
7
+ import type { InputFieldProps } from "@/input/input-props";
8
+ import type {
9
+ FieldLayoutConfig,
10
+ LayoutResolveContext,
11
+ SlotPlacement,
12
+ ValidateResult,
13
+ } from "@/schema/input-field";
14
+ import type { VariantKey, VariantValueFor } from "@/schema/variant";
15
+ import { getVariant } from "@/variants";
16
+
17
+ import {
18
+ Field as UiField,
19
+ FieldContent,
20
+ FieldDescription,
21
+ FieldError,
22
+ FieldGroup,
23
+ FieldLabel,
24
+ FieldTitle,
25
+ } from "@/presets/ui/field";
26
+ import { ChangeDetail } from "@/variants/shared";
27
+ import {
28
+ buildLayoutGraph,
29
+ type HelperSlot,
30
+ } from "@/input/input-layout-graph";
31
+ import { cn } from "@/lib/utils";
32
+
33
+ /**
34
+ * Normalise a ValidateResult into an array of error messages.
35
+ */
36
+ function normalizeValidateResult(result: ValidateResult): string[] {
37
+ if (result === undefined || result === null || result === true) return [];
38
+ if (result === false) return ["Invalid value."];
39
+ if (typeof result === "string") return result ? [result] : [];
40
+ if (Array.isArray(result)) return result.filter(Boolean);
41
+ return [];
42
+ }
43
+
44
+ /**
45
+ * Build the layout for this field using:
46
+ * - variant defaults
47
+ * - host overrides
48
+ * - optional variant-level resolveLayout()
49
+ */
50
+ function resolveLayoutForField(
51
+ defaults: FieldLayoutConfig | undefined,
52
+ overrides: Partial<FieldLayoutConfig>,
53
+ props: unknown,
54
+ variantResolve?: (ctx: LayoutResolveContext) => FieldLayoutConfig
55
+ ): FieldLayoutConfig {
56
+ const base: FieldLayoutConfig = defaults ? { ...defaults } : {};
57
+
58
+ if (variantResolve) {
59
+ return variantResolve({
60
+ defaults: base,
61
+ overrides,
62
+ props,
63
+ });
64
+ }
65
+
66
+ // Fallback: shallow merge defaults + overrides
67
+ return {
68
+ ...base,
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Render a single helper slot using the Shadcn field primitives.
75
+ */
76
+ function renderHelperSlot(
77
+ root: "label" | "input",
78
+ slot: HelperSlot,
79
+ classes: any
80
+ ): React.ReactNode {
81
+ const placement: SlotPlacement = slot.placement;
82
+
83
+ switch (slot.id) {
84
+ case "sublabel":
85
+ return (
86
+ <FieldDescription
87
+ key={`sublabel-${placement}-${root}`}
88
+ className={[
89
+ "text-xs text-muted-foreground",
90
+ classes?.sublabel,
91
+ ]
92
+ .filter(Boolean)
93
+ .join(" ")}
94
+ data-slot={`sublabel-${placement}`}
95
+ >
96
+ {slot.content}
97
+ </FieldDescription>
98
+ );
99
+
100
+ case "description":
101
+ return (
102
+ <FieldDescription
103
+ key={`description-${placement}-${root}`}
104
+ className={[
105
+ "text-xs text-muted-foreground",
106
+ classes?.description,
107
+ ]
108
+ .filter(Boolean)
109
+ .join(" ")}
110
+ data-slot={`description-${placement}`}
111
+ >
112
+ {slot.content}
113
+ </FieldDescription>
114
+ );
115
+
116
+ case "helpText":
117
+ return (
118
+ <FieldDescription
119
+ key={`helpText-${placement}-${root}`}
120
+ className={[
121
+ "text-xs text-muted-foreground",
122
+ classes?.helpText,
123
+ ]
124
+ .filter(Boolean)
125
+ .join(" ")}
126
+ data-slot={`helptext-${placement}`}
127
+ >
128
+ {slot.content}
129
+ </FieldDescription>
130
+ );
131
+
132
+ case "errorText":
133
+ return (
134
+ <FieldError
135
+ key={`error-${placement}-${root}`}
136
+ className={[
137
+ "text-xs text-destructive",
138
+ classes?.error,
139
+ ]
140
+ .filter(Boolean)
141
+ .join(" ")}
142
+ data-slot={`error-${placement}`}
143
+ >
144
+ {slot.content}
145
+ </FieldError>
146
+ );
147
+
148
+ case "tags":
149
+ return (
150
+ <div
151
+ key={`tags-${placement}-${root}`}
152
+ className={[
153
+ "flex items-center gap-1",
154
+ classes?.tags,
155
+ ]
156
+ .filter(Boolean)
157
+ .join(" ")}
158
+ data-slot={`tags-${placement}`}
159
+ >
160
+ {slot.content}
161
+ </div>
162
+ );
163
+
164
+ default:
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Public InputField component.
171
+ *
172
+ * - Uses `useField` to register a Field and manage value/error/loading.
173
+ * - Delegates rendering to the chosen variant's `Variant` component.
174
+ * - Uses Shadcn's Field primitives for structure.
175
+ * - Lets variants influence layout via defaults + optional resolveLayout().
176
+ * - Uses a layout graph (buildLayoutGraph) + getSlotsFor().render(...) to
177
+ * position helpers (sublabel, description, helpText, error, tags) relative to
178
+ * "label" vs "input" roots without empty wrapper divs.
179
+ */
180
+ export function InputField<K extends VariantKey = VariantKey>(
181
+ props: InputFieldProps<K>
182
+ ) {
183
+ const {
184
+ variant,
185
+
186
+ // Field identity / wiring
187
+ name,
188
+ bind,
189
+ shared,
190
+ groupId,
191
+ alias,
192
+ main,
193
+ ignore,
194
+ required,
195
+ defaultValue,
196
+
197
+ // Chrome
198
+ label,
199
+ sublabel,
200
+ description,
201
+ helpText,
202
+ errorText,
203
+
204
+ // Container + tags
205
+ contain,
206
+ tags,
207
+ tagPlacement,
208
+
209
+ // Layout overrides
210
+ labelPlacement,
211
+ sublabelPlacement,
212
+ descriptionPlacement,
213
+ helpTextPlacement,
214
+ errorTextPlacement,
215
+ inline,
216
+ fullWidth,
217
+ size,
218
+ density,
219
+
220
+ // Validation hook
221
+ onValidate,
222
+ onChange,
223
+
224
+ // Field wrapper props
225
+ className,
226
+ style,
227
+ classes,
228
+
229
+ // Everything else → forwarded to variant
230
+ ...rest
231
+ } = props as InputFieldProps & {
232
+ className?: string;
233
+ style?: React.CSSProperties;
234
+ };
235
+
236
+ const module = getVariant(variant);
237
+
238
+ if (!module) {
239
+ if (process.env.NODE_ENV !== "production") {
240
+ // eslint-disable-next-line no-console
241
+ console.warn(
242
+ `[form-palette] InputField: variant "${String(
243
+ variant
244
+ )}" is not registered.`
245
+ );
246
+ }
247
+ return null;
248
+ }
249
+
250
+ type TValue = VariantValueFor<K>;
251
+
252
+ // Compute layout: defaults + host overrides + optional variant resolver
253
+ const layout = React.useMemo(() => {
254
+ const defaultsLayout = module.defaults?.layout;
255
+ const overrides: Partial<FieldLayoutConfig> = {};
256
+
257
+ if (labelPlacement !== undefined) {
258
+ overrides.labelPlacement = labelPlacement;
259
+ }
260
+ if (sublabelPlacement !== undefined) {
261
+ overrides.sublabelPlacement = sublabelPlacement;
262
+ }
263
+ if (descriptionPlacement !== undefined) {
264
+ overrides.descriptionPlacement = descriptionPlacement;
265
+ }
266
+ if (helpTextPlacement !== undefined) {
267
+ overrides.helpTextPlacement = helpTextPlacement;
268
+ }
269
+ if (errorTextPlacement !== undefined) {
270
+ overrides.errorTextPlacement = errorTextPlacement;
271
+ }
272
+ if (tagPlacement !== undefined) {
273
+ overrides.tagPlacement = tagPlacement;
274
+ }
275
+ if (inline !== undefined) {
276
+ overrides.inline = inline;
277
+ }
278
+ if (fullWidth !== undefined) {
279
+ overrides.fullWidth = fullWidth;
280
+ }
281
+
282
+ return resolveLayoutForField(
283
+ defaultsLayout,
284
+ overrides,
285
+ props,
286
+ module.resolveLayout as any
287
+ );
288
+ }, [
289
+ module,
290
+ labelPlacement,
291
+ sublabelPlacement,
292
+ descriptionPlacement,
293
+ helpTextPlacement,
294
+ errorTextPlacement,
295
+ tagPlacement,
296
+ inline,
297
+ fullWidth,
298
+ props,
299
+ ]);
300
+
301
+ const effectiveSize =
302
+ size ?? module.defaults?.layout?.defaultSize ?? undefined;
303
+ const effectiveDensity =
304
+ density ?? module.defaults?.layout?.defaultDensity ?? undefined;
305
+
306
+ /**
307
+ * Validation callback used by the field hook.
308
+ *
309
+ * It combines:
310
+ * - variant-level validation (module.validate)
311
+ * - per-field validation (props.onValidate)
312
+ */
313
+ const validate = React.useCallback(
314
+ (value: TValue | undefined, _report: boolean): boolean | string => {
315
+ const messages: string[] = [];
316
+
317
+ if (module.validate) {
318
+ const res = module.validate(value, {
319
+ required: !!required,
320
+ props: props as any,
321
+ field: undefined as any,
322
+ form: undefined as any,
323
+ });
324
+ messages.push(...normalizeValidateResult(res));
325
+ }
326
+
327
+ if (onValidate) {
328
+ const res = onValidate(
329
+ value as any,
330
+ undefined as any,
331
+ undefined as any
332
+ );
333
+ messages.push(...normalizeValidateResult(res));
334
+ }
335
+
336
+ if (!messages.length) return true;
337
+ return messages[0] ?? "Invalid value.";
338
+ },
339
+ [module, required, onValidate, props]
340
+ );
341
+
342
+ // Hook into the core: register field, track value/error/loading
343
+ const field = useField<TValue>({
344
+ name,
345
+ bind,
346
+ shared,
347
+ groupId,
348
+ alias,
349
+ main,
350
+ ignore,
351
+ required,
352
+ defaultValue: defaultValue as TValue | undefined,
353
+ validate,
354
+ } as any);
355
+
356
+ const { value, setValue, error, ref, key } = field;
357
+
358
+ const Variant = module.Variant as React.ComponentType<any>;
359
+ const visualError = (errorText ?? error) || "";
360
+
361
+ /**
362
+ * Central change handler for this field.
363
+ *
364
+ * Flow:
365
+ * Variant.onValue(next, detail) →
366
+ * InputField.handleValueChange →
367
+ * props.onChange?.({ value, detail, event, preventDefault }) →
368
+ * (if not prevented) setValue(final)
369
+ */
370
+ const handleValueChange = React.useCallback(
371
+ (next: TValue | undefined, detail?: ChangeDetail) => {
372
+ let finalValue = next;
373
+ let defaultPrevented = false;
374
+
375
+ if (onChange) {
376
+ const e = {
377
+ value: next,
378
+ preventDefault() {
379
+ defaultPrevented = true;
380
+ },
381
+ get isDefaultPrevented() {
382
+ return defaultPrevented;
383
+ },
384
+ event:
385
+ detail?.nativeEvent as
386
+ | React.SyntheticEvent
387
+ | undefined,
388
+ detail: detail as ChangeDetail,
389
+ };
390
+
391
+ onChange(e);
392
+
393
+ // If the handler returns a value, use it instead of `next`.
394
+ finalValue = e.value;
395
+ if (defaultPrevented) {
396
+ // Host took control and blocked the core update.
397
+ return;
398
+ }
399
+ }
400
+
401
+ // NOTE: Second argument is an optional "source" tag.
402
+ // If your setValue only accepts one arg, drop `String(variant)`.
403
+ (setValue as any)(finalValue, String(variant));
404
+ },
405
+ [onChange, setValue, variant]
406
+ );
407
+
408
+ const disabledProp = (rest as any).disabled;
409
+ const readOnlyProp = (rest as any).readOnly;
410
+
411
+ // Convenience shorthands for layout
412
+ const lp = layout.labelPlacement;
413
+ const sp = layout.sublabelPlacement;
414
+ const dp = layout.descriptionPlacement;
415
+ const hp = layout.helpTextPlacement;
416
+ const ep = layout.errorTextPlacement;
417
+ const tp = layout.tagPlacement;
418
+
419
+ const isInline = !!layout.inline;
420
+ const isCompactInline = isInline && layout.fullWidth === false;
421
+
422
+ const rootClassName = [
423
+ "gap-1",
424
+ contain && !inline
425
+ ? "rounded-xl border border-border bg-background"
426
+ : null,
427
+ classes?.root,
428
+ className,
429
+ ]
430
+ .filter(Boolean)
431
+ .join(" ");
432
+
433
+ // Variant-level className merge (host + classes.variant)
434
+ const hostVariantClass =
435
+ (rest as any).className as string | undefined;
436
+
437
+ const mergedVariantClass =
438
+ ([
439
+ // In compact inline mode, force the control to size to its content
440
+ isCompactInline ? "inline-flex w-auto" : null,
441
+ hostVariantClass,
442
+ classes?.variant,
443
+ ]
444
+ .filter(Boolean)
445
+ .join(" ")) || undefined;
446
+
447
+ // Build tags content cluster (individual pills)
448
+ const tagsContent = React.useMemo(() => {
449
+ const items = (tags ?? []) as any[];
450
+
451
+ if (!items.length) return null;
452
+
453
+ return (
454
+ <>
455
+ {items.map((tag, index) => (
456
+ <span
457
+ key={index}
458
+ className={[
459
+ "inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium",
460
+ tag.className,
461
+ classes?.tag,
462
+ ]
463
+ .filter(Boolean)
464
+ .join(" ")}
465
+ style={{
466
+ color: tag.color,
467
+ backgroundColor: tag.bgColor,
468
+ }}
469
+ >
470
+ {tag.icon && (
471
+ <span className="shrink-0">
472
+ {tag.icon}
473
+ </span>
474
+ )}
475
+ <span>{tag.label}</span>
476
+ </span>
477
+ ))}
478
+ </>
479
+ );
480
+ }, [tags, classes?.tag]);
481
+
482
+ // Build helper layout graph for this field
483
+ const graph = React.useMemo(
484
+ () =>
485
+ buildLayoutGraph({
486
+ layout,
487
+ sublabel,
488
+ description,
489
+ helpText,
490
+ errorText: visualError || undefined,
491
+ tags: tagsContent || undefined,
492
+ }),
493
+ [layout, sublabel, description, helpText, visualError, tagsContent]
494
+ );
495
+
496
+ // ─────────────────────────────────────────────────────
497
+ // INLINE LAYOUT
498
+ // ─────────────────────────────────────────────────────
499
+
500
+ // In inline mode, label can effectively be left / right / hidden.
501
+ const inlineLabelSide: "left" | "right" | "hidden" =
502
+ lp === "right" ? "right" : lp === "hidden" ? "hidden" : "left";
503
+
504
+ // Width semantics for inline:
505
+ // - compact inline (fullWidth === false) → input column is content-sized
506
+ // - normal inline → input grows, label minimal
507
+ const inlineInputColClass = [
508
+ isCompactInline ? "flex-none" : "flex-1 min-w-0",
509
+ classes?.inlineInputColumn,
510
+ ]
511
+ .filter(Boolean)
512
+ .join(" ");
513
+
514
+ const inlineLabelColClass = [
515
+ isCompactInline ? "flex-1 min-w-0" : "min-w-0",
516
+ classes?.inlineLabelColumn,
517
+ ]
518
+ .filter(Boolean)
519
+ .join(" ");
520
+
521
+ const inlineFieldGroupClass = isCompactInline
522
+ ? [
523
+ // compact, content-sized group
524
+ "inline-flex w-auto",
525
+ // kill the Shadcn container on this group in compact-inline mode
526
+ "[container-type:normal]",
527
+ "[container-name:none]",
528
+ classes?.group,
529
+ ]
530
+ .filter(Boolean)
531
+ .join(" ")
532
+ : classes?.group ?? undefined;
533
+
534
+ const inlineFieldContentClass = isCompactInline
535
+ ? ["flex-none w-auto", classes?.content]
536
+ .filter(Boolean)
537
+ .join(" ")
538
+ : ["w-full", classes?.content].filter(Boolean).join(" ");
539
+
540
+ const inlineInputColumn = (
541
+ <div className={inlineInputColClass}>
542
+ {/* Above input (input root) */}
543
+ {graph
544
+ .getSlotsFor("input", "above")
545
+ .render((slots) =>
546
+ slots.map((slot) =>
547
+ renderHelperSlot("input", slot, classes)
548
+ )
549
+ )}
550
+
551
+ <FieldGroup className={inlineFieldGroupClass}>
552
+ <FieldContent className={inlineFieldContentClass}>
553
+ <Variant
554
+ {...(rest as any)}
555
+ id={key}
556
+ ref={ref as any}
557
+ value={value}
558
+ onValue={handleValueChange}
559
+ error={error}
560
+ required={required}
561
+ disabled={disabledProp}
562
+ readOnly={readOnlyProp}
563
+ size={effectiveSize}
564
+ density={effectiveDensity}
565
+ className={mergedVariantClass}
566
+ />
567
+ </FieldContent>
568
+ </FieldGroup>
569
+
570
+ {/* Below input (input root) */}
571
+ {graph
572
+ .getSlotsFor("input", "below")
573
+ .render((slots) =>
574
+ slots.map((slot) =>
575
+ renderHelperSlot("input", slot, classes)
576
+ )
577
+ )}
578
+ </div>
579
+ );
580
+
581
+ const inlineLabelColumn =
582
+ inlineLabelSide === "hidden" ? null : (
583
+ <div
584
+ className={["flex flex-col gap-0", inlineLabelColClass]
585
+ .filter(Boolean)
586
+ .join(" ")}
587
+ >
588
+ {/* Above label (label root) */}
589
+ {graph
590
+ .getSlotsFor("label", "above")
591
+ .render((slots) =>
592
+ slots.map((slot) =>
593
+ renderHelperSlot("label", slot, classes)
594
+ )
595
+ )}
596
+
597
+ <div
598
+ className={[
599
+ "flex items-baseline justify-between gap-1",
600
+ classes?.labelRow,
601
+ ]
602
+ .filter(Boolean)
603
+ .join(" ")}
604
+ data-slot="label-row"
605
+ >
606
+ {/* Left-of-label helpers (label root) */}
607
+ {graph
608
+ .getSlotsFor("label", "left")
609
+ .render((slots) => (
610
+ <div className="flex items-baseline gap-1">
611
+ {slots.map((slot) =>
612
+ renderHelperSlot(
613
+ "label",
614
+ slot,
615
+ classes
616
+ )
617
+ )}
618
+ </div>
619
+ ))}
620
+
621
+ {label && (
622
+ <FieldLabel
623
+ htmlFor={key}
624
+ className={[
625
+ "text-sm font-medium text-foreground",
626
+ classes?.label,
627
+ ]
628
+ .filter(Boolean)
629
+ .join(" ")}
630
+ >
631
+ <FieldTitle>{label} {required ? <span className={cn("text-destructive", classes?.required)}>*</span> : ''}</FieldTitle>
632
+ </FieldLabel>
633
+ )}
634
+
635
+ {/* Right-of-label helpers (label root) */}
636
+ {graph
637
+ .getSlotsFor("label", "right")
638
+ .render((slots) => (
639
+ <div className="flex items-baseline gap-1">
640
+ {slots.map((slot) =>
641
+ renderHelperSlot(
642
+ "label",
643
+ slot,
644
+ classes
645
+ )
646
+ )}
647
+ </div>
648
+ ))}
649
+ </div>
650
+
651
+ {/* Below label (label root) */}
652
+ {graph
653
+ .getSlotsFor("label", "below")
654
+ .render((slots) =>
655
+ slots.map((slot) =>
656
+ renderHelperSlot("label", slot, classes)
657
+ )
658
+ )}
659
+ </div>
660
+ );
661
+
662
+ const inlineRowClassName = [
663
+ "flex items-start gap-2",
664
+ classes?.inlineRow,
665
+ ]
666
+ .filter(Boolean)
667
+ .join(" ");
668
+
669
+ // ─────────────────────────────────────────────────────
670
+ // STACKED LAYOUT
671
+ // ─────────────────────────────────────────────────────
672
+
673
+ const stackedGroupClassName = ["mt-1", classes?.group]
674
+ .filter(Boolean)
675
+ .join(" ");
676
+
677
+ const Element = contain ? 'div' : React.Fragment;
678
+ const attrs = (a: 'l' | 'i' = 'l') =>
679
+ contain
680
+ ? a === 'l'
681
+ ? { className: "p-4 border-b border-input" }
682
+ : { className: "px-4 pt-2 pb-4" }
683
+ : {};
684
+ return (
685
+ <UiField
686
+ className={rootClassName}
687
+ style={style}
688
+ data-variant={String(variant)}
689
+ data-label-placement={lp ?? undefined}
690
+ data-sublabel-placement={sp ?? undefined}
691
+ data-description-placement={dp ?? undefined}
692
+ data-helptext-placement={hp ?? undefined}
693
+ data-errortext-placement={ep ?? undefined}
694
+ data-tag-placement={tp ?? undefined}
695
+ data-inline={isInline ? "true" : "false"}
696
+ data-fullwidth={layout.fullWidth ? "true" : "false"}
697
+ >
698
+ {isInline ? (
699
+ // INLINE MODE: label + control on the same row
700
+ <div
701
+ className={inlineRowClassName}
702
+ data-slot="inline-row"
703
+ >
704
+ {inlineLabelSide === "right" ? (
705
+ <>
706
+ {inlineInputColumn}
707
+ {inlineLabelColumn}
708
+ </>
709
+ ) : inlineLabelSide === "hidden" ? (
710
+ <>{inlineInputColumn}</>
711
+ ) : (
712
+ <>
713
+ {inlineLabelColumn}
714
+ {inlineInputColumn}
715
+ </>
716
+ )}
717
+ </div>
718
+ ) : (
719
+ // STACKED MODE
720
+ <>
721
+ {lp !== "hidden" && (
722
+ <Element {...attrs()}>
723
+ {/* Above label (label root) */}
724
+ {graph
725
+ .getSlotsFor("label", "above")
726
+ .render((slots) =>
727
+ slots.map((slot) =>
728
+ renderHelperSlot(
729
+ "label",
730
+ slot,
731
+ classes
732
+ )
733
+ )
734
+ )}
735
+
736
+ <div
737
+ className={[
738
+ "flex items-baseline justify-between gap-1",
739
+ classes?.labelRow,
740
+ ]
741
+ .filter(Boolean)
742
+ .join(" ")}
743
+ data-slot="label-row"
744
+ >
745
+ {/* Left-of-label helpers (label root) */}
746
+ {graph
747
+ .getSlotsFor("label", "left")
748
+ .render((slots) => (
749
+ <div className="flex items-baseline gap-1">
750
+ {slots.map((slot) =>
751
+ renderHelperSlot(
752
+ "label",
753
+ slot,
754
+ classes
755
+ )
756
+ )}
757
+ </div>
758
+ ))}
759
+
760
+ {label && (
761
+ <FieldLabel
762
+ htmlFor={key}
763
+ className={[
764
+ "text-sm font-medium text-foreground",
765
+ classes?.label,
766
+ ]
767
+ .filter(Boolean)
768
+ .join(" ")}
769
+ >
770
+ <FieldTitle>{label} {required ? <span className={cn("text-destructive", classes?.required)}>*</span> : ''}</FieldTitle>
771
+ </FieldLabel>
772
+ )}
773
+
774
+ {/* Right-of-label helpers (label root) */}
775
+ {graph
776
+ .getSlotsFor("label", "right")
777
+ .render((slots) => (
778
+ <div className="flex items-baseline gap-1">
779
+ {slots.map((slot) =>
780
+ renderHelperSlot(
781
+ "label",
782
+ slot,
783
+ classes
784
+ )
785
+ )}
786
+ </div>
787
+ ))}
788
+ </div>
789
+
790
+ {/* Below label (label root) */}
791
+ {graph
792
+ .getSlotsFor("label", "below")
793
+ .render((slots) =>
794
+ slots.map((slot) =>
795
+ renderHelperSlot(
796
+ "label",
797
+ slot,
798
+ classes
799
+ )
800
+ )
801
+ )}
802
+ </Element>
803
+ )}
804
+
805
+ <Element {...attrs('i')}>
806
+ {/* Above input (input root) */}
807
+ {graph
808
+ .getSlotsFor("input", "above")
809
+ .render((slots) =>
810
+ slots.map((slot) =>
811
+ renderHelperSlot(
812
+ "input",
813
+ slot,
814
+ classes
815
+ )
816
+ )
817
+ )}
818
+
819
+ <FieldGroup className={stackedGroupClassName}>
820
+ <FieldContent
821
+ className={["w-full", classes?.content]
822
+ .filter(Boolean)
823
+ .join(" ")}
824
+ >
825
+ <Variant
826
+ {...(rest as any)}
827
+ ref={ref as any}
828
+ value={value}
829
+ onValue={handleValueChange}
830
+ error={error}
831
+ required={required}
832
+ disabled={disabledProp}
833
+ readOnly={readOnlyProp}
834
+ size={effectiveSize}
835
+ density={effectiveDensity}
836
+ className={mergedVariantClass}
837
+ />
838
+ </FieldContent>
839
+ </FieldGroup>
840
+
841
+ {/* Below input (input root) */}
842
+ {graph
843
+ .getSlotsFor("input", "below")
844
+ .render((slots) =>
845
+ slots.map((slot) =>
846
+ renderHelperSlot("input", slot, classes)
847
+ )
848
+ )}
849
+ </Element>
850
+ </>
851
+ )}
852
+ </UiField>
853
+ );
854
+ }