@pipelex/mthds-form 0.12.1 → 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 +20 -0
  2. package/dist/{chunk-IUWLOSAM.js → chunk-2EGRONBV.js} +3 -3
  3. package/dist/{chunk-IUWLOSAM.js.map → chunk-2EGRONBV.js.map} +1 -1
  4. package/dist/{chunk-2D4NEFUH.js → chunk-7JRT6ZII.js} +1729 -177
  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-4KGQ65TQ.js → chunk-VT5E5W4E.js} +4 -3
  13. package/dist/chunk-VT5E5W4E.js.map +1 -0
  14. package/dist/{chunk-PM55AHRO.js → chunk-XO5OENNF.js} +28 -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 +55 -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 +739 -185
  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-2D4NEFUH.js.map +0 -1
  37. package/dist/chunk-4KGQ65TQ.js.map +0 -1
  38. package/dist/chunk-7KQXGG6J.js.map +0 -1
  39. package/dist/chunk-DARLDVJM.js.map +0 -1
  40. package/dist/chunk-PM55AHRO.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 { isFilled } from './chunk-PM55AHRO.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,
@@ -181,12 +184,14 @@ function BooleanField({ field, value, onChange, id, disabled }) {
181
184
  }
182
185
  );
183
186
  }
187
+
188
+ // src/react/locales/en.ts
184
189
  function formatBytes(bytes) {
185
190
  if (bytes < 1024) return bytes === 1 ? "1 byte" : `${bytes} bytes`;
186
191
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
187
192
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
188
193
  }
