@pipelex/mthds-form 0.3.0 → 0.4.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/README.md +7 -2
- package/dist/chunk-6XD6UZSU.js +28 -0
- package/dist/chunk-6XD6UZSU.js.map +1 -0
- package/dist/chunk-7BMOPXDY.js +205 -0
- package/dist/chunk-7BMOPXDY.js.map +1 -0
- package/dist/chunk-GKV77RAS.js +46 -0
- package/dist/chunk-GKV77RAS.js.map +1 -0
- package/dist/chunk-HARYEWNG.js +31 -0
- package/dist/chunk-HARYEWNG.js.map +1 -0
- package/dist/chunk-KFZ5MZOM.js +44 -0
- package/dist/chunk-KFZ5MZOM.js.map +1 -0
- package/dist/chunk-NMT7BRLO.js +40 -0
- package/dist/chunk-NMT7BRLO.js.map +1 -0
- package/dist/chunk-O4O3QNVC.js +146 -0
- package/dist/chunk-O4O3QNVC.js.map +1 -0
- package/dist/chunk-PM55AHRO.js +52 -0
- package/dist/chunk-PM55AHRO.js.map +1 -0
- package/dist/chunk-RPA7DNSK.js +41 -0
- package/dist/chunk-RPA7DNSK.js.map +1 -0
- package/dist/chunk-V3OV2XQT.js +12 -0
- package/dist/chunk-V3OV2XQT.js.map +1 -0
- package/dist/chunk-VMLXO5JS.js +132 -0
- package/dist/chunk-VMLXO5JS.js.map +1 -0
- package/dist/chunk-VTPUDBSJ.js +61 -0
- package/dist/chunk-VTPUDBSJ.js.map +1 -0
- package/dist/chunk-WXBSM2CE.js +201 -0
- package/dist/chunk-WXBSM2CE.js.map +1 -0
- package/dist/chunk-XEPD5VJQ.js +99 -0
- package/dist/chunk-XEPD5VJQ.js.map +1 -0
- package/dist/core/contracts.d.ts +143 -0
- package/dist/core/contracts.js +4 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/date-format.d.ts +87 -0
- package/dist/core/date-format.js +3 -0
- package/dist/core/date-format.js.map +1 -0
- package/dist/core/derive.d.ts +20 -0
- package/dist/core/derive.js +7 -0
- package/dist/core/derive.js.map +1 -0
- package/dist/{descriptor-DPrrfAjL.d.ts → core/descriptor.d.ts} +28 -1
- package/dist/core/descriptor.js +3 -0
- package/dist/core/descriptor.js.map +1 -0
- package/dist/core/gate-validator.d.ts +27 -0
- package/dist/core/gate-validator.js +4 -0
- package/dist/core/gate-validator.js.map +1 -0
- package/dist/core/gate.d.ts +144 -0
- package/dist/core/gate.js +13 -0
- package/dist/core/gate.js.map +1 -0
- package/dist/core/index.d.ts +12 -623
- package/dist/core/index.js +14 -1
- package/dist/core/native-concepts.d.ts +78 -0
- package/dist/core/native-concepts.js +3 -0
- package/dist/core/native-concepts.js.map +1 -0
- package/dist/core/normalize-schema.d.ts +30 -0
- package/dist/core/normalize-schema.js +4 -0
- package/dist/core/normalize-schema.js.map +1 -0
- package/dist/core/own-property.d.ts +46 -0
- package/dist/core/own-property.js +3 -0
- package/dist/core/own-property.js.map +1 -0
- package/dist/core/readiness.d.ts +89 -0
- package/dist/core/readiness.js +4 -0
- package/dist/core/readiness.js.map +1 -0
- package/dist/core/schema-utils.d.ts +74 -0
- package/dist/core/schema-utils.js +3 -0
- package/dist/core/schema-utils.js.map +1 -0
- package/dist/core/validation-message.d.ts +39 -0
- package/dist/core/validation-message.js +4 -0
- package/dist/core/validation-message.js.map +1 -0
- package/dist/core/values.d.ts +91 -0
- package/dist/core/values.js +11 -0
- package/dist/core/values.js.map +1 -0
- package/dist/core/wire-format.d.ts +95 -0
- package/dist/core/wire-format.js +7 -0
- package/dist/core/wire-format.js.map +1 -0
- package/dist/react/index.d.ts +22 -1
- package/dist/react/index.js +128 -48
- package/dist/react/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +5 -1
- package/dist/chunk-S6KOTTGD.js +0 -990
- package/dist/chunk-S6KOTTGD.js.map +0 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { RunField, BooleanRunField, DateRunField, EnumRunField, FileRunField, ListRunField, NumberRunField, ObjectRunField, ProseRunField, TextRunField, UnknownRunField, ConceptCategory } from '../core/descriptor.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Ambient state that any field in the tree may need, threaded unchanged through
|
|
@@ -114,6 +114,25 @@ interface ListFieldProps {
|
|
|
114
114
|
* own remove control; "Add" appends a fresh entry. The element renders through
|
|
115
115
|
* the same `FieldRenderer`, so a list of documents gets dropzones and a list of
|
|
116
116
|
* structured concepts gets nested cards - no special-casing per element type.
|
|
117
|
+
*
|
|
118
|
+
* **A row is a thing, not a slot.** Its React key is generated when the row
|
|
119
|
+
* appears and travels with it, so removing an earlier row moves the later ones
|
|
120
|
+
* instead of renumbering them into each other - which is what used to drop a
|
|
121
|
+
* half-typed value or a pending preview onto the neighbouring row. The row's
|
|
122
|
+
* field ID stays POSITIONAL (`cvs.1`), because that ID is a PATH: a host writes
|
|
123
|
+
* an upload back with `setValueAtPath(values, id.split('.'), value)`, and
|
|
124
|
+
* `setValueAtPath` is a pure function of the value tree. Handing out an opaque
|
|
125
|
+
* token instead would mean the kernel keeping a live token-to-index registry for
|
|
126
|
+
* that function to consult, which is exactly the kind of hidden state the
|
|
127
|
+
* headless core does not have.
|
|
128
|
+
*
|
|
129
|
+
* The positional ID is why removal is BLOCKED while a file is arriving anywhere
|
|
130
|
+
* in this list - see `listIsBusy`. Together those two are the whole fix: a
|
|
131
|
+
* generated key cannot rescue a write-back either, because the keys live in this
|
|
132
|
+
* control's state and not in the value, so a host that replaces `values`
|
|
133
|
+
* wholesale reconciles by length just as positions do. What a stable identity
|
|
134
|
+
* buys over a position is exactly the reorderings the KERNEL performs, and it
|
|
135
|
+
* performs one.
|
|
117
136
|
*/
|
|
118
137
|
declare function ListField({ field, value, onChange, id, error, env }: ListFieldProps): react.JSX.Element;
|
|
119
138
|
|
|
@@ -249,6 +268,8 @@ interface FieldStrings {
|
|
|
249
268
|
addItem: string;
|
|
250
269
|
removeItemAria: (index: number) => string;
|
|
251
270
|
itemsCount: (count: number) => string;
|
|
271
|
+
/** The same badge for a list the method gave an exact count (`Concept[N]`). */
|
|
272
|
+
itemsCountOf: (count: number, total: number) => string;
|
|
252
273
|
uploading: string;
|
|
253
274
|
dropToUpload: string;
|
|
254
275
|
dropOrBrowse: string;
|
package/dist/react/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { isFilled } from '../chunk-PM55AHRO.js';
|
|
3
|
+
import { ownProp } from '../chunk-V3OV2XQT.js';
|
|
4
|
+
import { toStoredDateValue, toDateInputValue } from '../chunk-GKV77RAS.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';
|
|
@@ -137,6 +139,7 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
137
139
|
addItem: "Add item",
|
|
138
140
|
removeItemAria: (index) => `Remove item ${index}`,
|
|
139
141
|
itemsCount: (count) => count === 1 ? "1 item" : `${count} items`,
|
|
142
|
+
itemsCountOf: (count, total) => `${count} of ${total} ${total === 1 ? "item" : "items"}`,
|
|
140
143
|
uploading: "Uploading\u2026",
|
|
141
144
|
dropToUpload: "Drop to upload",
|
|
142
145
|
dropOrBrowse: "Drop a file or click to browse",
|
|
@@ -239,6 +242,13 @@ function DateField({ field, value, onChange, id, error, disabled }) {
|
|
|
239
242
|
}
|
|
240
243
|
var IMAGE_EXT_RE = /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)(\?|$)/i;
|
|
241
244
|
var PDF_EXT_RE = /\.pdf(\?|$)/i;
|
|
245
|
+
var DATA_URL_MIME_RE = /^data:([^;,]+)/i;
|
|
246
|
+
function isDirectlyViewable(url) {
|
|
247
|
+
return !!url && (/^https?:/i.test(url) || /^data:/i.test(url) || /^blob:/i.test(url));
|
|
248
|
+
}
|
|
249
|
+
function dataUrlMime(url) {
|
|
250
|
+
return DATA_URL_MIME_RE.exec(url)?.[1]?.toLowerCase();
|
|
251
|
+
}
|
|
242
252
|
function DocumentField(props) {
|
|
243
253
|
return /* @__PURE__ */ jsx(FileField, { ...props, category: "document" });
|
|
244
254
|
}
|
|
@@ -261,24 +271,31 @@ function FileField({
|
|
|
261
271
|
const [showUrl, setShowUrl] = useState(false);
|
|
262
272
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
263
273
|
const [localPreview, setLocalPreview] = useState(null);
|
|
274
|
+
const [resolved, setResolved] = useState(null);
|
|
264
275
|
const localUrlRef = useRef(null);
|
|
265
276
|
const setLocal = useCallback((next) => {
|
|
266
277
|
if (localUrlRef.current) URL.revokeObjectURL(localUrlRef.current);
|
|
267
|
-
localUrlRef.current = next?.
|
|
278
|
+
localUrlRef.current = next?.objectUrl ?? null;
|
|
268
279
|
setLocalPreview(next);
|
|
269
280
|
}, []);
|
|
270
281
|
useEffect(() => () => setLocal(null), [setLocal]);
|
|
271
282
|
const handleFile = useCallback(
|
|
272
283
|
(file) => {
|
|
273
|
-
setLocal({
|
|
284
|
+
setLocal({ objectUrl: URL.createObjectURL(file), type: file.type });
|
|
274
285
|
onDropFile(file);
|
|
275
286
|
},
|
|
276
287
|
[setLocal, onDropFile]
|
|
277
288
|
);
|
|
289
|
+
const busy = disabled || uploading;
|
|
278
290
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
279
291
|
maxFiles: 1,
|
|
280
292
|
multiple: false,
|
|
281
|
-
disabled:
|
|
293
|
+
disabled: busy,
|
|
294
|
+
// The tab stop belongs on the INPUT, not on this div - see the root element
|
|
295
|
+
// below. Without this, react-dropzone puts `tabIndex: 0` and its own key
|
|
296
|
+
// handlers on a `role="presentation"` div, and the element a keyboard or
|
|
297
|
+
// voice-control user lands on is a generic with no role and no name.
|
|
298
|
+
noKeyboard: true,
|
|
282
299
|
onDrop: (files) => {
|
|
283
300
|
const file = files[0];
|
|
284
301
|
if (file) handleFile(file);
|
|
@@ -286,28 +303,43 @@ function FileField({
|
|
|
286
303
|
});
|
|
287
304
|
const clear = useCallback(() => {
|
|
288
305
|
setLocal(null);
|
|
289
|
-
|
|
306
|
+
setResolved(null);
|
|
290
307
|
setPreviewOpen(false);
|
|
291
308
|
onChange(void 0);
|
|
292
309
|
}, [setLocal, onChange]);
|
|
310
|
+
const localIsCurrent = !!localPreview && (localPreview.boundUrl === void 0 || localPreview.boundUrl === value?.url);
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!localPreview) return;
|
|
313
|
+
if (localPreview.boundUrl === void 0) {
|
|
314
|
+
if (uploading || !value?.url) return;
|
|
315
|
+
setLocalPreview({ ...localPreview, boundUrl: value.url });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (value?.url !== localPreview.boundUrl) setLocal(null);
|
|
319
|
+
}, [localPreview, value?.url, uploading, setLocal]);
|
|
293
320
|
const hasFile = !!value?.url;
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const
|
|
321
|
+
const localType = localIsCurrent ? localPreview?.type : void 0;
|
|
322
|
+
const filename = value?.filename ?? "";
|
|
323
|
+
const url = value?.url ?? "";
|
|
324
|
+
const urlMime = dataUrlMime(url);
|
|
325
|
+
const isImage = category === "image" || localType?.startsWith("image/") === true || urlMime?.startsWith("image/") === true || IMAGE_EXT_RE.test(filename) || IMAGE_EXT_RE.test(url);
|
|
326
|
+
const isPdf = localType === "application/pdf" || urlMime === "application/pdf" || PDF_EXT_RE.test(filename) || PDF_EXT_RE.test(url);
|
|
297
327
|
const canPreview = isImage || isPdf;
|
|
298
|
-
const
|
|
299
|
-
const storageUri = value?.url && !value.url.startsWith("http") ? value.url : null;
|
|
328
|
+
const storageUri = value?.url && !isDirectlyViewable(value.url) ? value.url : null;
|
|
300
329
|
useEffect(() => {
|
|
301
|
-
if (!previewOpen || !storageUri ||
|
|
330
|
+
if (!previewOpen || !storageUri || localIsCurrent || !resolveUrl) return;
|
|
302
331
|
let cancelled = false;
|
|
303
|
-
void resolveUrl(storageUri).then((
|
|
304
|
-
if (!cancelled)
|
|
332
|
+
void resolveUrl(storageUri).then((src) => {
|
|
333
|
+
if (!cancelled) setResolved(src ? { uri: storageUri, src } : null);
|
|
334
|
+
}).catch(() => {
|
|
335
|
+
if (!cancelled) setResolved(null);
|
|
305
336
|
});
|
|
306
337
|
return () => {
|
|
307
338
|
cancelled = true;
|
|
308
339
|
};
|
|
309
|
-
}, [previewOpen, storageUri,
|
|
310
|
-
const
|
|
340
|
+
}, [previewOpen, storageUri, localIsCurrent, resolveUrl]);
|
|
341
|
+
const resolvedSrc = resolved && resolved.uri === storageUri ? resolved.src : void 0;
|
|
342
|
+
const previewSrc = (localIsCurrent ? localPreview?.objectUrl : void 0) ?? (isDirectlyViewable(value?.url) ? value.url : resolvedSrc);
|
|
311
343
|
return /* @__PURE__ */ jsxs(
|
|
312
344
|
FieldShell,
|
|
313
345
|
{
|
|
@@ -318,6 +350,7 @@ function FileField({
|
|
|
318
350
|
description: field.description,
|
|
319
351
|
required: field.required,
|
|
320
352
|
error,
|
|
353
|
+
htmlFor: id,
|
|
321
354
|
children: [
|
|
322
355
|
hasFile && !uploading ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
323
356
|
/* @__PURE__ */ jsx(
|
|
@@ -332,36 +365,53 @@ function FileField({
|
|
|
332
365
|
}
|
|
333
366
|
),
|
|
334
367
|
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
|
-
] }) :
|
|
336
|
-
"div
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
368
|
+
] }) : (
|
|
369
|
+
// `role="presentation"` and no tab stop: this div is the drop TARGET and
|
|
370
|
+
// the visual affordance, and the control inside it is a real
|
|
371
|
+
// `<input type="file">`, which already has the right role and now has a
|
|
372
|
+
// name. Giving the div a `role="button"` of its own would put a second
|
|
373
|
+
// named control in the accessibility tree for one value. The focus ring
|
|
374
|
+
// is `focus-within` because the element that takes focus is clip-hidden.
|
|
375
|
+
/* @__PURE__ */ jsxs(
|
|
376
|
+
"div",
|
|
377
|
+
{
|
|
378
|
+
...getRootProps(),
|
|
379
|
+
className: cn(
|
|
380
|
+
"flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center",
|
|
381
|
+
"cursor-pointer select-none transition-colors",
|
|
382
|
+
"focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
383
|
+
isDragActive ? "border-primary/60 bg-primary/5" : "border-border bg-input hover:border-border hover:bg-muted",
|
|
384
|
+
busy && "cursor-not-allowed opacity-60"
|
|
385
|
+
),
|
|
386
|
+
children: [
|
|
387
|
+
/* @__PURE__ */ jsx(
|
|
388
|
+
"input",
|
|
389
|
+
{
|
|
390
|
+
...getInputProps({ tabIndex: 0 }),
|
|
391
|
+
id,
|
|
392
|
+
disabled: busy,
|
|
393
|
+
"aria-invalid": !!error
|
|
394
|
+
}
|
|
395
|
+
),
|
|
396
|
+
uploading ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
397
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }),
|
|
398
|
+
/* @__PURE__ */ jsx("p", { className: "text-[12px] text-muted-foreground", children: s.uploading })
|
|
399
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
400
|
+
/* @__PURE__ */ jsx(Upload, { className: "h-5 w-5 text-muted-foreground", strokeWidth: 1.75 }),
|
|
401
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-0.5", children: [
|
|
402
|
+
/* @__PURE__ */ jsx("p", { className: "text-[13px] text-foreground", children: isDragActive ? s.dropToUpload : s.dropOrBrowse }),
|
|
403
|
+
field.accept && /* @__PURE__ */ jsx("p", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.accept })
|
|
404
|
+
] })
|
|
355
405
|
] })
|
|
356
|
-
]
|
|
357
|
-
|
|
358
|
-
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
)
|
|
359
409
|
),
|
|
360
410
|
!showUrl ? /* @__PURE__ */ jsxs(
|
|
361
411
|
"button",
|
|
362
412
|
{
|
|
363
413
|
type: "button",
|
|
364
|
-
disabled,
|
|
414
|
+
disabled: busy,
|
|
365
415
|
onClick: () => setShowUrl(true),
|
|
366
416
|
className: "inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
|
|
367
417
|
children: [
|
|
@@ -375,7 +425,7 @@ function FileField({
|
|
|
375
425
|
type: "text",
|
|
376
426
|
autoFocus: true,
|
|
377
427
|
value: value?.url ?? "",
|
|
378
|
-
disabled,
|
|
428
|
+
disabled: busy,
|
|
379
429
|
placeholder: s.urlPlaceholder,
|
|
380
430
|
onChange: (e) => {
|
|
381
431
|
setLocal(null);
|
|
@@ -842,8 +892,8 @@ function ObjectField({ field, value, onChange, id, error, env }) {
|
|
|
842
892
|
const isApp = presentation === "app";
|
|
843
893
|
const data = value ?? {};
|
|
844
894
|
const setChild = (name, childValue) => onChange({ ...data, [name]: childValue });
|
|
845
|
-
const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(data
|
|
846
|
-
const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || isFilled(data
|
|
895
|
+
const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(ownProp(data, f.name)));
|
|
896
|
+
const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || isFilled(ownProp(data, f.name)));
|
|
847
897
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
848
898
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
849
899
|
/* @__PURE__ */ jsx(
|
|
@@ -872,7 +922,7 @@ function ObjectField({ field, value, onChange, id, error, env }) {
|
|
|
872
922
|
FieldRenderer,
|
|
873
923
|
{
|
|
874
924
|
field: child,
|
|
875
|
-
value: data
|
|
925
|
+
value: ownProp(data, child.name),
|
|
876
926
|
onChange: (v) => setChild(child.name, v),
|
|
877
927
|
id: `${id}.${child.name}`,
|
|
878
928
|
env
|
|
@@ -899,13 +949,19 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
899
949
|
const presentation = useFieldPresentation();
|
|
900
950
|
const isApp = presentation === "app";
|
|
901
951
|
const items = value ?? [];
|
|
952
|
+
const [rowKeys, setRowKeys] = useRowKeys(items.length);
|
|
902
953
|
const setItem = (index, itemValue) => {
|
|
903
954
|
const next = items.slice();
|
|
904
955
|
next[index] = itemValue;
|
|
905
956
|
onChange(next);
|
|
906
957
|
};
|
|
907
|
-
const removeItem = (index) =>
|
|
958
|
+
const removeItem = (index) => {
|
|
959
|
+
setRowKeys((prev) => prev.filter((_, i) => i !== index));
|
|
960
|
+
onChange(items.filter((_, i) => i !== index));
|
|
961
|
+
};
|
|
908
962
|
const addItem = () => onChange([...items, emptyValue(field.item)]);
|
|
963
|
+
const busy = listIsBusy(id, env?.uploadingIds);
|
|
964
|
+
const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
|
|
909
965
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
910
966
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
911
967
|
/* @__PURE__ */ jsx(
|
|
@@ -919,7 +975,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
919
975
|
}
|
|
920
976
|
),
|
|
921
977
|
!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) }),
|
|
978
|
+
/* @__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
979
|
!field.required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground/70", children: s.optionalBadge })
|
|
924
980
|
] }),
|
|
925
981
|
field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
|
|
@@ -946,7 +1002,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
946
1002
|
{
|
|
947
1003
|
type: "button",
|
|
948
1004
|
onClick: () => removeItem(index),
|
|
949
|
-
disabled: env?.disabled,
|
|
1005
|
+
disabled: env?.disabled || busy,
|
|
950
1006
|
"aria-label": s.removeItemAria(index + 1),
|
|
951
1007
|
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
1008
|
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
@@ -954,14 +1010,14 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
954
1010
|
)
|
|
955
1011
|
]
|
|
956
1012
|
},
|
|
957
|
-
index
|
|
1013
|
+
rowKeys[index] ?? index
|
|
958
1014
|
)),
|
|
959
1015
|
/* @__PURE__ */ jsxs(
|
|
960
1016
|
"button",
|
|
961
1017
|
{
|
|
962
1018
|
type: "button",
|
|
963
1019
|
onClick: addItem,
|
|
964
|
-
disabled: env?.disabled,
|
|
1020
|
+
disabled: env?.disabled || isFull,
|
|
965
1021
|
className: cn(
|
|
966
1022
|
"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
1023
|
"hover:border-primary/40 hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
|
@@ -976,6 +1032,30 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
976
1032
|
error && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive", role: "alert", children: error })
|
|
977
1033
|
] });
|
|
978
1034
|
}
|
|
1035
|
+
function listIsBusy(id, uploadingIds) {
|
|
1036
|
+
if (!uploadingIds?.size) return false;
|
|
1037
|
+
const prefix = `${id}.`;
|
|
1038
|
+
for (const uploading of uploadingIds) if (uploading.startsWith(prefix)) return true;
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
function useRowKeys(length) {
|
|
1042
|
+
const [state, setState] = useState(() => ({
|
|
1043
|
+
keys: Array.from({ length }, (_, i) => i),
|
|
1044
|
+
next: length
|
|
1045
|
+
}));
|
|
1046
|
+
if (state.keys.length !== length) {
|
|
1047
|
+
setState((prev) => {
|
|
1048
|
+
if (length <= prev.keys.length) return { keys: prev.keys.slice(0, length), next: prev.next };
|
|
1049
|
+
const added = length - prev.keys.length;
|
|
1050
|
+
return {
|
|
1051
|
+
keys: [...prev.keys, ...Array.from({ length: added }, (_, i) => prev.next + i)],
|
|
1052
|
+
next: prev.next + added
|
|
1053
|
+
};
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
const update = (fn) => setState((prev) => ({ keys: fn(prev.keys), next: prev.next }));
|
|
1057
|
+
return [state.keys, update];
|
|
1058
|
+
}
|
|
979
1059
|
function emptyValue(field) {
|
|
980
1060
|
switch (field.kind) {
|
|
981
1061
|
case "boolean":
|