@pipelex/mthds-form 0.11.0 → 0.12.1

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.
@@ -1,5 +1,6 @@
1
- import { collectStuffFiles } from './chunk-4JQAJT7C.js';
2
- import { viewableUrl, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativeValueNode, readDateContent, formatDateContent, readImageContent, readDataUrl, readDocumentContent } from './chunk-TID7ZN63.js';
1
+ import { planStuffSave, planFileSave } from './chunk-7KQXGG6J.js';
2
+ import { readStuffFile } from './chunk-RRTI7AIR.js';
3
+ import { viewableUrl, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativeValueNode, readDateContent, formatDateContent, readImageContent, readDataUrl, readDocumentContent } from './chunk-CM4KVG53.js';
3
4
  import { conceptCategory } from './chunk-FABY3DEY.js';
4
5
  import { acceptLabel, acceptMap, isAcceptedFile, formatOfFilename, DOCUMENT_FORMATS, IMAGE_FORMATS } from './chunk-XCO6GTEC.js';
5
6
  import { isFilled } from './chunk-PM55AHRO.js';
@@ -7,7 +8,7 @@ import { toStoredDateValue, toDateInputValue } from './chunk-GKV77RAS.js';
7
8
  import { ownProp } from './chunk-V3OV2XQT.js';
8
9
  import { clsx } from 'clsx';
9
10
  import { twMerge } from 'tailwind-merge';
10
- import { ChevronDown, ChevronUp, Check, List, Braces, ListChecks, ToggleLeft, Hash, Image, FileText, CalendarDays, Type, Loader2, ImageOff, Upload, Link2, EyeOff, Eye, X, Minus, Plus, Trash2, Copy, Download, ChevronRight } from 'lucide-react';
11
+ import { ChevronDown, ChevronUp, Check, List, Braces, ListChecks, ToggleLeft, Hash, Image, FileText, CalendarDays, Type, Loader2, ImageOff, Upload, Link2, EyeOff, Eye, X, Minus, Plus, Trash2, Copy, Download, ChevronRight, TriangleAlert } from 'lucide-react';
11
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
13
  import * as React2 from 'react';
13
14
  import { createContext, use, useId, useMemo, useContext, useState, useRef, useCallback, useEffect, Fragment as Fragment$1, useLayoutEffect } from 'react';
@@ -221,6 +222,9 @@ var DEFAULT_FIELD_STRINGS = {
221
222
  copyText: "Copy the text",
222
223
  download: "Download",
223
224
  downloading: "Downloading\u2026",
225
+ downloadIncomplete: (names) => `${names.length === 1 ? "One file" : `${names.length} files`} could not be saved: ${names.join(", ")}`,
226
+ downloadFile: (name) => `Download ${name}`,
227
+ downloadFileFailed: "The file could not be saved",
224
228
  viewRendered: "Result",
225
229
  viewJson: "JSON",
226
230
  copyJson: "Copy the JSON",
@@ -1504,20 +1508,84 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
1504
1508
  );
1505
1509
  }
1506
1510
  }
1511
+
1512
+ // src/react/absolute-url.ts
1513
+ function absoluteUrl(url) {
1514
+ const gated = viewableUrl(url);
1515
+ if (!gated?.startsWith("/") || typeof document === "undefined") return url;
1516
+ try {
1517
+ return new URL(gated, document.baseURI).href;
1518
+ } catch {
1519
+ return url;
1520
+ }
1521
+ }
1522
+
1523
+ // src/react/save-in-browser.ts
1524
+ function saveBlob(blob, filename) {
1525
+ const href = URL.createObjectURL(blob);
1526
+ const anchor = document.createElement("a");
1527
+ anchor.href = href;
1528
+ anchor.download = filename;
1529
+ document.body.appendChild(anchor);
1530
+ anchor.click();
1531
+ anchor.remove();
1532
+ setTimeout(() => URL.revokeObjectURL(href), 0);
1533
+ }
1534
+ function blobType(mimeType) {
1535
+ return /^(text\/|application\/json)/.test(mimeType) && !/charset=/i.test(mimeType) ? `${mimeType};charset=utf-8` : mimeType;
1536
+ }
1537
+ async function saveInBrowser(files) {
1538
+ const failed = [];
1539
+ for (const file of files) {
1540
+ try {
1541
+ if (file.text !== void 0) {
1542
+ saveBlob(new Blob([file.text], { type: blobType(file.mimeType) }), file.name);
1543
+ continue;
1544
+ }
1545
+ const response = await fetch(file.url);
1546
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1547
+ saveBlob(await response.blob(), file.name);
1548
+ } catch (error) {
1549
+ failed.push({ file, reason: error instanceof Error ? error.message : String(error) });
1550
+ }
1551
+ }
1552
+ return { failed };
1553
+ }
1507
1554
  var ResultEnvContext = createContext({});
