@drawnagency/primitives 0.1.79 → 0.1.81

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 (125) hide show
  1. package/dist/{chunk-RWMSDDQM.js → chunk-DKXISNJ2.js} +79 -26
  2. package/dist/{chunk-RSITF24B.js → chunk-EVNEMJZ6.js} +9 -3
  3. package/dist/{chunk-DR22HM44.js → chunk-FDBZZ4AT.js} +1 -1
  4. package/dist/{chunk-QB5Q23G2.js → chunk-FN4NKUZU.js} +1 -1
  5. package/dist/{chunk-EMGLZQCM.js → chunk-MBJUWOM2.js} +1 -1
  6. package/dist/{chunk-FRTJNS5F.js → chunk-P4ZNHZMP.js} +6 -6
  7. package/dist/{chunk-UNUJPLZV.js → chunk-PJOHAWNV.js} +1 -1
  8. package/dist/{chunk-2DDWWTRX.js → chunk-Q6LZOH5M.js} +1 -1
  9. package/dist/chunk-WP5UX4MI.js +63 -0
  10. package/dist/{chunk-3LNKOLZP.js → chunk-YKOLOZB2.js} +1 -1
  11. package/dist/{chunk-4XXLH5CL.js → chunk-ZEXR7JYQ.js} +19 -12
  12. package/dist/components/brandguide/Colors.d.ts +2 -1
  13. package/dist/components/brandguide/Colors.d.ts.map +1 -1
  14. package/dist/components/editor/PagesModal.d.ts +2 -0
  15. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  16. package/dist/components/editor/index.js +6 -5
  17. package/dist/components/primitives/EditableGrid.d.ts +11 -1
  18. package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
  19. package/dist/components/primitives/gridDrop.d.ts +18 -0
  20. package/dist/components/primitives/gridDrop.d.ts.map +1 -0
  21. package/dist/components/primitives/index.js +7 -6
  22. package/dist/components/primitives/useEditableCollection.d.ts +1 -0
  23. package/dist/components/primitives/useEditableCollection.d.ts.map +1 -1
  24. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  25. package/dist/components/sections/brand-guide.js +7 -6
  26. package/dist/components/sections/register-schemas.js +10 -9
  27. package/dist/components/sections/register.js +10 -9
  28. package/dist/components/shell/DocInspector.d.ts +17 -0
  29. package/dist/components/shell/DocInspector.d.ts.map +1 -0
  30. package/dist/components/shell/DocumentEditorOverlay.d.ts +29 -0
  31. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +1 -0
  32. package/dist/components/shell/DocumentPresentOverlay.d.ts +30 -0
  33. package/dist/components/shell/DocumentPresentOverlay.d.ts.map +1 -0
  34. package/dist/components/shell/EditorContext.d.ts +2 -0
  35. package/dist/components/shell/EditorContext.d.ts.map +1 -1
  36. package/dist/components/shell/EditorShell.d.ts +44 -0
  37. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  38. package/dist/components/shell/HistoryToolbar.d.ts +8 -1
  39. package/dist/components/shell/HistoryToolbar.d.ts.map +1 -1
  40. package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  41. package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  42. package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  43. package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  44. package/dist/document/components/DocCoverPage.d.ts +11 -0
  45. package/dist/document/components/DocCoverPage.d.ts.map +1 -0
  46. package/dist/document/components/DocPage.d.ts +10 -0
  47. package/dist/document/components/DocPage.d.ts.map +1 -0
  48. package/dist/document/components/DocRail.d.ts +27 -0
  49. package/dist/document/components/DocRail.d.ts.map +1 -0
  50. package/dist/document/components/DocSectionPage.d.ts +12 -0
  51. package/dist/document/components/DocSectionPage.d.ts.map +1 -0
  52. package/dist/document/components/DocViewer.d.ts +18 -0
  53. package/dist/document/components/DocViewer.d.ts.map +1 -0
  54. package/dist/document/components/Wordmark.d.ts +4 -0
  55. package/dist/document/components/Wordmark.d.ts.map +1 -0
  56. package/dist/document/components/measure.d.ts +6 -0
  57. package/dist/document/components/measure.d.ts.map +1 -0
  58. package/dist/document/components/useDocPages.d.ts +12 -0
  59. package/dist/document/components/useDocPages.d.ts.map +1 -0
  60. package/dist/document/engine/blocks.d.ts +2 -0
  61. package/dist/document/engine/blocks.d.ts.map +1 -0
  62. package/dist/document/engine/harvest.d.ts +3 -0
  63. package/dist/document/engine/harvest.d.ts.map +1 -0
  64. package/dist/document/engine/masonry.d.ts +25 -0
  65. package/dist/document/engine/masonry.d.ts.map +1 -0
  66. package/dist/document/engine/paginate.d.ts +40 -0
  67. package/dist/document/engine/paginate.d.ts.map +1 -0
  68. package/dist/document/engine/settings.d.ts +70 -0
  69. package/dist/document/engine/settings.d.ts.map +1 -0
  70. package/dist/document/engine/types.d.ts +40 -0
  71. package/dist/document/engine/types.d.ts.map +1 -0
  72. package/dist/document/index.d.ts +13 -0
  73. package/dist/document/index.d.ts.map +1 -0
  74. package/dist/document/index.js +881 -0
  75. package/dist/document/styles/document.css +208 -0
  76. package/dist/index.js +29 -28
  77. package/dist/lib/document-input.d.ts +25 -0
  78. package/dist/lib/document-input.d.ts.map +1 -0
  79. package/dist/lib/index.js +10 -9
  80. package/dist/lib/pages.d.ts +2 -0
  81. package/dist/lib/pages.d.ts.map +1 -1
  82. package/dist/lib/registry.d.ts +3 -0
  83. package/dist/lib/registry.d.ts.map +1 -1
  84. package/dist/media/index.js +4 -3
  85. package/dist/schemas/index.js +3 -2
  86. package/dist/schemas/site-config.d.ts +223 -1
  87. package/dist/schemas/site-config.d.ts.map +1 -1
  88. package/package.json +5 -1
  89. package/src/components/brandguide/Colors.tsx +15 -5
  90. package/src/components/editor/PagesModal.tsx +16 -2
  91. package/src/components/primitives/EditableGrid.tsx +86 -24
  92. package/src/components/primitives/gridDrop.ts +26 -0
  93. package/src/components/primitives/useEditableCollection.ts +16 -1
  94. package/src/components/sections/Colors/index.tsx +2 -1
  95. package/src/components/shell/DocInspector.tsx +278 -0
  96. package/src/components/shell/DocumentEditorOverlay.tsx +169 -0
  97. package/src/components/shell/DocumentPresentOverlay.tsx +87 -0
  98. package/src/components/shell/EditorContext.tsx +6 -1
  99. package/src/components/shell/EditorShell.tsx +171 -10
  100. package/src/components/shell/HistoryToolbar.tsx +24 -2
  101. package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  102. package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  103. package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  104. package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  105. package/src/document/components/DocCoverPage.tsx +45 -0
  106. package/src/document/components/DocPage.tsx +40 -0
  107. package/src/document/components/DocRail.tsx +160 -0
  108. package/src/document/components/DocSectionPage.tsx +45 -0
  109. package/src/document/components/DocViewer.tsx +285 -0
  110. package/src/document/components/Wordmark.tsx +18 -0
  111. package/src/document/components/measure.tsx +105 -0
  112. package/src/document/components/useDocPages.ts +67 -0
  113. package/src/document/engine/blocks.ts +87 -0
  114. package/src/document/engine/harvest.ts +80 -0
  115. package/src/document/engine/masonry.ts +89 -0
  116. package/src/document/engine/paginate.ts +209 -0
  117. package/src/document/engine/settings.ts +77 -0
  118. package/src/document/engine/types.ts +54 -0
  119. package/src/document/index.ts +18 -0
  120. package/src/document/styles/document.css +208 -0
  121. package/src/lib/document-input.ts +57 -0
  122. package/src/lib/pages.ts +22 -0
  123. package/src/lib/registry.ts +3 -0
  124. package/src/schemas/site-config.ts +22 -7
  125. /package/dist/{chunk-4YEOTFVU.js → chunk-YBVK254P.js} +0 -0
