@pipelex/mthds-form 0.13.0 → 0.14.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 (40) hide show
  1. package/README.md +11 -0
  2. package/dist/{chunk-IR3OFSGD.js → chunk-2EGRONBV.js} +3 -3
  3. package/dist/{chunk-IR3OFSGD.js.map → chunk-2EGRONBV.js.map} +1 -1
  4. package/dist/{chunk-D2EQVWGN.js → chunk-7JRT6ZII.js} +1631 -171
  5. package/dist/chunk-7JRT6ZII.js.map +1 -0
  6. package/dist/{chunk-7KQXGG6J.js → chunk-ICUEHNEH.js} +19 -6
  7. package/dist/chunk-ICUEHNEH.js.map +1 -0
  8. package/dist/{chunk-DARLDVJM.js → chunk-JIGRWJTV.js} +2 -2
  9. package/dist/chunk-JIGRWJTV.js.map +1 -0
  10. package/dist/chunk-VH5CDQDR.js +41 -0
  11. package/dist/chunk-VH5CDQDR.js.map +1 -0
  12. package/dist/{chunk-G6SFMTL4.js → chunk-VT5E5W4E.js} +4 -3
  13. package/dist/chunk-VT5E5W4E.js.map +1 -0
  14. package/dist/{chunk-MJQ7CXUB.js → chunk-XO5OENNF.js} +25 -4
  15. package/dist/chunk-XO5OENNF.js.map +1 -0
  16. package/dist/core/gate.js +2 -2
  17. package/dist/core/index.d.ts +2 -1
  18. package/dist/core/index.js +6 -5
  19. package/dist/core/output-form.d.ts +5 -4
  20. package/dist/core/output-form.js +1 -1
  21. package/dist/core/readiness.d.ts +47 -2
  22. package/dist/core/readiness.js +1 -1
  23. package/dist/core/save-plan.d.ts +31 -1
  24. package/dist/core/save-plan.js +1 -1
  25. package/dist/core/seed.d.ts +56 -0
  26. package/dist/core/seed.js +5 -0
  27. package/dist/core/seed.js.map +1 -0
  28. package/dist/core/values.js +2 -2
  29. package/dist/generative/index.d.ts +2 -11
  30. package/dist/generative/index.js +11 -17
  31. package/dist/generative/index.js.map +1 -1
  32. package/dist/react/index.d.ts +699 -192
  33. package/dist/react/index.js +9 -3
  34. package/dist/styles.css +1 -1
  35. package/package.json +3 -1
  36. package/dist/chunk-7KQXGG6J.js.map +0 -1
  37. package/dist/chunk-D2EQVWGN.js.map +0 -1
  38. package/dist/chunk-DARLDVJM.js.map +0 -1
  39. package/dist/chunk-G6SFMTL4.js.map +0 -1
  40. package/dist/chunk-MJQ7CXUB.js.map +0 -1
@@ -1,14 +1,17 @@
1
- import { planStuffSave, planFileSave } from './chunk-7KQXGG6J.js';
1
+ import { seedInputs, closeOptionalStructures, openOptionalStructures } from './chunk-VH5CDQDR.js';
2
+ import { setValueAtPath, apiInputsFromRunValues } from './chunk-VT5E5W4E.js';
3
+ import { planFileSave, planStuffSave, splitFileName, typedFileStem } from './chunk-ICUEHNEH.js';
2
4
  import { readStuffFile } from './chunk-RRTI7AIR.js';
3
5
  import { viewableUrl, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativeValueNode, readDateContent, formatDateContent, readImageContent, readDataUrl, readDocumentContent } from './chunk-CM4KVG53.js';
6
+ import { buildRunFields } from './chunk-ZB4RYUKV.js';
4
7
  import { conceptCategory } from './chunk-FABY3DEY.js';
5
8
  import { acceptLabel, acceptMap, isAcceptedFile, formatOfFilename, DOCUMENT_FORMATS, IMAGE_FORMATS } from './chunk-XCO6GTEC.js';
6
- import { shownAsOptional, isFilled } from './chunk-MJQ7CXUB.js';
9
+ import { shownAsOptional, isFoldedOptional, anyOptionalStructureOpen, isOptionalStructure, computeReadiness, optionalStructureOpen, mustBeFilled, fieldFilled, isFilled } from './chunk-XO5OENNF.js';
7
10
  import { toStoredDateValue, toDateInputValue } from './chunk-GKV77RAS.js';
8
11
  import { ownProp } from './chunk-V3OV2XQT.js';
9
12
  import { clsx } from 'clsx';
10
13
  import { twMerge } from 'tailwind-merge';
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';
14
+ import { ChevronDown, ChevronUp, Check, List, Braces, ListChecks, ToggleLeft, Hash, Image as Image$1, FileText, CalendarDays, Type, Loader2, ImageOff, Upload, Link2, EyeOff, Eye, X, Minus, Plus, Trash2, TriangleAlert, FileDown, Copy, Download, ChevronRight } from 'lucide-react';
12
15
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
16
  import * as React2 from 'react';
14
17
  import { createContext, use, useId, useMemo, useContext, useState, useRef, useCallback, useEffect, Fragment as Fragment$1, useLayoutEffect } from 'react';