189
- var DEFAULT_FIELD_STRINGS = {
194
+ var EN_FIELD_STRINGS = {
190
195
  optionalBadge: "optional",
191
196
  selectPlaceholder: "Select\u2026",
192
197
  typeValuePlaceholder: "Type a value\u2026",
@@ -198,7 +203,7 @@ var DEFAULT_FIELD_STRINGS = {
198
203
  addItem: "Add item",
199
204
  removeItemAria: (index) => `Remove item ${index}`,
200
205
  itemsCount: (count) => count === 1 ? "1 item" : `${count} items`,
201
- itemsCountOf: (count, total) => `${count} of ${total} ${total === 1 ? "item" : "items"}`,
206
+ itemsCountOf: (count, total2) => `${count} of ${total2} ${total2 === 1 ? "item" : "items"}`,
202
207
  fieldsCount: (count) => count === 1 ? "1 field" : `${count} fields`,
203
208
  uploading: "Uploading\u2026",
204
209
  dropToUpload: "Drop to upload",
@@ -225,6 +230,14 @@ var DEFAULT_FIELD_STRINGS = {
225
230
  downloadIncomplete: (names) => `${names.length === 1 ? "One file" : `${names.length} files`} could not be saved: ${names.join(", ")}`,
226
231
  downloadFile: (name) => `Download ${name}`,
227
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",
228
241
  viewRendered: "Result",
229
242
  viewJson: "JSON",
230
243
  copyJson: "Copy the JSON",
@@ -235,14 +248,116 @@ var DEFAULT_FIELD_STRINGS = {
235
248
  hideOptionalFields: "Hide optional fields",
236
249
  hideOptionalInputs: "Hide optional inputs",
237
250
  optionalFieldsCount: (count) => count === 1 ? "1 optional field" : `${count} optional fields`,
238
- 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."
255
+ };
256
+
257
+ // src/react/locales/fr.ts
258
+ var PLURALS = new Intl.PluralRules("fr");
259
+ var BYTES = new Intl.NumberFormat("fr-FR", { style: "unit", unit: "byte", unitDisplay: "long" });
260
+ var KILOBYTES = new Intl.NumberFormat("fr-FR", {
261
+ style: "unit",
262
+ unit: "kilobyte",
263
+ unitDisplay: "short",
264
+ maximumFractionDigits: 0
265
+ });
266
+ var MEGABYTES = new Intl.NumberFormat("fr-FR", {
267
+ style: "unit",
268
+ unit: "megabyte",
269
+ unitDisplay: "short",
270
+ maximumFractionDigits: 1
271
+ });
272
+ function plural(count, one, other) {
273
+ return PLURALS.select(count) === "one" ? one : other;
274
+ }
275
+ function formatBytes2(bytes) {
276
+ if (bytes < 1024) return BYTES.format(bytes);
277
+ if (bytes < 1024 * 1024) return KILOBYTES.format(bytes / 1024);
278
+ return MEGABYTES.format(bytes / (1024 * 1024));
279
+ }
280
+ var FR_FIELD_STRINGS = {
281
+ optionalBadge: "facultatif",
282
+ selectPlaceholder: "Choisir\u2026",
283
+ typeValuePlaceholder: "Saisissez une valeur\u2026",
284
+ writeHerePlaceholder: "\xC9crivez ici\u2026",
285
+ jsonHint: "Saisissez cette valeur en JSON brut.",
286
+ decrease: "Diminuer",
287
+ increase: "Augmenter",
288
+ noItemsYet: "Aucun \xE9l\xE9ment pour le moment",
289
+ addItem: "Ajouter un \xE9l\xE9ment",
290
+ removeItemAria: (index) => `Supprimer l\u2019\xE9l\xE9ment ${index}`,
291
+ itemsCount: (count) => `${count} ${plural(count, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
292
+ itemsCountOf: (count, total2) => `${count} sur ${total2} ${plural(total2, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
293
+ fieldsCount: (count) => `${count} ${plural(count, "champ", "champs")}`,
294
+ uploading: "Envoi en cours\u2026",
295
+ dropToUpload: "D\xE9posez pour envoyer",
296
+ dropOrBrowse: "D\xE9posez un fichier ou cliquez pour parcourir",
297
+ pasteUrlInstead: "coller un lien \xE0 la place",
298
+ uploadUnavailable: "Impossible d\u2019envoyer un fichier ici. Collez plut\xF4t un lien vers le fichier.",
299
+ urlPlaceholder: "https://\u2026",
300
+ fileUrlAria: (label) => label ? `Lien vers le fichier pour ${label}` : "Lien vers le fichier",
301
+ uploadedFile: "Fichier joint",
302
+ encodedFileSummary: (format, bytes) => `${format} \xB7 ${formatBytes2(bytes)}`,
303
+ preview: "Aper\xE7u",
304
+ removeFileAria: "Retirer le fichier",
305
+ previewUnavailablePdf: "Aper\xE7u indisponible - ouvrez le fichier pour le consulter.",
306
+ // U+002D HYPHEN-MINUS, as in the English defaults.
307
+ resultAbsent: "-",
308
+ resultAbsentDescription: "Non renseign\xE9",
309
+ resultUndescribed: "Aucun descripteur de sortie pour ce pipe \u2014 affichage de la valeur brute.",
310
+ toggleRowDetails: (index) => `Afficher ou masquer le d\xE9tail de la ligne ${index}`,
311
+ rowDetailsColumn: "D\xE9tail",
312
+ copyUrl: "Copier le lien",
313
+ copyText: "Copier le texte",
314
+ download: "T\xE9l\xE9charger",
315
+ downloading: "T\xE9l\xE9chargement\u2026",
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(", ")}`,
317
+ downloadFile: (name) => `T\xE9l\xE9charger ${name}`,
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",
327
+ viewRendered: "R\xE9sultat",
328
+ viewJson: "JSON",
329
+ copyJson: "Copier le JSON",
330
+ resultViewGroup: "Affichage du r\xE9sultat",
331
+ yes: "Oui",
332
+ no: "Non",
333
+ unsupportedFileType: (formats) => `Ce type de fichier n\u2019est pas pris en charge. Formats accept\xE9s\xA0: ${formats}.`,
334
+ hideOptionalFields: "Masquer les champs facultatifs",
335
+ hideOptionalInputs: "Masquer les entr\xE9es facultatives",
336
+ optionalFieldsCount: (count) => `${count} ${plural(count, "champ facultatif", "champs facultatifs")}`,
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."
239
341
  };
342
+
343
+ // src/react/locales/index.ts
344
+ var FIELD_STRINGS_BY_LOCALE = {
345
+ en: EN_FIELD_STRINGS,
346
+ fr: FR_FIELD_STRINGS
347
+ };
348
+ var SUPPORTED_LOCALES = Object.keys(FIELD_STRINGS_BY_LOCALE);
349
+ function fieldStringsFor(locale) {
350
+ const language = (locale ?? "").trim().toLowerCase().split(/[-_]/)[0] ?? "";
351
+ return FIELD_STRINGS_BY_LOCALE[language] ?? EN_FIELD_STRINGS;
352
+ }
353
+ var DEFAULT_FIELD_STRINGS = EN_FIELD_STRINGS;
240
354
  var FieldStringsContext = createContext(DEFAULT_FIELD_STRINGS);
241
355
  function FieldStringsProvider({
356
+ locale,
242
357
  strings,
243
358
  children
244
359
  }) {
245
- const merged = useMemo(() => ({ ...DEFAULT_FIELD_STRINGS, ...strings }), [strings]);
360
+ const merged = useMemo(() => ({ ...fieldStringsFor(locale), ...strings }), [locale, strings]);
246
361
  return /* @__PURE__ */ jsx(FieldStringsContext.Provider, { value: merged, children });
247
362
  }
248
363
  function useFieldStrings() {
@@ -315,7 +430,7 @@ function DateField({ field, value, onChange, id, error, disabled }) {
315
430
  conceptRef: field.conceptRef,
316
431
  category: "date",
317
432
  description: field.description,
318
- required: field.required,
433
+ required: !shownAsOptional(field),
319
434
  error,
320
435
  htmlFor: domId,
321
436
  children: /* @__PURE__ */ jsx(
@@ -511,7 +626,7 @@ function FileField({
511
626
  conceptRef: field.conceptRef,
512
627
  category,
513
628
  description: field.description,
514
- required: field.required,
629
+ required: !shownAsOptional(field),
515
630
  error,
516
631
  htmlFor: domId,
517
632
  children: [
@@ -903,7 +1018,7 @@ function EnumField({ field, value, onChange, id, error, disabled }) {
903
1018
  conceptRef: field.conceptRef,
904
1019
  category: "choice",
905
1020
  description: field.description,
906
- required: field.required,
1021
+ required: !shownAsOptional(field),
907
1022
  error,
908
1023
  htmlFor: isSegmented ? void 0 : domId,
909
1024
  children: isSegmented ? (
@@ -972,7 +1087,7 @@ function NumberField({ field, value, onChange, id, error, disabled }) {
972
1087
  conceptRef: field.conceptRef,
973
1088
  category: "number",
974
1089
  description: field.description ?? rangeHint,
975
- required: field.required,
1090
+ required: !shownAsOptional(field),
976
1091
  error,
977
1092
  htmlFor: domId,
978
1093
  children: /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
@@ -1034,7 +1149,7 @@ function TextField({ field, value, onChange, id, error, disabled }) {
1034
1149
  conceptRef: field.conceptRef,
1035
1150
  category: "text",
1036
1151
  description: field.description,
1037
- required: field.required,
1152
+ required: !shownAsOptional(field),
1038
1153
  error,
1039
1154
  htmlFor: domId,
1040
1155
  children: /* @__PURE__ */ jsx(
@@ -1064,7 +1179,7 @@ function ProseField({ field, value, onChange, id, error, disabled }) {
1064
1179
  conceptRef: field.conceptRef,
1065
1180
  category: "text",
1066
1181
  description: field.description,
1067
- required: field.required,
1182
+ required: !shownAsOptional(field),
1068
1183
  error,
1069
1184
  htmlFor: domId,
1070
1185
  children: /* @__PURE__ */ jsx(
@@ -1093,6 +1208,7 @@ function OptionalToggle({ count, expanded, onToggle, noun = "field" }) {
1093
1208
  {
1094
1209
  type: "button",
1095
1210
  onClick: onToggle,
1211
+ "aria-expanded": expanded,
1096
1212
  className: "flex w-fit items-center gap-1.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground",
1097
1213
  children: [
1098
1214
  expanded ? /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
@@ -1102,14 +1218,24 @@ function OptionalToggle({ count, expanded, onToggle, noun = "field" }) {
1102
1218
  );
1103
1219
  }
1104
1220
  function ObjectField({ field, value, onChange, id, error, env }) {
1105
- const s = useFieldStrings();
1106
1221
  const [showOptional, setShowOptional] = useState(false);
1107
1222
  const presentation = useFieldPresentation();
1108
1223
  const isApp = presentation === "app";
1109
1224
  const data = value ?? {};
1110
1225
  const setChild = (name, childValue) => onChange({ ...data, [name]: childValue });
1111
- const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(ownProp(data, f.name)));
1112
- const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || 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
+ };
1113
1239
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1114
1240
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
1115
1241
  /* @__PURE__ */ jsx(
@@ -1122,8 +1248,7 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1122
1248
  children: fieldLabel(field.title, field.name, presentation)
1123
1249
  }
1124
1250
  ),
1125
- !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "structured" }),
1126
- !field.required && /* @__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" })
1127
1252
  ] }),
1128
1253
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
1129
1254
  /* @__PURE__ */ jsxs(
@@ -1145,12 +1270,12 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1145
1270
  },
1146
1271
  child.name
1147
1272
  )),
1148
- optionalEmpty.length > 0 && /* @__PURE__ */ jsx(
1273
+ (folded.length > 0 || structureOpen) && /* @__PURE__ */ jsx(
1149
1274
  OptionalToggle,
1150
1275
  {
1151
- count: optionalEmpty.length,
1152
- expanded: showOptional,
1153
- onToggle: () => setShowOptional((v) => !v),
1276
+ count: folded.length,
1277
+ expanded,
1278
+ onToggle: toggle,
1154
1279
  noun: "field"
1155
1280
  }
1156
1281
  )
@@ -1223,7 +1348,7 @@ function ListField({ field, value, onChange, id, error, env }) {
1223
1348
  ),
1224
1349
  !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "list" }),
1225
1350
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.itemCount !== void 0 && field.maxItemCount === field.itemCount ? s.itemsCountOf(items.length, field.itemCount) : s.itemsCount(items.length) }),
1226
- !field.required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1351
+ shownAsOptional(field) && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1227
1352
  ] }),
1228
1353
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
1229
1354
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
@@ -1237,7 +1362,12 @@ function ListField({ field, value, onChange, id, error, env }) {
1237
1362
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
1238
1363
  FieldRenderer,
1239
1364
  {
1240
- field: { ...field.item, title: "", name: `${field.name}[${index}]` },
1365
+ field: {
1366
+ ...field.item,
1367
+ title: "",
1368
+ name: `${field.name}[${index}]`,
1369
+ required: true
1370
+ },
1241
1371
  value: item,
1242
1372
  onChange: (v) => setItem(index, v),
1243
1373
  id: `${id}.${index}`,
@@ -1341,7 +1471,7 @@ function UnknownField({ field, value, onChange, id, error, disabled }) {
1341
1471
  conceptRef: field.conceptRef,
1342
1472
  category: "structured",
1343
1473
  description: field.description ?? s.jsonHint,
1344
- required: field.required,
1474
+ required: !shownAsOptional(field),
1345
1475
  error,
1346
1476
  htmlFor: domId,
1347
1477
  children: /* @__PURE__ */ jsx(
@@ -1508,6 +1638,192 @@ function FieldRenderer({ field, value, onChange, id, error, env }) {
1508
1638
  );
1509
1639
  }
1510
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
+ }
1511
1827
 
1512
1828
  // src/react/absolute-url.ts
1513
1829
  function absoluteUrl(url) {
@@ -1520,6 +1836,287 @@ function absoluteUrl(url) {
1520
1836
  }
1521
1837
  }
1522
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
+
1523
2120
  // src/react/save-in-browser.ts
1524
2121
  function saveBlob(blob, filename) {
1525
2122
  const href = URL.createObjectURL(blob);
@@ -1554,6 +2151,7 @@ async function saveInBrowser(files) {
1554
2151
  var ResultEnvContext = createContext({});
1555
2152
  function ResultEnvProvider({
1556
2153
  resolveUrl,
2154
+ resolveUrls,
1557
2155
  resolveShareUrl,
1558
2156
  proseImages,
1559
2157
  tableColumns,
@@ -1562,8 +2160,16 @@ function ResultEnvProvider({
1562
2160
  children
1563
2161
  }) {
1564
2162
  const env = useMemo(
1565
- () => ({ resolveUrl, resolveShareUrl, proseImages, tableColumns, saveFiles, downloads }),
1566
- [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]
1567
2173
  );
1568
2174
  return /* @__PURE__ */ jsx(ResultEnvContext, { value: env, children });
1569
2175
  }
@@ -1574,15 +2180,19 @@ function DownloadDisplayOverride({
1574
2180
  const env = use(ResultEnvContext);
1575
2181
  const result = downloads?.result;
1576
2182
  const files = downloads?.files;
2183
+ const pdf = downloads?.pdf;
2184
+ const askFileName = downloads?.askFileName;
1577
2185
  const merged = useMemo(
1578
- () => result === void 0 && files === void 0 ? env : {
2186
+ () => result === void 0 && files === void 0 && pdf === void 0 && askFileName === void 0 ? env : {
1579
2187
  ...env,
1580
2188
  downloads: {
1581
2189
  result: result ?? env.downloads?.result,
1582
- files: files ?? env.downloads?.files
2190
+ files: files ?? env.downloads?.files,
2191
+ pdf: pdf ?? env.downloads?.pdf,
2192
+ askFileName: askFileName ?? env.downloads?.askFileName
1583
2193
  }
1584
2194
  },
1585
- [env, result, files]
2195
+ [env, result, files, pdf, askFileName]
1586
2196
  );
1587
2197
  return /* @__PURE__ */ jsx(ResultEnvContext, { value: merged, children });
1588
2198
  }
@@ -1592,16 +2202,25 @@ function useResolvedUrl(url) {
1592
2202
  function useResolveUrl() {
1593
2203
  return use(ResultEnvContext).resolveUrl;
1594
2204
  }
2205
+ function useMarkupResolvers() {
2206
+ const { resolveUrl, resolveUrls } = use(ResultEnvContext);
2207
+ return useMemo(() => ({ resolveUrl, resolveUrls }), [resolveUrl, resolveUrls]);
2208
+ }
1595
2209
  function useResolveShareUrl() {
1596
2210
  return use(ResultEnvContext).resolveShareUrl;
1597
2211
  }
1598
2212
  function useSaveFiles() {
1599
2213
  const save = use(ResultEnvContext).saveFiles ?? saveInBrowser;
1600
- return useCallback((files) => deliver(save, files), [save]);
1601
- }
1602
- async function deliver(save, files) {
1603
- const handed = files.map(
1604
- (file) => file.url === void 0 ? file : { ...file, url: absoluteUrl(file.url) }
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
+ })
1605
2224
  );
1606
2225
  try {
1607
2226
  const result = await save(handed);
@@ -1621,6 +2240,12 @@ function useFileDownloadShown(kind) {
1621
2240
  const files = use(ResultEnvContext).downloads?.files ?? true;
1622
2241
  return typeof files === "boolean" ? files : files.includes(kind);
1623
2242
  }
2243
+ function usePdfSaveShown() {
2244
+ return use(ResultEnvContext).downloads?.pdf ?? true;
2245
+ }
2246
+ function useAskFileName() {
2247
+ return use(ResultEnvContext).downloads?.askFileName ?? false;
2248
+ }
1624
2249
  function useProseImages() {
1625
2250
  return use(ResultEnvContext).proseImages ?? "link";
1626
2251
  }
@@ -1669,88 +2294,857 @@ var TooltipContent = React2.forwardRef(({ className, sideOffset = 6, ...props },
1669
2294
  }
1670
2295
  ) }));
1671
2296
  TooltipContent.displayName = TooltipPrimitive.Content.displayName;
1672
- var DEFAULT_IMG_SRC = "data: https:";
1673
- function frameStyles(color, mutedColor, borderColor, font) {
1674
- return `
1675
- :root { color-scheme: inherit; }
1676
- body {
1677
- margin: 0;
1678
- color: ${color};
1679
- font-family: ${font};
1680
- font-size: 13px;
1681
- line-height: 1.55;
1682
- background: transparent;
1683
- 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)
1684
2325
  }
1685
- :where(h1, h2, h3, h4, h5, h6) { margin: 0 0 0.4em; line-height: 1.25; }
1686
- :where(h1) { font-size: 1.4em; }
1687
- :where(h2) { font-size: 1.2em; }
1688
- :where(h3) { font-size: 1.05em; }
1689
- :where(p, ul, ol, table, pre, blockquote) { margin: 0 0 0.75em; }
1690
- :where(ul, ol) { padding-inline-start: 1.25em; }
1691
- :where(table) { border-collapse: collapse; width: 100%; }
1692
- :where(th, td) {
1693
- border: 1px solid ${borderColor};
1694
- padding: 4px 8px;
1695
- text-align: left;
1696
- 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;
1697
2351
  }
1698
- :where(th) { font-weight: 600; }
1699
- :where(caption) { caption-side: top; color: ${mutedColor}; padding-bottom: 4px; text-align: left; }
1700
- :where(a) { color: inherit; }
1701
- :where(code, pre) { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; }
1702
- :where(pre) { white-space: pre-wrap; }
1703
- :where(blockquote) { border-inline-start: 2px solid ${borderColor}; margin-inline-start: 0; padding-inline-start: 0.75em; color: ${mutedColor}; }
1704
- :where(img) { max-width: 100%; height: auto; }
1705
- :where(hr) { border: 0; border-top: 1px solid ${borderColor}; }
1706
- :where(*:last-child) { margin-bottom: 0; }
1707
- `;
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
+ );
1708
2439
  }
1709
- function frameDocument(content, styles, imgSrc) {
1710
- const body = content.cssClass ? (
1711
- // The class the value states, honoured the way the runtime's own HTML
1712
- // rendering honours it: as a wrapper, not as something merged into ours.
1713
- `<div class="${escapeAttribute(content.cssClass)}">${content.innerHtml}</div>`
1714
- ) : content.innerHtml;
1715
- return [
1716
- '<!doctype html><html><head><meta charset="utf-8">',
1717
- `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src ${imgSrc}; base-uri 'none'; form-action 'none'">`,
1718
- `<style>${styles}</style></head><body>${body}</body></html>`
1719
- ].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
+ };
1720
2547
  }
1721
- function escapeAttribute(value) {
1722
- 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
+ });
1723
3092
  }
