@pipelex/mthds-form 0.3.0 → 0.5.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.
Files changed (81) hide show
  1. package/README.md +21 -7
  2. package/dist/chunk-25I6JDIK.js +28 -0
  3. package/dist/chunk-25I6JDIK.js.map +1 -0
  4. package/dist/chunk-5Q2W6HBA.js +201 -0
  5. package/dist/chunk-5Q2W6HBA.js.map +1 -0
  6. package/dist/chunk-GCXV76WU.js +213 -0
  7. package/dist/chunk-GCXV76WU.js.map +1 -0
  8. package/dist/chunk-GKV77RAS.js +46 -0
  9. package/dist/chunk-GKV77RAS.js.map +1 -0
  10. package/dist/chunk-IIJRJGZQ.js +128 -0
  11. package/dist/chunk-IIJRJGZQ.js.map +1 -0
  12. package/dist/chunk-IM2AHWBR.js +99 -0
  13. package/dist/chunk-IM2AHWBR.js.map +1 -0
  14. package/dist/chunk-LBFZXYCY.js +44 -0
  15. package/dist/chunk-LBFZXYCY.js.map +1 -0
  16. package/dist/chunk-OD3VYXUY.js +156 -0
  17. package/dist/chunk-OD3VYXUY.js.map +1 -0
  18. package/dist/chunk-PM55AHRO.js +52 -0
  19. package/dist/chunk-PM55AHRO.js.map +1 -0
  20. package/dist/chunk-RPA7DNSK.js +41 -0
  21. package/dist/chunk-RPA7DNSK.js.map +1 -0
  22. package/dist/chunk-V3OV2XQT.js +12 -0
  23. package/dist/chunk-V3OV2XQT.js.map +1 -0
  24. package/dist/chunk-VTPUDBSJ.js +61 -0
  25. package/dist/chunk-VTPUDBSJ.js.map +1 -0
  26. package/dist/chunk-XYQ7NXQZ.js +31 -0
  27. package/dist/chunk-XYQ7NXQZ.js.map +1 -0
  28. package/dist/chunk-YEVVR6FE.js +66 -0
  29. package/dist/chunk-YEVVR6FE.js.map +1 -0
  30. package/dist/core/contracts.d.ts +114 -0
  31. package/dist/core/contracts.js +4 -0
  32. package/dist/core/contracts.js.map +1 -0
  33. package/dist/core/date-format.d.ts +87 -0
  34. package/dist/core/date-format.js +3 -0
  35. package/dist/core/date-format.js.map +1 -0
  36. package/dist/core/derive.d.ts +55 -0
  37. package/dist/core/derive.js +6 -0
  38. package/dist/core/derive.js.map +1 -0
  39. package/dist/core/descriptor.d.ts +172 -0
  40. package/dist/core/descriptor.js +3 -0
  41. package/dist/core/descriptor.js.map +1 -0
  42. package/dist/core/gate-validator.d.ts +27 -0
  43. package/dist/core/gate-validator.js +4 -0
  44. package/dist/core/gate-validator.js.map +1 -0
  45. package/dist/core/gate.d.ts +144 -0
  46. package/dist/core/gate.js +12 -0
  47. package/dist/core/gate.js.map +1 -0
  48. package/dist/core/gating-fields.d.ts +38 -0
  49. package/dist/core/gating-fields.js +6 -0
  50. package/dist/core/gating-fields.js.map +1 -0
  51. package/dist/core/index.d.ts +13 -623
  52. package/dist/core/index.js +14 -1
  53. package/dist/core/normalize-schema.d.ts +30 -0
  54. package/dist/core/normalize-schema.js +4 -0
  55. package/dist/core/normalize-schema.js.map +1 -0
  56. package/dist/core/own-property.d.ts +46 -0
  57. package/dist/core/own-property.js +3 -0
  58. package/dist/core/own-property.js.map +1 -0
  59. package/dist/core/readiness.d.ts +91 -0
  60. package/dist/core/readiness.js +4 -0
  61. package/dist/core/readiness.js.map +1 -0
  62. package/dist/core/schema-utils.d.ts +86 -0
  63. package/dist/core/schema-utils.js +3 -0
  64. package/dist/core/schema-utils.js.map +1 -0
  65. package/dist/core/validation-message.d.ts +39 -0
  66. package/dist/core/validation-message.js +4 -0
  67. package/dist/core/validation-message.js.map +1 -0
  68. package/dist/core/values.d.ts +98 -0
  69. package/dist/core/values.js +10 -0
  70. package/dist/core/values.js.map +1 -0
  71. package/dist/core/wire-format.d.ts +95 -0
  72. package/dist/core/wire-format.js +6 -0
  73. package/dist/core/wire-format.js.map +1 -0
  74. package/dist/react/index.d.ts +43 -2
  75. package/dist/react/index.js +166 -64
  76. package/dist/react/index.js.map +1 -1
  77. package/dist/styles.css +1 -1
  78. package/package.json +7 -1
  79. package/dist/chunk-S6KOTTGD.js +0 -990
  80. package/dist/chunk-S6KOTTGD.js.map +0 -1
  81. package/dist/descriptor-DPrrfAjL.d.ts +0 -101
