@arqel-dev/fields 0.8.0

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.
@@ -0,0 +1,966 @@
1
+ import { registerField } from '@arqel-dev/ui/form';
2
+ import { cn } from '@arqel-dev/ui/utils';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+ import { useState, useRef, useEffect } from 'react';
5
+
6
+ // src/register.ts
7
+ var inputClasses = cn(
8
+ "h-9 w-full rounded-sm border border-[var(--input)]",
9
+ "bg-background px-3 text-sm text-foreground",
10
+ "placeholder:text-muted-foreground",
11
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
12
+ "disabled:cursor-not-allowed disabled:opacity-50",
13
+ "aria-invalid:border-destructive"
14
+ );
15
+ var checkboxClasses = cn(
16
+ "h-4 w-4 rounded border-[var(--input)]",
17
+ "text-primary",
18
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
19
+ "disabled:cursor-not-allowed disabled:opacity-50"
20
+ );
21
+ function Checkbox({
22
+ field,
23
+ value,
24
+ onChange,
25
+ errors,
26
+ disabled,
27
+ inputId,
28
+ describedBy
29
+ }) {
30
+ const f = field;
31
+ const hasError = errors !== void 0 && errors.length > 0;
32
+ return /* @__PURE__ */ jsx(
33
+ "input",
34
+ {
35
+ id: inputId,
36
+ type: "checkbox",
37
+ className: checkboxClasses,
38
+ checked: value === true,
39
+ disabled: disabled || f.disabled || f.readonly,
40
+ "aria-invalid": hasError || void 0,
41
+ "aria-describedby": describedBy,
42
+ onChange: (e) => onChange(e.target.checked)
43
+ }
44
+ );
45
+ }
46
+ function Toggle({
47
+ field,
48
+ value,
49
+ onChange,
50
+ errors,
51
+ disabled,
52
+ inputId,
53
+ describedBy
54
+ }) {
55
+ const f = field;
56
+ const checked = value === true;
57
+ const hasError = errors !== void 0 && errors.length > 0;
58
+ const isDisabled = disabled || f.disabled || f.readonly;
59
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
60
+ /* @__PURE__ */ jsx(
61
+ "button",
62
+ {
63
+ id: inputId,
64
+ type: "button",
65
+ role: "switch",
66
+ "aria-checked": checked,
67
+ "aria-invalid": hasError || void 0,
68
+ "aria-describedby": describedBy,
69
+ disabled: isDisabled,
70
+ onClick: () => onChange(!checked),
71
+ className: cn(
72
+ "relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
73
+ checked ? "bg-primary" : "bg-muted",
74
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
75
+ "disabled:cursor-not-allowed disabled:opacity-50"
76
+ ),
77
+ children: /* @__PURE__ */ jsx(
78
+ "span",
79
+ {
80
+ "aria-hidden": "true",
81
+ className: cn(
82
+ "inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform",
83
+ checked ? "translate-x-5" : "translate-x-0.5"
84
+ )
85
+ }
86
+ )
87
+ }
88
+ ),
89
+ (f.props.onLabel || f.props.offLabel) && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: checked ? f.props.onLabel : f.props.offLabel })
90
+ ] });
91
+ }
92
+ function ColorInput({
93
+ field,
94
+ value,
95
+ onChange,
96
+ errors,
97
+ disabled,
98
+ inputId,
99
+ describedBy
100
+ }) {
101
+ const f = field;
102
+ const hasError = errors !== void 0 && errors.length > 0;
103
+ const isDisabled = disabled || f.disabled || f.readonly;
104
+ const current = typeof value === "string" && value.length > 0 ? value : "#000000";
105
+ const presets = f.props.presets ?? [];
106
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
107
+ /* @__PURE__ */ jsx(
108
+ "input",
109
+ {
110
+ id: inputId,
111
+ type: "color",
112
+ className: cn(
113
+ "h-9 w-12 cursor-pointer rounded-sm border border-[var(--input)]",
114
+ "disabled:cursor-not-allowed disabled:opacity-50"
115
+ ),
116
+ value: current,
117
+ disabled: isDisabled,
118
+ "aria-invalid": hasError || void 0,
119
+ "aria-describedby": describedBy,
120
+ onChange: (e) => onChange(e.target.value)
121
+ }
122
+ ),
123
+ /* @__PURE__ */ jsx(
124
+ "input",
125
+ {
126
+ type: "text",
127
+ className: "h-9 w-28 rounded-sm border border-[var(--input)] bg-background px-2 font-mono text-xs",
128
+ value: current,
129
+ disabled: isDisabled,
130
+ onChange: (e) => onChange(e.target.value)
131
+ }
132
+ ),
133
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: presets.map((preset) => /* @__PURE__ */ jsx(
134
+ "button",
135
+ {
136
+ type: "button",
137
+ "aria-label": `Preset ${preset}`,
138
+ disabled: isDisabled,
139
+ onClick: () => onChange(preset),
140
+ className: "h-6 w-6 rounded-full border border-border disabled:cursor-not-allowed disabled:opacity-50",
141
+ style: { backgroundColor: preset }
142
+ },
143
+ preset
144
+ )) })
145
+ ] });
146
+ }
147
+ function DateInput({
148
+ field,
149
+ value,
150
+ onChange,
151
+ errors,
152
+ disabled,
153
+ inputId,
154
+ describedBy
155
+ }) {
156
+ const f = field;
157
+ const hasError = errors !== void 0 && errors.length > 0;
158
+ return /* @__PURE__ */ jsx(
159
+ "input",
160
+ {
161
+ id: inputId,
162
+ type: "date",
163
+ className: inputClasses,
164
+ value: typeof value === "string" ? value : "",
165
+ disabled: disabled || f.disabled || f.readonly,
166
+ readOnly: f.readonly === true,
167
+ required: f.required === true,
168
+ min: f.props.minDate,
169
+ max: f.props.maxDate,
170
+ "aria-invalid": hasError || void 0,
171
+ "aria-describedby": describedBy,
172
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
173
+ }
174
+ );
175
+ }
176
+ function DateTimeInput({
177
+ field,
178
+ value,
179
+ onChange,
180
+ errors,
181
+ disabled,
182
+ inputId,
183
+ describedBy
184
+ }) {
185
+ const f = field;
186
+ const hasError = errors !== void 0 && errors.length > 0;
187
+ return /* @__PURE__ */ jsx(
188
+ "input",
189
+ {
190
+ id: inputId,
191
+ type: "datetime-local",
192
+ className: inputClasses,
193
+ value: typeof value === "string" ? value : "",
194
+ disabled: disabled || f.disabled || f.readonly,
195
+ readOnly: f.readonly === true,
196
+ required: f.required === true,
197
+ min: f.props.minDate,
198
+ max: f.props.maxDate,
199
+ step: f.props.seconds ? 1 : void 0,
200
+ "aria-invalid": hasError || void 0,
201
+ "aria-describedby": describedBy,
202
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
203
+ }
204
+ );
205
+ }
206
+ function FileInput({
207
+ field,
208
+ value,
209
+ onChange,
210
+ errors,
211
+ disabled,
212
+ inputId,
213
+ describedBy
214
+ }) {
215
+ const f = field;
216
+ const hasError = errors !== void 0 && errors.length > 0;
217
+ const isDisabled = disabled || f.disabled || f.readonly;
218
+ const [dragOver, setDragOver] = useState(false);
219
+ const file = value instanceof File ? value : null;
220
+ const filename = file?.name ?? (typeof value === "string" ? value : null);
221
+ const handleFiles = (files) => {
222
+ if (!files || files.length === 0) return;
223
+ onChange(files[0]);
224
+ };
225
+ return /* @__PURE__ */ jsxs(
226
+ "section",
227
+ {
228
+ "aria-label": "File upload",
229
+ onDragOver: (e) => {
230
+ e.preventDefault();
231
+ if (!isDisabled) setDragOver(true);
232
+ },
233
+ onDragLeave: () => setDragOver(false),
234
+ onDrop: (e) => {
235
+ e.preventDefault();
236
+ setDragOver(false);
237
+ if (!isDisabled) handleFiles(e.dataTransfer.files);
238
+ },
239
+ className: cn(
240
+ "flex flex-col items-center justify-center gap-2 rounded-sm border-2 border-dashed px-4 py-6 text-sm",
241
+ dragOver ? "border-primary bg-muted" : "border-border",
242
+ hasError && "border-destructive",
243
+ isDisabled && "opacity-50"
244
+ ),
245
+ children: [
246
+ filename ? /* @__PURE__ */ jsx("span", { className: "font-medium", children: filename }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Drag a file here or click to browse" }),
247
+ /* @__PURE__ */ jsxs("label", { className: "cursor-pointer text-xs text-primary hover:underline", children: [
248
+ filename ? "Choose another file" : "Browse",
249
+ /* @__PURE__ */ jsx(
250
+ "input",
251
+ {
252
+ id: inputId,
253
+ type: "file",
254
+ className: "sr-only",
255
+ disabled: isDisabled,
256
+ "aria-invalid": hasError || void 0,
257
+ "aria-describedby": describedBy,
258
+ onChange: (e) => handleFiles(e.target.files)
259
+ }
260
+ )
261
+ ] })
262
+ ]
263
+ }
264
+ );
265
+ }
266
+ function ImageInput({
267
+ field,
268
+ value,
269
+ onChange,
270
+ errors,
271
+ disabled,
272
+ inputId,
273
+ describedBy
274
+ }) {
275
+ const f = field;
276
+ const hasError = errors !== void 0 && errors.length > 0;
277
+ const isDisabled = disabled || f.disabled || f.readonly;
278
+ const [previewUrl, setPreviewUrl] = useState(null);
279
+ useEffect(() => {
280
+ if (value instanceof File) {
281
+ const url = URL.createObjectURL(value);
282
+ setPreviewUrl(url);
283
+ return () => URL.revokeObjectURL(url);
284
+ }
285
+ if (typeof value === "string" && value.length > 0) {
286
+ setPreviewUrl(value);
287
+ return void 0;
288
+ }
289
+ setPreviewUrl(null);
290
+ return void 0;
291
+ }, [value]);
292
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
293
+ previewUrl && /* @__PURE__ */ jsx(
294
+ "img",
295
+ {
296
+ src: previewUrl,
297
+ alt: "Preview",
298
+ className: cn("max-h-40 w-auto rounded-sm border border-border")
299
+ }
300
+ ),
301
+ /* @__PURE__ */ jsxs(
302
+ "label",
303
+ {
304
+ className: cn(
305
+ "inline-flex w-fit cursor-pointer items-center gap-2 rounded-sm border border-border px-3 py-1.5 text-sm",
306
+ "hover:bg-muted",
307
+ isDisabled && "cursor-not-allowed opacity-50"
308
+ ),
309
+ children: [
310
+ previewUrl ? "Replace image" : "Choose image",
311
+ /* @__PURE__ */ jsx(
312
+ "input",
313
+ {
314
+ id: inputId,
315
+ type: "file",
316
+ accept: "image/*",
317
+ className: "sr-only",
318
+ disabled: isDisabled,
319
+ "aria-invalid": hasError || void 0,
320
+ "aria-describedby": describedBy,
321
+ onChange: (e) => {
322
+ const file = e.target.files?.[0];
323
+ if (file) onChange(file);
324
+ }
325
+ }
326
+ )
327
+ ]
328
+ }
329
+ )
330
+ ] });
331
+ }
332
+ function HiddenInput({ field, value, inputId }) {
333
+ return /* @__PURE__ */ jsx(
334
+ "input",
335
+ {
336
+ id: inputId,
337
+ type: "hidden",
338
+ name: field.name,
339
+ value: value === null || value === void 0 ? "" : String(value),
340
+ readOnly: true
341
+ }
342
+ );
343
+ }
344
+ function CurrencyInput({
345
+ field,
346
+ value,
347
+ onChange,
348
+ errors,
349
+ disabled,
350
+ inputId,
351
+ describedBy
352
+ }) {
353
+ const f = field;
354
+ const [focused, setFocused] = useState(false);
355
+ const hasError = errors !== void 0 && errors.length > 0;
356
+ const isDisabled = disabled || f.disabled || f.readonly;
357
+ const numeric = typeof value === "number" ? value : value === "" || value == null ? null : Number(value);
358
+ const decimals = f.props.decimals ?? 2;
359
+ const formatted = numeric === null || Number.isNaN(numeric) ? "" : `${f.props.prefix}${formatNumber(numeric, decimals, f.props.thousandsSeparator, f.props.decimalSeparator)}${f.props.suffix ?? ""}`;
360
+ const display = focused ? numeric === null ? "" : String(numeric) : formatted;
361
+ return /* @__PURE__ */ jsx(
362
+ "input",
363
+ {
364
+ id: inputId,
365
+ type: focused ? "number" : "text",
366
+ inputMode: "decimal",
367
+ className: inputClasses,
368
+ value: display,
369
+ placeholder: f.placeholder ?? void 0,
370
+ disabled: isDisabled,
371
+ readOnly: f.readonly === true,
372
+ required: f.required === true,
373
+ min: f.props.min,
374
+ max: f.props.max,
375
+ step: f.props.step ?? 10 ** -decimals,
376
+ "aria-invalid": hasError || void 0,
377
+ "aria-describedby": describedBy,
378
+ onFocus: () => setFocused(true),
379
+ onBlur: () => setFocused(false),
380
+ onChange: (e) => {
381
+ const raw = e.target.value;
382
+ if (raw === "") {
383
+ onChange(null);
384
+ return;
385
+ }
386
+ const num = Number(raw);
387
+ onChange(Number.isNaN(num) ? null : num);
388
+ }
389
+ }
390
+ );
391
+ }
392
+ function formatNumber(num, decimals, thousands, decimal) {
393
+ const fixed = num.toFixed(decimals);
394
+ const [whole, frac] = fixed.split(".");
395
+ const safeWhole = (whole ?? "0").replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
396
+ return frac ? `${safeWhole}${decimal}${frac}` : safeWhole;
397
+ }
398
+ function NumberInput({
399
+ field,
400
+ value,
401
+ onChange,
402
+ errors,
403
+ disabled,
404
+ inputId,
405
+ describedBy
406
+ }) {
407
+ const f = field;
408
+ const hasError = errors !== void 0 && errors.length > 0;
409
+ const isDisabled = disabled || f.disabled || f.readonly;
410
+ const setValue = (raw) => {
411
+ if (Number.isNaN(raw)) {
412
+ onChange(null);
413
+ return;
414
+ }
415
+ if (f.props.integer) onChange(Math.trunc(raw));
416
+ else onChange(raw);
417
+ };
418
+ const current = typeof value === "number" ? value : null;
419
+ const step = typeof f.props.step === "number" ? f.props.step : 1;
420
+ return /* @__PURE__ */ jsxs("div", { className: "relative inline-flex w-full", children: [
421
+ /* @__PURE__ */ jsx(
422
+ "input",
423
+ {
424
+ id: inputId,
425
+ type: "number",
426
+ inputMode: f.props.integer ? "numeric" : "decimal",
427
+ className: cn(inputClasses, "pr-16"),
428
+ value: current === null ? "" : String(current),
429
+ placeholder: f.placeholder ?? void 0,
430
+ disabled: isDisabled,
431
+ readOnly: f.readonly === true,
432
+ required: f.required === true,
433
+ min: f.props.min,
434
+ max: f.props.max,
435
+ step: f.props.step,
436
+ "aria-invalid": hasError || void 0,
437
+ "aria-describedby": describedBy,
438
+ onChange: (e) => {
439
+ if (e.target.value === "") {
440
+ onChange(null);
441
+ return;
442
+ }
443
+ setValue(Number(e.target.value));
444
+ }
445
+ }
446
+ ),
447
+ /* @__PURE__ */ jsxs("div", { className: "absolute inset-y-0 right-0 flex flex-col", children: [
448
+ /* @__PURE__ */ jsx(
449
+ "button",
450
+ {
451
+ type: "button",
452
+ "aria-label": "Increment",
453
+ disabled: isDisabled,
454
+ onClick: () => setValue((current ?? 0) + step),
455
+ className: "flex h-1/2 w-8 items-center justify-center text-xs text-muted-foreground hover:text-foreground disabled:opacity-50",
456
+ children: "\u25B2"
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx(
460
+ "button",
461
+ {
462
+ type: "button",
463
+ "aria-label": "Decrement",
464
+ disabled: isDisabled,
465
+ onClick: () => setValue((current ?? 0) - step),
466
+ className: "flex h-1/2 w-8 items-center justify-center text-xs text-muted-foreground hover:text-foreground disabled:opacity-50",
467
+ children: "\u25BC"
468
+ }
469
+ )
470
+ ] })
471
+ ] });
472
+ }
473
+ function BelongsToInput({
474
+ field,
475
+ value,
476
+ onChange,
477
+ errors,
478
+ disabled,
479
+ inputId,
480
+ describedBy
481
+ }) {
482
+ const f = field;
483
+ const hasError = errors !== void 0 && errors.length > 0;
484
+ const isDisabled = disabled || f.disabled || f.readonly;
485
+ const [query, setQuery] = useState("");
486
+ const [results, setResults] = useState([]);
487
+ const [open, setOpen] = useState(false);
488
+ const debounceRef = useRef(null);
489
+ const abortRef = useRef(null);
490
+ useEffect(() => {
491
+ if (!f.props.searchRoute || !query) {
492
+ setResults([]);
493
+ return;
494
+ }
495
+ if (debounceRef.current) clearTimeout(debounceRef.current);
496
+ debounceRef.current = setTimeout(() => {
497
+ abortRef.current?.abort();
498
+ const controller = new AbortController();
499
+ abortRef.current = controller;
500
+ const url = `${f.props.searchRoute}?q=${encodeURIComponent(query)}`;
501
+ fetch(url, { signal: controller.signal, headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : []).then((data) => {
502
+ if (Array.isArray(data)) setResults(data);
503
+ }).catch(() => {
504
+ });
505
+ }, 300);
506
+ return () => {
507
+ if (debounceRef.current) clearTimeout(debounceRef.current);
508
+ };
509
+ }, [query, f.props.searchRoute]);
510
+ const selectedLabel = results.find((r) => String(r.value) === String(value))?.label;
511
+ return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
512
+ /* @__PURE__ */ jsx(
513
+ "input",
514
+ {
515
+ id: inputId,
516
+ type: "text",
517
+ className: inputClasses,
518
+ value: open ? query : selectedLabel ?? (value === null || value === void 0 ? "" : String(value)),
519
+ placeholder: f.placeholder ?? `Search ${f.props.relatedResource}\u2026`,
520
+ disabled: isDisabled,
521
+ readOnly: !(f.readonly === true || f.readonly === void 0),
522
+ "aria-invalid": hasError || void 0,
523
+ "aria-describedby": describedBy,
524
+ "aria-expanded": open,
525
+ "aria-controls": `${inputId}-listbox`,
526
+ role: "combobox",
527
+ onFocus: () => setOpen(true),
528
+ onBlur: () => setTimeout(() => setOpen(false), 100),
529
+ onChange: (e) => setQuery(e.target.value)
530
+ }
531
+ ),
532
+ open && results.length > 0 && /* @__PURE__ */ jsx(
533
+ "div",
534
+ {
535
+ id: `${inputId}-listbox`,
536
+ role: "listbox",
537
+ className: cn(
538
+ "absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-sm border border-border bg-background shadow-md"
539
+ ),
540
+ children: results.map((r) => /* @__PURE__ */ jsx(
541
+ "div",
542
+ {
543
+ role: "option",
544
+ "aria-selected": String(r.value) === String(value),
545
+ tabIndex: -1,
546
+ className: "cursor-pointer px-3 py-1.5 text-sm hover:bg-muted",
547
+ onMouseDown: (e) => {
548
+ e.preventDefault();
549
+ onChange(r.value);
550
+ setQuery("");
551
+ setOpen(false);
552
+ },
553
+ children: r.label
554
+ },
555
+ String(r.value)
556
+ ))
557
+ }
558
+ )
559
+ ] });
560
+ }
561
+ function HasManyReadonly({ field, value, inputId, describedBy }) {
562
+ const f = field;
563
+ const items = Array.isArray(value) ? value : [];
564
+ if (items.length === 0) {
565
+ return /* @__PURE__ */ jsxs("p", { id: inputId, "aria-describedby": describedBy, className: "text-sm text-muted-foreground", children: [
566
+ "No ",
567
+ f.props.relatedResource,
568
+ " linked."
569
+ ] });
570
+ }
571
+ return /* @__PURE__ */ jsx(
572
+ "ul",
573
+ {
574
+ id: inputId,
575
+ "aria-describedby": describedBy,
576
+ className: "divide-y divide-[var(--border)] rounded-sm border border-border",
577
+ children: items.map((item) => /* @__PURE__ */ jsxs(
578
+ "li",
579
+ {
580
+ className: "flex items-center justify-between gap-3 px-3 py-2 text-sm",
581
+ children: [
582
+ /* @__PURE__ */ jsx("span", { children: item.label ?? `#${item.id}` }),
583
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground", children: item.id })
584
+ ]
585
+ },
586
+ String(item.id)
587
+ ))
588
+ }
589
+ );
590
+ }
591
+
592
+ // src/shared/options.ts
593
+ function normaliseOptions(options) {
594
+ if (!options) return [];
595
+ if (Array.isArray(options)) {
596
+ return options.filter(
597
+ (o) => typeof o === "object" && o !== null && "value" in o && "label" in o
598
+ );
599
+ }
600
+ if (typeof options === "object") {
601
+ return Object.entries(options).map(([value, label]) => ({
602
+ value,
603
+ label
604
+ }));
605
+ }
606
+ return [];
607
+ }
608
+ function MultiSelectInput({
609
+ field,
610
+ value,
611
+ onChange,
612
+ errors,
613
+ disabled,
614
+ inputId,
615
+ describedBy
616
+ }) {
617
+ const f = field;
618
+ const hasError = errors !== void 0 && errors.length > 0;
619
+ const options = normaliseOptions(f.props.options);
620
+ const arr = Array.isArray(value) ? value : [];
621
+ const selectedSet = new Set(arr.map(String));
622
+ const isDisabled = disabled || f.disabled || f.readonly;
623
+ const remove = (v) => {
624
+ onChange(arr.filter((x) => String(x) !== String(v)));
625
+ };
626
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
627
+ arr.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: arr.map((v) => {
628
+ const opt = options.find((o) => String(o.value) === String(v));
629
+ return /* @__PURE__ */ jsxs(
630
+ "span",
631
+ {
632
+ className: "inline-flex items-center gap-1 rounded-sm bg-muted px-2 py-0.5 text-xs",
633
+ children: [
634
+ opt?.label ?? String(v),
635
+ !isDisabled && /* @__PURE__ */ jsx(
636
+ "button",
637
+ {
638
+ type: "button",
639
+ "aria-label": `Remove ${opt?.label ?? String(v)}`,
640
+ onClick: () => remove(v),
641
+ className: "text-muted-foreground hover:text-foreground",
642
+ children: "\u2715"
643
+ }
644
+ )
645
+ ]
646
+ },
647
+ String(v)
648
+ );
649
+ }) }),
650
+ /* @__PURE__ */ jsx(
651
+ "select",
652
+ {
653
+ id: inputId,
654
+ multiple: true,
655
+ className: cn(inputClasses, "h-auto min-h-[6rem] py-1"),
656
+ value: Array.from(selectedSet),
657
+ disabled: isDisabled,
658
+ "aria-invalid": hasError || void 0,
659
+ "aria-describedby": describedBy,
660
+ onChange: (e) => {
661
+ const next = Array.from(e.target.selectedOptions, (o) => o.value);
662
+ onChange(next);
663
+ },
664
+ children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
665
+ }
666
+ )
667
+ ] });
668
+ }
669
+ function RadioGroup({
670
+ field,
671
+ value,
672
+ onChange,
673
+ errors,
674
+ disabled,
675
+ inputId,
676
+ describedBy
677
+ }) {
678
+ const f = field;
679
+ const hasError = errors !== void 0 && errors.length > 0;
680
+ const isDisabled = disabled || f.disabled || f.readonly;
681
+ return /* @__PURE__ */ jsx(
682
+ "div",
683
+ {
684
+ id: inputId,
685
+ role: "radiogroup",
686
+ "aria-invalid": hasError || void 0,
687
+ "aria-describedby": describedBy,
688
+ className: cn("flex gap-3", f.props.inline ? "flex-row flex-wrap" : "flex-col"),
689
+ children: (f.props.options ?? []).map((opt) => {
690
+ const checked = String(value ?? "") === String(opt.value);
691
+ return /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2 text-sm", children: [
692
+ /* @__PURE__ */ jsx(
693
+ "input",
694
+ {
695
+ type: "radio",
696
+ name: f.name,
697
+ value: String(opt.value),
698
+ checked,
699
+ disabled: isDisabled,
700
+ onChange: () => onChange(opt.value),
701
+ className: "h-4 w-4 border-[var(--input)] text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
702
+ }
703
+ ),
704
+ opt.label
705
+ ] }, String(opt.value));
706
+ })
707
+ }
708
+ );
709
+ }
710
+ function SelectInput({
711
+ field,
712
+ value,
713
+ onChange,
714
+ errors,
715
+ disabled,
716
+ inputId,
717
+ describedBy
718
+ }) {
719
+ const f = field;
720
+ const hasError = errors !== void 0 && errors.length > 0;
721
+ const options = normaliseOptions(f.props.options);
722
+ return /* @__PURE__ */ jsxs(
723
+ "select",
724
+ {
725
+ id: inputId,
726
+ className: inputClasses,
727
+ value: value === null || value === void 0 ? "" : String(value),
728
+ disabled: disabled || f.disabled || f.readonly,
729
+ required: f.required === true,
730
+ "aria-invalid": hasError || void 0,
731
+ "aria-describedby": describedBy,
732
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
733
+ children: [
734
+ (f.placeholder || !f.required) && /* @__PURE__ */ jsx("option", { value: "", children: f.placeholder ?? "\u2014" }),
735
+ options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
736
+ ]
737
+ }
738
+ );
739
+ }
740
+ function SlugInput({
741
+ field,
742
+ value,
743
+ onChange,
744
+ errors,
745
+ disabled,
746
+ inputId,
747
+ describedBy
748
+ }) {
749
+ const f = field;
750
+ const hasError = errors !== void 0 && errors.length > 0;
751
+ return /* @__PURE__ */ jsx(
752
+ "input",
753
+ {
754
+ id: inputId,
755
+ type: "text",
756
+ className: inputClasses,
757
+ value: typeof value === "string" ? value : "",
758
+ placeholder: f.placeholder ?? "my-resource-slug",
759
+ disabled: disabled || f.disabled || f.readonly,
760
+ readOnly: f.readonly === true,
761
+ required: f.required === true,
762
+ maxLength: f.props.maxLength,
763
+ autoComplete: "off",
764
+ pattern: "^[a-z0-9-]*$",
765
+ "aria-invalid": hasError || void 0,
766
+ "aria-describedby": describedBy,
767
+ onChange: (e) => onChange(slugify(e.target.value))
768
+ }
769
+ );
770
+ }
771
+ function slugify(input) {
772
+ return input.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
773
+ }
774
+ function EmailInput({
775
+ field,
776
+ value,
777
+ onChange,
778
+ errors,
779
+ disabled,
780
+ inputId,
781
+ describedBy
782
+ }) {
783
+ const f = field;
784
+ const hasError = errors !== void 0 && errors.length > 0;
785
+ return /* @__PURE__ */ jsx(
786
+ "input",
787
+ {
788
+ id: inputId,
789
+ type: "email",
790
+ className: inputClasses,
791
+ value: typeof value === "string" ? value : "",
792
+ placeholder: f.placeholder ?? void 0,
793
+ disabled: disabled || f.disabled || f.readonly,
794
+ readOnly: f.readonly === true,
795
+ required: f.required === true,
796
+ autoComplete: f.props.autocomplete ?? "email",
797
+ "aria-invalid": hasError || void 0,
798
+ "aria-describedby": describedBy,
799
+ onChange: (e) => onChange(e.target.value)
800
+ }
801
+ );
802
+ }
803
+ function PasswordInput({
804
+ field,
805
+ value,
806
+ onChange,
807
+ errors,
808
+ disabled,
809
+ inputId,
810
+ describedBy
811
+ }) {
812
+ const f = field;
813
+ const [revealed, setRevealed] = useState(false);
814
+ const hasError = errors !== void 0 && errors.length > 0;
815
+ const isDisabled = disabled || f.disabled || f.readonly;
816
+ return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
817
+ /* @__PURE__ */ jsx(
818
+ "input",
819
+ {
820
+ id: inputId,
821
+ type: revealed ? "text" : "password",
822
+ className: cn(inputClasses, "pr-10"),
823
+ value: typeof value === "string" ? value : "",
824
+ placeholder: f.placeholder ?? void 0,
825
+ disabled: isDisabled,
826
+ readOnly: f.readonly === true,
827
+ required: f.required === true,
828
+ autoComplete: f.props.autocomplete ?? "current-password",
829
+ "aria-invalid": hasError || void 0,
830
+ "aria-describedby": describedBy,
831
+ onChange: (e) => onChange(e.target.value)
832
+ }
833
+ ),
834
+ /* @__PURE__ */ jsx(
835
+ "button",
836
+ {
837
+ type: "button",
838
+ "aria-label": revealed ? "Hide password" : "Show password",
839
+ "aria-pressed": revealed,
840
+ disabled: isDisabled,
841
+ onClick: () => setRevealed((v) => !v),
842
+ className: cn(
843
+ "absolute inset-y-0 right-0 flex w-9 items-center justify-center text-sm",
844
+ "text-muted-foreground hover:text-foreground",
845
+ "disabled:cursor-not-allowed disabled:opacity-50"
846
+ ),
847
+ children: revealed ? "\u25C9" : "\u25CB"
848
+ }
849
+ )
850
+ ] });
851
+ }
852
+ function TextareaInput({
853
+ field,
854
+ value,
855
+ onChange,
856
+ errors,
857
+ disabled,
858
+ inputId,
859
+ describedBy
860
+ }) {
861
+ const f = field;
862
+ const hasError = errors !== void 0 && errors.length > 0;
863
+ return /* @__PURE__ */ jsx(
864
+ "textarea",
865
+ {
866
+ id: inputId,
867
+ className: cn(inputClasses, "h-auto min-h-[5rem] py-2"),
868
+ value: typeof value === "string" ? value : "",
869
+ placeholder: f.placeholder ?? void 0,
870
+ disabled: disabled || f.disabled || f.readonly,
871
+ readOnly: f.readonly === true,
872
+ required: f.required === true,
873
+ rows: 4,
874
+ "aria-invalid": hasError || void 0,
875
+ "aria-describedby": describedBy,
876
+ onChange: (e) => onChange(e.target.value)
877
+ }
878
+ );
879
+ }
880
+ function TextInput({
881
+ field,
882
+ value,
883
+ onChange,
884
+ errors,
885
+ disabled,
886
+ inputId,
887
+ describedBy
888
+ }) {
889
+ const f = field;
890
+ const hasError = errors !== void 0 && errors.length > 0;
891
+ return /* @__PURE__ */ jsx(
892
+ "input",
893
+ {
894
+ id: inputId,
895
+ type: "text",
896
+ className: inputClasses,
897
+ value: typeof value === "string" ? value : "",
898
+ placeholder: f.placeholder ?? void 0,
899
+ disabled: disabled || f.disabled || f.readonly,
900
+ readOnly: f.readonly === true,
901
+ required: f.required === true,
902
+ maxLength: f.props.maxLength,
903
+ minLength: f.props.minLength,
904
+ pattern: f.props.pattern,
905
+ autoComplete: f.props.autocomplete,
906
+ "aria-invalid": hasError || void 0,
907
+ "aria-describedby": describedBy,
908
+ onChange: (e) => onChange(e.target.value)
909
+ }
910
+ );
911
+ }
912
+ function UrlInput({
913
+ field,
914
+ value,
915
+ onChange,
916
+ errors,
917
+ disabled,
918
+ inputId,
919
+ describedBy
920
+ }) {
921
+ const f = field;
922
+ const hasError = errors !== void 0 && errors.length > 0;
923
+ return /* @__PURE__ */ jsx(
924
+ "input",
925
+ {
926
+ id: inputId,
927
+ type: "url",
928
+ inputMode: "url",
929
+ className: inputClasses,
930
+ value: typeof value === "string" ? value : "",
931
+ placeholder: f.placeholder ?? "https://\u2026",
932
+ disabled: disabled || f.disabled || f.readonly,
933
+ readOnly: f.readonly === true,
934
+ required: f.required === true,
935
+ autoComplete: f.props.autocomplete ?? "url",
936
+ "aria-invalid": hasError || void 0,
937
+ "aria-describedby": describedBy,
938
+ onChange: (e) => onChange(e.target.value)
939
+ }
940
+ );
941
+ }
942
+
943
+ // src/register.ts
944
+ registerField("TextInput", TextInput);
945
+ registerField("TextareaInput", TextareaInput);
946
+ registerField("EmailInput", EmailInput);
947
+ registerField("UrlInput", UrlInput);
948
+ registerField("PasswordInput", PasswordInput);
949
+ registerField("NumberInput", NumberInput);
950
+ registerField("CurrencyInput", CurrencyInput);
951
+ registerField("Checkbox", Checkbox);
952
+ registerField("Toggle", Toggle);
953
+ registerField("SelectInput", SelectInput);
954
+ registerField("MultiSelectInput", MultiSelectInput);
955
+ registerField("RadioGroup", RadioGroup);
956
+ registerField("BelongsToInput", BelongsToInput);
957
+ registerField("HasManyReadonly", HasManyReadonly);
958
+ registerField("DateInput", DateInput);
959
+ registerField("DateTimeInput", DateTimeInput);
960
+ registerField("FileInput", FileInput);
961
+ registerField("ImageInput", ImageInput);
962
+ registerField("SlugInput", SlugInput);
963
+ registerField("ColorInput", ColorInput);
964
+ registerField("HiddenInput", HiddenInput);
965
+ //# sourceMappingURL=register.js.map
966
+ //# sourceMappingURL=register.js.map