1508
1555
  function ResultEnvProvider({
1509
1556
  resolveUrl,
1510
1557
  resolveShareUrl,
1511
1558
  proseImages,
1512
1559
  tableColumns,
1560
+ saveFiles,
1561
+ downloads,
1513
1562
  children
1514
1563
  }) {
1515
1564
  const env = useMemo(
1516
- () => ({ resolveUrl, resolveShareUrl, proseImages, tableColumns }),
1517
- [resolveUrl, resolveShareUrl, proseImages, tableColumns]
1565
+ () => ({ resolveUrl, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads }),
1566
+ [resolveUrl, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads]
1518
1567
  );
1519
1568
  return /* @__PURE__ */ jsx(ResultEnvContext, { value: env, children });
1520
1569
  }
1570
+ function DownloadDisplayOverride({
1571
+ downloads,
1572
+ children
1573
+ }) {
1574
+ const env = use(ResultEnvContext);
1575
+ const result = downloads?.result;
1576
+ const files = downloads?.files;
1577
+ const merged = useMemo(
1578
+ () => result === void 0 && files === void 0 ? env : {
1579
+ ...env,
1580
+ downloads: {
1581
+ result: result ?? env.downloads?.result,
1582
+ files: files ?? env.downloads?.files
1583
+ }
1584
+ },
1585
+ [env, result, files]
1586
+ );
1587
+ return /* @__PURE__ */ jsx(ResultEnvContext, { value: merged, children });
1588
+ }
1521
1589
  function useResolvedUrl(url) {
1522
1590
  return use(ResultEnvContext).resolveUrl?.(url) ?? url;
1523
1591
  }
@@ -1527,6 +1595,32 @@ function useResolveUrl() {
1527
1595
  function useResolveShareUrl() {
1528
1596
  return use(ResultEnvContext).resolveShareUrl;
1529
1597
  }
1598
+ function useSaveFiles() {
1599
+ const save = use(ResultEnvContext).saveFiles ?? saveInBrowser;
1600
+ return useCallback((files) => deliver(save, files), [save]);
1601
+ }
1602
+ async function deliver(save, files) {
1603
+ const handed = files.map(
1604
+ (file) => file.url === void 0 ? file : { ...file, url: absoluteUrl(file.url) }
1605
+ );
1606
+ try {
1607
+ const result = await save(handed);
1608
+ if (Array.isArray(result?.failed)) return { failed: result.failed };
1609
+ return {
1610
+ failed: handed.map((file) => ({ file, reason: "The save function reported no result" }))
1611
+ };
1612
+ } catch (error) {
1613
+ const reason = error instanceof Error ? error.message : String(error);
1614
+ return { failed: handed.map((file) => ({ file, reason })) };
1615
+ }
1616
+ }
1617
+ function useResultDownloadShown() {
1618
+ return use(ResultEnvContext).downloads?.result ?? true;
1619
+ }
1620
+ function useFileDownloadShown(kind) {
1621
+ const files = use(ResultEnvContext).downloads?.files ?? true;
1622
+ return typeof files === "boolean" ? files : files.includes(kind);
1623
+ }
1530
1624
  function useProseImages() {
1531
1625
  return use(ResultEnvContext).proseImages ?? "link";
1532
1626
  }
@@ -1536,6 +1630,27 @@ function useTableColumns() {
1536
1630
  if (budget === void 0 || Number.isNaN(budget)) return DEFAULT_TABLE_COLUMNS;
1537
1631
  return Math.max(1, Math.floor(budget));
1538
1632
  }
1633
+ var ResultLocationContext = createContext(void 0);
1634
+ function useResultLocation() {
1635
+ return use(ResultLocationContext);
1636
+ }
1637
+ function ResultRoot({
1638
+ baseName,
1639
+ path,
1640
+ children
1641
+ }) {
1642
+ const key = path.join("\0");
1643
+ const location = useMemo(() => ({ baseName, path: key.split("\0") }), [baseName, key]);
1644
+ return /* @__PURE__ */ jsx(ResultLocationContext, { value: location, children });
1645
+ }
1646
+ function ResultAt({ segment, children }) {
1647
+ const parent = use(ResultLocationContext);
1648
+ const location = useMemo(
1649
+ () => parent ? { baseName: parent.baseName, path: [...parent.path, segment] } : void 0,
1650
+ [parent, segment]
1651
+ );
1652
+ return /* @__PURE__ */ jsx(ResultLocationContext, { value: location, children });
1653
+ }
1539
1654
  var TooltipProvider = TooltipPrimitive.Provider;
1540
1655
  var TooltipRoot = TooltipPrimitive.Root;
1541
1656
  var TooltipTrigger = TooltipPrimitive.Trigger;
@@ -2058,15 +2173,12 @@ function CopyUrlButton({ url, storageUrl }) {
2058
2173
  };
2059
2174
  return /* @__PURE__ */ jsx(CopyButton, { provide, label: s.copyUrl, title: url });
2060
2175
  }
