@pipelex/mthds-form 0.10.0 → 0.11.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/{chunk-G6BWZUGV.js → chunk-E2IVBAW3.js} +133 -30
- package/dist/chunk-E2IVBAW3.js.map +1 -0
- package/dist/field-renderer-C5Onx6qB.d.ts +65 -0
- package/dist/generative/index.d.ts +1 -1
- package/dist/generative/index.js +1 -1
- package/dist/react/index.d.ts +29 -4
- package/dist/react/index.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/dist/chunk-G6BWZUGV.js.map +0 -1
- package/dist/field-renderer-BSU2hMf6.d.ts +0 -34
|
@@ -203,6 +203,7 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
203
203
|
dropToUpload: "Drop to upload",
|
|
204
204
|
dropOrBrowse: "Drop a file or click to browse",
|
|
205
205
|
pasteUrlInstead: "paste a URL instead",
|
|
206
|
+
uploadUnavailable: "Files cannot be uploaded here. Paste a link to the file instead.",
|
|
206
207
|
urlPlaceholder: "https://\u2026",
|
|
207
208
|
fileUrlAria: (label) => label ? `Link to the file for ${label}` : "Link to the file",
|
|
208
209
|
uploadedFile: "Attached file",
|
|
@@ -278,7 +279,16 @@ function FieldShell({
|
|
|
278
279
|
!required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
|
|
279
280
|
] }),
|
|
280
281
|
description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: description }),
|
|
281
|
-
|
|
282
|
+
/* @__PURE__ */ jsx(
|
|
283
|
+
"div",
|
|
284
|
+
{
|
|
285
|
+
className: cn(
|
|
286
|
+
"space-y-2",
|
|
287
|
+
isApp && error && "rounded-lg ring-1 ring-destructive/70 ring-offset-4 ring-offset-background"
|
|
288
|
+
),
|
|
289
|
+
children
|
|
290
|
+
}
|
|
291
|
+
),
|
|
282
292
|
error && /* @__PURE__ */ jsx("p", { className: "flex items-center gap-1 text-[11px] text-destructive", role: "alert", children: error })
|
|
283
293
|
] });
|
|
284
294
|
}
|
|
@@ -348,9 +358,11 @@ function FileField({
|
|
|
348
358
|
field,
|
|
349
359
|
value,
|
|
350
360
|
onDropFile,
|
|
361
|
+
allowUrl = true,
|
|
351
362
|
onChange,
|
|
352
363
|
id,
|
|
353
364
|
uploading,
|
|
365
|
+
uploadError,
|
|
354
366
|
resolveUrl,
|
|
355
367
|
error,
|
|
356
368
|
disabled,
|
|
@@ -372,16 +384,28 @@ function FileField({
|
|
|
372
384
|
}, []);
|
|
373
385
|
useEffect(() => () => setLocal(null), [setLocal]);
|
|
374
386
|
const [rejected, setRejected] = useState(null);
|
|
387
|
+
const [dismissedUploadError, setDismissedUploadError] = useState(null);
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (uploadError === void 0) setDismissedUploadError(null);
|
|
390
|
+
else setRejected(null);
|
|
391
|
+
}, [uploadError]);
|
|
392
|
+
const dismissUploadError = useCallback(() => {
|
|
393
|
+
setDismissedUploadError(uploadError ?? null);
|
|
394
|
+
}, [uploadError]);
|
|
395
|
+
const failure = uploadError !== void 0 && uploadError !== dismissedUploadError ? uploadError : null;
|
|
375
396
|
const formats = field.formats;
|
|
376
397
|
const hint = acceptLabel(formats);
|
|
377
398
|
const accept = useMemo(() => acceptMap(formats), [formats]);
|
|
399
|
+
const canUpload = onDropFile !== void 0;
|
|
378
400
|
const handleFile = useCallback(
|
|
379
401
|
(file) => {
|
|
402
|
+
if (!onDropFile) return;
|
|
380
403
|
if (!isAcceptedFile(formats, file)) {
|
|
381
404
|
setRejected(file.name);
|
|
382
405
|
return;
|
|
383
406
|
}
|
|
384
407
|
setRejected(null);
|
|
408
|
+
setDismissedUploadError(null);
|
|
385
409
|
setLocal({ objectUrl: URL.createObjectURL(file), type: file.type });
|
|
386
410
|
onDropFile(file);
|
|
387
411
|
},
|
|
@@ -391,7 +415,9 @@ function FileField({
|
|
|
391
415
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
392
416
|
maxFiles: 1,
|
|
393
417
|
multiple: false,
|
|
394
|
-
|
|
418
|
+
// Disabled, not merely unrendered, when there is no upload path: the hook
|
|
419
|
+
// cannot be skipped, and nothing may open a picker through it.
|
|
420
|
+
disabled: busy || !canUpload,
|
|
395
421
|
// Filters the OS picker so a wrong file is hard to choose in the first
|
|
396
422
|
// place. It is NOT the enforcement: a drag-and-drop bypasses the picker
|
|
397
423
|
// entirely, and a browser reporting an empty `File.type` slips through
|
|
@@ -421,8 +447,9 @@ function FileField({
|
|
|
421
447
|
setResolved(null);
|
|
422
448
|
setPreviewOpen(false);
|
|
423
449
|
setRejected(null);
|
|
450
|
+
dismissUploadError();
|
|
424
451
|
onChange(void 0);
|
|
425
|
-
}, [setLocal, onChange]);
|
|
452
|
+
}, [setLocal, onChange, dismissUploadError]);
|
|
426
453
|
const localIsCurrent = !!localPreview && (localPreview.boundUrl === void 0 || localPreview.boundUrl === value?.url);
|
|
427
454
|
useEffect(() => {
|
|
428
455
|
if (!localPreview) return;
|
|
@@ -447,19 +474,31 @@ function FileField({
|
|
|
447
474
|
if (!previewOpen || !storageUri || localIsCurrent || !resolveUrl) return;
|
|
448
475
|
let cancelled = false;
|
|
449
476
|
void resolveUrl(storageUri).then((src) => {
|
|
450
|
-
if (!cancelled) setResolved(
|
|
477
|
+
if (!cancelled) setResolved({ uri: storageUri, src: src || null });
|
|
451
478
|
}).catch(() => {
|
|
452
|
-
if (!cancelled) setResolved(null);
|
|
479
|
+
if (!cancelled) setResolved({ uri: storageUri, src: null });
|
|
453
480
|
});
|
|
454
481
|
return () => {
|
|
455
482
|
cancelled = true;
|
|
456
483
|
};
|
|
457
484
|
}, [previewOpen, storageUri, localIsCurrent, resolveUrl]);
|
|
458
|
-
const resolvedSrc = resolved && resolved.uri === storageUri ? viewableUrl(resolved.src) : void 0;
|
|
485
|
+
const resolvedSrc = resolved && resolved.uri === storageUri ? viewableUrl(resolved.src ?? void 0) : void 0;
|
|
459
486
|
const previewSrc = (localIsCurrent ? localPreview?.objectUrl : void 0) ?? directSrc ?? resolvedSrc;
|
|
460
|
-
const previewPending = !previewSrc && !!storageUri && resolved?.uri !== storageUri;
|
|
487
|
+
const previewPending = !previewSrc && !!storageUri && !!resolveUrl && resolved?.uri !== storageUri;
|
|
488
|
+
const togglePreview = () => {
|
|
489
|
+
if (!previewOpen) setResolved((r) => r?.src === null ? null : r);
|
|
490
|
+
setPreviewOpen((v) => !v);
|
|
491
|
+
};
|
|
461
492
|
const urlValue = value?.url ?? "";
|
|
462
493
|
const urlText = presentation === "studio" || isReadableLink(urlValue) || urlValue === typedUrl ? urlValue : "";
|
|
494
|
+
if (!canUpload && !allowUrl) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`The file field at "${id}" has no way in: the form's environment supplies no onDropFile and sets allowUrl to false. Supply onDropFile so the file can be uploaded, or leave allowUrl unset so a link can be pasted.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const linkOpen = allowUrl && (!canUpload || showUrl);
|
|
500
|
+
const noUploadLineShown = !canUpload && !hasFile && !uploading;
|
|
501
|
+
const noUploadLineId = `${domId}-no-upload`;
|
|
463
502
|
return /* @__PURE__ */ jsxs(
|
|
464
503
|
FieldShell,
|
|
465
504
|
{
|
|
@@ -483,7 +522,7 @@ function FileField({
|
|
|
483
522
|
onClear: clear,
|
|
484
523
|
canPreview,
|
|
485
524
|
previewOpen,
|
|
486
|
-
onTogglePreview:
|
|
525
|
+
onTogglePreview: togglePreview
|
|
487
526
|
}
|
|
488
527
|
),
|
|
489
528
|
previewOpen && (previewSrc && isImage ? /* @__PURE__ */ jsx(ImagePreview, { src: previewSrc, filename: value?.filename }) : previewSrc && isPdf ? /* @__PURE__ */ jsx(PdfPreview, { src: previewSrc }) : previewPending ? /* @__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" }) }) : (
|
|
@@ -493,7 +532,19 @@ function FileField({
|
|
|
493
532
|
// load, which is the one thing a refusal must not look like.
|
|
494
533
|
/* @__PURE__ */ jsx("div", { className: "flex h-24 items-center justify-center rounded-md border border-border bg-muted", children: /* @__PURE__ */ jsx(ImageOff, { className: "h-5 w-5 text-muted-foreground" }) })
|
|
495
534
|
))
|
|
496
|
-
] }) : (
|
|
535
|
+
] }) : !canUpload ? (
|
|
536
|
+
// No upload path, so no dropzone and no picker: a control that takes a
|
|
537
|
+
// file with nowhere to send it drops the file without a word. The line
|
|
538
|
+
// says why the link input below is the way in, and the hint says what
|
|
539
|
+
// that link has to point at.
|
|
540
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: uploading ? /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 text-[12px] text-muted-foreground", children: [
|
|
541
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }),
|
|
542
|
+
s.uploading
|
|
543
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
544
|
+
/* @__PURE__ */ jsx("p", { id: noUploadLineId, className: "text-[12px] text-muted-foreground", children: s.uploadUnavailable }),
|
|
545
|
+
hint && /* @__PURE__ */ jsx("p", { className: "font-mono text-[10.5px] text-muted-foreground", children: hint })
|
|
546
|
+
] }) })
|
|
547
|
+
) : (
|
|
497
548
|
// `role="presentation"` and no tab stop: this div is the drop TARGET and
|
|
498
549
|
// the visual affordance, and the control inside it is a real
|
|
499
550
|
// `<input type="file">`, which already has the right role and now has a
|
|
@@ -535,39 +586,43 @@ function FileField({
|
|
|
535
586
|
}
|
|
536
587
|
)
|
|
537
588
|
),
|
|
538
|
-
|
|
589
|
+
!busy && (rejected ? /* @__PURE__ */ jsxs("p", { role: "alert", className: "text-[12px] text-destructive", children: [
|
|
539
590
|
/* @__PURE__ */ jsx("span", { className: "font-mono", children: rejected }),
|
|
540
591
|
" \u2014 ",
|
|
541
592
|
s.unsupportedFileType(hint)
|
|
542
|
-
] }),
|
|
543
|
-
|
|
544
|
-
"button",
|
|
545
|
-
{
|
|
546
|
-
type: "button",
|
|
547
|
-
disabled: busy,
|
|
548
|
-
onClick: () => setShowUrl(true),
|
|
549
|
-
className: "flex w-fit items-center gap-1 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
|
|
550
|
-
children: [
|
|
551
|
-
/* @__PURE__ */ jsx(Link2, { className: "h-3 w-3" }),
|
|
552
|
-
s.pasteUrlInstead
|
|
553
|
-
]
|
|
554
|
-
}
|
|
555
|
-
) : /* @__PURE__ */ jsx(
|
|
593
|
+
] }) : failure && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-[12px] text-destructive", children: failure })),
|
|
594
|
+
linkOpen ? /* @__PURE__ */ jsx(
|
|
556
595
|
"input",
|
|
557
596
|
{
|
|
558
597
|
type: "text",
|
|
559
|
-
|
|
598
|
+
id: canUpload ? void 0 : domId,
|
|
599
|
+
autoFocus: showUrl,
|
|
560
600
|
value: urlText,
|
|
561
601
|
disabled: busy,
|
|
562
602
|
"aria-label": s.fileUrlAria(fieldLabel(field.title, field.name, presentation)),
|
|
603
|
+
"aria-describedby": noUploadLineShown ? noUploadLineId : void 0,
|
|
604
|
+
"aria-invalid": canUpload ? void 0 : !!error,
|
|
563
605
|
placeholder: s.urlPlaceholder,
|
|
564
606
|
onChange: (e) => {
|
|
565
607
|
setLocal(null);
|
|
608
|
+
dismissUploadError();
|
|
566
609
|
setTypedUrl(e.target.value);
|
|
567
610
|
onChange(e.target.value ? { url: e.target.value } : void 0);
|
|
568
611
|
},
|
|
569
612
|
className: cn(fieldControlClass, "h-9 px-3 font-mono text-[12px]")
|
|
570
613
|
}
|
|
614
|
+
) : allowUrl && /* @__PURE__ */ jsxs(
|
|
615
|
+
"button",
|
|
616
|
+
{
|
|
617
|
+
type: "button",
|
|
618
|
+
disabled: busy,
|
|
619
|
+
onClick: () => setShowUrl(true),
|
|
620
|
+
className: "flex w-fit items-center gap-1 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
|
|
621
|
+
children: [
|
|
622
|
+
/* @__PURE__ */ jsx(Link2, { className: "h-3 w-3" }),
|
|
623
|
+
s.pasteUrlInstead
|
|
624
|
+
]
|
|
625
|
+
}
|
|
571
626
|
)
|
|
572
627
|
]
|
|
573
628
|
}
|
|
@@ -1107,6 +1162,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1107
1162
|
const isApp = presentation === "app";
|
|
1108
1163
|
const items = value ?? [];
|
|
1109
1164
|
const [rowKeys, setRowKeys] = useRowKeys(items.length);
|
|
1165
|
+
const [staleErrors, setStaleErrors] = useState(NO_ERRORS);
|
|
1110
1166
|
const setItem = (index, itemValue) => {
|
|
1111
1167
|
const next = items.slice();
|
|
1112
1168
|
next[index] = itemValue;
|
|
@@ -1114,10 +1170,40 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1114
1170
|
};
|
|
1115
1171
|
const removeItem = (index) => {
|
|
1116
1172
|
setRowKeys((prev) => prev.filter((_, i) => i !== index));
|
|
1173
|
+
setStaleErrors((prev) => withRenumberedErrors(prev, id, index, env?.uploadErrors));
|
|
1117
1174
|
onChange(items.filter((_, i) => i !== index));
|
|
1118
1175
|
};
|
|
1119
1176
|
const addItem = () => onChange([...items, emptyValue(field.item)]);
|
|
1120
1177
|
const busy = listIsBusy(id, env?.uploadingIds);
|
|
1178
|
+
const hostErrors = env?.uploadErrors;
|
|
1179
|
+
useEffect(() => {
|
|
1180
|
+
setStaleErrors((prev) => {
|
|
1181
|
+
if (prev.size === 0) return prev;
|
|
1182
|
+
const next = new Map([...prev].filter(([key, message]) => hostErrors?.get(key) === message));
|
|
1183
|
+
return next.size === prev.size ? prev : next;
|
|
1184
|
+
});
|
|
1185
|
+
}, [hostErrors]);
|
|
1186
|
+
const rowEnv = useMemo(() => {
|
|
1187
|
+
if (!env || staleErrors.size === 0) return env;
|
|
1188
|
+
const drop = env.onDropFile;
|
|
1189
|
+
return {
|
|
1190
|
+
...env,
|
|
1191
|
+
uploadErrors: new Map(
|
|
1192
|
+
[...env.uploadErrors ?? []].filter(([key, message]) => staleErrors.get(key) !== message)
|
|
1193
|
+
),
|
|
1194
|
+
// A new file at a position is a new attempt there, so what the host says
|
|
1195
|
+
// about that position from now on is about the file that is there.
|
|
1196
|
+
onDropFile: drop ? (dropId, file) => {
|
|
1197
|
+
setStaleErrors((prev) => {
|
|
1198
|
+
if (!prev.has(dropId)) return prev;
|
|
1199
|
+
const next = new Map(prev);
|
|
1200
|
+
next.delete(dropId);
|
|
1201
|
+
return next;
|
|
1202
|
+
});
|
|
1203
|
+
drop(dropId, file);
|
|
1204
|
+
} : void 0
|
|
1205
|
+
};
|
|
1206
|
+
}, [env, staleErrors]);
|
|
1121
1207
|
const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
|
|
1122
1208
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1123
1209
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
@@ -1151,7 +1237,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1151
1237
|
value: item,
|
|
1152
1238
|
onChange: (v) => setItem(index, v),
|
|
1153
1239
|
id: `${id}.${index}`,
|
|
1154
|
-
env
|
|
1240
|
+
env: rowEnv
|
|
1155
1241
|
}
|
|
1156
1242
|
) }),
|
|
1157
1243
|
/* @__PURE__ */ jsx(
|
|
@@ -1195,6 +1281,18 @@ function listIsBusy(id, uploadingIds) {
|
|
|
1195
1281
|
for (const uploading of uploadingIds) if (uploading.startsWith(prefix)) return true;
|
|
1196
1282
|
return false;
|
|
1197
1283
|
}
|
|
1284
|
+
var NO_ERRORS = /* @__PURE__ */ new Map();
|
|
1285
|
+
function withRenumberedErrors(stale, listId, removed, hostErrors) {
|
|
1286
|
+
if (!hostErrors?.size) return stale;
|
|
1287
|
+
const prefix = `${listId}.`;
|
|
1288
|
+
const next = new Map(stale);
|
|
1289
|
+
for (const [key, message] of hostErrors) {
|
|
1290
|
+
if (!key.startsWith(prefix)) continue;
|
|
1291
|
+
const row = key.slice(prefix.length).split(".")[0] ?? "";
|
|
1292
|
+
if (/^\d+$/.test(row) && Number(row) >= removed) next.set(key, message);
|
|
1293
|
+
}
|
|
1294
|
+
return next.size === stale.size && [...next].every(([k, m]) => stale.get(k) === m) ? stale : next;
|
|
1295
|
+
}
|
|
1198
1296
|
function useRowKeys(length) {
|
|
1199
1297
|
const [state, setState] = useState(() => ({
|
|
1200
1298
|
keys: Array.from({ length }, (_, i) => i),
|
|
@@ -1261,6 +1359,7 @@ function UnknownField({ field, value, onChange, id, error, disabled }) {
|
|
|
1261
1359
|
}
|
|
1262
1360
|
function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
1263
1361
|
const disabled = env?.disabled;
|
|
1362
|
+
const drop = env?.onDropFile;
|
|
1264
1363
|
switch (field.kind) {
|
|
1265
1364
|
case "text":
|
|
1266
1365
|
return /* @__PURE__ */ jsx(
|
|
@@ -1340,8 +1439,10 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
|
1340
1439
|
field,
|
|
1341
1440
|
value,
|
|
1342
1441
|
onChange,
|
|
1343
|
-
onDropFile: (file) =>
|
|
1442
|
+
onDropFile: drop ? (file) => drop(id, file) : void 0,
|
|
1443
|
+
allowUrl: env?.allowUrl,
|
|
1344
1444
|
uploading: env?.uploadingIds?.has(id),
|
|
1445
|
+
uploadError: env?.uploadErrors?.get(id),
|
|
1345
1446
|
resolveUrl: env?.resolveUrl,
|
|
1346
1447
|
id,
|
|
1347
1448
|
error,
|
|
@@ -1355,8 +1456,10 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
|
1355
1456
|
field,
|
|
1356
1457
|
value,
|
|
1357
1458
|
onChange,
|
|
1358
|
-
onDropFile: (file) =>
|
|
1459
|
+
onDropFile: drop ? (file) => drop(id, file) : void 0,
|
|
1460
|
+
allowUrl: env?.allowUrl,
|
|
1359
1461
|
uploading: env?.uploadingIds?.has(id),
|
|
1462
|
+
uploadError: env?.uploadErrors?.get(id),
|
|
1360
1463
|
resolveUrl: env?.resolveUrl,
|
|
1361
1464
|
id,
|
|
1362
1465
|
error,
|
|
@@ -2836,5 +2939,5 @@ function StuffViewer({
|
|
|
2836
2939
|
}
|
|
2837
2940
|
|
|
2838
2941
|
export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EnumField, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, NumberField, ObjectField, OptionalToggle, ProseField, ResultEnvProvider, ResultField, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, downloadStuff, fieldControlClass, humanizeFieldName, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
|
|
2839
|
-
//# sourceMappingURL=chunk-
|
|
2840
|
-
//# sourceMappingURL=chunk-
|
|
2942
|
+
//# sourceMappingURL=chunk-E2IVBAW3.js.map
|
|
2943
|
+
//# sourceMappingURL=chunk-E2IVBAW3.js.map
|