1724
3093
  var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
3094
+ var FRAGMENT_MAX_HEIGHT = 1400;
3095
+ var FILL_MIN_HEIGHT = 320;
1725
3096
  function HtmlPreview({
1726
3097
  content,
1727
- maxHeight = 1400,
1728
- imgSrc = DEFAULT_IMG_SRC
3098
+ maxHeight,
3099
+ imgSrc = DEFAULT_IMG_SRC,
3100
+ fontSrc,
3101
+ downloadPdf,
3102
+ pdfFileName: pdfFileName2,
3103
+ bare = false,
3104
+ fill = false
1729
3105
  }) {
1730
3106
  const hostRef = useRef(null);
1731
3107
  const frameRef = useRef(null);
3108
+ const s = useFieldStrings();
1732
3109
  const [doc, setDoc] = useState(null);
1733
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;
1734
3121
  useIsomorphicLayoutEffect(() => {
1735
3122
  const host = hostRef.current;
1736
- if (!host) return;
3123
+ if (!host || !resolved) return;
1737
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
+ };
1738
3131
  setDoc(
1739
- frameDocument(
1740
- content,
1741
- frameStyles(
1742
- computed.color,
1743
- computed.getPropertyValue("--muted-foreground").trim() || computed.color,
1744
- computed.borderColor || computed.color,
1745
- computed.fontFamily
1746
- ),
1747
- imgSrc
1748
- )
3132
+ frameDocument(resolved, {
3133
+ styles: frameStyles(typography),
3134
+ imgSrc,
3135
+ fontSrc: fontSrc ?? imgSrc
3136
+ })
1749
3137
  );
1750
- }, [content, imgSrc]);
3138
+ }, [resolved, imgSrc, fontSrc, page]);
1751
3139
  const measure = () => {
1752
- const body = frameRef.current?.contentDocument?.body;
1753
- 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);
1754
3148
  };