2061
- function absoluteUrl(url) {
2062
- if (!/^\/(?!\/)/.test(url) || typeof window === "undefined") return url;
2063
- return `${window.location.origin}${url}`;
2064
- }
2065
2176
  function FileRef({
2066
2177
  url,
2067
2178
  storageUrl,
2068
2179
  mimeType,
2069
- filename
2180
+ filename,
2181
+ file
2070
2182
  }) {
2071
2183
  const s = useFieldStrings();
2072
2184
  const encoded = encodedFileSummary(url, s);
@@ -2093,7 +2205,80 @@ function FileRef({
2093
2205
  children: label
2094
2206
  }
2095
2207
  ),
2096
- /* @__PURE__ */ jsx(CopyUrlButton, { url, ...storageUrl ? { storageUrl } : {} })
2208
+ /* @__PURE__ */ jsx(FileActions, { copy: { url, ...storageUrl ? { storageUrl } : {} }, file })
2209
+ ] });
2210
+ }
2211
+ function FileActions({
2212
+ copy,
2213
+ file
2214
+ }) {
2215
+ return /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-0.5", children: [
2216
+ copy && /* @__PURE__ */ jsx(
2217
+ CopyUrlButton,
2218
+ {
2219
+ url: copy.url,
2220
+ ...copy.storageUrl ? { storageUrl: copy.storageUrl } : {}
2221
+ }
2222
+ ),
2223
+ file && /* @__PURE__ */ jsx(FileDownloadButton, { file })
2224
+ ] });
2225
+ }
2226
+ function useStuffFile(kind, value) {
2227
+ const location = useResultLocation();
2228
+ return location ? readStuffFile(kind, value, location.path.join(".")) : void 0;
2229
+ }
2230
+ function FileDownloadButton({ file }) {
2231
+ const s = useFieldStrings();
2232
+ const location = useResultLocation();
2233
+ const resolve = useResolveUrl();
2234
+ const save = useSaveFiles();
2235
+ const shown = useFileDownloadShown(file.kind);
2236
+ const statusId = useId();
2237
+ const [attempt, setAttempt] = useState(null);
2238
+ useEffect(() => {
2239
+ if (attempt?.state !== "saved") return;
2240
+ const timer = window.setTimeout(
2241
+ () => setAttempt((current) => current === attempt ? null : current),
2242
+ 1500
2243
+ );
2244
+ return () => window.clearTimeout(timer);
2245
+ }, [attempt]);
2246
+ if (!shown || !location) return null;
2247
+ const planned = planFileSave(file, {
2248
+ baseName: location.baseName,
2249
+ ...resolve ? { resolveUrl: resolve } : {}
2250
+ });
2251
+ if (!planned) return null;
2252
+ const identity = `${planned.name}
2253
+ ${planned.url ?? planned.text}`;
2254
+ const state = attempt?.identity === identity ? attempt.state : "idle";
2255
+ const failed = state === "failed";
2256
+ const label = s.downloadFile(planned.name);
2257
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2258
+ /* @__PURE__ */ jsx(
2259
+ "button",
2260
+ {
2261
+ type: "button",
2262
+ onClick: () => {
2263
+ const started = { identity, state: "saving" };
2264
+ setAttempt(started);
2265
+ void save([planned]).then((result) => {
2266
+ const settled = result.failed.length > 0 ? "failed" : "saved";
2267
+ setAttempt((current) => current === started ? { identity, state: settled } : current);
2268
+ });
2269
+ },
2270
+ disabled: state === "saving",
2271
+ "aria-label": label,
2272
+ title: failed ? s.downloadFileFailed : label,
2273
+ ...failed ? { "aria-describedby": statusId } : {},
2274
+ className: cn(
2275
+ "shrink-0 rounded p-0.5 hover:bg-card focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 disabled:opacity-60",
2276
+ failed ? "text-destructive" : "text-muted-foreground hover:text-foreground"
2277
+ ),
2278
+ children: state === "saving" ? /* @__PURE__ */ jsx(Loader2, { "aria-hidden": true, className: "size-3.5 animate-spin" }) : state === "saved" ? /* @__PURE__ */ jsx(Check, { "aria-hidden": true, className: "size-3.5" }) : failed ? /* @__PURE__ */ jsx(TriangleAlert, { "aria-hidden": true, className: "size-3.5" }) : /* @__PURE__ */ jsx(Download, { "aria-hidden": true, className: "size-3.5" })
2279
+ }
2280
+ ),
2281
+ /* @__PURE__ */ jsx("span", { id: statusId, role: "status", className: "sr-only", children: failed ? s.downloadFileFailed : "" })
2097
2282
  ] });