@@ -28,7 +31,7 @@ var CATEGORY_ICON = {
28
31
  text: Type,
29
32
  date: CalendarDays,
30
33
  document: FileText,
31
- image: Image,
34
+ image: Image$1,
32
35
  number: Hash,
33
36
  boolean: ToggleLeft,
34
37
  choice: ListChecks,
@@ -200,7 +203,7 @@ var EN_FIELD_STRINGS = {
200
203
  addItem: "Add item",
201
204
  removeItemAria: (index) => `Remove item ${index}`,
202
205
  itemsCount: (count) => count === 1 ? "1 item" : `${count} items`,
203
- itemsCountOf: (count, total) => `${count} of ${total} ${total === 1 ? "item" : "items"}`,
206
+ itemsCountOf: (count, total2) => `${count} of ${total2} ${total2 === 1 ? "item" : "items"}`,
204
207
  fieldsCount: (count) => count === 1 ? "1 field" : `${count} fields`,
205
208
  uploading: "Uploading\u2026",
206
209
  dropToUpload: "Drop to upload",
@@ -227,6 +230,14 @@ var EN_FIELD_STRINGS = {
227
230
  downloadIncomplete: (names) => `${names.length === 1 ? "One file" : `${names.length} files`} could not be saved: ${names.join(", ")}`,
228
231
  downloadFile: (name) => `Download ${name}`,
229
232
  downloadFileFailed: "The file could not be saved",
233
+ downloadPdf: "Download PDF",
234
+ downloadPdfBusy: "Preparing the PDF\u2026",
235
+ downloadPdfFailed: "The PDF could not be made",
236
+ pageLoading: "Loading the page\u2026",
237
+ downloadHtml: "Download HTML",
238
+ fileNameTitle: "File name",
239
+ fileNameCancel: "Cancel",
240
+ fileNameConfirm: "Download",
230
241
  viewRendered: "Result",
231
242
  viewJson: "JSON",
232
243
  copyJson: "Copy the JSON",
@@ -237,7 +248,10 @@ var EN_FIELD_STRINGS = {
237
248
  hideOptionalFields: "Hide optional fields",
238
249
  hideOptionalInputs: "Hide optional inputs",
239
250
  optionalFieldsCount: (count) => count === 1 ? "1 optional field" : `${count} optional fields`,
240
- optionalInputsCount: (count) => count === 1 ? "1 optional input" : `${count} optional inputs`
251
+ optionalInputsCount: (count) => count === 1 ? "1 optional input" : `${count} optional inputs`,
252
+ requiredField: "Required",
253
+ incompleteField: "Incomplete: fill in its required fields",
254
+ uploadFailed: "This file could not be uploaded. Try again."
241
255
  };
242
256
 
243
257
  // src/react/locales/fr.ts
@@ -275,7 +289,7 @@ var FR_FIELD_STRINGS = {
275
289
  addItem: "Ajouter un \xE9l\xE9ment",
276
290
  removeItemAria: (index) => `Supprimer l\u2019\xE9l\xE9ment ${index}`,
277
291
  itemsCount: (count) => `${count} ${plural(count, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
278
- itemsCountOf: (count, total) => `${count} sur ${total} ${plural(total, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
292
+ itemsCountOf: (count, total2) => `${count} sur ${total2} ${plural(total2, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
279
293
  fieldsCount: (count) => `${count} ${plural(count, "champ", "champs")}`,
280
294
  uploading: "Envoi en cours\u2026",
281
295
  dropToUpload: "D\xE9posez pour envoyer",
@@ -302,6 +316,14 @@ var FR_FIELD_STRINGS = {
302
316
  downloadIncomplete: (names) => `${names.length <= 1 ? "Un fichier n\u2019a pas pu \xEAtre enregistr\xE9" : `${names.length} fichiers n\u2019ont pas pu \xEAtre enregistr\xE9s`}\xA0: ${names.join(", ")}`,
303
317
  downloadFile: (name) => `T\xE9l\xE9charger ${name}`,
304
318
  downloadFileFailed: "Le fichier n\u2019a pas pu \xEAtre enregistr\xE9",
319
+ downloadPdf: "T\xE9l\xE9charger le PDF",
320
+ downloadPdfBusy: "Pr\xE9paration du PDF\u2026",
321
+ downloadPdfFailed: "Le PDF n\u2019a pas pu \xEAtre cr\xE9\xE9",
322
+ pageLoading: "Chargement de la page\u2026",
323
+ downloadHtml: "T\xE9l\xE9charger le HTML",
324
+ fileNameTitle: "Nom du fichier",
325
+ fileNameCancel: "Annuler",
326
+ fileNameConfirm: "T\xE9l\xE9charger",
305
327
  viewRendered: "R\xE9sultat",
306
328
  viewJson: "JSON",
307
329
  copyJson: "Copier le JSON",
@@ -312,7 +334,10 @@ var FR_FIELD_STRINGS = {
312
334
  hideOptionalFields: "Masquer les champs facultatifs",
313
335
  hideOptionalInputs: "Masquer les entr\xE9es facultatives",
314
336
  optionalFieldsCount: (count) => `${count} ${plural(count, "champ facultatif", "champs facultatifs")}`,
315
- optionalInputsCount: (count) => `${count} ${plural(count, "entr\xE9e facultative", "entr\xE9es facultatives")}`
337
+ optionalInputsCount: (count) => `${count} ${plural(count, "entr\xE9e facultative", "entr\xE9es facultatives")}`,
338
+ requiredField: "Obligatoire",
339
+ incompleteField: "Incomplet\xA0: remplissez ses champs obligatoires",
340
+ uploadFailed: "Ce fichier n\u2019a pas pu \xEAtre envoy\xE9. R\xE9essayez."
316
341
  };
317
342
 
318
343
  // src/react/locales/index.ts
@@ -1183,6 +1208,7 @@ function OptionalToggle({ count, expanded, onToggle, noun = "field" }) {
1183
1208
  {
1184
1209
  type: "button",
1185
1210
  onClick: onToggle,
1211
+ "aria-expanded": expanded,
1186
1212
  className: "flex w-fit items-center gap-1.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground",
1187
1213
  children: [
1188
1214
  expanded ? /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
@@ -1192,16 +1218,24 @@ function OptionalToggle({ count, expanded, onToggle, noun = "field" }) {
1192
1218
  );
1193
1219
  }
1194
1220
  function ObjectField({ field, value, onChange, id, error, env }) {
1195
- const s = useFieldStrings();
1196
1221
  const [showOptional, setShowOptional] = useState(false);
1197
1222
  const presentation = useFieldPresentation();
1198
1223
  const isApp = presentation === "app";
1199
1224
  const data = value ?? {};
1200
1225
  const setChild = (name, childValue) => onChange({ ...data, [name]: childValue });
1201
- const optionalEmpty = field.fields.filter(
1202
- (f) => shownAsOptional(f) && !isFilled(ownProp(data, f.name))
1203
- );
1204
- const visible = showOptional ? field.fields : field.fields.filter((f) => !shownAsOptional(f) || isFilled(ownProp(data, f.name)));
1226
+ const folded = field.fields.filter((f) => isFoldedOptional(f, ownProp(data, f.name)));
1227
+ const structureOpen = anyOptionalStructureOpen(field.fields, data);
1228
+ const expanded = showOptional || structureOpen;
1229
+ const visible = expanded ? field.fields : field.fields.filter((f) => !isFoldedOptional(f, ownProp(data, f.name)));
1230
+ const hasStructure = field.fields.some(isOptionalStructure);
1231
+ const toggle = () => {
1232
+ setShowOptional(!expanded);
1233
+ if (hasStructure) {
1234
+ onChange(
1235
+ expanded ? closeOptionalStructures(field.fields, data) : openOptionalStructures(field.fields, data)
1236
+ );
1237
+ }
1238
+ };
1205
1239
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1206
1240
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
1207
1241
  /* @__PURE__ */ jsx(
@@ -1214,8 +1248,7 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1214
1248
  children: fieldLabel(field.title, field.name, presentation)
1215
1249
  }
1216
1250
  ),
1217
- !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "structured" }),
1218
- shownAsOptional(field) && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1251
+ !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "structured" })
1219
1252
  ] }),
1220
1253
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
1221
1254
  /* @__PURE__ */ jsxs(
@@ -1237,12 +1270,12 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1237
1270
  },
1238
1271
  child.name
1239
1272
  )),
1240
- optionalEmpty.length > 0 && /* @__PURE__ */ jsx(
1273
+ (folded.length > 0 || structureOpen) && /* @__PURE__ */ jsx(
1241
1274
  OptionalToggle,
1242
1275
  {
1243
- count: optionalEmpty.length,
1244
- expanded: showOptional,
1245
- onToggle: () => setShowOptional((v) => !v),
1276
+ count: folded.length,
1277
+ expanded,
1278
+ onToggle: toggle,
1246
1279
  noun: "field"
1247
1280
  }
1248
1281
  )
@@ -1329,7 +1362,12 @@ function ListField({ field, value, onChange, id, error, env }) {
1329
1362
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
1330
1363
  FieldRenderer,
1331
1364
  {
1332
- field: { ...field.item, title: "", name: `${field.name}[${index}]` },
1365
+ field: {
1366
+ ...field.item,
1367
+ title: "",
1368
+ name: `${field.name}[${index}]`,
1369
+ required: true
1370
+ },
1333
1371
  value: item,
1334
1372
  onChange: (v) => setItem(index, v),
1335
1373
  id: `${id}.${index}`,
@@ -1600,6 +1638,192 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
1600
1638
  );
1601
1639
  }
1602
1640
  }
1641
+ var EMPTY_IDS = /* @__PURE__ */ new Set();
1642
+ function withId(ids, id) {
1643
+ if (ids.has(id)) return ids;
1644
+ const next = new Set(ids);
1645
+ next.add(id);
1646
+ return next;
1647
+ }
1648
+ function withoutId(ids, id) {
1649
+ if (!ids.has(id)) return ids;
1650
+ const next = new Set(ids);
1651
+ next.delete(id);
1652
+ return next;
1653
+ }
1654
+ function orderFields(fields, order) {
1655
+ if (!order || order.length === 0) return fields;
1656
+ const rank = (field) => {
1657
+ const index = order.indexOf(field.name);
1658
+ return index === -1 ? order.length : index;
1659
+ };
1660
+ return fields.map((field, index) => ({ field, index })).sort((a, b) => rank(a.field) - rank(b.field) || a.index - b.index).map(({ field }) => field);
1661
+ }
1662
+ function useMethodForm({
1663
+ descriptor,
1664
+ contract,
1665
+ prepareFields,
1666
+ order,
1667
+ initialValues,
1668
+ uploadFile
1669
+ }) {
1670
+ const orderKey = order ? order.join("\0") : "";
1671
+ const fields = useMemo(() => {
1672
+ const derived = buildRunFields(descriptor, contract.inputs);
1673
+ const prepared = prepareFields ? prepareFields(derived) : derived;
1674
+ return orderFields(prepared, orderKey ? orderKey.split("\0") : void 0);
1675
+ }, [descriptor, contract.inputs, prepareFields, orderKey]);
1676
+ const initial = useMemo(() => {
1677
+ if (typeof initialValues === "function") return initialValues(seedInputs(fields));
1678
+ return initialValues ?? seedInputs(fields);
1679
+ }, [initialValues, fields]);
1680
+ const [edited, setEdited] = useState(null);
1681
+ const [uploadingIds, setUploadingIds] = useState(EMPTY_IDS);
1682
+ const [failedUploadIds, setFailedUploadIds] = useState(EMPTY_IDS);
1683
+ const [attempted, setAttempted] = useState(false);
1684
+ const pendingUploads = useRef(/* @__PURE__ */ new Map());
1685
+ const uploadCounter = useRef(0);
1686
+ const values = edited ?? initial;
1687
+ const setValues = useCallback(
1688
+ (next) => setEdited((previous) => typeof next === "function" ? next(previous ?? initial) : next),
1689
+ [initial]
1690
+ );
1691
+ const reset = useCallback((next) => {
1692
+ pendingUploads.current.clear();
1693
+ setEdited(next ?? null);
1694
+ setUploadingIds(EMPTY_IDS);
1695
+ setFailedUploadIds(EMPTY_IDS);
1696
+ setAttempted(false);
1697
+ }, []);
1698
+ const dropFile = useMemo(() => {
1699
+ if (!uploadFile) return void 0;
1700
+ return (id, file) => {
1701
+ const token = ++uploadCounter.current;
1702
+ pendingUploads.current.set(id, token);
1703
+ const current = () => pendingUploads.current.get(id) === token;
1704
+ setUploadingIds((previous) => withId(previous, id));
1705
+ setFailedUploadIds((previous) => withoutId(previous, id));
1706
+ const path = id.split(".");
1707
+ let upload;
1708
+ try {
1709
+ upload = Promise.resolve(uploadFile(file, { id, path }));
1710
+ } catch (error) {
1711
+ upload = Promise.reject(error);
1712
+ }
1713
+ upload.then(
1714
+ (stored) => {
1715
+ if (!current()) return;
1716
+ const value = typeof stored === "string" ? { url: stored, filename: file.name } : { url: stored.url, filename: stored.filename ?? file.name };
1717
+ setEdited((previous) => setValueAtPath(previous ?? initial, path, value));
1718
+ },
1719
+ () => {
1720
+ if (current()) setFailedUploadIds((previous) => withId(previous, id));
1721
+ }
1722
+ ).finally(() => {
1723
+ if (!current()) return;
1724
+ pendingUploads.current.delete(id);
1725
+ setUploadingIds((previous) => withoutId(previous, id));
1726
+ });
1727
+ };
1728
+ }, [uploadFile, initial]);
1729
+ const readiness = useMemo(() => computeReadiness(fields, values), [fields, values]);
1730
+ const inputs = useMemo(
1731
+ () => apiInputsFromRunValues(values, fields, contract.inputs),
1732
+ [values, fields, contract.inputs]
1733
+ );
1734
+ const uploading = uploadingIds.size > 0;
1735
+ const ready = readiness.ready === readiness.total && !uploading;
1736
+ const attempt = useCallback(() => {
1737
+ setAttempted(true);
1738
+ return ready;
1739
+ }, [ready]);
1740
+ return {
1741
+ fields,
1742
+ values,
1743
+ inputs,
1744
+ readiness,
1745
+ uploading,
1746
+ ready,
1747
+ attempted,
1748
+ setValues,
1749
+ reset,
1750
+ attempt,
1751
+ uploadingIds,
1752
+ failedUploadIds,
1753
+ dropFile
1754
+ };
1755
+ }
1756
+ function MethodForm({ locale, strings, presentation, ...props }) {
1757
+ let body = /* @__PURE__ */ jsx(MethodFormFields, { ...props });
1758
+ if (presentation) {
1759
+ body = /* @__PURE__ */ jsx(FieldPresentationProvider, { presentation, children: body });
1760
+ }
1761
+ if (locale !== void 0 || strings !== void 0) {
1762
+ body = /* @__PURE__ */ jsx(FieldStringsProvider, { locale, strings, children: body });
1763
+ }
1764
+ return body;
1765
+ }
1766
+ function MethodFormFields({
1767
+ form,
1768
+ disabled,
1769
+ allowUrl,
1770
+ resolveUrl,
1771
+ foldOptional = true,
1772
+ errors,
1773
+ className
1774
+ }) {
1775
+ const s = useFieldStrings();
1776
+ const [showOptional, setShowOptional] = useState(false);
1777
+ const { fields, values, setValues, attempted, uploadingIds, failedUploadIds, dropFile } = form;
1778
+ const uploadErrors = useMemo(
1779
+ () => new Map([...failedUploadIds].map((id) => [id, s.uploadFailed])),
1780
+ [failedUploadIds, s.uploadFailed]
1781
+ );
1782
+ const env = useMemo(
1783
+ () => ({ disabled, onDropFile: dropFile, uploadingIds, uploadErrors, allowUrl, resolveUrl }),
1784
+ [disabled, dropFile, uploadingIds, uploadErrors, allowUrl, resolveUrl]
1785
+ );
1786
+ const folds = (field) => {
1787
+ const value = ownProp(values, field.name);
1788
+ return foldOptional ? isFoldedOptional(field, value) : isOptionalStructure(field) && !optionalStructureOpen(value);
1789
+ };
1790
+ const folded = fields.filter(folds);
1791
+ const structureOpen = anyOptionalStructureOpen(fields, values);
1792
+ const expanded = showOptional || structureOpen;
1793
+ const visible = expanded ? fields : fields.filter((field) => !folds(field));
1794
+ const hasStructure = fields.some(isOptionalStructure);
1795
+ const toggle = () => {
1796
+ setShowOptional(!expanded);
1797
+ if (hasStructure) {
1798
+ setValues(
1799
+ (previous) => expanded ? closeOptionalStructures(fields, previous) : openOptionalStructures(fields, previous)
1800
+ );
1801
+ }
1802
+ };
1803
+ const errorFor = (field) => {
1804
+ const hostError = errors ? ownProp(errors, field.name) : void 0;
1805
+ if (hostError) return hostError;
1806
+ if (!attempted) return void 0;
1807
+ const value = ownProp(values, field.name);
1808
+ if (mustBeFilled(field)) return fieldFilled(field, value) ? void 0 : s.requiredField;
1809
+ return isFilled(value) && !fieldFilled(field, value) ? s.incompleteField : void 0;
1810
+ };
1811
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-6", className), children: [
1812
+ visible.map((field) => /* @__PURE__ */ jsx(
1813
+ FieldRenderer,
1814
+ {
1815
+ field,
1816
+ id: field.name,
1817
+ value: ownProp(values, field.name),
1818
+ error: errorFor(field),
1819
+ env,
1820
+ onChange: (next) => setValues((previous) => ({ ...previous, [field.name]: next }))
1821
+ },
1822
+ field.name
1823
+ )),
1824
+ (folded.length > 0 || structureOpen) && /* @__PURE__ */ jsx(OptionalToggle, { count: folded.length, expanded, onToggle: toggle, noun: "input" })
1825
+ ] });
1826
+ }
1603
1827
 
1604
1828
  // src/react/absolute-url.ts
1605
1829
  function absoluteUrl(url) {
@@ -1612,6 +1836,287 @@ function absoluteUrl(url) {
1612
1836
  }
1613
1837
  }
1614
1838
 
1839
+ // src/react/html-frame.ts
1840
+ var DEFAULT_IMG_SRC = "'self' data: https:";
1841
+ function pageTypography(font) {
1842
+ return { color: "#000", mutedColor: "#555", borderColor: "#bbb", font };
1843
+ }
1844
+ function frameStyles({ color, mutedColor, borderColor, font }) {
1845
+ return `
1846
+ :root { color-scheme: inherit; }
1847
+ body {
1848
+ margin: 0;
1849
+ color: ${color};
1850
+ font-family: ${font};
1851
+ font-size: 13px;
1852
+ line-height: 1.55;
1853
+ background: transparent;
1854
+ overflow-wrap: anywhere;
1855
+ }
1856
+ :where(h1, h2, h3, h4, h5, h6) { margin: 0 0 0.4em; line-height: 1.25; }
1857
+ :where(h1) { font-size: 1.4em; }
1858
+ :where(h2) { font-size: 1.2em; }
1859
+ :where(h3) { font-size: 1.05em; }
1860
+ :where(p, ul, ol, table, pre, blockquote) { margin: 0 0 0.75em; }
1861
+ :where(ul, ol) { padding-inline-start: 1.25em; }
1862
+ :where(table) { border-collapse: collapse; width: 100%; }
1863
+ :where(th, td) {
1864
+ border: 1px solid ${borderColor};
1865
+ padding: 4px 8px;
1866
+ text-align: left;
1867
+ vertical-align: top;
1868
+ }
1869
+ :where(th) { font-weight: 600; }
1870
+ :where(caption) { caption-side: top; color: ${mutedColor}; padding-bottom: 4px; text-align: left; }
1871
+ :where(a) { color: inherit; }
1872
+ :where(code, pre) { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
1873
+ :where(pre) { white-space: pre-wrap; }
1874
+ :where(blockquote) { border-inline-start: 2px solid ${borderColor}; margin-inline-start: 0; padding-inline-start: 0.75em; color: ${mutedColor}; }
1875
+ :where(img) { max-width: 100%; height: auto; }
1876
+ :where(hr) { border: 0; border-top: 1px solid ${borderColor}; }
1877
+ :where(*:last-child) { margin-bottom: 0; }
1878
+ `;
1879
+ }
1880
+ function isWholeDocument(markup) {
1881
+ return /^\s*(?:<!--[\s\S]*?-->\s*)*<(?:!doctype\b|html[\s>]|head[\s>])/i.test(markup);
1882
+ }
1883
+ function frameDocument(content, { styles, imgSrc, fontSrc }) {
1884
+ const head = [
1885
+ '<!doctype html><html><head><meta charset="utf-8">',
1886
+ `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src ${imgSrc}; font-src ${fontSrc}; base-uri 'none'; form-action 'none'">`,
1887
+ `<style>${styles}</style></head>`
1888
+ ].join("");
1889
+ if (isWholeDocument(content.innerHtml)) return head + content.innerHtml;
1890
+ const body = content.cssClass ? (
1891
+ // The class the value states, honoured the way the runtime's own HTML
1892
+ // rendering honours it: as a wrapper, not as something merged into ours.
1893
+ `<div class="${escapeAttribute(content.cssClass)}">${content.innerHtml}</div>`
1894
+ ) : content.innerHtml;
1895
+ return `${head}<body>${body}</body></html>`;
1896
+ }
1897
+ function escapeAttribute(value) {
1898
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1899
+ }
1900
+
1901
+ // src/react/markup-storage.ts
1902
+ var MARKUP_RESOLVE_TIMEOUT_MS = 1e4;
1903
+ var STORAGE_SCHEME = "pipelex-storage:";
1904
+ function mayHoldStorageRefs(markup) {
1905
+ return markup.toLowerCase().includes("pipelex-storage");
1906
+ }
1907
+ function isStorageRef(value) {
1908
+ return value.trim().toLowerCase().startsWith(STORAGE_SCHEME);
1909
+ }
1910
+ function parseMarkup(markup) {
1911
+ if (typeof DOMParser === "undefined") return void 0;
1912
+ const parser = new DOMParser();
1913
+ if (isWholeDocument(markup)) {
1914
+ const doc2 = parser.parseFromString(markup, "text/html");
1915
+ return {
1916
+ root: doc2,
1917
+ serialize: () => (doc2.doctype ? `<!DOCTYPE ${doc2.doctype.name}>` : "") + doc2.documentElement.outerHTML
1918
+ };
1919
+ }
1920
+ const doc = parser.parseFromString("<!doctype html><body></body>", "text/html");
1921
+ const template = doc.createElement("template");
1922
+ template.innerHTML = markup;
1923
+ return { root: template.content, serialize: () => template.innerHTML };
1924
+ }
1925
+ var CSS_URL = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
1926
+ function parseSrcset(value) {
1927
+ const candidates = [];
1928
+ let at = 0;
1929
+ while (at < value.length) {
1930
+ while (at < value.length && /[\s,]/.test(value[at] ?? "")) at += 1;
1931
+ if (at >= value.length) break;
1932
+ const start = at;
1933
+ while (at < value.length && !/\s/.test(value[at] ?? "")) at += 1;
1934
+ let url = value.slice(start, at);
1935
+ if (url.endsWith(",")) {
1936
+ url = url.replace(/,+$/, "");
1937
+ candidates.push({ url, descriptor: "" });
1938
+ continue;
1939
+ }
1940
+ let depth = 0;
1941
+ const descriptorStart = at;
1942
+ while (at < value.length) {
1943
+ const char = value[at];
1944
+ if (char === "(") depth += 1;
1945
+ else if (char === ")") depth = Math.max(0, depth - 1);
1946
+ else if (char === "," && depth === 0) break;
1947
+ at += 1;
1948
+ }
1949
+ candidates.push({ url, descriptor: value.slice(descriptorStart, at).trim() });
1950
+ }
1951
+ return candidates;
1952
+ }
1953
+ function cssUrl(url) {
1954
+ return `url("${url.replace(/[\\"]/g, (char) => `\\${char}`).replace(/[\n\r\f]/g, " ")}")`;
1955
+ }
1956
+ function attributeSite(element, name) {
1957
+ return {
1958
+ refs: () => {
1959
+ const value = element.getAttribute(name) ?? "";
1960
+ return isStorageRef(value) ? [value.trim()] : [];
1961
+ },
1962
+ rewrite: (answer) => {
1963
+ const value = element.getAttribute(name) ?? "";
1964
+ if (!isStorageRef(value)) return false;
1965
+ const url = answer(value.trim());
1966
+ if (url === void 0) return false;
1967
+ element.setAttribute(name, url);
1968
+ return true;
1969
+ }
1970
+ };
1971
+ }
1972
+ function srcsetSite(element, name) {
1973
+ const read = () => parseSrcset(element.getAttribute(name) ?? "");
1974
+ return {
1975
+ refs: () => read().flatMap(({ url }) => isStorageRef(url) ? [url] : []),
1976
+ rewrite: (answer) => {
1977
+ let changed = false;
1978
+ const candidates = read().map((candidate) => {
1979
+ if (!isStorageRef(candidate.url)) return candidate;
1980
+ const url = answer(candidate.url);
1981
+ if (url === void 0) return candidate;
1982
+ changed = true;
1983
+ return { ...candidate, url: url.replace(/,/g, "%2C").replace(/\s/g, "%20") };
1984
+ });
1985
+ if (changed) {
1986
+ element.setAttribute(
1987
+ name,
1988
+ candidates.map(({ url, descriptor }) => descriptor ? `${url} ${descriptor}` : url).join(", ")
1989
+ );
1990
+ }
1991
+ return changed;
1992
+ }
1993
+ };
1994
+ }
1995
+ function cssSite(read, write) {
1996
+ return {
1997
+ refs: () => Array.from(read().matchAll(CSS_URL), (match) => (match[2] ?? "").trim()).filter(isStorageRef),
1998
+ rewrite: (answer) => {
1999
+ let changed = false;
2000
+ const css = read().replace(CSS_URL, (token, _quote, inner) => {
2001
+ const ref = inner.trim();
2002
+ if (!isStorageRef(ref)) return token;
2003
+ const url = answer(ref);
2004
+ if (url === void 0) return token;
2005
+ changed = true;
2006
+ return cssUrl(url);
2007
+ });
2008
+ if (changed) write(css);
2009
+ return changed;
2010
+ }
2011
+ };
2012
+ }
2013
+ function urlSites(root) {
2014
+ const sites = [];
2015
+ for (const element of Array.from(root.querySelectorAll("*"))) {
2016
+ const tag = element.localName;
2017
+ if (tag === "img" || tag === "source") {
2018
+ sites.push(attributeSite(element, "src"), srcsetSite(element, "srcset"));
2019
+ }
2020
+ if (tag === "video") sites.push(attributeSite(element, "poster"));
2021
+ if (tag === "image") {
2022
+ sites.push(attributeSite(element, "href"), attributeSite(element, "xlink:href"));
2023
+ }
2024
+ if (element.hasAttribute("background")) sites.push(attributeSite(element, "background"));
2025
+ if (element.hasAttribute("style")) {
2026
+ sites.push(
2027
+ cssSite(
2028
+ () => element.getAttribute("style") ?? "",
2029
+ (css) => element.setAttribute("style", css)
2030
+ )
2031
+ );
2032
+ }
2033
+ if (tag === "style") {
2034
+ sites.push(
2035
+ cssSite(
2036
+ () => element.textContent ?? "",
2037
+ (css) => {
2038
+ element.textContent = css;
2039
+ }
2040
+ )
2041
+ );
2042
+ }
2043
+ }
2044
+ return sites;
2045
+ }
2046
+ function storageRefsIn(markup) {
2047
+ if (!mayHoldStorageRefs(markup)) return [];
2048
+ const parsed = parseMarkup(markup);
2049
+ if (!parsed) return [];
2050
+ return Array.from(new Set(urlSites(parsed.root).flatMap((site) => site.refs())));
2051
+ }
2052
+ function rewriteStorageRefs(markup, answer) {
2053
+ if (!mayHoldStorageRefs(markup)) return markup;
2054
+ const parsed = parseMarkup(markup);
2055
+ if (!parsed) return markup;
2056
+ let changed = false;
2057
+ for (const site of urlSites(parsed.root)) {
2058
+ if (site.rewrite(answer)) changed = true;
2059
+ }
2060
+ return changed ? parsed.serialize() : markup;
2061
+ }
2062
+ function readBulk(result) {
2063
+ if (result instanceof Map) {
2064
+ return (ref) => {
2065
+ const url = result.get(ref);
2066
+ return typeof url === "string" ? url : void 0;
2067
+ };
2068
+ }
2069
+ if (result !== null && typeof result === "object") {
2070
+ const record = result;
2071
+ return (ref) => {
2072
+ const url = ownProp(record, ref);
2073
+ return typeof url === "string" ? url : void 0;
2074
+ };
2075
+ }
2076
+ return () => void 0;
2077
+ }
2078
+ async function askBulkUntimed(resolveUrls, refs) {
2079
+ try {
2080
+ return readBulk(await resolveUrls(refs));
2081
+ } catch {
2082
+ return () => void 0;
2083
+ }
2084
+ }
2085
+ async function askBulk(resolveUrls, refs, timeoutMs = MARKUP_RESOLVE_TIMEOUT_MS) {
2086
+ let timer;
2087
+ const timeout = new Promise((resolve) => {
2088
+ timer = setTimeout(() => resolve(() => void 0), timeoutMs);
2089
+ });
2090
+ const asked = askBulkUntimed(resolveUrls, refs);
2091
+ try {
2092
+ return await Promise.race([asked, timeout]);
2093
+ } finally {
2094
+ clearTimeout(timer);
2095
+ }
2096
+ }
2097
+ function answerFor(bulk, resolveUrl) {
2098
+ return (ref) => {
2099
+ const fromBulk = viewableUrl(bulk?.(ref));
2100
+ if (fromBulk !== void 0) return fromBulk;
2101
+ try {
2102
+ return viewableUrl(resolveUrl?.(ref));
2103
+ } catch {
2104
+ return void 0;
2105
+ }
2106
+ };
2107
+ }
2108
+ async function resolveHtmlStorageUrls(markup, { resolveUrl, resolveUrls }, { timeoutMs = MARKUP_RESOLVE_TIMEOUT_MS } = {}) {
2109
+ if (!resolveUrl && !resolveUrls) return markup;
2110
+ try {
2111
+ const refs = storageRefsIn(markup);
2112
+ if (refs.length === 0) return markup;
2113
+ const bulk = resolveUrls ? await askBulk(resolveUrls, refs, timeoutMs) : void 0;
2114
+ return rewriteStorageRefs(markup, answerFor(bulk, resolveUrl));
2115
+ } catch {
2116
+ return markup;
2117
+ }
2118
+ }
2119
+
1615
2120
  // src/react/save-in-browser.ts
1616
2121
  function saveBlob(blob, filename) {
1617
2122
  const href = URL.createObjectURL(blob);
@@ -1646,6 +2151,7 @@ async function saveInBrowser(files) {
1646
2151
  var ResultEnvContext = createContext({});
1647
2152
  function ResultEnvProvider({
1648
2153
  resolveUrl,
2154
+ resolveUrls,
1649
2155
  resolveShareUrl,
1650
2156
  proseImages,
1651
2157
  tableColumns,
@@ -1654,8 +2160,16 @@ function ResultEnvProvider({
1654
2160
  children
1655
2161
  }) {
1656
2162
  const env = useMemo(
1657
- () => ({ resolveUrl, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads }),
1658
- [resolveUrl, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads]
2163
+ () => ({
2164
+ resolveUrl,
2165
+ resolveUrls,
2166
+ resolveShareUrl,
2167
+ proseImages,
2168
+ tableColumns,
2169
+ saveFiles,
2170
+ downloads
2171
+ }),
2172
+ [resolveUrl, resolveUrls, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads]
1659
2173
  );
1660
2174
  return /* @__PURE__ */ jsx(ResultEnvContext, { value: env, children });
1661
2175
  }
@@ -1666,15 +2180,19 @@ function DownloadDisplayOverride({
1666
2180
  const env = use(ResultEnvContext);
1667
2181
  const result = downloads?.result;
1668
2182
  const files = downloads?.files;
2183
+ const pdf = downloads?.pdf;
2184
+ const askFileName = downloads?.askFileName;
1669
2185
  const merged = useMemo(
1670
- () => result === void 0 && files === void 0 ? env : {
2186
+ () => result === void 0 && files === void 0 && pdf === void 0 && askFileName === void 0 ? env : {
1671
2187
  ...env,
1672
2188
  downloads: {
1673
2189
  result: result ?? env.downloads?.result,
1674
- files: files ?? env.downloads?.files
2190
+ files: files ?? env.downloads?.files,
2191
+ pdf: pdf ?? env.downloads?.pdf,
2192
+ askFileName: askFileName ?? env.downloads?.askFileName
1675
2193
  }
1676
2194
  },
1677
- [env, result, files]
2195
+ [env, result, files, pdf, askFileName]
1678
2196
  );
1679
2197
  return /* @__PURE__ */ jsx(ResultEnvContext, { value: merged, children });
1680
2198
  }
@@ -1684,16 +2202,25 @@ function useResolvedUrl(url) {
1684
2202
  function useResolveUrl() {
1685
2203
  return use(ResultEnvContext).resolveUrl;
1686
2204
  }
2205
+ function useMarkupResolvers() {
2206
+ const { resolveUrl, resolveUrls } = use(ResultEnvContext);
2207
+ return useMemo(() => ({ resolveUrl, resolveUrls }), [resolveUrl, resolveUrls]);
2208
+ }
1687
2209
  function useResolveShareUrl() {
1688
2210
  return use(ResultEnvContext).resolveShareUrl;
1689
2211
  }
1690
2212
  function useSaveFiles() {
1691
2213
  const save = use(ResultEnvContext).saveFiles ?? saveInBrowser;
1692
- return useCallback((files) => deliver(save, files), [save]);
1693
- }
1694
- async function deliver(save, files) {
1695
- const handed = files.map(
1696
- (file) => file.url === void 0 ? file : { ...file, url: absoluteUrl(file.url) }
2214
+ const resolvers = useMarkupResolvers();
2215
+ return useCallback((files) => deliver(save, files, resolvers), [save, resolvers]);
2216
+ }
2217
+ async function deliver(save, files, resolvers) {
2218
+ const handed = await Promise.all(
2219
+ files.map(async (file) => {
2220
+ if (file.url !== void 0) return { ...file, url: absoluteUrl(file.url) };
2221
+ if (file.mimeType !== "text/html") return file;
2222
+ return { ...file, text: await resolveHtmlStorageUrls(file.text, resolvers) };
2223
+ })
1697
2224
  );
1698
2225
  try {
1699
2226
  const result = await save(handed);
@@ -1713,6 +2240,12 @@ function useFileDownloadShown(kind) {
1713
2240
  const files = use(ResultEnvContext).downloads?.files ?? true;
1714
2241
  return typeof files === "boolean" ? files : files.includes(kind);
1715
2242
  }
2243
+ function usePdfSaveShown() {
2244
+ return use(ResultEnvContext).downloads?.pdf ?? true;
2245
+ }
2246
+ function useAskFileName() {
2247
+ return use(ResultEnvContext).downloads?.askFileName ?? false;
2248
+ }
1716
2249
  function useProseImages() {
1717
2250
  return use(ResultEnvContext).proseImages ?? "link";
1718
2251
  }
@@ -1761,88 +2294,857 @@ var TooltipContent = React2.forwardRef(({ className, sideOffset = 6, ...props },
1761
2294
  }
1762
2295
  ) }));
1763
2296
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
1764
- var DEFAULT_IMG_SRC = "data: https:";
1765
- function frameStyles(color, mutedColor, borderColor, font) {
1766
- return `
1767
- :root { color-scheme: inherit; }
1768
- body {
1769
- margin: 0;
1770
- color: ${color};
1771
- font-family: ${font};
1772
- font-size: 13px;
1773
- line-height: 1.55;
1774
- background: transparent;
1775
- overflow-wrap: anywhere;
2297
+ function useFileNamePrompt() {
2298
+ const asking = useAskFileName();
2299
+ const [pending, setPending] = useState(null);
2300
+ const ask = useCallback(
2301
+ (stem2, extension) => {
2302
+ if (!asking) return Promise.resolve(`${stem2}${extension}`);
2303
+ return new Promise((resolve) => {
2304
+ setPending({
2305
+ stem: stem2,
2306
+ extension,
2307
+ settle: (name) => {
2308
+ setPending(null);
2309
+ resolve(name);
2310
+ }
2311
+ });
2312
+ });
2313
+ },
2314
+ [asking]
2315
+ );
2316
+ const dialog = pending ? /* @__PURE__ */ jsx(
2317
+ FileNameDialog,
2318
+ {
2319
+ stem: pending.stem,
2320
+ extension: pending.extension,
2321
+ onConfirm: (typed) => pending.settle(
2322
+ `${typedFileStem(typed, pending.extension) || pending.stem}${pending.extension}`
2323
+ ),
2324
+ onCancel: () => pending.settle(null)
1776
2325
  }
1777
- :where(h1, h2, h3, h4, h5, h6) { margin: 0 0 0.4em; line-height: 1.25; }
1778
- :where(h1) { font-size: 1.4em; }
1779
- :where(h2) { font-size: 1.2em; }
1780
- :where(h3) { font-size: 1.05em; }
1781
- :where(p, ul, ol, table, pre, blockquote) { margin: 0 0 0.75em; }
1782
- :where(ul, ol) { padding-inline-start: 1.25em; }
1783
- :where(table) { border-collapse: collapse; width: 100%; }
1784
- :where(th, td) {
1785
- border: 1px solid ${borderColor};
1786
- padding: 4px 8px;
1787
- text-align: left;
1788
- vertical-align: top;
2326
+ ) : null;
2327
+ return { ask, dialog };
2328
+ }
2329
+ function FileNameDialog({ stem: stem2, extension, onConfirm, onCancel }) {
2330
+ const s = useFieldStrings();
2331
+ const titleId = useId();
2332
+ const extensionId = useId();
2333
+ const [value, setValue] = useState(stem2);
2334
+ const panel = useRef(null);
2335
+ const field = useRef(null);
2336
+ const pressedBackdrop = useRef(false);
2337
+ useEffect(() => {
2338
+ const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
2339
+ field.current?.focus();
2340
+ field.current?.select();
2341
+ return () => {
2342
+ if (opener?.isConnected) opener.focus();
2343
+ };
2344
+ }, []);
2345
+ const onKeyDown = (event) => {
2346
+ event.stopPropagation();
2347
+ if (event.key === "Escape") {
2348
+ event.preventDefault();
2349
+ onCancel();
2350
+ return;
1789
2351
  }
1790
- :where(th) { font-weight: 600; }
1791
- :where(caption) { caption-side: top; color: ${mutedColor}; padding-bottom: 4px; text-align: left; }
1792
- :where(a) { color: inherit; }
1793
- :where(code, pre) { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
1794
- :where(pre) { white-space: pre-wrap; }
1795
- :where(blockquote) { border-inline-start: 2px solid ${borderColor}; margin-inline-start: 0; padding-inline-start: 0.75em; color: ${mutedColor}; }
1796
- :where(img) { max-width: 100%; height: auto; }
1797
- :where(hr) { border: 0; border-top: 1px solid ${borderColor}; }
1798
- :where(*:last-child) { margin-bottom: 0; }
1799
- `;
2352
+ if (event.key !== "Tab") return;
2353
+ const controls = Array.from(
2354
+ panel.current?.querySelectorAll("input, button") ?? []
2355
+ );
2356
+ const first = controls[0];
2357
+ const last = controls[controls.length - 1];
2358
+ if (!first || !last) return;
2359
+ if (event.shiftKey && document.activeElement === first) {
2360
+ event.preventDefault();
2361
+ last.focus();
2362
+ } else if (!event.shiftKey && document.activeElement === last) {
2363
+ event.preventDefault();
2364
+ first.focus();
2365
+ }
2366
+ };
2367
+ return /* @__PURE__ */ jsx(
2368
+ "div",
2369
+ {
2370
+ "data-file-name-backdrop": true,
2371
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4",
2372
+ onMouseDown: (event) => {
2373
+ event.stopPropagation();
2374
+ pressedBackdrop.current = event.target === event.currentTarget;
2375
+ },
2376
+ onClick: (event) => {
2377
+ event.stopPropagation();
2378
+ if (pressedBackdrop.current && event.target === event.currentTarget) onCancel();
2379
+ pressedBackdrop.current = false;
2380
+ },
2381
+ children: /* @__PURE__ */ jsxs(
2382
+ "div",
2383
+ {
2384
+ ref: panel,
2385
+ role: "dialog",
2386
+ "aria-modal": "true",
2387
+ "aria-labelledby": titleId,
2388
+ onKeyDown,
2389
+ className: "w-full max-w-sm space-y-3 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-lg",
2390
+ children: [
2391
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "text-[14px] font-medium", children: s.fileNameTitle }),
2392
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md border border-border bg-input focus-within:outline-solid focus-within:outline-2 focus-within:outline-offset-1 focus-within:outline-ring", children: [
2393
+ /* @__PURE__ */ jsx(
2394
+ "input",
2395
+ {
2396
+ ref: field,
2397
+ type: "text",
2398
+ value,
2399
+ onChange: (event) => setValue(event.target.value),
2400
+ onKeyDown: (event) => {
2401
+ if (event.key !== "Enter") return;
2402
+ event.preventDefault();
2403
+ onConfirm(value);
2404
+ },
2405
+ "aria-labelledby": titleId,
2406
+ ...extension ? { "aria-describedby": extensionId } : {},
2407
+ autoComplete: "off",
2408
+ spellCheck: false,
2409
+ className: "min-w-0 flex-1 bg-transparent px-2 py-1.5 text-[13px] text-foreground outline-none"
2410
+ }
2411
+ ),
2412
+ extension ? /* @__PURE__ */ jsx("span", { id: extensionId, className: "shrink-0 pr-2 text-[13px] text-muted-foreground", children: extension }) : null
2413
+ ] }),
2414
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
2415
+ /* @__PURE__ */ jsx(
2416
+ "button",
2417
+ {
2418
+ type: "button",
2419
+ onClick: onCancel,
2420
+ className: "rounded-md border border-border px-3 py-1 text-[12px] text-muted-foreground hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1",
2421
+ children: s.fileNameCancel
2422
+ }
2423
+ ),
2424
+ /* @__PURE__ */ jsx(
2425
+ "button",
2426
+ {
2427
+ type: "button",
2428
+ onClick: () => onConfirm(value),
2429
+ className: "rounded-md bg-primary px-3 py-1 text-[12px] font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1",
2430
+ children: s.fileNameConfirm
2431
+ }
2432
+ )
2433
+ ] })
2434
+ ]
2435
+ }
2436
+ )
2437
+ }
2438
+ );
1800
2439
  }
1801
- function frameDocument(content, styles, imgSrc) {
1802
- const body = content.cssClass ? (
1803
- // The class the value states, honoured the way the runtime's own HTML
1804
- // rendering honours it: as a wrapper, not as something merged into ours.
1805
- `<div class="${escapeAttribute(content.cssClass)}">${content.innerHtml}</div>`
1806
- ) : content.innerHtml;
1807
- return [
1808
- '<!doctype html><html><head><meta charset="utf-8">',
1809
- `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src ${imgSrc}; base-uri 'none'; form-action 'none'">`,
1810
- `<style>${styles}</style></head><body>${body}</body></html>`
1811
- ].join("");
2440
+
2441
+ // src/react/page-layout.ts
2442
+ var FIXED_ATTRIBUTE = "data-mthds-pdf-fixed";
2443
+ var ATOMIC_TAGS = /* @__PURE__ */ new Set([
2444
+ "IMG",
2445
+ "SVG",
2446
+ "CANVAS",
2447
+ "VIDEO",
2448
+ "IFRAME",
2449
+ "OBJECT",
2450
+ "EMBED",
2451
+ "INPUT",
2452
+ "TEXTAREA",
2453
+ "SELECT",
2454
+ "BUTTON",
2455
+ "HR",
2456
+ "PICTURE",
2457
+ "svg"
2458
+ ]);
2459
+ var HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4", "H5", "H6"]);
2460
+ var FORCED = /* @__PURE__ */ new Set(["page", "always", "left", "right", "recto", "verso"]);
2461
+ var AVOIDED = /* @__PURE__ */ new Set(["avoid", "avoid-page"]);
2462
+ function measureFlow(doc) {
2463
+ const win = doc.defaultView;
2464
+ const root = doc.documentElement;
2465
+ const offset = root.getBoundingClientRect().top;
2466
+ const y = (value) => value - offset;
2467
+ const spanOf = (el) => {
2468
+ const rect = el.getBoundingClientRect();
2469
+ return { top: y(rect.top), bottom: y(rect.bottom) };
2470
+ };
2471
+ const hard = [];
2472
+ const avoid = [];
2473
+ const forced = [];
2474
+ const tables = [];
2475
+ const fixed = [];
2476
+ const keepAfter = [];
2477
+ const keepBefore = [];
2478
+ const walk = (el) => {
2479
+ const style = win.getComputedStyle(el);
2480
+ if (style.display === "none") return;
2481
+ if (style.position === "fixed") {
2482
+ fixed.push(el);
2483
+ return;
2484
+ }
2485
+ const span = spanOf(el);
2486
+ const display = style.display;
2487
+ if (FORCED.has(style.breakBefore) || FORCED.has(style.getPropertyValue("page-break-before"))) {
2488
+ forced.push(span.top);
2489
+ }
2490
+ if (FORCED.has(style.breakAfter) || FORCED.has(style.getPropertyValue("page-break-after"))) {
2491
+ forced.push(span.bottom);
2492
+ }
2493
+ if (AVOIDED.has(style.breakAfter) || HEADING_TAGS.has(el.tagName)) keepAfter.push(span);
2494
+ if (AVOIDED.has(style.breakBefore)) keepBefore.push(span);
2495
+ const atomic = ATOMIC_TAGS.has(el.tagName) || display.startsWith("inline-");
2496
+ if (atomic) {
2497
+ if (span.bottom > span.top) hard.push(span);
2498
+ return;
2499
+ }
2500
+ if (AVOIDED.has(style.breakInside) || AVOIDED.has(style.getPropertyValue("page-break-inside")) || display === "table-row") {
2501
+ avoid.push(span);
2502
+ }
2503
+ if (display === "table" || display === "inline-table") {
2504
+ tables.push(tableBox(el, span));
2505
+ }
2506
+ for (const child of Array.from(el.childNodes)) {
2507
+ if (child.nodeType === 1) walk(child);
2508
+ else if (child.nodeType === 3 && /\S/.test(child.textContent ?? "")) {
2509
+ hard.push(...lineSpans(doc, child, y));
2510
+ }
2511
+ }
2512
+ };
2513
+ const tableBox = (table, span) => {
2514
+ const box = { ...span };
2515
+ for (const part of Array.from(table.children)) {
2516
+ const display = win.getComputedStyle(part).display;
2517
+ if (display === "table-header-group" && !box.head) box.head = spanOf(part);
2518
+ if (display === "table-footer-group" && !box.foot) box.foot = spanOf(part);
2519
+ }
2520
+ return box;
2521
+ };
2522
+ walk(doc.body ?? root);
2523
+ fixed.forEach((el) => el.setAttribute(FIXED_ATTRIBUTE, ""));
2524
+ const lines = [...hard].sort((a, b) => a.top - b.top);
2525
+ const firstLineFrom = (at) => lines.find((line) => line.top >= at - 0.5);
2526
+ const lastLineBefore = (at) => [...lines].reverse().find((line) => line.bottom <= at + 0.5);
2527
+ for (const span of keepAfter) {
2528
+ const next = firstLineFrom(span.bottom);
2529
+ if (next) avoid.push({ top: span.top, bottom: next.bottom });
2530
+ }
2531
+ for (const span of keepBefore) {
2532
+ const previous = lastLineBefore(span.top);
2533
+ const first = firstLineFrom(span.top);
2534
+ if (previous && first) avoid.push({ top: previous.top, bottom: first.bottom });
2535
+ }
2536
+ for (const table of tables) {
2537
+ if (!table.head) continue;
2538
+ const next = firstLineFrom(table.head.bottom);
2539
+ if (next && next.bottom <= (table.foot?.top ?? table.bottom) + 0.5) {
2540
+ avoid.push({ top: table.top, bottom: next.bottom });
2541
+ }
2542
+ }
2543
+ return {
2544
+ layout: { height: root.scrollHeight, hard, avoid, forced, tables },
2545
+ fixed
2546
+ };
1812
2547
  }
1813
- function escapeAttribute(value) {
1814
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
2548
+ function lineSpans(doc, text, y) {
2549
+ const range = doc.createRange();
2550
+ range.selectNodeContents(text);
2551
+ const spans = [];
2552
+ for (const rect of Array.from(range.getClientRects())) {
2553
+ if (rect.height === 0) continue;
2554
+ const span = { top: y(rect.top), bottom: y(rect.bottom) };
2555
+ const same = spans.find((line) => span.top < line.bottom && span.bottom > line.top);
2556
+ if (same) {
2557
+ same.top = Math.min(same.top, span.top);
2558
+ same.bottom = Math.max(same.bottom, span.bottom);
2559
+ } else spans.push(span);
2560
+ }
2561
+ return spans;
2562
+ }
2563
+
2564
+ // src/react/paginate.ts
2565
+ var EPSILON = 0.5;
2566
+ function paginate(layout, pageHeight) {
2567
+ const pages = [];
2568
+ const end = layout.height;
2569
+ const inkBottom = Math.max(0, ...layout.hard.map((span) => span.bottom));
2570
+ const forced = [...layout.forced].sort((a, b) => a - b);
2571
+ let start = 0;
2572
+ for (let guard = 0; start < end - EPSILON && guard < 1e4; guard++) {
2573
+ const heads = openAt(layout.tables, start).filter((table) => table.head && start >= table.head.bottom - EPSILON).map((table) => table.head);
2574
+ const room = pageHeight - total(heads);
2575
+ const nextForced = forced.find((at) => at > start + EPSILON && at < end - EPSILON);
2576
+ if (end - start <= room + EPSILON && nextForced === void 0) {
2577
+ pages.push({ heads, body: { top: start, bottom: end }, feet: [] });
2578
+ break;
2579
+ }
2580
+ let feet = [];
2581
+ let cut = start;
2582
+ for (let pass = 0; pass < 4; pass++) {
2583
+ const limit = start + room - total(feet);
2584
+ cut = nextForced !== void 0 && nextForced <= limit + EPSILON ? nextForced : bestCut(layout, start, limit);
2585
+ const needed = openAt(layout.tables, cut).filter((table) => table.foot).map((table) => table.foot).reverse();
2586
+ if (total(needed) <= total(feet) + EPSILON) {
2587
+ feet = needed;
2588
+ break;
2589
+ }
2590
+ feet = needed;
2591
+ }
2592
+ pages.push({ heads, body: { top: start, bottom: cut }, feet });
2593
+ start = cut;
2594
+ if (inkBottom <= cut + EPSILON) break;
2595
+ }
2596
+ return pages;
2597
+ }
2598
+ function bestCut(layout, start, limit) {
2599
+ const room = limit - start;
2600
+ const hard = layout.hard.filter((span) => span.bottom - span.top <= room);
2601
+ const avoid = layout.avoid.filter((span) => span.bottom - span.top <= room);
2602
+ const candidates = /* @__PURE__ */ new Set([limit]);
2603
+ for (const span of [...hard, ...avoid]) {
2604
+ candidates.add(span.top);
2605
+ candidates.add(span.bottom);
2606
+ }
2607
+ for (const table of layout.tables) {
2608
+ candidates.add(table.top);
2609
+ candidates.add(table.bottom);
2610
+ }
2611
+ const sorted = [...candidates].filter((at) => at > start + EPSILON && at <= limit + EPSILON).sort((a, b) => b - a);
2612
+ for (const constraints of [[...hard, ...avoid], hard]) {
2613
+ const cut = sorted.find((at) => !constraints.some((span) => inside(span, at)));
2614
+ if (cut !== void 0) return Math.min(cut, limit);
2615
+ }
2616
+ return limit;
2617
+ }
2618
+ function inside(span, at) {
2619
+ return at > span.top + EPSILON && at < span.bottom - EPSILON;
2620
+ }
2621
+ function openAt(tables, at) {
2622
+ return tables.filter((table) => at > table.top + EPSILON && at < (table.foot?.top ?? table.bottom) - EPSILON).sort((a, b) => a.top - b.top);
2623
+ }
2624
+ function total(spans) {
2625
+ return spans.reduce((sum, span) => sum + (span.bottom - span.top), 0);
2626
+ }
2627
+
2628
+ // src/react/print-media.ts
2629
+ var PX_PER_MM = 96 / 25.4;
2630
+ var PAPER = {
2631
+ a3: [297, 420],
2632
+ a4: [210, 297],
2633
+ a5: [148, 210],
2634
+ b4: [250, 353],
2635
+ b5: [176, 250],
2636
+ "jis-b4": [257, 364],
2637
+ "jis-b5": [182, 257],
2638
+ letter: [215.9, 279.4],
2639
+ legal: [215.9, 355.6],
2640
+ ledger: [279.4, 431.8]
2641
+ };
2642
+ var DEFAULT_PAGE_BOX = {
2643
+ width: 210 * PX_PER_MM,
2644
+ height: 297 * PX_PER_MM,
2645
+ margin: {
2646
+ top: 10 * PX_PER_MM,
2647
+ right: 10 * PX_PER_MM,
2648
+ bottom: 10 * PX_PER_MM,
2649
+ left: 10 * PX_PER_MM
2650
+ }
2651
+ };
2652
+ function promotePrintRules(doc) {
2653
+ for (const sheet of Array.from(doc.styleSheets)) {
2654
+ if (sheet.media.length > 0) rewriteMedia(sheet.media);
2655
+ let rules;
2656
+ try {
2657
+ rules = sheet.cssRules;
2658
+ } catch {
2659
+ continue;
2660
+ }
2661
+ rewriteRules(rules);
2662
+ }
2663
+ }
2664
+ function rewriteRules(rules) {
2665
+ for (const rule of Array.from(rules)) {
2666
+ if (isMediaRule(rule)) rewriteMedia(rule.media);
2667
+ if ("cssRules" in rule) rewriteRules(rule.cssRules);
2668
+ }
2669
+ }
2670
+ function isMediaRule(rule) {
2671
+ return rule.constructor.name === "CSSMediaRule" || rule.type === 4;
2672
+ }
2673
+ function rewriteMedia(media) {
2674
+ const queries = Array.from({ length: media.length }, (_, i) => media.item(i) ?? "");
2675
+ const printed = queries.map(onPaper);
2676
+ if (printed.every((query, i) => query === queries[i])) return;
2677
+ media.mediaText = printed.join(", ");
2678
+ }
2679
+ var MEDIA_QUERY = /^\s*(not\s+|only\s+)?([a-z][a-z-]*)(\s+and\s+[\s\S]*)?\s*$/i;
2680
+ function onPaper(query) {
2681
+ const match = MEDIA_QUERY.exec(query);
2682
+ if (!match) return query;
2683
+ const [, prefix = "", type = "", rest = ""] = match;
2684
+ const negated = /^not/i.test(prefix);
2685
+ const typeHolds = ["print", "all"].includes(type.toLowerCase());
2686
+ if (negated) return typeHolds ? `not all${rest}` : "all";
2687
+ return typeHolds ? `all${rest}` : "not all";
2688
+ }
2689
+ function readPageBox(doc) {
2690
+ let size = "";
2691
+ const margin = {};
2692
+ const visit = (rules) => {
2693
+ for (const rule of Array.from(rules)) {
2694
+ if (rule.constructor.name === "CSSPageRule" || rule.type === 6) {
2695
+ const page = rule;
2696
+ if (page.selectorText.trim() !== "") continue;
2697
+ size = page.style.getPropertyValue("size") || size;
2698
+ for (const side2 of ["top", "right", "bottom", "left"]) {
2699
+ const value = page.style.getPropertyValue(`margin-${side2}`);
2700
+ if (value) margin[side2] = value;
2701
+ }
2702
+ } else if (isMediaRule(rule)) {
2703
+ if (doc.defaultView?.matchMedia(rule.media.mediaText).matches !== false)
2704
+ visit(rule.cssRules);
2705
+ } else if ("cssRules" in rule) {
2706
+ visit(rule.cssRules);
2707
+ }
2708
+ }
2709
+ };
2710
+ for (const sheet of Array.from(doc.styleSheets)) {
2711
+ try {
2712
+ visit(sheet.cssRules);
2713
+ } catch {
2714
+ }
2715
+ }
2716
+ const [width, height] = pageSize(size);
2717
+ const side = (name) => {
2718
+ const declared = margin[name];
2719
+ const px = declared === void 0 ? void 0 : toPx(declared);
2720
+ return px ?? DEFAULT_PAGE_BOX.margin[name];
2721
+ };
2722
+ return {
2723
+ width,
2724
+ height,
2725
+ margin: { top: side("top"), right: side("right"), bottom: side("bottom"), left: side("left") }
2726
+ };
2727
+ }
2728
+ function pageSize(value) {
2729
+ const words = value.trim().toLowerCase().split(/\s+/).filter(Boolean);
2730
+ const landscape = words.includes("landscape");
2731
+ const named = words.find((word) => word in PAPER);
2732
+ const lengths = words.map(toPx).filter((px) => px !== void 0);
2733
+ let width = DEFAULT_PAGE_BOX.width;
2734
+ let height = DEFAULT_PAGE_BOX.height;
2735
+ if (named) {
2736
+ [width, height] = PAPER[named].map((mm) => mm * PX_PER_MM);
2737
+ } else if (lengths.length === 2) {
2738
+ [width, height] = lengths;
2739
+ } else if (lengths.length === 1) {
2740
+ width = height = lengths[0];
2741
+ }
2742
+ if (landscape && width < height) [width, height] = [height, width];
2743
+ return [width, height];
2744
+ }
2745
+ var UNIT_PX = {
2746
+ px: 1,
2747
+ in: 96,
2748
+ cm: 10 * PX_PER_MM,
2749
+ mm: PX_PER_MM,
2750
+ q: PX_PER_MM / 4,
2751
+ pt: 96 / 72,
2752
+ pc: 16
2753
+ };
2754
+ function toPx(value) {
2755
+ const match = /^(-?\d*\.?\d+)([a-z]*)$/i.exec(value.trim());
2756
+ if (!match) return void 0;
2757
+ const amount = Number(match[1]);
2758
+ const unit = (match[2] || "px").toLowerCase();
2759
+ if (amount === 0) return 0;
2760
+ const factor = UNIT_PX[unit];
2761
+ return factor === void 0 ? void 0 : amount * factor;
2762
+ }
2763
+
2764
+ // src/react/html-pdf.ts
2765
+ var SETTLE_TIMEOUT_MS = 1e4;
2766
+ var PT_PER_PX = 72 / 96;
2767
+ async function renderHtmlPdf(content, {
2768
+ imgSrc = DEFAULT_IMG_SRC,
2769
+ fontSrc,
2770
+ fontFamily = "system-ui, sans-serif",
2771
+ title,
2772
+ scale = 3,
2773
+ quality = 0.92
2774
+ } = {}) {
2775
+ const libraries = Promise.all([import('modern-screenshot'), import('jspdf')]);
2776
+ const copy = await openCopy(content, {
2777
+ imgSrc,
2778
+ fontSrc: fontSrc ?? imgSrc,
2779
+ fontFamily
2780
+ });
2781
+ try {
2782
+ const { doc } = copy;
2783
+ promotePrintRules(doc);
2784
+ const page = readPageBox(doc);
2785
+ const area = {
2786
+ width: page.width - page.margin.left - page.margin.right,
2787
+ height: page.height - page.margin.top - page.margin.bottom
2788
+ };
2789
+ copy.frame.style.width = `${area.width}px`;
2790
+ copy.frame.style.height = `${area.height}px`;
2791
+ await nextFrame();
2792
+ await settle(doc);
2793
+ const { layout, fixed } = measureFlow(doc);
2794
+ const slices = paginate(layout, area.height);
2795
+ const [{ domToForeignObjectSvg }, { jsPDF }] = await libraries;
2796
+ const root = doc.documentElement;
2797
+ const notOurs = (node) => node.nodeType !== 1 || !node.id.startsWith("__SANDBOX__");
2798
+ const flow = await domToForeignObjectSvg(root, {
2799
+ width: area.width,
2800
+ height: layout.height,
2801
+ filter: (node) => notOurs(node) && !isFixed(node)
2802
+ });
2803
+ const overlay = fixed.length > 0 ? await paintFixed(doc, fixed, area, domToForeignObjectSvg, notOurs) : void 0;
2804
+ const landscape = page.width > page.height;
2805
+ const pdf = new jsPDF({
2806
+ unit: "pt",
2807
+ format: [page.width * PT_PER_PX, page.height * PT_PER_PX],
2808
+ orientation: landscape ? "landscape" : "portrait",
2809
+ compress: true
2810
+ });
2811
+ if (title) pdf.setProperties({ title });
2812
+ for (const [index, slice] of slices.entries()) {
2813
+ const jpeg = await paintPage(slice, page, area, scale, quality, flow, overlay);
2814
+ if (index > 0)
2815
+ pdf.addPage(
2816
+ [page.width * PT_PER_PX, page.height * PT_PER_PX],
2817
+ landscape ? "landscape" : "portrait"
2818
+ );
2819
+ pdf.addImage(
2820
+ jpeg,
2821
+ "JPEG",
2822
+ 0,
2823
+ 0,
2824
+ page.width * PT_PER_PX,
2825
+ page.height * PT_PER_PX,
2826
+ void 0,
2827
+ "NONE"
2828
+ );
2829
+ }
2830
+ return { blob: pdf.output("blob"), pageCount: slices.length, page };
2831
+ } finally {
2832
+ copy.frame.remove();
2833
+ }
2834
+ }
2835
+ function isFixed(node) {
2836
+ return node.nodeType === 1 && node.hasAttribute(FIXED_ATTRIBUTE);
2837
+ }
2838
+ async function paintFixed(doc, fixed, area, toSvg, notOurs) {
2839
+ const root = doc.documentElement;
2840
+ const keep = /* @__PURE__ */ new Set();
2841
+ for (const el of fixed) {
2842
+ for (let node = el; node; node = node.parentNode) keep.add(node);
2843
+ }
2844
+ const pins = new Map(fixed.map((el, i) => [String(i), el.getBoundingClientRect()]));
2845
+ fixed.forEach((el, i) => el.setAttribute(FIXED_ATTRIBUTE, String(i)));
2846
+ const previous = root.style.getPropertyValue("visibility");
2847
+ root.style.setProperty("visibility", "hidden", "important");
2848
+ fixed.forEach((el) => el.style.setProperty("visibility", "visible", "important"));
2849
+ try {
2850
+ return await toSvg(root, {
2851
+ width: area.width,
2852
+ height: area.height,
2853
+ filter: (node) => notOurs(node) && (keep.has(node) || insideFixed(node, fixed)),
2854
+ onCloneNode: (clone) => {
2855
+ const cloned = clone;
2856
+ cloned.style.setProperty("position", "relative");
2857
+ for (const el of Array.from(cloned.querySelectorAll(`[${FIXED_ATTRIBUTE}]`))) {
2858
+ const pin = pins.get(el.getAttribute(FIXED_ATTRIBUTE) ?? "");
2859
+ if (!pin) continue;
2860
+ Object.assign(el.style, {
2861
+ position: "absolute",
2862
+ top: `${pin.top}px`,
2863
+ left: `${pin.left}px`,
2864
+ width: `${pin.width}px`,
2865
+ height: `${pin.height}px`,
2866
+ right: "auto",
2867
+ bottom: "auto",
2868
+ margin: "0"
2869
+ });
2870
+ }
2871
+ }
2872
+ });
2873
+ } finally {
2874
+ if (previous) root.style.setProperty("visibility", previous);
2875
+ else root.style.removeProperty("visibility");
2876
+ fixed.forEach((el) => el.style.removeProperty("visibility"));
2877
+ }
2878
+ }
2879
+ function insideFixed(node, fixed) {
2880
+ return fixed.some((el) => el.contains(node));
2881
+ }
2882
+ async function paintPage(slice, page, area, scale, quality, flow, overlay) {
2883
+ const canvas = document.createElement("canvas");
2884
+ canvas.width = Math.round(page.width * scale);
2885
+ canvas.height = Math.round(page.height * scale);
2886
+ const context = canvas.getContext("2d");
2887
+ if (!context) throw new Error("No 2D canvas to paint the PDF page on");
2888
+ context.fillStyle = "#fff";
2889
+ context.fillRect(0, 0, canvas.width, canvas.height);
2890
+ let y = page.margin.top;
2891
+ for (const span of [...slice.heads, slice.body, ...slice.feet]) {
2892
+ const height = span.bottom - span.top;
2893
+ if (height <= 0) continue;
2894
+ const image = await svgImage(flow, { top: span.top, width: area.width, height }, scale);
2895
+ context.drawImage(
2896
+ image,
2897
+ page.margin.left * scale,
2898
+ y * scale,
2899
+ area.width * scale,
2900
+ height * scale
2901
+ );
2902
+ y += height;
2903
+ }
2904
+ if (overlay) {
2905
+ const image = await svgImage(overlay, { top: 0, ...area }, scale);
2906
+ context.drawImage(
2907
+ image,
2908
+ page.margin.left * scale,
2909
+ page.margin.top * scale,
2910
+ area.width * scale,
2911
+ area.height * scale
2912
+ );
2913
+ }
2914
+ const blob = await new Promise(
2915
+ (resolve) => canvas.toBlob(resolve, "image/jpeg", quality)
2916
+ );
2917
+ if (!blob) throw new Error("The PDF page could not be encoded");
2918
+ return new Uint8Array(await blob.arrayBuffer());
2919
+ }
2920
+ async function svgImage(svg, { top, width, height }, scale) {
2921
+ const copy = svg.cloneNode(false);
2922
+ copy.setAttribute("viewBox", `0 ${top} ${width} ${height}`);
2923
+ copy.setAttribute("width", String(Math.max(1, Math.round(width * scale))));
2924
+ copy.setAttribute("height", String(Math.max(1, Math.round(height * scale))));
2925
+ copy.setAttribute("preserveAspectRatio", "none");
2926
+ for (const child of Array.from(svg.childNodes)) copy.appendChild(child.cloneNode(true));
2927
+ const object = Array.from(copy.children).find((child) => child.localName === "foreignObject");
2928
+ object?.setAttribute("width", svg.getAttribute("width") ?? String(width));
2929
+ object?.setAttribute("height", svg.getAttribute("height") ?? String(top + height));
2930
+ const markup = new XMLSerializer().serializeToString(copy).replace(/[\u0000-\u0008\v\f\u000E-\u001F￾￿]/g, "");
2931
+ const image = new Image();
2932
+ image.decoding = "sync";
2933
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
2934
+ await image.decode();
2935
+ return image;
2936
+ }
2937
+ async function openCopy(content, { imgSrc, fontSrc, fontFamily }) {
2938
+ const frame = document.createElement("iframe");
2939
+ frame.setAttribute("sandbox", "allow-same-origin");
2940
+ frame.setAttribute("aria-hidden", "true");
2941
+ frame.setAttribute("data-mthds-pdf-copy", "");
2942
+ frame.tabIndex = -1;
2943
+ frame.title = "PDF";
2944
+ Object.assign(frame.style, {
2945
+ position: "fixed",
2946
+ left: "-10000px",
2947
+ top: "0",
2948
+ width: "794px",
2949
+ height: "1123px",
2950
+ border: "0",
2951
+ opacity: "0",
2952
+ pointerEvents: "none"
2953
+ });
2954
+ frame.srcdoc = frameDocument(content, {
2955
+ styles: frameStyles(pageTypography(fontFamily)),
2956
+ imgSrc,
2957
+ fontSrc
2958
+ });
2959
+ const loaded = new Promise(
2960
+ (resolve) => frame.addEventListener("load", () => resolve(), { once: true })
2961
+ );
2962
+ document.body.append(frame);
2963
+ try {
2964
+ await loaded;
2965
+ const doc = frame.contentDocument;
2966
+ if (!doc?.defaultView) throw new Error("The PDF copy could not be opened");
2967
+ await settle(doc);
2968
+ return { frame, doc };
2969
+ } catch (error) {
2970
+ frame.remove();
2971
+ throw error;
2972
+ }
2973
+ }
2974
+ async function settle(doc) {
2975
+ const images = Array.from(doc.images, (image) => {
2976
+ image.loading = "eager";
2977
+ if (image.complete) return Promise.resolve();
2978
+ return new Promise((resolve) => {
2979
+ image.addEventListener("load", () => resolve(), { once: true });
2980
+ image.addEventListener("error", () => resolve(), { once: true });
2981
+ });
2982
+ });
2983
+ const fonts = doc.fonts ? [
2984
+ ...Array.from(doc.fonts, (face) => face.load().then(noop, noop)),
2985
+ doc.fonts.ready.then(noop, noop)
2986
+ ] : [];
2987
+ let timer;
2988
+ await Promise.race([
2989
+ Promise.all([...images, ...fonts]),
2990
+ new Promise((resolve) => {
2991
+ timer = setTimeout(resolve, SETTLE_TIMEOUT_MS);
2992
+ })
2993
+ ]);
2994
+ clearTimeout(timer);
2995
+ }
2996
+ function nextFrame() {
2997
+ return new Promise((resolve) => {
2998
+ const timer = setTimeout(resolve, 100);
2999
+ requestAnimationFrame(() => {
3000
+ clearTimeout(timer);
3001
+ resolve();
3002
+ });
3003
+ });
3004
+ }
3005
+ function noop() {
3006
+ }
3007
+ function PdfDownloadButton({
3008
+ content,
3009
+ fileName,
3010
+ path = "page",
3011
+ imgSrc,
3012
+ fontSrc,
3013
+ primary = false,
3014
+ className
3015
+ }) {
3016
+ const s = useFieldStrings();
3017
+ const save = useSaveFiles();
3018
+ const resolvers = useMarkupResolvers();
3019
+ const ref = useRef(null);
3020
+ const statusId = useId();
3021
+ const [state, setState] = useState("idle");
3022
+ const failed = state === "failed";
3023
+ const { ask, dialog } = useFileNamePrompt();
3024
+ const name = `${pdfStem(fileName) || "page"}.pdf`;
3025
+ const download = async () => {
3026
+ const chosen = await ask(pdfStem(fileName) || "page", ".pdf");
3027
+ if (chosen === null) return;
3028
+ const title = chosen === name ? pdfStem(fileName) : pdfStem(chosen);
3029
+ setState("busy");
3030
+ try {
3031
+ const fontFamily = ref.current ? getComputedStyle(ref.current).fontFamily : void 0;
3032
+ const innerHtml = await resolveHtmlStorageUrls(content.innerHtml, resolvers);
3033
+ const page = innerHtml === content.innerHtml ? content : { ...content, innerHtml };
3034
+ const { blob } = await renderHtmlPdf(page, {
3035
+ ...title ? { title } : {},
3036
+ ...imgSrc === void 0 ? {} : { imgSrc },
3037
+ ...fontSrc === void 0 ? {} : { fontSrc },
3038
+ ...fontFamily ? { fontFamily } : {}
3039
+ });
3040
+ const { failed: missed } = await save([
3041
+ {
3042
+ name: chosen,
3043
+ mimeType: "application/pdf",
3044
+ kind: "markup",
3045
+ path,
3046
+ url: await dataUrl(blob)
3047
+ }
3048
+ ]);
3049
+ setState(missed.length > 0 ? "failed" : "idle");
3050
+ } catch {
3051
+ setState("failed");
3052
+ }
3053
+ };
3054
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3055
+ /* @__PURE__ */ jsxs(
3056
+ "button",
3057
+ {
3058
+ ref,
3059
+ type: "button",
3060
+ "data-pdf-download": true,
3061
+ onClick: () => void download(),
3062
+ disabled: state === "busy",
3063
+ "aria-busy": state === "busy",
3064
+ title: failed ? s.downloadPdfFailed : name,
3065
+ ...failed ? { "aria-describedby": statusId } : {},
3066
+ className: cn(
3067
+ "inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1 text-[12px] font-medium focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 disabled:opacity-70",
3068
+ primary ? "bg-primary text-primary-foreground hover:bg-primary/90" : "border border-border text-muted-foreground hover:text-foreground",
3069
+ failed && (primary ? "bg-destructive text-white hover:bg-destructive/90" : "text-destructive"),
3070
+ className
3071
+ ),
3072
+ children: [
3073
+ state === "busy" ? /* @__PURE__ */ jsx(Loader2, { "aria-hidden": true, className: "size-3.5 animate-spin" }) : failed ? /* @__PURE__ */ jsx(TriangleAlert, { "aria-hidden": true, className: "size-3.5" }) : /* @__PURE__ */ jsx(FileDown, { "aria-hidden": true, className: "size-3.5" }),
3074
+ state === "busy" ? s.downloadPdfBusy : s.downloadPdf
3075
+ ]
3076
+ }
3077
+ ),
3078
+ /* @__PURE__ */ jsx("span", { id: statusId, role: "status", className: "sr-only", children: failed ? s.downloadPdfFailed : "" }),
3079
+ dialog
3080
+ ] });
3081
+ }
3082
+ function pdfStem(name) {
3083
+ return (name ?? "").trim().replace(/\.pdf$/i, "").trim();
3084
+ }
3085
+ function dataUrl(blob) {
3086
+ return new Promise((resolve, reject) => {
3087
+ const reader = new FileReader();
3088
+ reader.onload = () => resolve(String(reader.result));
3089
+ reader.onerror = () => reject(reader.error ?? new Error("The PDF could not be read"));
3090
+ reader.readAsDataURL(blob);
3091
+ });
1815
3092
  }
1816
3093
  var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
3094
+ var FRAGMENT_MAX_HEIGHT = 1400;
3095
+ var FILL_MIN_HEIGHT = 320;
1817
3096
  function HtmlPreview({
1818
3097
  content,
1819
- maxHeight = 1400,
1820
- imgSrc = DEFAULT_IMG_SRC
3098
+ maxHeight,
3099
+ imgSrc = DEFAULT_IMG_SRC,
3100
+ fontSrc,
3101
+ downloadPdf,
3102
+ pdfFileName: pdfFileName2,
3103
+ bare = false,
3104
+ fill = false
1821
3105
  }) {
1822
3106
  const hostRef = useRef(null);
1823
3107
  const frameRef = useRef(null);
3108
+ const s = useFieldStrings();
1824
3109
  const [doc, setDoc] = useState(null);
1825
3110
  const [height, setHeight] = useState(0);
3111
+ const resolvedHtml = useResolvedMarkup(content.innerHtml);
3112
+ const resolved = useMemo(
3113
+ () => resolvedHtml === void 0 ? void 0 : resolvedHtml === content.innerHtml ? content : { ...content, innerHtml: resolvedHtml },
3114
+ [content, resolvedHtml]
3115
+ );
3116
+ const page = isWholeDocument(content.innerHtml);
3117
+ const pdfSetting = usePdfSaveShown();
3118
+ const pdfShown = downloadPdf ?? pdfSetting;
3119
+ const limit = maxHeight ?? (page ? void 0 : FRAGMENT_MAX_HEIGHT);
3120
+ const filling = page && fill;
1826
3121
  useIsomorphicLayoutEffect(() => {
1827
3122
  const host = hostRef.current;
1828
- if (!host) return;
3123
+ if (!host || !resolved) return;
1829
3124
  const computed = getComputedStyle(host);
3125
+ const typography = page ? pageTypography(computed.fontFamily) : {
3126
+ color: computed.color,
3127
+ mutedColor: computed.getPropertyValue("--muted-foreground").trim() || computed.color,
3128
+ borderColor: computed.borderColor || computed.color,
3129
+ font: computed.fontFamily
3130
+ };
1830
3131
  setDoc(
1831
- frameDocument(
1832
- content,
1833
- frameStyles(
1834
- computed.color,
1835
- computed.getPropertyValue("--muted-foreground").trim() || computed.color,
1836
- computed.borderColor || computed.color,
1837
- computed.fontFamily
1838
- ),
1839
- imgSrc
1840
- )
3132
+ frameDocument(resolved, {
3133
+ styles: frameStyles(typography),
3134
+ imgSrc,
3135
+ fontSrc: fontSrc ?? imgSrc
3136
+ })
1841
3137
  );
1842
- }, [content, imgSrc]);
3138
+ }, [resolved, imgSrc, fontSrc, page]);
1843
3139
  const measure = () => {
1844
- const body = frameRef.current?.contentDocument?.body;
1845
- if (body) setHeight(body.scrollHeight);
3140
+ const frameDoc = frameRef.current?.contentDocument;
3141
+ const body = frameDoc?.body;
3142
+ if (!frameDoc || !body) return;
3143
+ setHeight(Math.max(body.scrollHeight, frameDoc.documentElement.scrollHeight));
3144
+ };
3145
+ const onLoad = () => {
3146
+ measure();
3147
+ void frameRef.current?.contentDocument?.fonts?.ready.then(measure, () => void 0);
1846
3148
  };
1847
3149
  useEffect(() => {
1848
3150
  if (!doc || !hostRef.current || typeof ResizeObserver === "undefined") return;
@@ -1850,30 +3152,110 @@ function HtmlPreview({
1850
3152
  observer.observe(hostRef.current);
1851
3153
  return () => observer.disconnect();
1852
3154
  }, [doc]);
1853
- return /* @__PURE__ */ jsx(
3155
+ const frameBox = /* @__PURE__ */ jsx(
1854
3156
  "div",
1855
3157
  {
1856
3158
  ref: hostRef,
1857
- className: "overflow-hidden rounded-lg border border-border bg-card/40 px-3.5 py-3 text-[13px] text-foreground",
1858
- children: doc === null ? null : /* @__PURE__ */ jsx(
3159
+ ...page ? { "data-html-page": "" } : {},
3160
+ className: cn(
3161
+ page ? bare ? "overflow-hidden bg-white text-[13px] text-black" : "overflow-hidden rounded-lg border border-border bg-white text-[13px] text-black" : "overflow-hidden rounded-lg border border-border bg-card/40 px-3.5 py-3 text-[13px] text-foreground",
3162
+ // A flex column that may shrink, so the frame inside can.
3163
+ filling && "flex min-h-0 flex-col"
3164
+ ),
3165
+ children: resolved === void 0 ? (
3166
+ // The page's stored pictures are being resolved: a short wait, said
3167
+ // as such, rather than a page painted with broken images first.
3168
+ /* @__PURE__ */ jsxs(
3169
+ "div",
3170
+ {
3171
+ role: "status",
3172
+ "data-html-resolving": true,
3173
+ className: "flex min-h-24 items-center justify-center gap-2 text-[12px] text-muted-foreground",
3174
+ children: [
3175
+ /* @__PURE__ */ jsx(Loader2, { "aria-hidden": true, className: "size-4 animate-spin" }),
3176
+ /* @__PURE__ */ jsx("span", { children: s.pageLoading })
3177
+ ]
3178
+ }
3179
+ )
3180
+ ) : doc === null ? null : /* @__PURE__ */ jsx(
1859
3181
  "iframe",
1860
3182
  {
1861
3183
  ref: frameRef,
1862
3184
  title: content.cssClass ? `HTML result (${content.cssClass})` : "HTML result",
1863
3185
  srcDoc: doc,
1864
- onLoad: measure,
3186
+ onLoad,
1865
3187
  sandbox: "allow-same-origin",
1866
3188
  style: {
1867
3189
  display: "block",
1868
3190
  width: "100%",
1869
3191
  border: 0,
1870
- height: height === 0 ? void 0 : Math.min(height, maxHeight),
1871
- maxHeight
3192
+ height: height === 0 ? void 0 : limit === void 0 ? height : Math.min(height, limit),
3193
+ ...limit === void 0 ? {} : { maxHeight: limit },
3194
+ // Its height is the page's, as a flex basis: in a bounded flex
3195
+ // column it shrinks to what is left (never below the floor) and the
3196
+ // page scrolls inside the frame; anywhere else it is the page's.
3197
+ // `minHeight` is explicit because a replaced element's automatic
3198
+ // minimum is its own height, which would forbid the shrink.
3199
+ ...filling && height > 0 ? { flex: "0 1 auto", minHeight: Math.min(height, FILL_MIN_HEIGHT) } : {}
1872
3200
  }
1873
3201
  }
1874
3202
  )
1875
3203
  }
1876
3204
  );
3205
+ if (!pdfShown) return frameBox;
3206
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
3207
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
3208
+ PdfDownloadButton,
3209
+ {
3210
+ content,
3211
+ imgSrc,
3212
+ ...fontSrc === void 0 ? {} : { fontSrc },
3213
+ ...pdfFileName2 === void 0 ? {} : { fileName: pdfFileName2 }
3214
+ }
3215
+ ) }),
3216
+ frameBox
3217
+ ] });
3218
+ }
3219
+ function useResolvedMarkup(markup) {
3220
+ const resolvers = useMarkupResolvers();
3221
+ const needed = (resolvers.resolveUrl !== void 0 || resolvers.resolveUrls !== void 0) && mayHoldStorageRefs(markup);
3222
+ const [shown, setShown] = useState(null);
3223
+ useEffect(() => {
3224
+ if (!needed) return;
3225
+ let live = true;
3226
+ const show = (bulk) => {
3227
+ if (!live) return;
3228
+ let html = markup;
3229
+ try {
3230
+ html = rewriteStorageRefs(markup, answerFor(bulk, resolvers.resolveUrl));
3231
+ } catch {
3232
+ }
3233
+ setShown({ source: markup, html });
3234
+ };
3235
+ let refs = [];
3236
+ try {
3237
+ refs = storageRefsIn(markup);
3238
+ } catch {
3239
+ refs = [];
3240
+ }
3241
+ if (refs.length === 0 || !resolvers.resolveUrls) {
3242
+ show();
3243
+ return () => {
3244
+ live = false;
3245
+ };
3246
+ }
3247
+ const timer = setTimeout(() => show(), MARKUP_RESOLVE_TIMEOUT_MS);
3248
+ void askBulkUntimed(resolvers.resolveUrls, refs).then((bulk) => {
3249
+ clearTimeout(timer);
3250
+ show(bulk);
3251
+ });
3252
+ return () => {
3253
+ live = false;
3254
+ clearTimeout(timer);
3255
+ };
3256
+ }, [needed, markup, resolvers]);
3257
+ if (!needed) return markup;
3258
+ return shown?.source === markup ? shown.html : void 0;
1877
3259
  }