1755
3149
  useEffect(() => {
1756
3150
  if (!doc || !hostRef.current || typeof ResizeObserver === "undefined") return;
@@ -1758,30 +3152,110 @@ function HtmlPreview({
1758
3152
  observer.observe(hostRef.current);
1759
3153
  return () => observer.disconnect();
1760
3154
  }, [doc]);
1761
- return /* @__PURE__ */ jsx(
3155
+ const frameBox = /* @__PURE__ */ jsx(
1762
3156
  "div",
1763
3157
  {
1764
3158
  ref: hostRef,
1765
- className: "overflow-hidden rounded-lg border border-border bg-card/40 px-3.5 py-3 text-[13px] text-foreground",
1766
- 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(
1767
3181
  "iframe",
1768
3182
  {
1769
3183
  ref: frameRef,
1770
3184
  title: content.cssClass ? `HTML result (${content.cssClass})` : "HTML result",
1771
3185
  srcDoc: doc,
1772
- onLoad: measure,
3186
+ onLoad,
1773
3187
  sandbox: "allow-same-origin",
1774
3188
  style: {
1775
3189
  display: "block",
1776
3190
  width: "100%",
1777
3191
  border: 0,
1778
- height: height === 0 ? void 0 : Math.min(height, maxHeight),
1779
- 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) } : {}
1780
3200
  }
1781
3201
  }
1782
3202
  )
1783
3203
  }
1784
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;
1785
3259
  }
1786
3260
  var LEX_OPTIONS = { gfm: true, breaks: true };
1787
3261
  function Markdown({ text, className, proseImages }) {
@@ -2233,6 +3707,7 @@ function FileDownloadButton({ file }) {
2233
3707
  const resolve = useResolveUrl();
2234
3708
  const save = useSaveFiles();
2235
3709
  const shown = useFileDownloadShown(file.kind);
3710
+ const { ask, dialog } = useFileNamePrompt();
2236
3711
  const statusId = useId();
2237
3712
  const [attempt, setAttempt] = useState(null);
2238
3713
  useEffect(() => {
@@ -2259,13 +3734,15 @@ ${planned.url ?? planned.text}`;
2259
3734
  "button",
2260
3735
  {
2261
3736
  type: "button",
2262
- onClick: () => {
3737
+ onClick: async () => {
3738
+ const { stem: stem2, extension } = splitFileName(planned.name);
3739
+ const name = await ask(stem2, extension);
3740
+ if (name === null) return;
2263
3741
  const started = { identity, state: "saving" };
2264
3742
  setAttempt(started);
2265
- void save([planned]).then((result) => {
2266
- const settled = result.failed.length > 0 ? "failed" : "saved";
2267
- setAttempt((current) => current === started ? { identity, state: settled } : current);
2268
- });
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);
2269
3746
  },
2270
3747
  disabled: state === "saving",
2271
3748
  "aria-label": label,
@@ -2278,7 +3755,8 @@ ${planned.url ?? planned.text}`;
2278
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" })
2279
3756
  }
2280
3757
  ),
2281
- /* @__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
2282
3760
  ] });
2283
3761
  }
2284
3762
  function paintable(content, resolve) {
@@ -2715,7 +4193,7 @@ function CompositeValue({ members }) {
2715
4193
  function NativeValue({ value }) {
2716
4194
  if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx(Absent, {});
2717
4195
  const html = readHtmlContent(value);
2718
- if (html) return /* @__PURE__ */ jsx(HtmlPreview, { content: html });
4196
+ if (html) return /* @__PURE__ */ jsx(HtmlPreview, { content: html, downloadPdf: false });
2719
4197
  const date = readDateContent(value);
2720
4198
  if (date) return /* @__PURE__ */ jsx("span", { className: "text-[13px] text-foreground", children: formatDateContent(date) });
2721
4199
  const document2 = readDocumentContent(value);
@@ -2795,6 +4273,7 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2795
4273
  const s = useFieldStrings();
2796
4274
  const unwrapped = unwrap(field, value);
2797
4275
  const pageDownloadShown = useFileDownloadShown("markup");
4276
+ const pdfShown = usePdfSaveShown();
2798
4277
  const location = useResultLocation();
2799
4278
  const header = hideLabel ? null : /* @__PURE__ */ jsx(Label2, { field, describe: true });
2800
4279
  const copyText = (field.kind === "text" || field.kind === "prose") && typeof unwrapped === "string" && unwrapped !== "" ? unwrapped : void 0;
@@ -2806,13 +4285,24 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2806
4285
  }
2807
4286
  if (isNativeHtmlNode(field)) {
2808
4287
  const content = readHtmlContent(unwrapped);
2809
- 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;
2810
4292
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2811
- 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: [
2812
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,
2813
4303
  /* @__PURE__ */ jsx(FileActions, { file: page })
2814
4304
  ] }) : header,
2815
- content ? /* @__PURE__ */ jsx(HtmlPreview, { content }) : /* @__PURE__ */ jsx(Absent, {})
4305
+ content ? /* @__PURE__ */ jsx(HtmlPreview, { content, downloadPdf: false }) : /* @__PURE__ */ jsx(Absent, {})
2816
4306
  ] });