@@ -1,11 +1,13 @@
1
1
  "use client";
2
- import { toStoredDateValue, toDateInputValue, isFilled } from '../chunk-S6KOTTGD.js';
2
+ import { isFilled } from '../chunk-PM55AHRO.js';
3
+ import { toStoredDateValue, toDateInputValue } from '../chunk-GKV77RAS.js';
4
+ import { ownProp } from '../chunk-V3OV2XQT.js';
3
5
  import { clsx } from 'clsx';
4
6
  import { twMerge } from 'tailwind-merge';
5
7
  import { ChevronDown, ChevronUp, Check, List, Braces, ListChecks, ToggleLeft, Hash, Image, FileText, CalendarDays, Type, Loader2, Upload, Link2, ImageOff, EyeOff, Eye, X, Minus, Plus, Trash2 } from 'lucide-react';
6
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
9
  import * as React2 from 'react';
8
- import { createContext, use, useMemo, useContext, useState, useRef, useCallback, useEffect } from 'react';
10
+ import { createContext, use, useId, useMemo, useContext, useState, useRef, useCallback, useEffect } from 'react';
9
11
  import * as SwitchPrimitives from '@radix-ui/react-switch';
10
12
  import { useDropzone } from 'react-dropzone';
11
13
  import * as SelectPrimitive from '@radix-ui/react-select';
@@ -83,9 +85,24 @@ var Switch = React2.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
83
85
  }
84
86
  ));
85
87
  Switch.displayName = SwitchPrimitives.Root.displayName;
