@pipelex/mthds-form 0.10.1 → 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-W56WIIVC.js → chunk-E2IVBAW3.js} +124 -25
- 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-W56WIIVC.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;
|
|
@@ -464,6 +491,14 @@ function FileField({
|
|
|
464
491
|
};
|
|
465
492
|
const urlValue = value?.url ?? "";
|
|
466
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`;
|
|
467
502
|
return /* @__PURE__ */ jsxs(
|
|
468
503
|
FieldShell,
|
|
469
504
|
{
|
|
@@ -497,7 +532,19 @@ function FileField({
|
|
|
497
532
|
// load, which is the one thing a refusal must not look like.
|
|
498
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" }) })
|
|
499
534
|
))
|
|
500
|
-
] }) : (
|
|
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
|
+
) : (
|
|
501
548
|
// `role="presentation"` and no tab stop: this div is the drop TARGET and
|
|
502
549
|
// the visual affordance, and the control inside it is a real
|
|
503
550
|
// `<input type="file">`, which already has the right role and now has a
|
|
@@ -539,39 +586,43 @@ function FileField({
|
|
|
539
586
|
}
|
|
540
587
|
)
|
|
541
588
|
),
|
|
542
|
-
|
|
589
|
+
!busy && (rejected ? /* @__PURE__ */ jsxs("p", { role: "alert", className: "text-[12px] text-destructive", children: [
|
|
543
590
|
/* @__PURE__ */ jsx("span", { className: "font-mono", children: rejected }),
|
|
544
591
|
" \u2014 ",
|
|
545
592
|
s.unsupportedFileType(hint)
|
|
546
|
-
] }),
|
|
547
|
-
|
|
548
|
-
"button",
|
|
549
|
-
{
|
|
550
|
-
type: "button",
|
|
551
|
-
disabled: busy,
|
|
552
|
-
onClick: () => setShowUrl(true),
|
|
553
|
-
className: "flex w-fit items-center gap-1 font-mono text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
|
|
554
|
-
children: [
|
|
555
|
-
/* @__PURE__ */ jsx(Link2, { className: "h-3 w-3" }),
|
|
556
|
-
s.pasteUrlInstead
|
|
557
|
-
]
|
|
558
|
-
}
|
|
559
|
-
) : /* @__PURE__ */ jsx(
|
|
593
|
+
] }) : failure && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-[12px] text-destructive", children: failure })),
|
|
594
|
+
linkOpen ? /* @__PURE__ */ jsx(
|
|
560
595
|
"input",
|
|
561
596
|
{
|
|
562
597
|
type: "text",
|
|
563
|
-
|
|
598
|
+
id: canUpload ? void 0 : domId,
|
|
599
|
+
autoFocus: showUrl,
|
|
564
600
|
value: urlText,
|
|
565
601
|
disabled: busy,
|
|
566
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,
|
|
567
605
|
placeholder: s.urlPlaceholder,
|
|
568
606
|
onChange: (e) => {
|
|
569
607
|
setLocal(null);
|
|
608
|
+
dismissUploadError();
|
|
570
609
|
setTypedUrl(e.target.value);
|
|
571
610
|
onChange(e.target.value ? { url: e.target.value } : void 0);
|
|
572
611
|
},
|
|
573
612
|
className: cn(fieldControlClass, "h-9 px-3 font-mono text-[12px]")
|
|
574
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
|
+
}
|
|
575
626
|
)
|
|
576
627
|
]
|
|
577
628
|
}
|
|
@@ -1111,6 +1162,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1111
1162
|
const isApp = presentation === "app";
|
|
1112
1163
|
const items = value ?? [];
|
|
1113
1164
|
const [rowKeys, setRowKeys] = useRowKeys(items.length);
|
|
1165
|
+
const [staleErrors, setStaleErrors] = useState(NO_ERRORS);
|
|
1114
1166
|
const setItem = (index, itemValue) => {
|
|
1115
1167
|
const next = items.slice();
|
|
1116
1168
|
next[index] = itemValue;
|
|
@@ -1118,10 +1170,40 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1118
1170
|
};
|
|
1119
1171
|
const removeItem = (index) => {
|
|
1120
1172
|
setRowKeys((prev) => prev.filter((_, i) => i !== index));
|
|
1173
|
+
setStaleErrors((prev) => withRenumberedErrors(prev, id, index, env?.uploadErrors));
|
|
1121
1174
|
onChange(items.filter((_, i) => i !== index));
|
|
1122
1175
|
};
|
|
1123
1176
|
const addItem = () => onChange([...items, emptyValue(field.item)]);
|
|
1124
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]);
|
|
1125
1207
|
const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
|
|
1126
1208
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
1127
1209
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
@@ -1155,7 +1237,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
1155
1237
|
value: item,
|
|
1156
1238
|
onChange: (v) => setItem(index, v),
|
|
1157
1239
|
id: `${id}.${index}`,
|
|
1158
|
-
env
|
|
1240
|
+
env: rowEnv
|
|
1159
1241
|
}
|
|
1160
1242
|
) }),
|
|
1161
1243
|
/* @__PURE__ */ jsx(
|
|
@@ -1199,6 +1281,18 @@ function listIsBusy(id, uploadingIds) {
|
|
|
1199
1281
|
for (const uploading of uploadingIds) if (uploading.startsWith(prefix)) return true;
|
|
1200
1282
|
return false;
|
|
1201
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
|
+
}
|
|
1202
1296
|
function useRowKeys(length) {
|
|
1203
1297
|
const [state, setState] = useState(() => ({
|
|
1204
1298
|
keys: Array.from({ length }, (_, i) => i),
|
|
@@ -1265,6 +1359,7 @@ function UnknownField({ field, value, onChange, id, error, disabled }) {
|
|
|
1265
1359
|
}
|
|
1266
1360
|
function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
1267
1361
|
const disabled = env?.disabled;
|
|
1362
|
+
const drop = env?.onDropFile;
|
|
1268
1363
|
switch (field.kind) {
|
|
1269
1364
|
case "text":
|
|
1270
1365
|
return /* @__PURE__ */ jsx(
|
|
@@ -1344,8 +1439,10 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
|
1344
1439
|
field,
|
|
1345
1440
|
value,
|
|
1346
1441
|
onChange,
|
|
1347
|
-
onDropFile: (file) =>
|
|
1442
|
+
onDropFile: drop ? (file) => drop(id, file) : void 0,
|
|
1443
|
+
allowUrl: env?.allowUrl,
|
|
1348
1444
|
uploading: env?.uploadingIds?.has(id),
|
|
1445
|
+
uploadError: env?.uploadErrors?.get(id),
|
|
1349
1446
|
resolveUrl: env?.resolveUrl,
|
|
1350
1447
|
id,
|
|
1351
1448
|
error,
|
|
@@ -1359,8 +1456,10 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
|
|
|
1359
1456
|
field,
|
|
1360
1457
|
value,
|
|
1361
1458
|
onChange,
|
|
1362
|
-
onDropFile: (file) =>
|
|
1459
|
+
onDropFile: drop ? (file) => drop(id, file) : void 0,
|
|
1460
|
+
allowUrl: env?.allowUrl,
|
|
1363
1461
|
uploading: env?.uploadingIds?.has(id),
|
|
1462
|
+
uploadError: env?.uploadErrors?.get(id),
|
|
1364
1463
|
resolveUrl: env?.resolveUrl,
|
|
1365
1464
|
id,
|
|
1366
1465
|
error,
|
|
@@ -2840,5 +2939,5 @@ function StuffViewer({
|
|
|
2840
2939
|
}
|
|
2841
2940
|
|
|
2842
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 };
|
|
2843
|
-
//# sourceMappingURL=chunk-
|
|
2844
|
-
//# sourceMappingURL=chunk-
|
|
2942
|
+
//# sourceMappingURL=chunk-E2IVBAW3.js.map
|
|
2943
|
+
//# sourceMappingURL=chunk-E2IVBAW3.js.map
|