2098
2283
  }
2099
2284
  function paintable(content, resolve) {
@@ -2135,6 +2320,7 @@ function DocumentValue({ value }) {
2135
2320
  const s = useFieldStrings();
2136
2321
  const resolve = useResolveUrl();
2137
2322
  const [open, setOpen] = useState(false);
2323
+ const file = useStuffFile("document", value);
2138
2324
  const content = readDocumentContent(value);
2139
2325
  if (!content) return /* @__PURE__ */ jsx(Absent, {});
2140
2326
  const name = content.title ?? content.filename ?? fileLabel(content.url, content.filename, s);
@@ -2150,7 +2336,8 @@ function DocumentValue({ value }) {
2150
2336
  url: paintableUrl(content, resolve) ?? content.url,
2151
2337
  storageUrl: content.url,
2152
2338
  mimeType: content.mimeType,
2153
- ...content.filename ? { filename: content.filename } : {}
2339
+ ...content.filename ? { filename: content.filename } : {},
2340
+ file
2154
2341
  }
2155
2342
  ),
2156
2343
  content.snippet && /* @__PURE__ */ jsx("p", { className: "line-clamp-2 text-[12px] leading-relaxed text-muted-foreground", children: content.snippet })
@@ -2180,6 +2367,7 @@ function ImageValue({
2180
2367
  }) {
2181
2368
  const s = useFieldStrings();
2182
2369
  const resolve = useResolveUrl();
2370
+ const file = useStuffFile("image", value);
2183
2371
  const content = readImageContent(value);
2184
2372
  if (!content) return /* @__PURE__ */ jsx(Absent, {});
2185
2373
  const src = paintableUrl(content, resolve);
@@ -2230,7 +2418,8 @@ function ImageValue({
2230
2418
  {
2231
2419
  url: content.url,
2232
2420
  mimeType: content.mimeType,
2233
- ...content.filename ? { filename: content.filename } : {}
2421
+ ...content.filename ? { filename: content.filename } : {},
2422
+ file
2234
2423
  }
2235
2424
  ) })
2236
2425
  ] })
@@ -2255,7 +2444,8 @@ function ImageValue({
2255
2444
  {
2256
2445
  url: content.url,
2257
2446
  mimeType: content.mimeType,
2258
- ...content.filename ? { filename: content.filename } : {}
2447
+ ...content.filename ? { filename: content.filename } : {},
2448
+ file
2259
2449
  }
2260
2450
  ),
2261
2451
  src && !compact && /* @__PURE__ */ jsx("div", { className: inGallery ? "px-2.5" : void 0, children: /* @__PURE__ */ jsx(
@@ -2264,7 +2454,8 @@ function ImageValue({
2264
2454
  url: src,
2265
2455
  storageUrl: content.url,
2266
2456
  mimeType: content.mimeType,
2267
- ...content.filename ? { filename: content.filename } : {}
2457
+ ...content.filename ? { filename: content.filename } : {},
2458
+ file
2268
2459
  }
2269
2460
  ) }),
2270
2461
  content.caption && /* @__PURE__ */ jsx(
@@ -2467,7 +2658,7 @@ function ObjectTable({
2467
2658
  ] }) }),
