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