2817
4307
  }
2818
4308
  if (isNativeCompositeNode(field)) {
@@ -2943,6 +4433,11 @@ function ResultNode({ field, value, depth = 0, hideLabel = false }) {
2943
4433
  }
2944
4434
  return null;
2945
4435
  }
4436
+ function stem(name) {
4437
+ return name === void 0 ? "" : name.replace(/\.[^.]+$/, "");
4438
+ }
4439
+ var ALL_VIEWS = ["rendered", "json"];
4440
+ var RESULT_ONLY = ["rendered"];
2946
4441
  var JSON_TOKEN_RE = /("(?:\\.|[^"\\])*"\s*:)|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
2947
4442
  function highlight(json) {
2948
4443
  const out = [];
@@ -3025,14 +4520,18 @@ function StuffPanel({
3025
4520
  field,
3026
4521
  value,
3027
4522
  name,
4523
+ views: viewsProp,
3028
4524
  defaultView = "rendered",
3029
4525
  downloadBaseName,
3030
4526
  className
3031
4527
  }) {
3032
4528
  const s = useFieldStrings();
4529
+ const presentation = useFieldPresentation();
4530
+ const offered = viewsProp && viewsProp.length > 0 ? viewsProp : viewsProp || presentation === "app" ? RESULT_ONLY : ALL_VIEWS;
3033
4531
  const named = name ? { ...field, name } : field;
3034
4532
  const baseName = downloadBaseName ?? named.name ?? "result";
3035
- const [view, setView] = useState(defaultView);
4533
+ const [chosen, setView] = useState(defaultView);
4534
+ const view = offered.includes(chosen) ? chosen : offered[0] ?? "rendered";
3036
4535
  const [saving, setSaving] = useState(null);
3037
4536
  const [missed, setMissed] = useState(null);
3038
4537
  const latest = useRef(null);
@@ -3040,8 +4539,28 @@ function StuffPanel({
3040
4539
  const missedShown = missed && isShownResult(missed.attempt, field, baseName, value) ? missed.names : [];
3041
4540
  const resolveUrl = useResolveUrl();
3042
4541
  const save = useSaveFiles();
3043
- 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;
3044
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
+ }
3045
4564
  const attempt = {
3046
4565
  field,
3047
4566
  baseName,
@@ -3052,10 +4571,6 @@ function StuffPanel({
3052
4571
  setSaving(attempt);
3053
4572
  setMissed(null);
3054
4573
  try {
3055
- const plan = planStuffSave(field, value, {
3056
- baseName,
3057
- ...resolveUrl ? { resolveUrl } : {}
3058
- });
3059
4574
  const { failed } = await save(plan.files);
3060
4575
  if (latest.current !== attempt) return;
3061
4576
  setMissed({
@@ -3068,65 +4583,102 @@ function StuffPanel({
3068
4583
  } finally {
3069
4584
  setSaving((current) => current === attempt ? null : current);
3070
4585
  }
3071
- }, [field, value, baseName, resolveUrl, save]);
3072
- const views = [
3073
- { id: "rendered", label: s.viewRendered },
3074
- { id: "json", label: s.viewJson }
3075
- ];
3076
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", className), children: [
3077
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-4 gap-y-1", children: [
3078
- /* @__PURE__ */ jsx("div", { className: "min-w-0 space-y-1", children: /* @__PURE__ */ jsx(ResultHeader, { field: named }) }),
3079
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
3080
- downloadShown && /* @__PURE__ */ jsxs(
3081
- "button",
3082
- {
3083
- type: "button",
3084
- onClick: () => void handleDownload(),
3085
- disabled: savingShown,
3086
- "aria-label": s.download,
3087
- className: "inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1 disabled:opacity-60",
3088
- children: [
3089
- savingShown ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" }),
3090
- savingShown ? s.downloading : s.download
3091
- ]
3092
- }
3093
- ),
3094
- /* @__PURE__ */ jsx(
3095
- "div",
3096
- {
3097
- role: "group",
3098
- "aria-label": s.resultViewGroup,
3099
- className: "flex rounded-md border border-border p-0.5",
3100
- 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(
3101
4617
  "button",
3102
4618
  {
3103
4619
  type: "button",
3104
- onClick: () => setView(id),
3105
- "aria-pressed": view === id,
3106
- className: cn(
3107
- "rounded px-2 py-0.5 text-[12px] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-1",
3108
- view === id ? "bg-card font-medium text-foreground" : "text-muted-foreground hover:text-foreground"
3109
- ),
3110
- children: label
3111
- },
3112
- id
3113
- ))
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) : ""
3114
4664
  }
3115
- )
3116
- ] })
3117
- ] }),
3118
- downloadShown && /* @__PURE__ */ jsx(
3119
- "p",
3120
- {
3121
- role: "status",
3122
- className: cn("text-[12px] text-destructive", missedShown.length === 0 && "sr-only"),
3123
- children: missedShown.length > 0 ? s.downloadIncomplete(missedShown) : ""
3124
- }
3125
- ),
3126
- view === "json" ? /* @__PURE__ */ jsx(JsonView, { value }) : /* @__PURE__ */ jsx(ResultRoot, { baseName, path: [field.name], children: /* @__PURE__ */ jsx(ResultField, { field, value, hideLabel: true }) })
3127
- ] });
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(/\.[^.]+$/, "");
3128
4680
  }
3129
4681
 
3130
- export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EnumField, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, NumberField, ObjectField, OptionalToggle, ProseField, ResultEnvProvider, ResultField, ResultRoot, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, humanizeFieldName, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
3131
- //# sourceMappingURL=chunk-2D4NEFUH.js.map
3132
- //# sourceMappingURL=chunk-2D4NEFUH.js.map
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