2468
2659
  /* @__PURE__ */ jsx("tbody", { children: items.map((item, index) => {
2469
2660
  const isOpen = canExpand && expanded.has(index);
2470
- return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border/60 last:border-b-0", children: [
2661
+ return /* @__PURE__ */ jsx(ResultAt, { segment: String(index), children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border/60 last:border-b-0", children: [
2471
2662
  canExpand && /* @__PURE__ */ jsx("td", { className: "px-1 pt-px align-top", children: /* @__PURE__ */ jsx(
2472
2663
  "button",
2473
2664
  {
@@ -2504,7 +2695,7 @@ function ObjectTable({
2504
2695
  column === name ? "max-w-none min-w-[16ch] wrap-anywhere [&>span]:whitespace-normal" : column.kind === "list" && isInlineColumn(column) ? "min-w-[16ch]" : "truncate"
2505
2696
  ),
2506
2697
  ...typeof cell === "string" || typeof cell === "number" ? { title: cellTitle(column, cell, presentation) } : {},
2507
- children: /* @__PURE__ */ jsx(LeafValue, { field: column, value: cell, compact: true })
2698
+ children: /* @__PURE__ */ jsx(ResultAt, { segment: column.name, children: /* @__PURE__ */ jsx(LeafValue, { field: column, value: cell, compact: true }) })
2508
2699
  }
2509
2700
  ) }, column.name);
2510
2701
  })
@@ -2565,10 +2756,10 @@ function ImageGallery({ items }) {
2565
2756
  return content ? !!(viewableUrl(content.publicUrl) ?? viewableUrl(content.url)) : false;
2566
2757
  });
2567
2758
  if (!anyViewable) return /* @__PURE__ */ jsx(FileRows, { items, kind: "image" });
2568
- return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border border-border bg-card/40", children: /* @__PURE__ */ jsx(ImageValue, { value: item, inGallery: true }) }, index)) });
2759
+ return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-lg border border-border bg-card/40", children: /* @__PURE__ */ jsx(ResultAt, { segment: String(index), children: /* @__PURE__ */ jsx(ImageValue, { value: item, inGallery: true }) }) }, index)) });
2569
2760
  }
2570
2761
  function FileRows({ items, kind }) {
2571
- return /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-lg border border-border", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "bg-card/40 px-3.5 py-2", children: kind === "document" ? /* @__PURE__ */ jsx(DocumentValue, { value: item }) : /* @__PURE__ */ jsx(ImageValue, { value: item, inRow: true }) }, index)) });
2762
+ return /* @__PURE__ */ jsx("div", { className: "divide-y divide-border overflow-hidden rounded-lg border border-border", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "bg-card/40 px-3.5 py-2", children: /* @__PURE__ */ jsx(ResultAt, { segment: String(index), children: kind === "document" ? /* @__PURE__ */ jsx(DocumentValue, { value: item }) : /* @__PURE__ */ jsx(ImageValue, { value: item, inRow: true }) }) }, index)) });
2572
2763
  }
2573
2764
  function recordColumns(item) {
2574
2765
  if (item.kind !== "object" || item.fields.length === 0) return void 0;
@@ -2595,9 +2786,16 @@ function chooseColumns(fields, budget) {
2595
2786
  ...name ? { name } : {}
2596
2787
  };
2597
2788
  }
2598
- function ResultField({ field, value, depth = 0, hideLabel = false }) {
2789
+ function ResultField(props) {
2790
+ const location = useResultLocation();
2791
+ if (location) return /* @__PURE__ */ jsx(ResultNode, { ...props });
2792
+ return /* @__PURE__ */ jsx(ResultRoot, { baseName: props.field.name, path: [props.field.name], children: /* @__PURE__ */ jsx(ResultNode, { ...props }) });
2793
+ }
2794
+ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2599
2795
  const s = useFieldStrings();
2600
2796
  const unwrapped = unwrap(field, value);
2797
+ const pageDownloadShown = useFileDownloadShown("markup");
2798
+ const location = useResultLocation();
2601
2799
  const header = hideLabel ? null : /* @__PURE__ */ jsx(Label2, { field, describe: true });
2602
2800
  const copyText = (field.kind === "text" || field.kind === "prose") && typeof unwrapped === "string" && unwrapped !== "" ? unwrapped : void 0;
2603
2801
  if (isNativeDateNode(field)) {
@@ -2608,8 +2806,12 @@ function ResultField({ field, value, depth = 0, hideLabel = false }) {
2608
2806
  }
2609
2807
  if (isNativeHtmlNode(field)) {
2610
2808
  const content = readHtmlContent(unwrapped);
2809
+ const page = pageDownloadShown && location ? readStuffFile("markup", unwrapped, location.path.join(".")) : void 0;
2611
2810
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2612
- header,
2811
+ page ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
2812
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: header }),
2813
+ /* @__PURE__ */ jsx(FileActions, { file: page })
2814
+ ] }) : header,
2613
2815
  content ? /* @__PURE__ */ jsx(HtmlPreview, { content }) : /* @__PURE__ */ jsx(Absent, {})