1878
3260
  var LEX_OPTIONS = { gfm: true, breaks: true };
1879
3261
  function Markdown({ text, className, proseImages }) {
@@ -2325,6 +3707,7 @@ function FileDownloadButton({ file }) {
2325
3707
  const resolve = useResolveUrl();
2326
3708
  const save = useSaveFiles();
2327
3709
  const shown = useFileDownloadShown(file.kind);
3710
+ const { ask, dialog } = useFileNamePrompt();
2328
3711
  const statusId = useId();
2329
3712
  const [attempt, setAttempt] = useState(null);
2330
3713
  useEffect(() => {
@@ -2351,13 +3734,15 @@ ${planned.url ?? planned.text}`;
2351
3734
  "button",
2352
3735
  {
2353
3736
  type: "button",
2354
- onClick: () => {
3737
+ onClick: async () => {
3738
+ const { stem: stem2, extension } = splitFileName(planned.name);
3739
+ const name = await ask(stem2, extension);
3740
+ if (name === null) return;
2355
3741
  const started = { identity, state: "saving" };
2356
3742
  setAttempt(started);
2357
- void save([planned]).then((result) => {
2358
- const settled = result.failed.length > 0 ? "failed" : "saved";
2359
- setAttempt((current) => current === started ? { identity, state: settled } : current);
2360
- });
3743
+ const result = await save([name === planned.name ? planned : { ...planned, name }]);
3744
+ const settled = result.failed.length > 0 ? "failed" : "saved";
3745
+ setAttempt((current) => current === started ? { identity, state: settled } : current);
2361
3746
  },
2362
3747
  disabled: state === "saving",
2363
3748
  "aria-label": label,
@@ -2370,7 +3755,8 @@ ${planned.url ?? planned.text}`;
2370
3755
  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" })
2371
3756
  }
2372
3757
  ),
2373
- /* @__PURE__ */ jsx("span", { id: statusId, role: "status", className: "sr-only", children: failed ? s.downloadFileFailed : "" })
3758
+ /* @__PURE__ */ jsx("span", { id: statusId, role: "status", className: "sr-only", children: failed ? s.downloadFileFailed : "" }),
3759
+ dialog
2374
3760
  ] });
