@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,556 @@
1
+ import * as React from "react";
2
+ import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ import { Button } from "@/presets/ui/button";
6
+ import { Input } from "@/presets/ui/input";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogFooter,
13
+ DialogDescription,
14
+ } from "@/presets/ui/dialog";
15
+ import { X, Plus, MoreHorizontal, Tag, PenLine } from "lucide-react";
16
+
17
+ type Size = "sm" | "md" | "lg";
18
+ type Density = "compact" | "comfortable" | "loose";
19
+
20
+ export type KeyValueMap = Record<string, string>;
21
+
22
+ export interface KV {
23
+ key: string;
24
+ value: string;
25
+ }
26
+
27
+ export interface ShadcnKeyValueVariantProps
28
+ extends Pick<
29
+ VariantBaseProps<KeyValueMap | undefined>,
30
+ "value" | "onValue" | "error" | "disabled" | "readOnly" | "size" | "density"
31
+ > {
32
+ min?: number;
33
+ max?: number;
34
+ minVisible?: number;
35
+ maxVisible?: number;
36
+ showAddButton?: boolean;
37
+ showMenuButton?: boolean;
38
+ placeholder?: React.ReactNode;
39
+ dialogTitle?: React.ReactNode;
40
+ keyLabel?: React.ReactNode;
41
+ valueLabel?: React.ReactNode;
42
+ submitLabel?: React.ReactNode;
43
+ moreLabel?: (count: number) => React.ReactNode;
44
+ emptyLabel?: React.ReactNode;
45
+ className?: string;
46
+ chipsClassName?: string;
47
+ chipClassName?: string;
48
+ renderChip?: (ctx: {
49
+ pair: KV;
50
+ index: number;
51
+ onEdit: () => void;
52
+ onRemove: () => void;
53
+ defaultChip: React.ReactNode;
54
+ }) => React.ReactNode;
55
+ }
56
+
57
+ // ─────────────────────────────────────────────
58
+ // Helpers
59
+ // ─────────────────────────────────────────────
60
+
61
+ function mapToItems(map: KeyValueMap | undefined): KV[] {
62
+ if (!map) return [];
63
+ return Object.entries(map).map(([key, value]) => ({
64
+ key,
65
+ value: value ?? "",
66
+ }));
67
+ }
68
+
69
+ function itemsToMap(items: KV[]): KeyValueMap {
70
+ const out: KeyValueMap = {};
71
+ for (const { key, value } of items) {
72
+ if (!key) continue;
73
+ out[key] = value;
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function clampVisible(
79
+ total: number,
80
+ minVisible: number,
81
+ maxVisible: number
82
+ ): number {
83
+ if (total === 0) return 0;
84
+ const clampedMax = Math.max(minVisible, maxVisible);
85
+ return Math.min(total, clampedMax);
86
+ }
87
+
88
+ function sizeClasses(size?: Size) {
89
+ switch (size) {
90
+ case "sm":
91
+ return "text-xs min-h-[2rem]";
92
+ case "lg":
93
+ return "text-sm min-h-[2.75rem]";
94
+ case "md":
95
+ default:
96
+ return "text-sm min-h-[2.5rem]";
97
+ }
98
+ }
99
+
100
+ function densityPadding(density?: Density) {
101
+ switch (density) {
102
+ case "compact":
103
+ return "py-1 px-2 gap-1.5";
104
+ case "loose":
105
+ return "py-3 px-3 gap-3";
106
+ case "comfortable":
107
+ default:
108
+ return "py-2 px-3 gap-2";
109
+ }
110
+ }
111
+
112
+ function defaultMoreLabel(count: number): React.ReactNode {
113
+ return `+${count} more`;
114
+ }
115
+
116
+ // ─────────────────────────────────────────────
117
+ // Component
118
+ // ─────────────────────────────────────────────
119
+
120
+ export const ShadcnKeyValueVariant = React.forwardRef<
121
+ HTMLDivElement,
122
+ ShadcnKeyValueVariantProps
123
+ >(function ShadcnKeyValueVariant(props, _ref) {
124
+ const {
125
+ value,
126
+ onValue,
127
+ error,
128
+ disabled,
129
+ readOnly,
130
+ size,
131
+ density,
132
+
133
+ min = 0,
134
+ max = Infinity,
135
+ minVisible = 0,
136
+ maxVisible = 6,
137
+
138
+ showAddButton = true,
139
+ showMenuButton = true,
140
+
141
+ placeholder,
142
+ dialogTitle = "Edit Item",
143
+ keyLabel = "Key",
144
+ valueLabel = "Value",
145
+ submitLabel = "Save Changes",
146
+ moreLabel = defaultMoreLabel,
147
+ emptyLabel = "No items added",
148
+
149
+ className,
150
+ chipsClassName,
151
+ chipClassName,
152
+ renderChip,
153
+ } = props;
154
+
155
+ const isDisabled = disabled || readOnly;
156
+
157
+ const items: KV[] = React.useMemo(
158
+ () => mapToItems(value),
159
+ [value]
160
+ );
161
+
162
+ const [dialogOpen, setDialogOpen] = React.useState(false);
163
+ const [editingIndex, setEditingIndex] = React.useState<number | null>(
164
+ null
165
+ );
166
+ const [draft, setDraft] = React.useState<KV>({ key: "", value: "" });
167
+
168
+ const canAdd = items.length < max;
169
+ const canDelete = items.length > min;
170
+
171
+ // visible vs overflow
172
+ const visibleCount = clampVisible(
173
+ items.length,
174
+ minVisible,
175
+ maxVisible
176
+ );
177
+ const visibleItems = items.slice(0, visibleCount);
178
+ const overflowCount = Math.max(0, items.length - visibleCount);
179
+
180
+ // ────────────────────────────────
181
+ // Change Logic
182
+ // ────────────────────────────────
183
+
184
+ const commitItems = React.useCallback(
185
+ (next: KV[], meta: ChangeDetail["meta"]) => {
186
+ if (!onValue) return;
187
+
188
+ const nextMap = itemsToMap(next);
189
+ const detail: ChangeDetail = {
190
+ source: "variant",
191
+ raw: next,
192
+ nativeEvent: undefined,
193
+ meta,
194
+ };
195
+ onValue(nextMap, detail);
196
+ },
197
+ [onValue]
198
+ );
199
+
200
+ const openForNew = React.useCallback(() => {
201
+ if (isDisabled || !canAdd) return;
202
+ setEditingIndex(null);
203
+ setDraft({ key: "", value: "" });
204
+ setDialogOpen(true);
205
+ }, [isDisabled, canAdd]);
206
+
207
+ const openForEdit = React.useCallback(
208
+ (index: number) => {
209
+ if (isDisabled) return;
210
+ const item = items[index];
211
+ if (!item) return;
212
+ setEditingIndex(index);
213
+ setDraft(item);
214
+ setDialogOpen(true);
215
+ },
216
+ [isDisabled, items]
217
+ );
218
+
219
+ const handleDelete = React.useCallback(() => {
220
+ if (editingIndex == null) return;
221
+ if (!canDelete) return;
222
+
223
+ const next = items.slice();
224
+ next.splice(editingIndex, 1);
225
+
226
+ setDialogOpen(false);
227
+ commitItems(next, {
228
+ action: "delete",
229
+ index: editingIndex,
230
+ });
231
+ }, [editingIndex, items, canDelete, commitItems]);
232
+
233
+ const handleSubmit = React.useCallback(() => {
234
+ const trimmedKey = draft.key.trim();
235
+ const trimmedValue = draft.value;
236
+
237
+ if (!trimmedKey) return;
238
+
239
+ let next = items.slice();
240
+
241
+ if (editingIndex != null) {
242
+ // edit
243
+ next[editingIndex] = { key: trimmedKey, value: trimmedValue };
244
+ } else {
245
+ // add / upsert
246
+ const existingIndex = next.findIndex(
247
+ (kv) => kv.key === trimmedKey
248
+ );
249
+ if (existingIndex !== -1) {
250
+ next[existingIndex] = {
251
+ key: trimmedKey,
252
+ value: trimmedValue,
253
+ };
254
+ } else {
255
+ if (!canAdd) return;
256
+ next.push({ key: trimmedKey, value: trimmedValue });
257
+ }
258
+ }
259
+
260
+ setDialogOpen(false);
261
+ commitItems(next, {
262
+ action: editingIndex != null ? "edit" : "add",
263
+ index: editingIndex ?? next.length - 1,
264
+ });
265
+ }, [draft, items, editingIndex, canAdd, commitItems]);
266
+
267
+ const handleQuickRemove = React.useCallback(
268
+ (index: number) => {
269
+ if (isDisabled || !canDelete) return;
270
+ const next = items.slice();
271
+ next.splice(index, 1);
272
+ commitItems(next, { action: "delete", index });
273
+ },
274
+ [isDisabled, canDelete, items, commitItems]
275
+ );
276
+
277
+ // ────────────────────────────────
278
+ // Visuals
279
+ // ────────────────────────────────
280
+
281
+ const sizeCls = sizeClasses(size as Size | undefined);
282
+ const densityCls = densityPadding(density as Density | undefined);
283
+
284
+ const renderChipNode = (pair: KV, index: number) => {
285
+ const baseChip = (
286
+ <button
287
+ type="button"
288
+ key={index}
289
+ className={cn(
290
+ "group inline-flex items-center gap-1.5 rounded-md",
291
+ "bg-secondary/50 border border-transparent",
292
+ "px-2 py-1 text-xs transition-all duration-200",
293
+ "hover:bg-secondary hover:border-border/50 hover:shadow-sm",
294
+ "animate-in fade-in zoom-in-95 fill-mode-both",
295
+ isDisabled && "opacity-50 cursor-not-allowed",
296
+ chipClassName
297
+ )}
298
+ onClick={() => openForEdit(index)}
299
+ disabled={isDisabled}
300
+ >
301
+ <span className="font-semibold text-foreground truncate max-w-[120px]">
302
+ {pair.key}
303
+ </span>
304
+ <span className="text-muted-foreground/40">:</span>
305
+ <span className="text-muted-foreground truncate max-w-[120px]">
306
+ {pair.value}
307
+ </span>
308
+
309
+ {canDelete && !isDisabled && (
310
+ <div
311
+ role="button"
312
+ tabIndex={0}
313
+ className={cn(
314
+ "ml-1 flex h-4 w-4 items-center justify-center rounded-full",
315
+ "text-muted-foreground/60 opacity-0 transition-all",
316
+ "hover:bg-destructive hover:text-destructive-foreground",
317
+ "group-hover:opacity-100",
318
+ "focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring"
319
+ )}
320
+ onClick={(e) => {
321
+ e.stopPropagation();
322
+ handleQuickRemove(index);
323
+ }}
324
+ onKeyDown={(e) => {
325
+ if (e.key === "Enter" || e.key === " ") {
326
+ e.stopPropagation();
327
+ handleQuickRemove(index);
328
+ }
329
+ }}
330
+ aria-label={`Remove ${pair.key}`}
331
+ >
332
+ <X className="h-3 w-3" />
333
+ </div>
334
+ )}
335
+ </button>
336
+ );
337
+
338
+ if (!renderChip) return baseChip;
339
+
340
+ return renderChip({
341
+ pair,
342
+ index,
343
+ onEdit: () => openForEdit(index),
344
+ onRemove: () => handleQuickRemove(index),
345
+ defaultChip: baseChip,
346
+ });
347
+ };
348
+
349
+ const hasItems = items.length > 0;
350
+
351
+ // ────────────────────────────────
352
+ // Dialog
353
+ // ────────────────────────────────
354
+
355
+ const ManageDialog = (
356
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
357
+ <DialogContent className="sm:max-w-[425px]">
358
+ <DialogHeader>
359
+ <DialogTitle className="flex items-center gap-2">
360
+ <PenLine className="h-4 w-4 text-muted-foreground" />
361
+ {dialogTitle}
362
+ </DialogTitle>
363
+ <DialogDescription>
364
+ {editingIndex !== null ? "Modify the existing key-value pair." : "Add a new key-value pair to the list."}
365
+ </DialogDescription>
366
+ </DialogHeader>
367
+
368
+ <div className="grid gap-4 py-4">
369
+ <div className="grid grid-cols-4 items-center gap-4">
370
+ <label className="text-right text-sm font-medium text-muted-foreground">
371
+ {keyLabel}
372
+ </label>
373
+ <Input
374
+ value={draft.key}
375
+ onChange={(e) =>
376
+ setDraft((prev) => ({
377
+ ...prev,
378
+ key: e.target.value,
379
+ }))
380
+ }
381
+ className="col-span-3"
382
+ autoFocus
383
+ disabled={isDisabled}
384
+ placeholder="e.g. Color"
385
+ />
386
+ </div>
387
+ <div className="grid grid-cols-4 items-center gap-4">
388
+ <label className="text-right text-sm font-medium text-muted-foreground">
389
+ {valueLabel}
390
+ </label>
391
+ <Input
392
+ value={draft.value}
393
+ onChange={(e) =>
394
+ setDraft((prev) => ({
395
+ ...prev,
396
+ value: e.target.value,
397
+ }))
398
+ }
399
+ className="col-span-3"
400
+ disabled={isDisabled}
401
+ placeholder="e.g. Blue"
402
+ onKeyDown={(e) => {
403
+ if (e.key === 'Enter') handleSubmit();
404
+ }}
405
+ />
406
+ </div>
407
+ </div>
408
+
409
+ <DialogFooter className="flex sm:justify-between flex-row items-center">
410
+ <div>
411
+ {editingIndex != null && canDelete && (
412
+ <Button
413
+ type="button"
414
+ variant="destructive"
415
+ size="sm"
416
+ onClick={handleDelete}
417
+ disabled={isDisabled}
418
+ >
419
+ Delete
420
+ </Button>
421
+ )}
422
+ </div>
423
+
424
+ <div className="flex gap-2">
425
+ <Button
426
+ type="button"
427
+ variant="outline"
428
+ size="sm"
429
+ onClick={() => setDialogOpen(false)}
430
+ >
431
+ Cancel
432
+ </Button>
433
+ <Button
434
+ type="button"
435
+ size="sm"
436
+ onClick={handleSubmit}
437
+ disabled={isDisabled}
438
+ >
439
+ {submitLabel}
440
+ </Button>
441
+ </div>
442
+ </DialogFooter>
443
+ </DialogContent>
444
+ </Dialog>
445
+ );
446
+
447
+ // ────────────────────────────────
448
+ // Render
449
+ // ────────────────────────────────
450
+
451
+ return (
452
+ <div
453
+ className={cn(
454
+ "group/container w-full",
455
+ isDisabled && "opacity-60 cursor-not-allowed",
456
+ className
457
+ )}
458
+ aria-disabled={isDisabled}
459
+ aria-invalid={error ? "true" : undefined}
460
+ >
461
+ {/* Container mimicking an Input */}
462
+ <div
463
+ className={cn(
464
+ "relative flex w-full flex-wrap items-center rounded-md border border-input bg-background transition-all",
465
+ // Focus within styles to mimic Input focus
466
+ !isDisabled && "focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
467
+ densityCls,
468
+ sizeCls,
469
+ chipsClassName
470
+ )}
471
+ >
472
+ {hasItems ? (
473
+ <>
474
+ {visibleItems.map((pair, index) =>
475
+ renderChipNode(pair, index)
476
+ )}
477
+
478
+ {overflowCount > 0 && (
479
+ <button
480
+ type="button"
481
+ className={cn(
482
+ "inline-flex h-6 items-center gap-1 rounded-full",
483
+ "bg-muted px-2 text-[11px] font-medium text-muted-foreground",
484
+ "hover:bg-muted/80 hover:text-foreground transition-colors"
485
+ )}
486
+ onClick={() => {
487
+ setDialogOpen(true);
488
+ setEditingIndex(null);
489
+ setDraft({ key: "", value: "" });
490
+ }}
491
+ disabled={isDisabled}
492
+ >
493
+ {moreLabel(overflowCount)}
494
+ </button>
495
+ )}
496
+ </>
497
+ ) : (
498
+ <div className="flex items-center gap-2 text-muted-foreground/60 select-none">
499
+ <Tag className="h-3.5 w-3.5" />
500
+ <span className="text-sm">{placeholder ?? emptyLabel}</span>
501
+ </div>
502
+ )}
503
+
504
+ {/* Inline Add Button */}
505
+ {showAddButton && canAdd && !isDisabled && (
506
+ <button
507
+ type="button"
508
+ onClick={openForNew}
509
+ className={cn(
510
+ "inline-flex h-6 items-center gap-1 rounded-full",
511
+ "border border-dashed border-muted-foreground/30 px-2",
512
+ "text-[11px] font-medium text-muted-foreground",
513
+ "hover:border-primary/50 hover:bg-accent hover:text-accent-foreground transition-all",
514
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
515
+ )}
516
+ >
517
+ <Plus className="h-3 w-3" />
518
+ <span>Add</span>
519
+ </button>
520
+ )}
521
+
522
+ {/* Menu/Manage Button */}
523
+ {showMenuButton && hasItems && !isDisabled && (
524
+ <div className="ml-auto pl-1">
525
+ <button
526
+ type="button"
527
+ onClick={() => {
528
+ // Default behavior: open "Add New"
529
+ setDialogOpen(true);
530
+ setEditingIndex(null);
531
+ setDraft({ key: "", value: "" });
532
+ }}
533
+ className="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground"
534
+ aria-label="Add another"
535
+ >
536
+ <MoreHorizontal className="h-4 w-4" />
537
+ </button>
538
+ </div>
539
+ )}
540
+ </div>
541
+
542
+ {/* Error Message Support (Optional usage) */}
543
+ {error && typeof error === 'string' && (
544
+ <p className="mt-1.5 text-xs font-medium text-destructive">
545
+ {error}
546
+ </p>
547
+ )}
548
+
549
+ {ManageDialog}
550
+ </div>
551
+ );
552
+ });
553
+
554
+ ShadcnKeyValueVariant.displayName = "ShadcnKeyValueVariant";
555
+
556
+ export default ShadcnKeyValueVariant;