2614
2816
  ] });
2615
2817
  }
@@ -2657,7 +2859,7 @@ function ResultField({ field, value, depth = 0, hideLabel = false }) {
2657
2859
  isInlineColumn(child) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
2658
2860
  /* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsx(Label2, { field: child, describe: true }) }),
2659
2861
  /* @__PURE__ */ jsx("div", { className: "flex min-w-0 justify-end **:data-chips:justify-end", children: /* @__PURE__ */ jsx("div", { className: "min-w-0 text-left wrap-break-word", children: /* @__PURE__ */ jsx(LeafValue, { field: child, value: memberValue(child) }) }) })
2660
- ] }, child.name) : /* @__PURE__ */ jsx("div", { className: "col-span-2 min-w-0", children: /* @__PURE__ */ jsx(ResultField, { field: child, value: memberValue(child), depth: depth + 1 }) }, child.name)
2862
+ ] }, child.name) : /* @__PURE__ */ jsx("div", { className: "col-span-2 min-w-0", children: /* @__PURE__ */ jsx(ResultAt, { segment: child.name, children: /* @__PURE__ */ jsx(ResultField, { field: child, value: memberValue(child), depth: depth + 1 }) }) }, child.name)
2661
2863
  )
2662
2864
  )
2663
2865
  }
@@ -2679,7 +2881,7 @@ function ResultField({ field, value, depth = 0, hideLabel = false }) {
2679
2881
  // date's `date` and `time`, print a page's markup source down a
2680
2882
  // column named by it, and show a composite as JSON. The arm keyed by
2681
2883
  // concept is the one that knows what each entry is.
2682
- /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60 overflow-hidden rounded-lg border border-border", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "bg-card/40 px-3 py-1.5", children: /* @__PURE__ */ jsx(ResultField, { field: field.item, value: item, depth: depth + 1, hideLabel: true }) }, index)) })
2884
+ /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/60 overflow-hidden rounded-lg border border-border", children: items.map((item, index) => /* @__PURE__ */ jsx("div", { className: "bg-card/40 px-3 py-1.5", children: /* @__PURE__ */ jsx(ResultAt, { segment: String(index), children: /* @__PURE__ */ jsx(ResultField, { field: field.item, value: item, depth: depth + 1, hideLabel: true }) }) }, index)) })
2683
2885
  ) : columns ? (
2684
2886
  // Same shape every row: the labels are column headers, not per-row
2685
2887
  // labels. See `ObjectTable`.
@@ -2714,7 +2916,7 @@ function ResultField({ field, value, depth = 0, hideLabel = false }) {
2714
2916
  className: "space-y-2 rounded-lg border border-border bg-card/40 px-3.5 py-3",
2715
2917
  children: [
2716
2918
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-muted-foreground", children: index + 1 }),
2717
- /* @__PURE__ */ jsx(ResultField, { field: field.item, value: item, depth: depth + 1, hideLabel: true })
2919
+ /* @__PURE__ */ jsx(ResultAt, { segment: String(index), children: /* @__PURE__ */ jsx(ResultField, { field: field.item, value: item, depth: depth + 1, hideLabel: true }) })
2718
2920
  ]
2719
2921
  },
2720
2922
  index
@@ -2741,67 +2943,6 @@ function ResultField({ field, value, depth = 0, hideLabel = false }) {
2741
2943
  }
2742
2944
  return null;
2743
2945
  }