2375
3761
  }
2376
3762
  function paintable(content, resolve) {
@@ -2807,7 +4193,7 @@ function CompositeValue({ members }) {
2807
4193
  function NativeValue({ value }) {
2808
4194
  if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx(Absent, {});
2809
4195
  const html = readHtmlContent(value);
2810
- if (html) return /* @__PURE__ */ jsx(HtmlPreview, { content: html });
4196
+ if (html) return /* @__PURE__ */ jsx(HtmlPreview, { content: html, downloadPdf: false });
2811
4197
  const date = readDateContent(value);
2812
4198
  if (date) return /* @__PURE__ */ jsx("span", { className: "text-[13px] text-foreground", children: formatDateContent(date) });
2813
4199
  const document2 = readDocumentContent(value);
@@ -2887,6 +4273,7 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2887
4273
  const s = useFieldStrings();
2888
4274
  const unwrapped = unwrap(field, value);
2889
4275
  const pageDownloadShown = useFileDownloadShown("markup");
4276
+ const pdfShown = usePdfSaveShown();
2890
4277
  const location = useResultLocation();
2891
4278
  const header = hideLabel ? null : /* @__PURE__ */ jsx(Label2, { field, describe: true });
2892
4279
  const copyText = (field.kind === "text" || field.kind === "prose") && typeof unwrapped === "string" && unwrapped !== "" ? unwrapped : void 0;
@@ -2898,13 +4285,24 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2898
4285
  }
2899
4286
  if (isNativeHtmlNode(field)) {
2900
4287
  const content = readHtmlContent(unwrapped);
2901
- const page = pageDownloadShown && location ? readStuffFile("markup", unwrapped, location.path.join(".")) : void 0;
4288
+ const pagePath = location?.path.join(".");
4289
+ const pageFile = pagePath === void 0 ? void 0 : readStuffFile("markup", unwrapped, pagePath);
4290
+ const page = pageDownloadShown ? pageFile : void 0;
4291
+ const pdfName = pdfShown && content && pageFile && location ? stem(planFileSave(pageFile, { baseName: location.baseName })?.name) : void 0;
2902
4292
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2903
- page ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
4293
+ page || pdfName !== void 0 ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
2904
4294
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: header }),
4295
+ content && pdfName !== void 0 ? /* @__PURE__ */ jsx(
4296
+ PdfDownloadButton,
4297
+ {
4298
+ content,
4299
+ ...pagePath === void 0 ? {} : { path: pagePath },
4300
+ ...pdfName ? { fileName: pdfName } : {}
4301
+ }
4302
+ ) : null,
2905
4303
  /* @__PURE__ */ jsx(FileActions, { file: page })
2906
4304
  ] }) : header,