88
+ var FieldDomIdContext = createContext(null);
89
+ function FieldDomIdProvider({
90
+ prefix,
91
+ children
92
+ }) {
93
+ const generated = useId();
94
+ return /* @__PURE__ */ jsx(FieldDomIdContext, { value: prefix ?? generated, children });
95
+ }
96
+ function useFieldDomId(path) {
97
+ const scoped = use(FieldDomIdContext);
98
+ const local = useId();
99
+ const prefix = scoped ?? local;
100
+ return prefix === "" ? path : `${prefix}-${path}`;
101
+ }
86
102
  function BooleanField({ field, value, onChange, id, disabled }) {
87
103
  const presentation = useFieldPresentation();
88
104
  const isApp = presentation === "app";
105
+ const domId = useFieldDomId(id);
89
106
  return /* @__PURE__ */ jsxs(
90
107
  "div",
91
108
  {
@@ -99,7 +116,7 @@ function BooleanField({ field, value, onChange, id, disabled }) {
99
116
  /* @__PURE__ */ jsx(
100
117
  "label",
101
118
  {
102
- htmlFor: id,
119
+ htmlFor: domId,
103
120
  className: cn(
104
121
  "text-[13px] font-medium leading-none text-foreground",
105
122
  isApp ? "text-[13.5px]" : "font-mono"
@@ -114,7 +131,7 @@ function BooleanField({ field, value, onChange, id, disabled }) {
114
131
  /* @__PURE__ */ jsx(
115
132
  Switch,
116
133
  {
117
- id,
134
+ id: domId,
118
135
  checked: value ?? false,
119
136
  onCheckedChange: onChange,
120
137
  disabled,
@@ -137,6 +154,7 @@ var DEFAULT_FIELD_STRINGS = {
137
154
  addItem: "Add item",
138
155
  removeItemAria: (index) => `Remove item ${index}`,
139
156
  itemsCount: (count) => count === 1 ? "1 item" : `${count} items`,
157
+ itemsCountOf: (count, total) => `${count} of ${total} ${total === 1 ? "item" : "items"}`,
140
158
  uploading: "Uploading\u2026",
141
159
  dropToUpload: "Drop to upload",
142
160
  dropOrBrowse: "Drop a file or click to browse",
@@ -211,6 +229,7 @@ var fieldControlClass = cn(
211
229
  "aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/30"
212
230
  );
213
231
  function DateField({ field, value, onChange, id, error, disabled }) {
232
+ const domId = useFieldDomId(id);
214
233
  return /* @__PURE__ */ jsx(
215
234
  FieldShell,
216
235
  {
@@ -221,11 +240,11 @@ function DateField({ field, value, onChange, id, error, disabled }) {
221
240
  description: field.description,
222
241
  required: field.required,
223
242
  error,
224
- htmlFor: id,
243
+ htmlFor: domId,
225
244
  children: /* @__PURE__ */ jsx(
226
245
  "input",
227
246
  {
228
- id,
247
+ id: domId,
229
248
  type: "date",
230
249
  value: toDateInputValue(value),
231
250
  disabled,
@@ -239,6 +258,13 @@ function DateField({ field, value, onChange, id, error, disabled }) {
239
258
  }
240
259
  var IMAGE_EXT_RE = /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)(\?|$)/i;
241
260
  var PDF_EXT_RE = /\.pdf(\?|$)/i;
261
+ var DATA_URL_MIME_RE = /^data:([^;,]+)/i;
262
+ function isDirectlyViewable(url) {
263
+ return !!url && (/^https?:/i.test(url) || /^data:/i.test(url) || /^blob:/i.test(url));
264
+ }
265
+ function dataUrlMime(url) {
266
+ return DATA_URL_MIME_RE.exec(url)?.[1]?.toLowerCase();
267
+ }
242
268
  function DocumentField(props) {
243
269
  return /* @__PURE__ */ jsx(FileField, { ...props, category: "document" });
244
270
  }
@@ -258,27 +284,35 @@ function FileField({
258
284
  category
259
285
  }) {
260
286
  const s = useFieldStrings();
287
+ const domId = useFieldDomId(id);
261
288
  const [showUrl, setShowUrl] = useState(false);
262
289
  const [previewOpen, setPreviewOpen] = useState(false);
263
290
  const [localPreview, setLocalPreview] = useState(null);
291
+ const [resolved, setResolved] = useState(null);
264
292
  const localUrlRef = useRef(null);
265
293
  const setLocal = useCallback((next) => {
266
294
  if (localUrlRef.current) URL.revokeObjectURL(localUrlRef.current);
267
- localUrlRef.current = next?.url ?? null;
295
+ localUrlRef.current = next?.objectUrl ?? null;
268
296
  setLocalPreview(next);
269
297
  }, []);
270
298
  useEffect(() => () => setLocal(null), [setLocal]);
271
299
  const handleFile = useCallback(
272
300
  (file) => {
273
- setLocal({ url: URL.createObjectURL(file), type: file.type });
301
+ setLocal({ objectUrl: URL.createObjectURL(file), type: file.type });
274
302
  onDropFile(file);
275
303
  },
276
304
  [setLocal, onDropFile]
277
305
  );
306
+ const busy = disabled || uploading;
278
307
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
279
308
  maxFiles: 1,
280
309
  multiple: false,
281
- disabled: disabled || uploading,
310
+ disabled: busy,
311
+ // The tab stop belongs on the INPUT, not on this div - see the root element
312
+ // below. Without this, react-dropzone puts `tabIndex: 0` and its own key
313
+ // handlers on a `role="presentation"` div, and the element a keyboard or
314
+ // voice-control user lands on is a generic with no role and no name.
315
+ noKeyboard: true,
282
316
  onDrop: (files) => {
283
317
  const file = files[0];
284
318
  if (file) handleFile(file);
@@ -286,28 +320,43 @@ function FileField({
286
320
  });
287
321
  const clear = useCallback(() => {
288
322
  setLocal(null);
289
- setResolvedSrc(null);
323
+ setResolved(null);
290
324
  setPreviewOpen(false);
291
325
  onChange(void 0);
292
326
  }, [setLocal, onChange]);
327
+ const localIsCurrent = !!localPreview && (localPreview.boundUrl === void 0 || localPreview.boundUrl === value?.url);
328
+ useEffect(() => {
329
+ if (!localPreview) return;
330
+ if (localPreview.boundUrl === void 0) {
331
+ if (uploading || !value?.url) return;
332
+ setLocalPreview({ ...localPreview, boundUrl: value.url });
333
+ return;
334
+ }
335
+ if (value?.url !== localPreview.boundUrl) setLocal(null);
336
+ }, [localPreview, value?.url, uploading, setLocal]);
293
337
  const hasFile = !!value?.url;
294
- const ref = `${value?.filename ?? ""} ${value?.url ?? ""}`;
295
- const isImage = category === "image" || localPreview?.type.startsWith("image/") || IMAGE_EXT_RE.test(ref);
296
- const isPdf = localPreview?.type === "application/pdf" || PDF_EXT_RE.test(ref);
338
+ const localType = localIsCurrent ? localPreview?.type : void 0;
339
+ const filename = value?.filename ?? "";
340
+ const url = value?.url ?? "";
341
+ const urlMime = dataUrlMime(url);
342
+ const isImage = category === "image" || localType?.startsWith("image/") === true || urlMime?.startsWith("image/") === true || IMAGE_EXT_RE.test(filename) || IMAGE_EXT_RE.test(url);
343
+ const isPdf = localType === "application/pdf" || urlMime === "application/pdf" || PDF_EXT_RE.test(filename) || PDF_EXT_RE.test(url);
297
344
  const canPreview = isImage || isPdf;
298
- const [resolvedSrc, setResolvedSrc] = useState(null);
299
- const storageUri = value?.url && !value.url.startsWith("http") ? value.url : null;
345
+ const storageUri = value?.url && !isDirectlyViewable(value.url) ? value.url : null;
300
346
  useEffect(() => {
301
- if (!previewOpen || !storageUri || localPreview || !resolveUrl) return;
347
+ if (!previewOpen || !storageUri || localIsCurrent || !resolveUrl) return;
302
348
  let cancelled = false;
303
- void resolveUrl(storageUri).then((url) => {
304
- if (!cancelled) setResolvedSrc(url);
349
+ void resolveUrl(storageUri).then((src) => {
350
+ if (!cancelled) setResolved(src ? { uri: storageUri, src } : null);
351
+ }).catch(() => {
352
+ if (!cancelled) setResolved(null);
305
353
  });
306
354
  return () => {
307
355
  cancelled = true;
308
356
  };
309
- }, [previewOpen, storageUri, localPreview, resolveUrl]);
310
- const previewSrc = localPreview?.url ?? (value?.url?.startsWith("http") ? value.url : resolvedSrc ?? void 0);
357
+ }, [previewOpen, storageUri, localIsCurrent, resolveUrl]);
358
+ const resolvedSrc = resolved && resolved.uri === storageUri ? resolved.src : void 0;
359
+ const previewSrc = (localIsCurrent ? localPreview?.objectUrl : void 0) ?? (isDirectlyViewable(value?.url) ? value.url : resolvedSrc);
311
360
  return /* @__PURE__ */ jsxs(
312
361
  FieldShell,
313
362
  {
@@ -318,6 +367,7 @@ function FileField({
318
367
  description: field.description,
319
368
  required: field.required,
320
369
  error,
370
+ htmlFor: domId,
321
371
  children: [
322
372
  hasFile && !uploading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
323
373
  /* @__PURE__ */ jsx(
@@ -332,36 +382,53 @@ function FileField({
332
382
  }
333
383
  ),
334
384
  previewOpen && (previewSrc && isImage ? /* @__PURE__ */ jsx(ImagePreview, { src: previewSrc, filename: value?.filename }) : previewSrc && isPdf ? /* @__PURE__ */ jsx(PdfPreview, { src: previewSrc }) : /* @__PURE__ */ jsx("div", { className: "flex h-24 items-center justify-center rounded-md border border-border bg-muted", children: /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }) }))
335
- ] }) : /* @__PURE__ */ jsxs(
336
- "div",
337
- {
338
- ...getRootProps(),
339
- className: cn(
340
- "flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center",
341
- "cursor-pointer select-none transition-colors",
342
- isDragActive ? "border-primary/60 bg-primary/5" : "border-border bg-input hover:border-border hover:bg-muted",
343
- (disabled || uploading) && "cursor-not-allowed opacity-60"
344
- ),
345
- children: [
346
- /* @__PURE__ */ jsx("input", { ...getInputProps(), id, "aria-invalid": !!error }),
347
- uploading ? /* @__PURE__ */ jsxs(Fragment, { children: [
348
- /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }),
349
- /* @__PURE__ */ jsx("p", { className: "text-[12px] text-muted-foreground", children: s.uploading })
350
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
351
- /* @__PURE__ */ jsx(Upload, { className: "h-5 w-5 text-muted-foreground", strokeWidth: 1.75 }),
352
- /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
353
- /* @__PURE__ */ jsx("p", { className: "text-[13px] text-foreground", children: isDragActive ? s.dropToUpload : s.dropOrBrowse }),
354
- field.accept && /* @__PURE__ */ jsx("p", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.accept })
385
+ ] }) : (
386
+ // `role="presentation"` and no tab stop: this div is the drop TARGET and
387
+ // the visual affordance, and the control inside it is a real
388
+ // `<input type="file">`, which already has the right role and now has a
389
+ // name. Giving the div a `role="button"` of its own would put a second
390
+ // named control in the accessibility tree for one value. The focus ring
391
+ // is `focus-within` because the element that takes focus is clip-hidden.
392
+ /* @__PURE__ */ jsxs(
393
+ "div",
394
+ {
395
+ ...getRootProps(),
396
+ className: cn(
397
+ "flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center",
398
+ "cursor-pointer select-none transition-colors",
399
+ "focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
400
+ isDragActive ? "border-primary/60 bg-primary/5" : "border-border bg-input hover:border-border hover:bg-muted",
401
+ busy && "cursor-not-allowed opacity-60"
402
+ ),
403
+ children: [
404
+ /* @__PURE__ */ jsx(
405
+ "input",
406
+ {
407
+ ...getInputProps({ tabIndex: 0 }),
408
+ id: domId,
409
+ disabled: busy,
410
+ "aria-invalid": !!error
411
+ }
412
+ ),
413
+ uploading ? /* @__PURE__ */ jsxs(Fragment, { children: [
414
+ /* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }),
415
+ /* @__PURE__ */ jsx("p", { className: "text-[12px] text-muted-foreground", children: s.uploading })
416
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
417
+ /* @__PURE__ */ jsx(Upload, { className: "h-5 w-5 text-muted-foreground", strokeWidth: 1.75 }),
418
+ /* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
419
+ /* @__PURE__ */ jsx("p", { className: "text-[13px] text-foreground", children: isDragActive ? s.dropToUpload : s.dropOrBrowse }),
420
+ field.accept && /* @__PURE__ */ jsx("p", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.accept })
421
+ ] })
355
422
  ] })
356
- ] })
357
- ]
358
- }
423
+ ]
424
+ }
425
+ )
359
426
  ),
360
427
  !showUrl ? /* @__PURE__ */ jsxs(
361
428
  "button",
362
429
  {
363
430
  type: "button",
364
- disabled,
431
+ disabled: busy,
365
432
  onClick: () => setShowUrl(true),
366
433
  className: "inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
367
434
  children: [
@@ -375,7 +442,7 @@ function FileField({
375
442
  type: "text",
376
443
  autoFocus: true,
377
444
  value: value?.url ?? "",
378
- disabled,
445
+ disabled: busy,
379
446
  placeholder: s.urlPlaceholder,
380
447
  onChange: (e) => {
381
448
  setLocal(null);
@@ -632,6 +699,7 @@ var CLEAR_VALUE = "__none__";
632
699
  function EnumField({ field, value, onChange, id, error, disabled }) {
633
700
  const s = useFieldStrings();
634
701
  const isSegmented = field.options.length <= 4 && field.options.every((o) => o.length <= 16);
702
+ const domId = useFieldDomId(id);
635
703
  return /* @__PURE__ */ jsx(
636
704
  FieldShell,
637
705
  {
@@ -642,7 +710,7 @@ function EnumField({ field, value, onChange, id, error, disabled }) {
642
710
  description: field.description,
643
711
  required: field.required,
644
712
  error,
645
- htmlFor: isSegmented ? void 0 : id,
713
+ htmlFor: isSegmented ? void 0 : domId,
646
714
  children: isSegmented ? (
647
715
  // `type="single"` gives the roving tabindex and arrow-key navigation a
648
716
  // radiogroup owes its user, and emits `''` when the active item is
@@ -678,7 +746,7 @@ function EnumField({ field, value, onChange, id, error, disabled }) {
678
746
  disabled,
679
747
  onValueChange: (next) => onChange(next === CLEAR_VALUE ? void 0 : next),
680
748
  children: [
681
- /* @__PURE__ */ jsx(SelectTrigger, { id, "aria-invalid": !!error, className: "h-11", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: s.selectPlaceholder }) }),
749
+ /* @__PURE__ */ jsx(SelectTrigger, { id: domId, "aria-invalid": !!error, className: "h-11", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: s.selectPlaceholder }) }),
682
750
  /* @__PURE__ */ jsxs(SelectContent, { children: [
683
751
  /* @__PURE__ */ jsx(SelectItem, { value: CLEAR_VALUE, children: s.selectPlaceholder }),
684
752
  field.options.map((option) => /* @__PURE__ */ jsx(SelectItem, { value: option, children: option }, option))
@@ -691,6 +759,7 @@ function EnumField({ field, value, onChange, id, error, disabled }) {
691
759
  }
692
760
  function NumberField({ field, value, onChange, id, error, disabled }) {
693
761
  const s = useFieldStrings();
762
+ const domId = useFieldDomId(id);
694
763
  const step = field.integer ? 1 : 0.1;
695
764
  const clamp = (n) => {
696
765
  let next = n;
@@ -710,12 +779,12 @@ function NumberField({ field, value, onChange, id, error, disabled }) {
710
779
  description: field.description ?? rangeHint,
711
780
  required: field.required,
712
781
  error,
713
- htmlFor: id,
782
+ htmlFor: domId,
714
783
  children: /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
715
784
  /* @__PURE__ */ jsx(
716
785
  "input",
717
786
  {
718
- id,
787
+ id: domId,
719
788
  type: "number",
720
789
  inputMode: field.integer ? "numeric" : "decimal",
721
790
  value: value ?? "",
@@ -761,6 +830,7 @@ function StepButton({
761
830
  }
762
831
  function TextField({ field, value, onChange, id, error, disabled }) {
763
832
  const s = useFieldStrings();
833
+ const domId = useFieldDomId(id);
764
834
  return /* @__PURE__ */ jsx(
765
835
  FieldShell,
766
836
  {
@@ -771,11 +841,11 @@ function TextField({ field, value, onChange, id, error, disabled }) {
771
841
  description: field.description,
772
842
  required: field.required,
773
843
  error,
774
- htmlFor: id,
844
+ htmlFor: domId,
775
845
  children: /* @__PURE__ */ jsx(
776
846
  "input",
777
847
  {
778
- id,
848
+ id: domId,
779
849
  type: "text",
780
850
  value: value ?? "",
781
851
  disabled,
@@ -790,6 +860,7 @@ function TextField({ field, value, onChange, id, error, disabled }) {
790
860
  }
791
861
  function ProseField({ field, value, onChange, id, error, disabled }) {
792
862
  const s = useFieldStrings();
863
+ const domId = useFieldDomId(id);
793
864
  return /* @__PURE__ */ jsx(
794
865
  FieldShell,
795
866
  {
@@ -800,11 +871,11 @@ function ProseField({ field, value, onChange, id, error, disabled }) {
800
871
  description: field.description,
801
872
  required: field.required,
802
873
  error,
803
- htmlFor: id,
874
+ htmlFor: domId,
804
875
  children: /* @__PURE__ */ jsx(
805
876
  "textarea",
806
877
  {
807
- id,
878
+ id: domId,
808
879
  value: value ?? "",
809
880
  disabled,
810
881
  "aria-invalid": !!error,
@@ -842,8 +913,8 @@ function ObjectField({ field, value, onChange, id, error, env }) {
842
913
  const isApp = presentation === "app";
843
914
  const data = value ?? {};
844
915
  const setChild = (name, childValue) => onChange({ ...data, [name]: childValue });
845
- const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(data[f.name]));
846
- const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || isFilled(data[f.name]));
916
+ const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(ownProp(data, f.name)));
917
+ const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || isFilled(ownProp(data, f.name)));
847
918
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
848
919
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
849
920
  /* @__PURE__ */ jsx(
@@ -872,7 +943,7 @@ function ObjectField({ field, value, onChange, id, error, env }) {
872
943
  FieldRenderer,
873
944
  {
874
945
  field: child,
875
- value: data[child.name],
946
+ value: ownProp(data, child.name),
876
947
  onChange: (v) => setChild(child.name, v),
877
948
  id: `${id}.${child.name}`,
878
949
  env
@@ -899,13 +970,19 @@ function ListField({ field, value, onChange, id, error, env }) {
899
970
  const presentation = useFieldPresentation();
900
971
  const isApp = presentation === "app";
901
972
  const items = value ?? [];
973
+ const [rowKeys, setRowKeys] = useRowKeys(items.length);
902
974
  const setItem = (index, itemValue) => {
903
975
  const next = items.slice();
904
976
  next[index] = itemValue;
905
977
  onChange(next);
906
978
  };
907
- const removeItem = (index) => onChange(items.filter((_, i) => i !== index));
979
+ const removeItem = (index) => {
980
+ setRowKeys((prev) => prev.filter((_, i) => i !== index));
981
+ onChange(items.filter((_, i) => i !== index));
982
+ };
908
983
  const addItem = () => onChange([...items, emptyValue(field.item)]);
984
+ const busy = listIsBusy(id, env?.uploadingIds);
985
+ const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
909
986
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
910
987
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
911
988
  /* @__PURE__ */ jsx(
@@ -919,7 +996,7 @@ function ListField({ field, value, onChange, id, error, env }) {
919
996
  }
920
997
  ),
921
998
  !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "list" }),
922
- /* @__PURE__ */ jsx("span", { className: "font-mono text-[10.5px] text-muted-foreground", children: s.itemsCount(items.length) }),
999
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.itemCount !== void 0 && field.maxItemCount === field.itemCount ? s.itemsCountOf(items.length, field.itemCount) : s.itemsCount(items.length) }),
923
1000
  !field.required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground/70", children: s.optionalBadge })
924
1001
  ] }),
925
1002
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
@@ -946,7 +1023,7 @@ function ListField({ field, value, onChange, id, error, env }) {
946
1023
  {
947
1024
  type: "button",
948
1025
  onClick: () => removeItem(index),
949
- disabled: env?.disabled,
1026
+ disabled: env?.disabled || busy,
950
1027
  "aria-label": s.removeItemAria(index + 1),
951
1028
  className: "mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-destructive disabled:opacity-50",
952
1029
  children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
@@ -954,14 +1031,14 @@ function ListField({ field, value, onChange, id, error, env }) {
954
1031
  )
955
1032
  ]
956
1033
  },
957
- index
1034
+ rowKeys[index] ?? index
958
1035
  )),
959
1036
  /* @__PURE__ */ jsxs(
960
1037
  "button",
961
1038
  {
962
1039
  type: "button",
963
1040
  onClick: addItem,
964
- disabled: env?.disabled,
1041
+ disabled: env?.disabled || isFull,
965
1042
  className: cn(
966
1043
  "inline-flex h-9 w-full items-center justify-center gap-1.5 rounded-md border border-dashed border-border text-[13px] text-muted-foreground transition-colors",
967
1044
  "hover:border-primary/40 hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
@@ -976,6 +1053,30 @@ function ListField({ field, value, onChange, id, error, env }) {
976
1053
  error && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive", role: "alert", children: error })
977
1054
  ] });
978
1055
  }
1056
+ function listIsBusy(id, uploadingIds) {
1057
+ if (!uploadingIds?.size) return false;
1058
+ const prefix = `${id}.`;
1059
+ for (const uploading of uploadingIds) if (uploading.startsWith(prefix)) return true;
1060
+ return false;
1061
+ }
1062
+ function useRowKeys(length) {
1063
+ const [state, setState] = useState(() => ({
1064
+ keys: Array.from({ length }, (_, i) => i),
1065
+ next: length
1066
+ }));
1067
+ if (state.keys.length !== length) {
1068
+ setState((prev) => {
1069
+ if (length <= prev.keys.length) return { keys: prev.keys.slice(0, length), next: prev.next };
1070
+ const added = length - prev.keys.length;
1071
+ return {
1072
+ keys: [...prev.keys, ...Array.from({ length: added }, (_, i) => prev.next + i)],
1073
+ next: prev.next + added
1074
+ };
1075
+ });
1076
+ }
1077
+ const update = (fn) => setState((prev) => ({ keys: fn(prev.keys), next: prev.next }));
1078
+ return [state.keys, update];
1079
+ }
979
1080
  function emptyValue(field) {
980
1081
  switch (field.kind) {
981
1082
  case "boolean":
@@ -993,6 +1094,7 @@ function emptyValue(field) {
993
1094
  }
994
1095
  function UnknownField({ field, value, onChange, id, error, disabled }) {
995
1096
  const s = useFieldStrings();
1097
+ const domId = useFieldDomId(id);
996
1098
  return /* @__PURE__ */ jsx(
997
1099
  FieldShell,
998
1100
  {
@@ -1003,11 +1105,11 @@ function UnknownField({ field, value, onChange, id, error, disabled }) {
1003
1105
  description: field.description ?? s.jsonHint,
1004
1106
  required: field.required,
1005
1107
  error,
1006
- htmlFor: id,
1108
+ htmlFor: domId,
1007
1109
  children: /* @__PURE__ */ jsx(
1008
1110
  "textarea",
1009
1111
  {
1010
- id,
1112
+ id: domId,
1011
1113
  value: value ?? "",
1012
1114
  disabled,
1013
1115
  "aria-invalid": !!error,
@@ -1164,6 +1266,6 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
1164
1266
  }
1165
1267
  }
1166
1268
 
1167
- export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EnumField, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, ImageField, ListField, NumberField, ObjectField, OptionalToggle, ProseField, TextField, UnknownField, fieldControlClass, humanizeFieldName, useFieldPresentation, useFieldStrings };
1269
+ export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EnumField, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, ImageField, ListField, NumberField, ObjectField, OptionalToggle, ProseField, TextField, UnknownField, fieldControlClass, humanizeFieldName, useFieldDomId, useFieldPresentation, useFieldStrings };
1168
1270
  //# sourceMappingURL=index.js.map
1169
1271
  //# sourceMappingURL=index.js.map