2744
-
2745
- // src/react/download-stuff.ts
2746
- function saveBlob(blob, filename) {
2747
- const href = URL.createObjectURL(blob);
2748
- const anchor = document.createElement("a");
2749
- anchor.href = href;
2750
- anchor.download = filename;
2751
- document.body.appendChild(anchor);
2752
- anchor.click();
2753
- anchor.remove();
2754
- setTimeout(() => URL.revokeObjectURL(href), 0);
2755
- }
2756
- function mimeForExtension(extension) {
2757
- return extension === "html" ? "text/html;charset=utf-8" : "text/plain;charset=utf-8";
2758
- }
2759
- function extensionOf(url, filename) {
2760
- const named = filename?.match(/\.([A-Za-z0-9]{1,8})$/)?.[1];
2761
- if (named) return named.toLowerCase();
2762
- const path = url.split(/[?#]/)[0] ?? url;
2763
- const match = path.match(/\.([A-Za-z0-9]{1,8})$/)?.[1];
2764
- return match ? match.toLowerCase() : "";
2765
- }
2766
- function fileNameFor(file, base) {
2767
- if (file.filename) return file.filename;
2768
- const extension = file.extension ?? extensionOf(file.url ?? "", file.filename);
2769
- const stem = `${base}-${file.path.replace(/\./g, "-")}`;
2770
- return extension ? `${stem}.${extension}` : stem;
2771
- }
2772
- async function downloadStuff({
2773
- field,
2774
- value,
2775
- baseName,
2776
- resolveUrl
2777
- }) {
2778
- const files = collectStuffFiles(field, value);
2779
- const isBareFile = files.length === 1 && files[0]?.path === field.name;
2780
- let deliveredAFile = false;
2781
- for (const file of files) {
2782
- const name = fileNameFor(file, baseName);
2783
- if (file.text !== void 0) {
2784
- saveBlob(new Blob([file.text], { type: mimeForExtension(file.extension) }), name);
2785
- deliveredAFile = true;
2786
- continue;
2787
- }
2788
- const href = viewableUrl(resolveUrl?.(file.url ?? "")) ?? viewableUrl(file.publicUrl) ?? viewableUrl(file.url);
2789
- if (!href) continue;
2790
- try {
2791
- const response = await fetch(href);
2792
- if (!response.ok) throw new Error(String(response.status));
2793
- saveBlob(await response.blob(), name);
2794
- } catch {
2795
- window.open(href, "_blank", "noopener");
2796
- }
2797
- deliveredAFile = true;
2798
- }
2799
- if (isBareFile && deliveredAFile) return;
2800
- saveBlob(
2801
- new Blob([JSON.stringify(value, null, 2)], { type: "application/json;charset=utf-8" }),
2802
- `${baseName}.json`
2803
- );
2804
- }
2805
2946
  var JSON_TOKEN_RE = /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
2806
2947
  function highlight(json) {
2807
2948
  const out = [];
@@ -2861,33 +3002,73 @@ function JsonView({ value }) {
2861
3002
  /* @__PURE__ */ jsx("pre", { className: "overflow-x-auto rounded-lg border border-border bg-card/40 px-3.5 py-3 pr-12 font-mono text-[12px] leading-relaxed", children: highlight(text) })
2862
3003
  ] });
2863
3004
  }