@@ -0,0 +1,881 @@
1
+ import {
2
+ documentOptionsSchema,
3
+ sectionSettingsSchema,
4
+ settingsFor
5
+ } from "../chunk-WP5UX4MI.js";
6
+ import "../chunk-JSBRDJBE.js";
7
+
8
+ // src/document/engine/types.ts
9
+ var PREAMBLE_ID = "__preamble";
10
+
11
+ // src/document/engine/blocks.ts
12
+ var DISALLOWED_TAGS = /* @__PURE__ */ new Set([
13
+ "script",
14
+ "style",
15
+ "iframe",
16
+ "object",
17
+ "embed",
18
+ "link",
19
+ "meta",
20
+ "base",
21
+ "form",
22
+ // Raw-text / escapable-raw-text and declarative elements whose contents are
23
+ // not parsed as normal markup. Left in place they enable serialize/reparse
24
+ // (mXSS) bypasses — e.g. an <img onerror> smuggled inside <noscript> text.
25
+ // Removing the element drops its contents too (including <template> content).
26
+ "noscript",
27
+ "xmp",
28
+ "noembed",
29
+ "noframes",
30
+ "plaintext",
31
+ "template"
32
+ ]);
33
+ var DISALLOWED_SELECTOR = Array.from(DISALLOWED_TAGS).join(",");
34
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction"]);
35
+ function isDangerousUrl(attrName, rawValue) {
36
+ const value = rawValue.toLowerCase().replace(/[\u0000-\u0020]/g, "");
37
+ if (value.startsWith("javascript:") || value.startsWith("vbscript:")) return true;
38
+ if (attrName === "src" && value.startsWith("data:text/html")) return true;
39
+ return false;
40
+ }
41
+ function scrubElement(root) {
42
+ root.querySelectorAll(DISALLOWED_SELECTOR).forEach((el) => el.remove());
43
+ const elements = [root, ...Array.from(root.querySelectorAll("*"))];
44
+ for (const el of elements) {
45
+ for (const attr of Array.from(el.attributes)) {
46
+ const name = attr.name.toLowerCase();
47
+ if (name.startsWith("on") || name === "srcdoc") {
48
+ el.removeAttribute(attr.name);
49
+ continue;
50
+ }
51
+ if (URL_ATTRS.has(name) && isDangerousUrl(name, attr.value)) {
52
+ el.removeAttribute(attr.name);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ function splitProseBlocks(html) {
58
+ if (!html.trim()) return [];
59
+ const doc = new DOMParser().parseFromString(html, "text/html");
60
+ const blocks = [];
61
+ for (const node of Array.from(doc.body.childNodes)) {
62
+ if (node.nodeType === Node.ELEMENT_NODE) {
63
+ const el = node;
64
+ if (DISALLOWED_TAGS.has(el.tagName.toLowerCase())) continue;
65
+ scrubElement(el);
66
+ if ((el.textContent ?? "").trim() || el.matches("img") || el.querySelector("img")) {
67
+ blocks.push(el.outerHTML);
68
+ }
69
+ } else if (node.nodeType === Node.TEXT_NODE) {
70
+ const text = (node.textContent ?? "").trim();
71
+ if (text) {
72
+ const p = doc.createElement("p");
73
+ p.textContent = text;
74
+ blocks.push(p.outerHTML);
75
+ }
76
+ }
77
+ }
78
+ return blocks;
79
+ }
80
+
81
+ // src/document/engine/harvest.ts
82
+ function str(v) {
83
+ return typeof v === "string" ? v : "";
84
+ }
85
+ function asRecord(v) {
86
+ return v && typeof v === "object" ? v : {};
87
+ }
88
+ function harvestInto(target, type, content, imagesById) {
89
+ const c = asRecord(content);
90
+ switch (type) {
91
+ case "sub_heading":
92
+ case "link_heading":
93
+ target.textBlocks.push({ kind: "heading", level: 2, text: str(c.heading) });
94
+ break;
95
+ case "sub_sub_heading":
96
+ target.textBlocks.push({ kind: "heading", level: 3, text: str(c.heading) });
97
+ break;
98
+ case "prose":
99
+ for (const html of splitProseBlocks(str(c.body))) {
100
+ target.textBlocks.push({ kind: "prose", html });
101
+ }
102
+ break;
103
+ case "media": {
104
+ const ref = asRecord(c.ref);
105
+ const image = imagesById[str(ref.imageId)];
106
+ if (image) target.images.push(image);
107
+ break;
108
+ }
109
+ case "container": {
110
+ const children = Array.isArray(c.children) ? c.children : [];
111
+ for (const child of children) {
112
+ const cc = asRecord(child);
113
+ if (typeof cc.type === "string") {
114
+ harvestInto(target, cc.type, cc.content, imagesById);
115
+ }
116
+ }
117
+ break;
118
+ }
119
+ default:
120
+ break;
121
+ }
122
+ }
123
+ function harvest(sections, pageTitle, imagesById) {
124
+ const chunks = [];
125
+ let current = { id: PREAMBLE_ID, title: pageTitle, textBlocks: [], images: [] };
126
+ let preamble = current;
127
+ for (const section of sections) {
128
+ if (section.type === "link_heading") {
129
+ current = { id: section.id, title: str(asRecord(section.content).heading), textBlocks: [], images: [] };
130
+ chunks.push(current);
131
+ } else {
132
+ harvestInto(current, section.type, section.content, imagesById);
133
+ }
134
+ }
135
+ const preambleHasContent = preamble.textBlocks.length > 0 || preamble.images.length > 0;
136
+ return preambleHasContent ? [preamble, ...chunks] : chunks;
137
+ }
138
+
139
+ // src/document/engine/masonry.ts
140
+ var EPS = 1e-6;
141
+ var effectiveAspect = (im, fit) => fit === "cover" ? 1 : im.width / im.height;
142
+ function layoutRows(images, o, fits) {
143
+ const asp = images.map((im, i2) => effectiveAspect(im, fits[i2]));
144
+ const boxes = [];
145
+ let y = 0, i = 0;
146
+ let used = null;
147
+ while (i < images.length) {
148
+ const row = [];
149
+ let natW = 0;
150
+ while (i + row.length < images.length) {
151
+ row.push(i + row.length);
152
+ const gaps2 = o.gapIn * (row.length - 1);
153
+ natW = row.reduce((s, idx) => s + asp[idx] * o.targetRowHeightIn, 0) + gaps2;
154
+ if (natW >= o.bodyWIn) break;
155
+ }
156
+ const gaps = o.gapIn * (row.length - 1);
157
+ const sumAspect = row.reduce((s, idx) => s + asp[idx], 0);
158
+ const isFull = natW >= o.bodyWIn;
159
+ const rowH = isFull ? (o.bodyWIn - gaps) / sumAspect : o.targetRowHeightIn;
160
+ if (used === null && y + rowH > o.bodyHIn + EPS && boxes.length > 0) used = boxes.length;
161
+ let x = 0;
162
+ for (const idx of row) {
163
+ const w = asp[idx] * rowH;
164
+ boxes.push({ asset: images[idx], xIn: x, yIn: y, wIn: w, hIn: rowH });
165
+ x += w + o.gapIn;
166
+ }
167
+ y += rowH + o.gapIn;
168
+ i += row.length;
169
+ }
170
+ return { boxes, used: used ?? images.length, endY: y };
171
+ }
172
+ function layoutColumns(images, o, fits) {
173
+ const n = o.columns;
174
+ const colW = (o.bodyWIn - o.gapIn * (n - 1)) / n;
175
+ const colH = new Array(n).fill(0);
176
+ const boxes = [];
177
+ let used = null;
178
+ images.forEach((im, idx) => {
179
+ let c = 0;
180
+ for (let k = 1; k < n; k++) if (colH[k] < colH[c]) c = k;
181
+ const y = colH[c];
182
+ const h = colW / effectiveAspect(im, fits[idx]);
183
+ if (used === null && y + h > o.bodyHIn + EPS && idx > 0) used = idx;
184
+ boxes.push({ asset: im, xIn: c * (colW + o.gapIn), yIn: y, wIn: colW, hIn: h });
185
+ colH[c] = y + h + o.gapIn;
186
+ });
187
+ return { boxes, used: used ?? images.length, colHeights: colH, colW };
188
+ }
189
+ function layoutMasonryPage(images, o, fits = []) {
190
+ if (o.mode === "columns") {
191
+ const r2 = layoutColumns(images, o, fits);
192
+ return { boxes: r2.boxes, used: r2.used };
193
+ }
194
+ const r = layoutRows(images, o, fits);
195
+ return { boxes: r.boxes, used: r.used };
196
+ }
197
+
198
+ // src/document/engine/paginate.ts
199
+ var DOC_GEOM = {
200
+ bodyWIn: 15.8,
201
+ bodyHIn: 9.05,
202
+ titleBandIn: 0.9,
203
+ gapIn: 0.15,
204
+ leftColWIn: 4.6,
205
+ colGutterIn: 0.4,
206
+ minImageRegionIn: 1.2,
207
+ masonryTargetRowIn: 2.6,
208
+ // Top text fills at most this fraction of the body height below the title
209
+ // band, so a text-heavy top section always leaves ≥ half the page for images.
210
+ topTextMaxFraction: 0.5
211
+ };
212
+ var EPS2 = 1e-6;
213
+ function fitImages(images, layout, region, geom) {
214
+ if (images.length === 0 || region.hIn < geom.minImageRegionIn) return { boxes: [], used: 0 };
215
+ if (layout.mode === "grid") {
216
+ const used = Math.min(layout.rows * layout.columns, images.length);
217
+ const cellW = (region.wIn - (layout.columns - 1) * geom.gapIn) / layout.columns;
218
+ const cellH = (region.hIn - (layout.rows - 1) * geom.gapIn) / layout.rows;
219
+ const boxes2 = images.slice(0, used).map((image, i) => {
220
+ const r2 = Math.floor(i / layout.columns);
221
+ const c = i % layout.columns;
222
+ return {
223
+ image,
224
+ xIn: region.xIn + c * (cellW + geom.gapIn),
225
+ yIn: region.yIn + r2 * (cellH + geom.gapIn),
226
+ wIn: cellW,
227
+ hIn: cellH,
228
+ fit: "cover"
229
+ };
230
+ });
231
+ return { boxes: boxes2, used };
232
+ }
233
+ const r = layoutMasonryPage(images, {
234
+ mode: layout.flow,
235
+ columns: layout.columns,
236
+ bodyWIn: region.wIn,
237
+ bodyHIn: region.hIn,
238
+ gapIn: geom.gapIn,
239
+ targetRowHeightIn: geom.masonryTargetRowIn
240
+ });
241
+ const boxes = r.boxes.slice(0, r.used).map((b) => ({
242
+ image: b.asset,
243
+ xIn: region.xIn + b.xIn,
244
+ yIn: region.yIn + b.yIn,
245
+ wIn: b.wIn,
246
+ hIn: b.hIn,
247
+ fit: "contain"
248
+ }));
249
+ return { boxes, used: r.used };
250
+ }
251
+ function paginateChunk(chunk, settings, textHeightsIn, geom = DOC_GEOM) {
252
+ if (settings.excluded) return [];
253
+ if (chunk.textBlocks.length !== textHeightsIn.length) {
254
+ throw new Error("paginateChunk: textHeightsIn must match chunk.textBlocks");
255
+ }
256
+ if (chunk.textBlocks.length === 0 && chunk.images.length === 0) return [];
257
+ const pages = [];
258
+ let ti = 0;
259
+ let ii = 0;
260
+ const top = geom.titleBandIn;
261
+ while (ti < chunk.textBlocks.length || ii < chunk.images.length) {
262
+ const cont = pages.length > 0;
263
+ const warnings = [];
264
+ const textW = settings.textPlacement === "top" ? geom.bodyWIn : geom.leftColWIn;
265
+ const capY = settings.textPlacement === "top" ? geom.titleBandIn + (geom.bodyHIn - geom.titleBandIn) * geom.topTextMaxFraction : geom.bodyHIn;
266
+ const placed = [];
267
+ let textEnd = top;
268
+ while (ti < chunk.textBlocks.length) {
269
+ const h = textHeightsIn[ti];
270
+ const yAfter = placed.length === 0 ? top + h : textEnd + geom.gapIn + h;
271
+ if (yAfter <= capY + EPS2) {
272
+ placed.push(chunk.textBlocks[ti]);
273
+ textEnd = yAfter;
274
+ ti++;
275
+ } else if (placed.length === 0) {
276
+ placed.push(chunk.textBlocks[ti]);
277
+ textEnd = capY;
278
+ ti++;
279
+ warnings.push("A text block is taller than the page and was clipped.");
280
+ break;
281
+ } else {
282
+ break;
283
+ }
284
+ }
285
+ if (!cont && settings.dropTextOverflow && ti < chunk.textBlocks.length) {
286
+ const dropped = chunk.textBlocks.length - ti;
287
+ ti = chunk.textBlocks.length;
288
+ warnings.push(`${dropped} text block(s) hidden (overflow dropped)`);
289
+ }
290
+ const textRegion = placed.length > 0 ? { xIn: 0, yIn: top, wIn: textW, hIn: textEnd - top } : null;
291
+ let region;
292
+ if (settings.textPlacement === "leftColumn" && settings.imagePlacement === "right" && placed.length > 0) {
293
+ region = {
294
+ xIn: geom.leftColWIn + geom.colGutterIn,
295
+ yIn: top,
296
+ wIn: geom.bodyWIn - geom.leftColWIn - geom.colGutterIn,
297
+ hIn: geom.bodyHIn - top
298
+ };
299
+ } else {
300
+ const startY = placed.length > 0 ? textEnd + geom.gapIn : top;
301
+ region = { xIn: 0, yIn: startY, wIn: geom.bodyWIn, hIn: Math.max(0, geom.bodyHIn - startY) };
302
+ }
303
+ const { boxes, used } = fitImages(chunk.images.slice(ii), settings.imageLayout, region, geom);
304
+ ii += used;
305
+ if (boxes.some((b) => b.yIn + b.hIn > region.yIn + region.hIn + 0.01)) {
306
+ warnings.push("An image overflows the page and is clipped.");
307
+ }
308
+ if (placed.length === 0 && used === 0) {
309
+ warnings.push("Pagination could not place remaining content.");
310
+ pages.push({
311
+ id: `${chunk.id}#${pages.length}`,
312
+ chunkId: chunk.id,
313
+ title: cont ? `${chunk.title} Cont.` : chunk.title,
314
+ cont,
315
+ textBlocks: placed,
316
+ textRegion,
317
+ imageBoxes: boxes,
318
+ warnings
319
+ });
320
+ break;
321
+ }
322
+ pages.push({
323
+ id: `${chunk.id}#${pages.length}`,
324
+ chunkId: chunk.id,
325
+ title: cont ? `${chunk.title} Cont.` : chunk.title,
326
+ cont,
327
+ textBlocks: placed,
328
+ textRegion,
329
+ imageBoxes: boxes,
330
+ warnings
331
+ });
332
+ }
333
+ return pages;
334
+ }
335
+
336
+ // src/document/components/Wordmark.tsx
337
+ import { jsx, jsxs } from "react/jsx-runtime";
338
+ function Wordmark({ className }) {
339
+ return /* @__PURE__ */ jsxs(
340
+ "svg",
341
+ {
342
+ viewBox: "0 0 360 154.474",
343
+ fill: "currentColor",
344
+ role: "img",
345
+ "aria-label": "Drawn",
346
+ className,
347
+ xmlns: "http://www.w3.org/2000/svg",
348
+ children: [
349
+ /* @__PURE__ */ jsx("path", { d: "M93.211 18.436c-2.177.868 2.024 1.375 6.374 5.14 3.913 3.392 5.542 6.526 8.126 5.794l.084-.019c15.002-4.018 30.605-4.343 45.642-3.985 37.577.895 76.5 13.868 127.052 10.736 52.074-3.226 64.063-33.293 64.423-35.34.335-1.903-4.473.223-6.71 1.341-2.461 1.23-12.375 22.056-56.817 24.605-54.58 3.13-76.949-13.42-127.948-16.105-24.157-1.272-44.308 1.912-60.225 7.832ZM353.301 104.05c-3.497-.813-8.22-.393-14.355 3.083a42.817 42.817 0 0 0-1.464.863l-.646.396c-.153.094-.302.192-.452.286-.185.121-.375.239-.56.36-.158.1-.315.202-.47.305-.18.119-.36.234-.539.354-.177.118-.353.235-.53.35l-.557.373c-.113.076-.23.15-.344.225-2.209 1.475-4.596 3.046-7.903 4.822l-.002-.001c-.178.093-.354.189-.534.285-.25.132-.504.264-.764.398-.28.145-.574.292-.87.442-.268.133-.537.269-.817.403-.32.16-.653.319-.988.477-.282.136-.561.27-.857.407a124.187 124.187 0 0 1-4.258 1.882c-22.372 9.426-46.637 12.894-83.75 9.349-37.286-3.562-76.22-13.87-126.593-10.74-51.887 3.228-63.837 33.296-64.195 35.346-.336 1.898 4.455-.225 6.685-1.345 2.45-1.228 12.332-22.055 56.612-24.603 54.385-3.132 84.076 13.469 131.52 17.447 34.494 2.89 59.914-3.453 77.117-14.422 3.42-2.183 6.945-4.758 10.342-7.353.093-.067.176-.134.264-.202.111-.085.217-.168.329-.25.563-.434 1.122-.87 1.68-1.302.098-.077.2-.152.3-.23 1.027-.8 2.038-1.593 3.021-2.37 2.518-1.98 4.866-3.835 6.939-5.395l.823-.614.382-.28c.344-.251.675-.49.999-.718a42.728 42.728 0 0 1 1.547-1.058c.07-.047.14-.084.206-.13.31-.197.608-.379.89-.544.094-.054.189-.11.281-.16a12.3 12.3 0 0 1 .933-.486c4.427-2.017 8.968-2.27 9.22 1.837.124 2.074-1.999 4.029-5.691 4.449-2.565.292-6.454.442-9.699-.803-.026-.012-.048-.012-.077-.023-.5-.17-1.841 1.036-2.762 2-.922.966-2.05 1.673-1.084 2.163 4.29 2.29 8.832 3.056 11.611 3.137 8.07.221 14.336-1.887 15.567-8.04.93-4.653-1.559-9.227-6.507-10.371Z" }),
350
+ /* @__PURE__ */ jsx("path", { d: "M340.048 55.675c-3.094-2.81-8.125-2.34-11.568-2.069-5.92.466-11.54 3.713-15.45 6.582-.477-3.394-1.684-5.828-3.908-6.64-6.393-2.34-11.45 2.035-16.42 5.387-1.95 1.254-4.322-4.012-5.878-4.895-1.55-.882-4.995-1.347-7.803.126-4.404 2.305-7.465 5.91-7.21 8.136.257 2.297 4.149 4.28 4.065 6.879-.13 4.019-2.04 7.87-4.668 13.225-1.998 4.068-4.108 7.762-6.38 10.711-1.537 1.954-3.631 4.144-6.278 5.748-.514.277-1.04.496-1.57.685-3.299.82-4.712-3.26-3.843-8.882 1.006-6.444 9.235-29.756 9.738-30.966.502-1.21-.382-1.121-2.396-1.49-2.015-.37-6.724-.041-8.45.241-1.727.279-5.21 15.477-7.354 22.253-2.143 6.778-5.96 14.898-8.942 17.284-2.568 2.058-4.896 3.11-7.137 2.31-6.262-2.243-2.25-19.315 1.117-28.63 3.801-10.515 9.353-20.009 6.934-19.91-5.368.224-11.976 2.01-13.28 4.058-2.33 3.673-8.074 21.308-9.999 26.677-1.072 2.989-1.506 6.316-1.412 9.579-1.124.544-6.17 6.036-12.888 5.767-7.49-.303 4.697-29.98 5.369-31.32.67-1.342-11.354-1.567-12.026-.222-.668 1.34-6.037 31.487-20.409 31.487-10.457 0-5.145-25.45 7.604-34.614 10.985-7.9 25.36-2.188 26.924-2.862 1.565-.668 4.17-9.668 4.17-10.786s-4.698-1.119-18.792-.222c-14.09.893-21.92 7.38-30.195 20.69-8.278 13.31-10.068 26.73-19.412 26.73-9.22 0-1.169-18.788 3.082-27.738 4.248-8.946 6.937-17.895 5.592-19.907-1.343-2.014-16.332-.46-17.673-1.58-1.344-1.119 1.121-3.12.45-4.913-.67-1.787-10.515-1.787-14.205 0-1.887.916-2.185 3.287-2.021 5.893 1.254 7.163 1.232 14.979-.318 22.603.064.302.197.384.438.138 2.586-2.63 8.876-11.227 9.55-12.12.669-.897 8.836-.71 9.239.043.645 1.203-6.796 17.224-8.723 22.59-3.61 10.057-2.573 16.894 2.973 20.234 4.967 2.996 9.777 2.584 16.73-1.624 6.977-4.222 9.599-6.526 9.599-6.526s0 14.087 15.437 14.087c11.406 0 14.089-8.048 14.089-8.048s1.797 10.698 12.414 9.087c6.176-.937 11.652-9.47 13.41-12.763 1.045 6.066 3.97 11.25 8.174 12.625 5.887 1.922 13.06.388 19.818-4.645a15.195 15.195 0 0 0 2.495-2.362c2.356 5.703 5.865 9.538 9.681 10.008 19.478 2.399 28.458-19.807 33.299-34.217.993-2.955 1.71-5.846 2.654-9.364.268-1.007 1.183-2.504 2.994-2.939 4.188-.933 7.848-1.282 8.583.371 3.784 8.503-13.171 43.36-10.747 43.36 3.3 0 10.047-1.037 12.48-4.353 4.145-5.64 10.948-25.812 11.45-39.149 2.636-2.101 5.458-3.664 8.179-3.883 9.395-.757 6.167 14.162 3.22 22.537-4.906 13.966-3.351 22.982.632 28.735.709.9 1.775-.014 3.49-.917 1.718-.906 2.7-1.413 2.527-2.26-.011-.062-.019-.12-.029-.183-1.323-5.592 3.407-13.519 7.318-24.085 3.762-10.158 7.001-24.749 1.465-29.782ZM46.09 89.63c1.282-2.736 2.616-5.56 4.002-8.468C56.528 67.684 66.09 53.946 75.049 46.32a80.636 80.636 0 0 1 14.747-9.98c.07-.044.148-.092.221-.132 1.173-.631 1.17-1.394-.013-3.301-1.354-2.185-3.87-4.955-8.296-7.144-1.703-.842-3.224-.362-4.618.396-4.817 2.932-9.012 6.161-12.569 9.6C46.448 53.23 37.23 76.32 33.231 88.318l-1.407 4.22c-.406 1.22-.8 2.397-1.185 3.546l-.149.437c-.38 1.13-.748 2.214-1.11 3.27-.008.02-.014.043-.022.061-.993 2.89-1.932 5.525-2.846 7.885h-.01c-.22.568-.44 1.153-.66 1.689l-.006.015a79.329 79.329 0 0 1-1.105 2.609 51.44 51.44 0 0 1-1.022 2.16l-.106.22c-2.926 5.805-5.963 8.77-10.193 8.77-8.5 0-6.377-11.156 6.71-15.516a18.611 18.611 0 0 1 3.791-.83c.064-.01.118-.024.188-.035 1.761-.268 1.54-1.49 2.059-3.246.376-1.273 1.282-2.543-.193-2.404a57.899 57.899 0 0 0-6.293 1.147c-23.152 5.477-24.366 25.123-11.446 28.413 9.656 2.457 17.26-3.099 23.006-11.475.24-.356.493-.743.75-1.154.084-.132.173-.28.26-.42.177-.29.354-.584.54-.901.102-.175.208-.362.313-.545.176-.305.355-.623.537-.95.11-.198.22-.393.33-.598.198-.364.4-.74.604-1.122.098-.188.197-.368.294-.557.304-.578.614-1.173.93-1.796.028-.052.055-.112.083-.164l.337-.668c1.406-2.791 2.94-5.988 4.603-9.504 1.64-3.475 3.4-7.248 5.277-11.245Z" }),
351
+ /* @__PURE__ */ jsx("path", { d: "M79.206 111.375c35.531-11.52 39.87-52.054 28.828-74.15a34.694 34.694 0 0 0-2.573-4.302c-4.379-6.306-10.605-10.865-17.802-14.027-5.346-2.35-11.22-3.94-17.254-4.914C43.94 9.708 6.14 18.749 5.74 39.95 5.354 60.374 30.548 57.9 32.533 58.025c2.247.14 1.678-1.678-.67-4.028-2.35-2.347-7.946-7.147-7.382-15.433.844-12.415 17.78-22.482 44.612-18.03 3.758.625 6.956 1.438 9.75 2.425 8.204 2.907 12.703 7.418 16.072 13.73a55.714 55.714 0 0 1 2.154 4.559c8.11 19.313-4.926 52.567-24.438 59.737-14.466 5.317-21.625 3.08-29.713 1.281-1.304-.287-1.495 1.185-2.543 3.446-1.047 2.264-2.092 3.906-1.052 4.293 9.395 3.44 20.77 7.563 39.883 1.37Z" })
352
+ ]
353
+ }
354
+ );
355
+ }
356
+
357
+ // src/document/components/DocPage.tsx
358
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
359
+ function DocPage({
360
+ meta,
361
+ bleed = false,
362
+ chrome = true,
363
+ children
364
+ }) {
365
+ const crumb = [meta.client, meta.project, meta.dateLabel].map((s) => s.trim().toUpperCase()).filter(Boolean).join(" :: ");
366
+ return /* @__PURE__ */ jsxs2("section", { className: "deck-page", children: [
367
+ bleed ? /* @__PURE__ */ jsx2("div", { className: "deck-bleed", children }) : null,
368
+ chrome ? /* @__PURE__ */ jsxs2("header", { className: "deck-chrome", children: [
369
+ /* @__PURE__ */ jsx2("span", { className: "deck-crumb", children: crumb }),
370
+ /* @__PURE__ */ jsx2("div", { className: "deck-wordmark-clip", children: /* @__PURE__ */ jsx2(Wordmark, { className: "deck-wordmark" }) })
371
+ ] }) : null,
372
+ !bleed ? /* @__PURE__ */ jsx2("div", { className: "deck-body", children }) : null
373
+ ] });
374
+ }
375
+
376
+ // src/document/components/DocSectionPage.tsx
377
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
378
+ var inch = (v) => `${v}in`;
379
+ function DocSectionPage({ page, meta, host }) {
380
+ return /* @__PURE__ */ jsxs3(DocPage, { meta, children: [
381
+ /* @__PURE__ */ jsx3("h2", { className: "pdoc-title", children: page.title }),
382
+ page.textRegion ? /* @__PURE__ */ jsx3(
383
+ "div",
384
+ {
385
+ className: "pdoc-text",
386
+ style: { left: inch(page.textRegion.xIn), top: inch(page.textRegion.yIn), width: inch(page.textRegion.wIn) },
387
+ children: page.textBlocks.map((b, i) => {
388
+ const spacing = i > 0 ? { marginTop: inch(0.15) } : void 0;
389
+ if (b.kind === "heading") {
390
+ return b.level === 2 ? /* @__PURE__ */ jsx3("h3", { className: "pdoc-h2", style: spacing, children: b.text }, i) : /* @__PURE__ */ jsx3("h4", { className: "pdoc-h3", style: spacing, children: b.text }, i);
391
+ }
392
+ return /* @__PURE__ */ jsx3("div", { className: "pdoc-prose", style: spacing, dangerouslySetInnerHTML: { __html: b.html } }, i);
393
+ })
394
+ }
395
+ ) : null,
396
+ page.imageBoxes.map((box, i) => /* @__PURE__ */ jsx3(
397
+ "div",
398
+ {
399
+ className: "pdoc-img",
400
+ style: { left: inch(box.xIn), top: inch(box.yIn), width: inch(box.wIn), height: inch(box.hIn) },
401
+ children: /* @__PURE__ */ jsx3("img", { src: host.imageSrc(box.image, host.variant), alt: box.image.alt, style: { objectFit: box.fit } })
402
+ },
403
+ `${box.image.imageId}-${i}`
404
+ ))
405
+ ] });
406
+ }
407
+
408
+ // src/document/components/DocCoverPage.tsx
409
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
410
+ function DocCoverPage({
411
+ cover,
412
+ meta,
413
+ image,
414
+ host
415
+ }) {
416
+ return /* @__PURE__ */ jsx4(DocPage, { meta, bleed: true, chrome: false, children: /* @__PURE__ */ jsxs4("div", { className: "layout-titlecard", style: { background: cover.bgColor }, children: [
417
+ /* @__PURE__ */ jsxs4("header", { className: "layout-titlecard__masthead", children: [
418
+ /* @__PURE__ */ jsx4(Wordmark, { className: "layout-titlecard__wordmark" }),
419
+ /* @__PURE__ */ jsx4("span", { className: "layout-titlecard__rule" })
420
+ ] }),
421
+ image && host ? /* @__PURE__ */ jsx4("div", { className: "layout-titlecard__image", children: /* @__PURE__ */ jsx4("img", { src: host.imageSrc(image, host.variant), alt: image.alt }) }) : null,
422
+ /* @__PURE__ */ jsxs4("div", { className: "layout-titlecard__lockup", style: { color: cover.accentColor }, children: [
423
+ /* @__PURE__ */ jsx4("h1", { className: "layout-titlecard__title", children: cover.title }),
424
+ /* @__PURE__ */ jsx4("span", { className: "layout-titlecard__titlerule", style: { background: cover.accentColor } }),
425
+ /* @__PURE__ */ jsx4("p", { className: "layout-titlecard__label", children: cover.label }),
426
+ /* @__PURE__ */ jsx4("p", { className: "layout-titlecard__date", children: cover.date || meta.dateLabel })
427
+ ] })
428
+ ] }) });
429
+ }
430
+
431
+ // src/document/components/useDocPages.ts
432
+ import { useMemo } from "react";
433
+
434
+ // src/document/components/measure.tsx
435
+ import { useEffect, useLayoutEffect, useState } from "react";
436
+ var PX_PER_IN = 96;
437
+ function widthInFor(settings) {
438
+ return settings?.textPlacement === "leftColumn" ? DOC_GEOM.leftColWIn : DOC_GEOM.bodyWIn;
439
+ }
440
+ function blockClassName(block) {
441
+ if (block.kind === "heading") return block.level === 2 ? "pdoc-h2" : "pdoc-h3";
442
+ return "pdoc-prose";
443
+ }
444
+ function signatureFor(chunks, settingsMap) {
445
+ return JSON.stringify(
446
+ chunks.map((c) => [
447
+ c.id,
448
+ widthInFor(settingsMap[c.id]),
449
+ c.textBlocks.map((b) => b.kind === "heading" ? ["h", b.level, b.text] : ["p", b.html])
450
+ ])
451
+ );
452
+ }
453
+ function useTextMeasurements(chunks, settingsMap) {
454
+ const [heights, setHeights] = useState(null);
455
+ const [fontsReady, setFontsReady] = useState(false);
456
+ useEffect(() => {
457
+ let cancelled = false;
458
+ const markReady = () => {
459
+ if (!cancelled) setFontsReady(true);
460
+ };
461
+ Promise.resolve(document.fonts?.ready).then(markReady).catch(markReady);
462
+ return () => {
463
+ cancelled = true;
464
+ };
465
+ }, []);
466
+ const signature = signatureFor(chunks, settingsMap);
467
+ useLayoutEffect(() => {
468
+ if (!fontsReady) return;
469
+ const containers = [];
470
+ const next = {};
471
+ for (const chunk of chunks) {
472
+ if (chunk.textBlocks.length === 0) {
473
+ next[chunk.id] = [];
474
+ continue;
475
+ }
476
+ const container = document.createElement("div");
477
+ container.className = "pdoc-measure";
478
+ container.style.width = `${widthInFor(settingsMap[chunk.id])}in`;
479
+ document.body.appendChild(container);
480
+ containers.push(container);
481
+ const measured = [];
482
+ for (const block of chunk.textBlocks) {
483
+ const wrapper = document.createElement("div");
484
+ wrapper.className = blockClassName(block);
485
+ if (block.kind === "heading") {
486
+ wrapper.textContent = block.text;
487
+ } else {
488
+ wrapper.innerHTML = block.html;
489
+ }
490
+ container.appendChild(wrapper);
491
+ measured.push(wrapper.offsetHeight / PX_PER_IN);
492
+ }
493
+ next[chunk.id] = measured;
494
+ }
495
+ setHeights(next);
496
+ return () => {
497
+ for (const container of containers) container.remove();
498
+ };
499
+ }, [signature, fontsReady]);
500
+ return { heights };
501
+ }
502
+
503
+ // src/document/components/useDocPages.ts
504
+ function useDocPages(input) {
505
+ const { source, sections } = input;
506
+ const chunks = useMemo(
507
+ () => source ? harvest(source.sections, source.pageTitle, source.images) : [],
508
+ [source]
509
+ );
510
+ const settingsMap = useMemo(() => {
511
+ const map = {};
512
+ for (const c of chunks) map[c.id] = settingsFor(sections, c.id);
513
+ return map;
514
+ }, [chunks, sections]);
515
+ const { heights } = useTextMeasurements(chunks, settingsMap);
516
+ const pages = useMemo(() => {
517
+ return chunks.flatMap((c) => {
518
+ const h = heights?.[c.id];
519
+ if (c.textBlocks.length > 0 && (!h || h.length !== c.textBlocks.length)) return [];
520
+ return paginateChunk(c, settingsMap[c.id], h ?? []);
521
+ });
522
+ }, [chunks, settingsMap, heights]);
523
+ const measuring = useMemo(
524
+ () => chunks.some((c) => {
525
+ if (c.textBlocks.length === 0) return false;
526
+ const h = heights?.[c.id];
527
+ return !h || h.length !== c.textBlocks.length;
528
+ }),
529
+ [chunks, heights]
530
+ );
531
+ const warningsByChunk = useMemo(() => {
532
+ const map = {};
533
+ for (const c of chunks) {
534
+ const noContent = c.textBlocks.length === 0 && c.images.length === 0;
535
+ const pageWarnings = pages.filter((p) => p.chunkId === c.id).flatMap((p) => p.warnings);
536
+ const warnings = noContent ? ["Section has no content", ...pageWarnings] : pageWarnings;
537
+ if (warnings.length > 0) map[c.id] = warnings;
538
+ }
539
+ return map;
540
+ }, [chunks, pages]);
541
+ return { chunks, pages, measuring, warningsByChunk };
542
+ }
543
+
544
+ // src/document/components/DocViewer.tsx
545
+ import { useCallback, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
546
+
547
+ // src/document/components/DocRail.tsx
548
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
549
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
550
+ var PAGE_W = 1632;
551
+ var PAGE_H = 1056;
552
+ function entryId(entry) {
553
+ if (entry.kind === "cover") return "cover";
554
+ if (entry.kind === "page") return entry.page.id;
555
+ return `ghost:${entry.chunkId}`;
556
+ }
557
+ function ThumbButton({
558
+ id,
559
+ selectedId,
560
+ scale,
561
+ label,
562
+ indent,
563
+ onSelect,
564
+ children
565
+ }) {
566
+ return /* @__PURE__ */ jsx5("li", { className: indent ? "pl-4" : void 0, children: /* @__PURE__ */ jsxs5(
567
+ "button",
568
+ {
569
+ type: "button",
570
+ onClick: () => onSelect(id),
571
+ className: `relative block w-full rounded border-2 bg-white ${id === selectedId ? "border-teal-600" : "border-neutral-200 hover:border-neutral-300"}`,
572
+ children: [
573
+ /* @__PURE__ */ jsx5("span", { className: "pointer-events-none block w-full overflow-hidden aspect-[17/11]", children: /* @__PURE__ */ jsx5(
574
+ "span",
575
+ {
576
+ className: "block origin-top-left",
577
+ style: { transform: `scale(${scale})`, width: PAGE_W, height: PAGE_H },
578
+ children
579
+ }
580
+ ) }),
581
+ /* @__PURE__ */ jsx5("span", { className: "absolute bottom-0.5 left-1 max-w-[90%] truncate text-[10px] text-neutral-400", children: label })
582
+ ]
583
+ }
584
+ ) });
585
+ }
586
+ function DocRail({
587
+ entries,
588
+ selectedId,
589
+ meta,
590
+ cover,
591
+ coverImage,
592
+ host,
593
+ onSelect,
594
+ onCollapse
595
+ }) {
596
+ const listRef = useRef(null);
597
+ const [scale, setScale] = useState2(0.11);
598
+ useEffect2(() => {
599
+ const el = listRef.current;
600
+ if (!el || typeof ResizeObserver === "undefined") return;
601
+ const obs = new ResizeObserver(() => {
602
+ const w = el.clientWidth - 4;
603
+ if (w > 0) setScale(w / PAGE_W);
604
+ });
605
+ obs.observe(el);
606
+ return () => obs.disconnect();
607
+ }, []);
608
+ return /* @__PURE__ */ jsxs5(
609
+ "nav",
610
+ {
611
+ "data-testid": "doc-rail",
612
+ className: "flex w-52 shrink-0 flex-col overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3",
613
+ children: [
614
+ /* @__PURE__ */ jsxs5(
615
+ "button",
616
+ {
617
+ type: "button",
618
+ "aria-label": "Collapse page rail",
619
+ onClick: onCollapse,
620
+ className: "mb-2 flex w-full items-center justify-between rounded px-2 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900",
621
+ children: [
622
+ /* @__PURE__ */ jsx5("span", { children: "Pages" }),
623
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", className: "text-lg leading-none", children: "\u2039" })
624
+ ]
625
+ }
626
+ ),
627
+ /* @__PURE__ */ jsx5("ol", { ref: listRef, className: "flex flex-col gap-2", children: entries.map((entry) => {
628
+ const id = entryId(entry);
629
+ if (entry.kind === "cover") {
630
+ return /* @__PURE__ */ jsx5(ThumbButton, { id, selectedId, scale, label: "Cover", onSelect, children: /* @__PURE__ */ jsx5(DocCoverPage, { cover, meta, image: coverImage, host }) }, id);
631
+ }
632
+ if (entry.kind === "page") {
633
+ return /* @__PURE__ */ jsx5(
634
+ ThumbButton,
635
+ {
636
+ id,
637
+ selectedId,
638
+ scale,
639
+ label: entry.page.title,
640
+ indent: entry.page.cont,
641
+ onSelect,
642
+ children: /* @__PURE__ */ jsx5(DocSectionPage, { page: entry.page, meta, host })
643
+ },
644
+ id
645
+ );
646
+ }
647
+ return /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs5(
648
+ "button",
649
+ {
650
+ type: "button",
651
+ onClick: () => onSelect(id),
652
+ className: `flex w-full items-center justify-between gap-2 rounded border px-2 py-2 text-left text-xs ${id === selectedId ? "border-teal-600 bg-white" : "border-dashed border-neutral-300 bg-neutral-50 hover:border-neutral-400"}`,
653
+ children: [
654
+ /* @__PURE__ */ jsx5("span", { className: "truncate text-neutral-400", children: entry.title }),
655
+ /* @__PURE__ */ jsx5("span", { className: "shrink-0 rounded bg-neutral-200 px-1 text-[10px] uppercase tracking-wide text-neutral-500", children: entry.reason })
656
+ ]
657
+ }
658
+ ) }, id);
659
+ }) })
660
+ ]
661
+ }
662
+ );
663
+ }
664
+
665
+ // src/document/components/DocViewer.tsx
666
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
667
+ var PAGE_W2 = 1632;
668
+ var PAGE_H2 = 1056;
669
+ function chunkIdOf(id) {
670
+ if (id === "cover") return null;
671
+ if (id.startsWith("ghost:")) return id.slice("ghost:".length);
672
+ if (id.includes("#")) return id.slice(0, id.lastIndexOf("#"));
673
+ return id;
674
+ }
675
+ function DocViewer({
676
+ source,
677
+ document: doc,
678
+ host,
679
+ mode,
680
+ selectedChunkId,
681
+ onSelectChunk,
682
+ onClose,
683
+ onDocState
684
+ }) {
685
+ const { chunks, pages, measuring, warningsByChunk } = useDocPages({ source, sections: doc.sections });
686
+ const [activeId, setActiveId] = useState3("cover");
687
+ const [railOpen, setRailOpen] = useState3(true);
688
+ const coverImage = doc.cover.imageId ? source?.images[doc.cover.imageId] ?? null : null;
689
+ const onDocStateRef = useRef2(onDocState);
690
+ onDocStateRef.current = onDocState;
691
+ const lastEmitRef = useRef2(null);
692
+ useEffect3(() => {
693
+ if (lastEmitRef.current?.chunks === chunks && lastEmitRef.current?.warningsByChunk === warningsByChunk) return;
694
+ lastEmitRef.current = { chunks, warningsByChunk };
695
+ onDocStateRef.current?.({ chunks, warningsByChunk });
696
+ }, [chunks, warningsByChunk]);
697
+ const entries = useMemo2(() => {
698
+ const list = [{ kind: "cover" }];
699
+ for (const chunk of chunks) {
700
+ const s = settingsFor(doc.sections, chunk.id);
701
+ const empty = chunk.textBlocks.length === 0 && chunk.images.length === 0;
702
+ if (s.excluded || empty) {
703
+ if (mode === "edit") {
704
+ list.push({ kind: "ghost", chunkId: chunk.id, title: chunk.title, reason: s.excluded ? "excluded" : "empty" });
705
+ }
706
+ } else {
707
+ for (const page of pages.filter((p) => p.chunkId === chunk.id)) list.push({ kind: "page", page });
708
+ }
709
+ }
710
+ return list;
711
+ }, [chunks, pages, doc.sections, mode]);
712
+ const selectEntry = useCallback(
713
+ (id) => {
714
+ setActiveId(id);
715
+ if (mode === "edit") onSelectChunk?.(chunkIdOf(id));
716
+ },
717
+ [mode, onSelectChunk]
718
+ );
719
+ useEffect3(() => {
720
+ if (measuring) return;
721
+ if (entries.some((e) => entryId(e) === activeId)) return;
722
+ const chunkId = chunkIdOf(activeId);
723
+ const ghost = entries.find((e) => e.kind === "ghost" && e.chunkId === chunkId);
724
+ setActiveId(ghost ? entryId(ghost) : "cover");
725
+ }, [entries, activeId, measuring]);
726
+ useEffect3(() => {
727
+ if (mode !== "edit" || selectedChunkId === void 0) return;
728
+ if (chunkIdOf(activeId) === selectedChunkId) return;
729
+ if (selectedChunkId === null) {
730
+ setActiveId("cover");
731
+ return;
732
+ }
733
+ const match = entries.find(
734
+ (e) => e.kind === "page" && e.page.chunkId === selectedChunkId || e.kind === "ghost" && e.chunkId === selectedChunkId
735
+ );
736
+ if (match) setActiveId(entryId(match));
737
+ }, [selectedChunkId, mode, entries, activeId]);
738
+ const activeIdRef = useRef2(activeId);
739
+ activeIdRef.current = activeId;
740
+ useEffect3(() => {
741
+ const onKey = (e) => {
742
+ const t = e.target;
743
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) return;
744
+ let dir = 0;
745
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") dir = 1;
746
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") dir = -1;
747
+ else return;
748
+ e.preventDefault();
749
+ const ids = entries.map(entryId);
750
+ const cur = Math.max(0, ids.indexOf(activeIdRef.current));
751
+ const next = Math.min(ids.length - 1, Math.max(0, cur + dir));
752
+ if (ids[next] !== activeIdRef.current) selectEntry(ids[next]);
753
+ };
754
+ window.addEventListener("keydown", onKey);
755
+ return () => window.removeEventListener("keydown", onKey);
756
+ }, [entries, selectEntry]);
757
+ const active = entries.find((e) => entryId(e) === activeId) ?? null;
758
+ return (
759
+ // min-w-0 (here and on the rail/canvas row): without it these flex items
760
+ // refuse to shrink below their content width, and opening the rail shoves
761
+ // the overlay's inspector off the right edge instead of narrowing the
762
+ // canvas.
763
+ /* @__PURE__ */ jsxs6("div", { className: "flex h-full min-h-0 min-w-0 flex-1 flex-col", children: [
764
+ mode === "present" ? /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between border-b border-base-200 bg-base px-4 py-2", children: [
765
+ /* @__PURE__ */ jsx6("span", { className: "text-sm font-medium text-base-contrast", children: source?.pageTitle ?? "Document" }),
766
+ onClose ? /* @__PURE__ */ jsx6(
767
+ "button",
768
+ {
769
+ type: "button",
770
+ "aria-label": "Close",
771
+ onClick: onClose,
772
+ className: "rounded border border-base-200 px-3 py-1 text-xs font-medium text-base-contrast hover:bg-base-accent/30",
773
+ children: "\u2715"
774
+ }
775
+ ) : null
776
+ ] }) : null,
777
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 min-w-0 flex-1", children: [
778
+ railOpen ? /* @__PURE__ */ jsx6(
779
+ DocRail,
780
+ {
781
+ entries,
782
+ selectedId: activeId,
783
+ meta: doc.meta,
784
+ cover: doc.cover,
785
+ coverImage,
786
+ host,
787
+ onSelect: selectEntry,
788
+ onCollapse: () => setRailOpen(false)
789
+ }
790
+ ) : /* @__PURE__ */ jsx6("div", { className: "flex w-10 shrink-0 flex-col border-r border-neutral-200 bg-neutral-50", children: /* @__PURE__ */ jsx6(
791
+ "button",
792
+ {
793
+ type: "button",
794
+ "aria-label": "Expand page rail",
795
+ onClick: () => setRailOpen(true),
796
+ className: "px-2 py-3 text-lg leading-none text-neutral-500 hover:text-neutral-800",
797
+ children: "\u203A"
798
+ }
799
+ ) }),
800
+ /* @__PURE__ */ jsx6(
801
+ DocCanvas,
802
+ {
803
+ active,
804
+ cover: doc.cover,
805
+ coverImage,
806
+ meta: doc.meta,
807
+ host,
808
+ measuring,
809
+ noContent: chunks.length === 0
810
+ }
811
+ )
812
+ ] })
813
+ ] })
814
+ );
815
+ }
816
+ function DocCanvas({
817
+ active,
818
+ cover,
819
+ coverImage,
820
+ meta,
821
+ host,
822
+ measuring,
823
+ noContent
824
+ }) {
825
+ const containerRef = useRef2(null);
826
+ const [scale, setScale] = useState3(0.4);
827
+ useEffect3(() => {
828
+ const el = containerRef.current;
829
+ if (!el || typeof ResizeObserver === "undefined") return;
830
+ const observer = new ResizeObserver(() => {
831
+ const available = el.clientWidth - 48;
832
+ setScale(Math.min(0.75, Math.max(0.15, available / PAGE_W2)));
833
+ });
834
+ observer.observe(el);
835
+ return () => observer.disconnect();
836
+ }, []);
837
+ const note = (text) => /* @__PURE__ */ jsx6("p", { className: "mt-24 text-sm text-neutral-400", children: text });
838
+ let body;
839
+ if (active?.kind === "cover") {
840
+ body = noContent ? /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-center gap-4", children: [
841
+ /* @__PURE__ */ jsx6(Scaled, { scale, children: /* @__PURE__ */ jsx6(DocCoverPage, { cover, meta, image: coverImage, host }) }),
842
+ /* @__PURE__ */ jsx6("p", { className: "text-sm text-neutral-400", children: "This page has no displayable content." })
843
+ ] }) : /* @__PURE__ */ jsx6(Scaled, { scale, children: /* @__PURE__ */ jsx6(DocCoverPage, { cover, meta, image: coverImage, host }) });
844
+ } else if (active?.kind === "page") {
845
+ body = /* @__PURE__ */ jsx6(Scaled, { scale, children: /* @__PURE__ */ jsx6(DocSectionPage, { page: active.page, meta, host }) });
846
+ } else if (active?.kind === "ghost") {
847
+ body = note(active.reason === "excluded" ? "Excluded \u2014 re-include from the inspector" : "No displayable content");
848
+ } else {
849
+ body = measuring ? note("Laying out\u2026") : note("This page has no displayable content.");
850
+ }
851
+ return /* @__PURE__ */ jsx6(
852
+ "div",
853
+ {
854
+ ref: containerRef,
855
+ "data-testid": "doc-canvas",
856
+ className: "flex flex-1 items-start justify-center overflow-auto bg-neutral-100 p-6",
857
+ children: body
858
+ }
859
+ );
860
+ }
861
+ function Scaled({ scale, children }) {
862
+ return /* @__PURE__ */ jsx6("div", { className: "shrink-0 overflow-hidden shadow-lg", style: { width: PAGE_W2 * scale, height: PAGE_H2 * scale }, children: /* @__PURE__ */ jsx6("div", { className: "origin-top-left", style: { transform: `scale(${scale})`, width: PAGE_W2, height: PAGE_H2 }, children }) });
863
+ }
864
+ export {
865
+ DOC_GEOM,
866
+ DocCoverPage,
867
+ DocPage,
868
+ DocRail,
869
+ DocSectionPage,
870
+ DocViewer,
871
+ PREAMBLE_ID,
872
+ documentOptionsSchema,
873
+ entryId,
874
+ harvest,
875
+ layoutMasonryPage,
876
+ paginateChunk,
877
+ sectionSettingsSchema,
878
+ settingsFor,
879
+ splitProseBlocks,
880
+ useDocPages
881
+ };