2907
- content ? /* @__PURE__ */ jsx(HtmlPreview, { content }) : /* @__PURE__ */ jsx(Absent, {})
4305
+ content ? /* @__PURE__ */ jsx(HtmlPreview, { content, downloadPdf: false }) : /* @__PURE__ */ jsx(Absent, {})
2908
4306
  ] });
2909
4307
  }
2910
4308
  if (isNativeCompositeNode(field)) {
@@ -3035,6 +4433,11 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
3035
4433
  }
3036
4434
  return null;
3037
4435
  }
4436
+ function stem(name) {
4437
+ return name === void 0 ? "" : name.replace(/\.[^.]+$/, "");
4438
+ }
4439
+ var ALL_VIEWS = ["rendered", "json"];
4440
+ var RESULT_ONLY = ["rendered"];
3038
4441
  var JSON_TOKEN_RE = /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
3039
4442
  function highlight(json) {
3040
4443
  const out = [];
@@ -3117,14 +4520,18 @@ function StuffPanel({
3117
4520
  field,
3118
4521
  value,
3119
4522
  name,
4523
+ views: viewsProp,
3120
4524
  defaultView = "rendered",
3121
4525
  downloadBaseName,
3122
4526
  className
3123
4527
  }) {
3124
4528
  const s = useFieldStrings();
4529
+ const presentation = useFieldPresentation();
4530
+ const offered = viewsProp && viewsProp.length > 0 ? viewsProp : viewsProp || presentation === "app" ? RESULT_ONLY : ALL_VIEWS;
3125
4531
  const named = name ? { ...field, name } : field;
3126
4532
  const baseName = downloadBaseName ?? named.name ?? "result";
3127
- const [view, setView] = useState(defaultView);
4533
+ const [chosen, setView] = useState(defaultView);
4534
+ const view = offered.includes(chosen) ? chosen : offered[0] ?? "rendered";
3128
4535
  const [saving, setSaving] = useState(null);
3129
4536
  const [missed, setMissed] = useState(null);
3130
4537
  const latest = useRef(null);
@@ -3132,8 +4539,28 @@ function StuffPanel({
3132
4539
  const missedShown = missed && isShownResult(missed.attempt, field, baseName, value) ? missed.names : [];
3133
4540
  const resolveUrl = useResolveUrl();
3134
4541
  const save = useSaveFiles();
3135
- const downloadShown = useResultDownloadShown();
4542
+ const resultDownloadShown = useResultDownloadShown();
4543
+ const pageDownloadShown = useFileDownloadShown("markup");
4544
+ const pdfShown = usePdfSaveShown();
4545
+ const { ask, dialog } = useFileNamePrompt();
4546
+ const page = isNativeHtmlNode(field) ? readHtmlContent(value) : void 0;
4547
+ const downloadShown = resultDownloadShown || page !== void 0 && pageDownloadShown;
3136
4548
  const handleDownload = useCallback(async () => {
4549
+ const options = resolveUrl ? { resolveUrl } : {};
4550
+ let plan = planStuffSave(field, value, { baseName, ...options });
4551
+ const only = plan.files.length === 1 ? plan.files[0] : void 0;
4552
+ const json = plan.files.length > 1 ? plan.files[plan.files.length - 1] : void 0;
4553
+ if (only) {
4554
+ const { stem: stem2, extension } = splitFileName(only.name);
4555
+ const name2 = await ask(stem2, extension);
4556
+ if (name2 === null) return;
4557
+ if (name2 !== only.name) plan = { ...plan, files: [{ ...only, name: name2 }] };
4558
+ } else if (json) {
4559
+ const { stem: stem2 } = splitFileName(json.name);
4560
+ const name2 = await ask(stem2, "");
4561
+ if (name2 === null) return;
4562
+ if (name2 !== stem2) plan = planStuffSave(field, value, { baseName: name2, ...options });
4563
+ }
3137
4564
  const attempt = {
3138
4565
  field,
3139
4566
  baseName,
@@ -3144,10 +4571,6 @@ function StuffPanel({
3144
4571
  setSaving(attempt);
3145
4572
  setMissed(null);
3146
4573
  try {
3147
- const plan = planStuffSave(field, value, {
3148
- baseName,
3149
- ...resolveUrl ? { resolveUrl } : {}
3150
- });
3151
4574
  const { failed } = await save(plan.files);
3152
4575
  if (latest.current !== attempt) return;
3153
4576
  setMissed({
@@ -3160,65 +4583,102 @@ function StuffPanel({
3160
4583
  } finally {
3161
4584
  setSaving((current) => current === attempt ? null : current);
3162
4585
  }
3163
- }, [field, value, baseName, resolveUrl, save]);
3164
- const views = [
3165
- { id: "rendered", label: s.viewRendered },
3166
- { id: "json", label: s.viewJson }
3167
- ];
3168
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
3169
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-4 gap-y-1", children: [
3170
- /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-1", children: /* @__PURE__ */ jsx(ResultHeader, { field: named }) }),
3171
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
3172
- downloadShown && /* @__PURE__ */ jsxs(
3173
- "button",
3174
- {
3175
- type: "button",
3176
- onClick: () => void handleDownload(),
3177
- disabled: savingShown,
3178
- "aria-label": s.download,
3179
- 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",
3180
- children: [
3181
- savingShown ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
3182
- savingShown ? s.downloading : s.download
3183
- ]
3184
- }
3185
- ),
3186
- /* @__PURE__ */ jsx(
3187
- "div",
3188
- {
3189
- role: "group",
3190
- "aria-label": s.resultViewGroup,
3191
- className: "flex rounded-md border border-border p-0.5",
3192
- children: views.map(({ id, label }) => /* @__PURE__ */ jsx(
4586
+ }, [field, value, baseName, resolveUrl, save, ask]);
4587
+ const labels = { rendered: s.viewRendered, json: s.viewJson };
4588
+ const pdfName = page && pdfShown ? pdfFileName(field, value, baseName) : void 0;
4589
+ const pageView = page !== void 0 && view !== "json";
4590
+ return /* @__PURE__ */ jsxs(
4591
+ "div",
4592
+ {
4593
+ className: cn(
4594
+ // A page shown as the view is a flex column that may shrink: in a host
4595
+ // whose scrolling panel is a flex column (`flex min-h-0 flex-col
4596
+ // overflow-y-auto`), the page's frame takes the panel's remaining
4597
+ // height and scrolls inside itself, so there is one scroll container
4598
+ // under the pointer and the first wheel scrolls the page. Anywhere
4599
+ // else the column is as tall as its content and nothing changes.
4600
+ pageView ? "flex min-h-0 flex-col gap-2" : "space-y-2",
4601
+ className
4602
+ ),
4603
+ children: [
4604
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-wrap items-center justify-between gap-x-4 gap-y-1", children: [
4605
+ page && presentation === "app" ? /* @__PURE__ */ jsx("div", {}) : /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-1", children: /* @__PURE__ */ jsx(ResultHeader, { field: named }) }),
4606
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
4607
+ page && pdfName !== void 0 && /* @__PURE__ */ jsx(
4608
+ PdfDownloadButton,
4609
+ {
4610
+ content: page,
4611
+ path: field.name,
4612
+ primary: true,
4613
+ ...pdfName ? { fileName: pdfName } : {}
4614
+ }
4615
+ ),
4616
+ downloadShown && /* @__PURE__ */ jsxs(
3193
4617
  "button",
3194
4618
  {
3195
4619
  type: "button",
3196
- onClick: () => setView(id),
3197
- "aria-pressed": view === id,
3198
- className: cn(
3199
- "rounded px-2 py-0.5 text-[12px] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1",
3200
- view === id ? "bg-card font-medium text-foreground" : "text-muted-foreground hover:text-foreground"
3201
- ),
3202
- children: label
3203
- },
3204
- id
3205
- ))
4620
+ onClick: () => void handleDownload(),
4621
+ disabled: savingShown,
4622
+ "aria-label": page ? s.downloadHtml : s.download,
4623
+ 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",
4624
+ children: [
4625
+ savingShown ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
4626
+ savingShown ? s.downloading : page ? s.downloadHtml : s.download
4627
+ ]
4628
+ }
4629
+ ),
4630
+ dialog,
4631
+ offered.length > 1 && /* @__PURE__ */ jsx(
4632
+ "div",
4633
+ {
4634
+ role: "group",
4635
+ "aria-label": s.resultViewGroup,
4636
+ className: "flex rounded-md border border-border p-0.5",
4637
+ children: offered.map((id) => /* @__PURE__ */ jsx(
4638
+ "button",
4639
+ {
4640
+ type: "button",
4641
+ onClick: () => setView(id),
4642
+ "aria-pressed": view === id,
4643
+ className: cn(
4644
+ "rounded px-2 py-0.5 text-[12px] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1",
4645
+ view === id ? "bg-card font-medium text-foreground" : "text-muted-foreground hover:text-foreground"
4646
+ ),
4647
+ children: labels[id]
4648
+ },
4649
+ id
4650
+ ))
4651
+ }
4652
+ )
4653
+ ] })
4654
+ ] }),
4655
+ downloadShown && /* @__PURE__ */ jsx(
4656
+ "p",
4657
+ {
4658
+ role: "status",
4659
+ className: cn(
4660
+ "shrink-0 text-[12px] text-destructive",
4661
+ missedShown.length === 0 && "sr-only"
4662
+ ),
4663
+ children: missedShown.length > 0 ? s.downloadIncomplete(missedShown) : ""
3206
4664
  }
3207
- )
3208
- ] })
3209
- ] }),
3210
- downloadShown && /* @__PURE__ */ jsx(
3211
- "p",
3212
- {
3213
- role: "status",
3214
- className: cn("text-[12px] text-destructive", missedShown.length === 0 && "sr-only"),
3215
- children: missedShown.length > 0 ? s.downloadIncomplete(missedShown) : ""
3216
- }
3217
- ),
3218
- view === "json" ? /* @__PURE__ */ jsx(JsonView, { value }) : /* @__PURE__ */ jsx(ResultRoot, { baseName, path: [field.name], children: /* @__PURE__ */ jsx(ResultField, { field, value, hideLabel: true }) })
3219
- ] });
4665
+ ),
4666
+ view === "json" ? /* @__PURE__ */ jsx(JsonView, { value }) : page ? (
4667
+ // The page is the panel's main view: drawn here rather than through
4668
+ // `ResultField`, whose page arm would add a second control row saving
4669
+ // the same file, and with no box around it.
4670
+ /* @__PURE__ */ jsx(HtmlPreview, { content: page, downloadPdf: false, bare: true, fill: true })
4671
+ ) : /* @__PURE__ */ jsx(ResultRoot, { baseName, path: [field.name], children: /* @__PURE__ */ jsx(ResultField, { field, value, hideLabel: true }) })
4672
+ ]
4673
+ }
4674
+ );
4675
+ }
4676
+ function pdfFileName(field, value, baseName) {
4677
+ const file = readStuffFile("markup", value, field.name);
4678
+ const name = file ? planFileSave(file, { baseName })?.name : void 0;
4679
+ return name === void 0 ? "" : name.replace(/\.[^.]+$/, "");
3220
4680
  }
3221
4681
 
3222
- export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, NumberField, ObjectField, OptionalToggle, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, fieldStringsFor, humanizeFieldName, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
3223
- //# sourceMappingURL=chunk-D2EQVWGN.js.map
3224
- //# sourceMappingURL=chunk-D2EQVWGN.js.map
4682
+ export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, MethodForm, NumberField, ObjectField, OptionalToggle, PdfDownloadButton, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, fieldStringsFor, humanizeFieldName, renderHtmlPdf, resolveHtmlStorageUrls, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useMethodForm, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
4683
+ //# sourceMappingURL=chunk-7JRT6ZII.js.map
4684
+ //# sourceMappingURL=chunk-7JRT6ZII.js.map