2864
- function StuffViewer({
3005
+ function isShownResult(attempt, field, baseName, value) {
3006
+ if (attempt.field === field && attempt.baseName === baseName && attempt.value === value) {
3007
+ return true;
3008
+ }
3009
+ return attempt.key !== void 0 && attempt.key === resultKey(field, baseName, value);
3010
+ }
3011
+ function resultKey(field, baseName, value) {
3012
+ try {
3013
+ return JSON.stringify(
3014
+ [baseName, field, value],
3015
+ (_key, member) => typeof member === "bigint" ? `${member}n` : member
3016
+ );
3017
+ } catch {
3018
+ return void 0;
3019
+ }
3020
+ }
3021
+ function StuffViewer({ downloads, ...props }) {
3022
+ return /* @__PURE__ */ jsx(DownloadDisplayOverride, { downloads, children: /* @__PURE__ */ jsx(StuffPanel, { ...props }) });
3023
+ }
3024
+ function StuffPanel({
2865
3025
  field,
2866
3026
  value,
2867
3027
  name,
2868
3028
  defaultView = "rendered",
2869
3029
  downloadBaseName,
2870
- hideDownload = false,
2871
3030
  className
2872
3031
  }) {
2873
3032
  const s = useFieldStrings();
2874
3033
  const named = name ? { ...field, name } : field;
3034
+ const baseName = downloadBaseName ?? named.name ?? "result";
2875
3035
  const [view, setView] = useState(defaultView);
2876
- const [saving, setSaving] = useState(false);
3036
+ const [saving, setSaving] = useState(null);
3037
+ const [missed, setMissed] = useState(null);
3038
+ const latest = useRef(null);
3039
+ const savingShown = saving !== null && isShownResult(saving, field, baseName, value);
3040
+ const missedShown = missed && isShownResult(missed.attempt, field, baseName, value) ? missed.names : [];
2877
3041
  const resolveUrl = useResolveUrl();
3042
+ const save = useSaveFiles();
3043
+ const downloadShown = useResultDownloadShown();
2878
3044
  const handleDownload = useCallback(async () => {
2879
- setSaving(true);
3045
+ const attempt = {
3046
+ field,
3047
+ baseName,
3048
+ value,
3049
+ key: resultKey(field, baseName, value)
3050
+ };
3051
+ latest.current = attempt;
3052
+ setSaving(attempt);
3053
+ setMissed(null);
2880
3054
  try {
2881
- await downloadStuff({
2882
- field,
2883
- value,
2884
- baseName: downloadBaseName ?? named.name ?? "result",
2885
- resolveUrl
3055
+ const plan = planStuffSave(field, value, {
3056
+ baseName,
3057
+ ...resolveUrl ? { resolveUrl } : {}
3058
+ });
3059
+ const { failed } = await save(plan.files);
3060
+ if (latest.current !== attempt) return;
3061
+ setMissed({
3062
+ attempt,
3063
+ names: [
3064
+ ...plan.unavailable.map((file) => file.name),
3065
+ ...failed.map((failure) => failure.file.name)
3066
+ ]
2886
3067
  });
2887
3068
  } finally {
2888
- setSaving(false);
3069
+ setSaving((current) => current === attempt ? null : current);
2889
3070
  }
2890
- }, [field, named.name, value, downloadBaseName, resolveUrl]);
3071
+ }, [field, value, baseName, resolveUrl, save]);
2891
3072
  const views = [
2892
3073
  { id: "rendered", label: s.viewRendered },
2893
3074
  { id: "json", label: s.viewJson }
@@ -2896,17 +3077,17 @@ function StuffViewer({
2896
3077
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-4 gap-y-1", children: [
2897
3078
  /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-1", children: /* @__PURE__ */ jsx(ResultHeader, { field: named }) }),
2898
3079
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
2899
- !hideDownload && /* @__PURE__ */ jsxs(
3080
+ downloadShown && /* @__PURE__ */ jsxs(
2900
3081
  "button",
2901
3082
  {
2902
3083
  type: "button",
2903
3084
  onClick: () => void handleDownload(),
2904
- disabled: saving,
3085
+ disabled: savingShown,
2905
3086
  "aria-label": s.download,
2906
3087
  className: "inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 disabled:opacity-60",
2907
3088
  children: [
2908
- saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
2909
- saving ? s.downloading : s.download
3089
+ savingShown ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
3090
+ savingShown ? s.downloading : s.download
2910
3091
  ]
2911
3092
  }
2912
3093
  ),
@@ -2934,10 +3115,18 @@ function StuffViewer({
2934
3115
  )
2935
3116
  ] })
2936
3117
  ] }),
2937
- view === "json" ? /* @__PURE__ */ jsx(JsonView, { value }) : /* @__PURE__ */ jsx(ResultField, { field, value, hideLabel: true })
3118
+ downloadShown && /* @__PURE__ */ jsx(
3119
+ "p",
3120
+ {
3121
+ role: "status",
3122
+ className: cn("text-[12px] text-destructive", missedShown.length === 0 && "sr-only"),
3123
+ children: missedShown.length > 0 ? s.downloadIncomplete(missedShown) : ""
3124
+ }
3125
+ ),
3126
+ view === "json" ? /* @__PURE__ */ jsx(JsonView, { value }) : /* @__PURE__ */ jsx(ResultRoot, { baseName, path: [field.name], children: /* @__PURE__ */ jsx(ResultField, { field, value, hideLabel: true }) })
2938
3127
  ] });
2939
3128
  }
2940
3129
 
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 };
2942
- //# sourceMappingURL=chunk-E2IVBAW3.js.map
2943
- //# sourceMappingURL=chunk-E2IVBAW3.js.map
3130
+ 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, ResultRoot, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, humanizeFieldName, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
3131
+ //# sourceMappingURL=chunk-2D4NEFUH.js.map
3132
+ //# sourceMappingURL=chunk-2D4NEFUH.js.map