@drawnagency/primitives 0.1.117 → 0.1.119

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 (263) hide show
  1. package/dist/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
  2. package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
  3. package/dist/{chunk-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
  4. package/dist/{chunk-L7WTERU5.js → chunk-HFHM4MNK.js} +33 -10
  5. package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
  6. package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
  7. package/dist/{chunk-LNQUUC2S.js → chunk-RIN2QCYJ.js} +9 -11
  8. package/dist/{chunk-HUOW3ZV2.js → chunk-UNFPATDE.js} +8 -10
  9. package/dist/{chunk-MSBHMY3C.js → chunk-YKKGVSJH.js} +35 -37
  10. package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
  11. package/dist/collab/rebase.js +4 -5
  12. package/dist/components/chat/ChatComposer.d.ts +0 -4
  13. package/dist/components/chat/ChatComposer.d.ts.map +1 -1
  14. package/dist/components/chat/ChatPanel.d.ts +2 -3
  15. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  16. package/dist/components/chat/markdown.d.ts +2 -3
  17. package/dist/components/chat/markdown.d.ts.map +1 -1
  18. package/dist/components/editor/PagesModal.d.ts +0 -2
  19. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  20. package/dist/components/editor/index.js +15 -19
  21. package/dist/components/primitives/EditablePlainText.d.ts.map +1 -1
  22. package/dist/components/primitives/index.js +11 -15
  23. package/dist/components/sections/brand-guide.js +10 -13
  24. package/dist/components/sections/register-schemas.js +16 -21
  25. package/dist/components/sections/register.js +16 -21
  26. package/dist/components/shell/AudienceChips.d.ts +1 -3
  27. package/dist/components/shell/AudienceChips.d.ts.map +1 -1
  28. package/dist/components/shell/AudienceDetailsPanel.d.ts +3 -1
  29. package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -1
  30. package/dist/components/shell/EditorShell.d.ts +0 -34
  31. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  32. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  33. package/dist/hooks/useSortableNavRow.d.ts +1 -1
  34. package/dist/hooks/useSortableRow.d.ts +1 -1
  35. package/dist/index.js +83 -90
  36. package/dist/lib/index-ops.js +5 -6
  37. package/dist/lib/index.js +27 -32
  38. package/dist/lib/pages.d.ts +0 -2
  39. package/dist/lib/pages.d.ts.map +1 -1
  40. package/dist/lib/sidecars.js +2 -2
  41. package/dist/lib/site-validation.js +13 -14
  42. package/dist/lib/storage-tier.js +4 -4
  43. package/dist/schemas/index.js +42 -45
  44. package/dist/schemas/site-config.d.ts +1 -277
  45. package/dist/schemas/site-config.d.ts.map +1 -1
  46. package/package.json +2 -42
  47. package/src/components/chat/ChatComposer.tsx +0 -4
  48. package/src/components/chat/ChatPanel.tsx +2 -3
  49. package/src/components/chat/markdown.ts +2 -3
  50. package/src/components/editor/PagesModal.tsx +1 -15
  51. package/src/components/editor/StatusIndicator.tsx +4 -4
  52. package/src/components/primitives/EditablePlainText.tsx +27 -3
  53. package/src/components/shell/AudienceChips.tsx +1 -3
  54. package/src/components/shell/AudienceDetailsPanel.tsx +6 -4
  55. package/src/components/shell/EditorShell.tsx +18 -114
  56. package/src/components/shell/SiteSettingsModal.tsx +0 -11
  57. package/src/lib/pages.ts +0 -22
  58. package/src/schemas/index.ts +0 -4
  59. package/src/schemas/site-config.ts +4 -32
  60. package/dist/chunk-4TZJZU2T.js +0 -92
  61. package/dist/chunk-DIMDRBFK.js +0 -11
  62. package/dist/chunk-FV7JQRCN.js +0 -94
  63. package/dist/chunk-IIDKW7AV.js +0 -41
  64. package/dist/chunk-JCDZAFX2.js +0 -224
  65. package/dist/chunk-MM7KZS5C.js +0 -30
  66. package/dist/chunk-QQ3S3P5P.js +0 -38
  67. package/dist/chunk-TEOAKGZA.js +0 -784
  68. package/dist/chunk-VAYZJKVR.js +0 -209
  69. package/dist/components/shell/DocInspector.d.ts +0 -18
  70. package/dist/components/shell/DocInspector.d.ts.map +0 -1
  71. package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
  72. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
  73. package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
  74. package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
  75. package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
  76. package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
  77. package/dist/deck/DeckComponentView.d.ts +0 -13
  78. package/dist/deck/DeckComponentView.d.ts.map +0 -1
  79. package/dist/deck/DeckPlayer.d.ts +0 -26
  80. package/dist/deck/DeckPlayer.d.ts.map +0 -1
  81. package/dist/deck/DeckSlideView.d.ts +0 -17
  82. package/dist/deck/DeckSlideView.d.ts.map +0 -1
  83. package/dist/deck/DeckStage.d.ts +0 -16
  84. package/dist/deck/DeckStage.d.ts.map +0 -1
  85. package/dist/deck/components/DeckImage.d.ts +0 -14
  86. package/dist/deck/components/DeckImage.d.ts.map +0 -1
  87. package/dist/deck/components/DeckImageGrid.d.ts +0 -27
  88. package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
  89. package/dist/deck/components/DeckRichText.d.ts +0 -22
  90. package/dist/deck/components/DeckRichText.d.ts.map +0 -1
  91. package/dist/deck/components/DeckVideo.d.ts +0 -15
  92. package/dist/deck/components/DeckVideo.d.ts.map +0 -1
  93. package/dist/deck/editing.d.ts +0 -50
  94. package/dist/deck/editing.d.ts.map +0 -1
  95. package/dist/deck/flip.d.ts +0 -11
  96. package/dist/deck/flip.d.ts.map +0 -1
  97. package/dist/deck/index.d.ts +0 -24
  98. package/dist/deck/index.d.ts.map +0 -1
  99. package/dist/deck/index.js +0 -51
  100. package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
  101. package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
  102. package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
  103. package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
  104. package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
  105. package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
  106. package/dist/deck/media.d.ts +0 -33
  107. package/dist/deck/media.d.ts.map +0 -1
  108. package/dist/deck/tokens.d.ts +0 -22
  109. package/dist/deck/tokens.d.ts.map +0 -1
  110. package/dist/deck/useStageScale.d.ts +0 -10
  111. package/dist/deck/useStageScale.d.ts.map +0 -1
  112. package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
  113. package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
  114. package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
  115. package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
  116. package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
  117. package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
  118. package/dist/deck-builder/DeckEditor.d.ts +0 -37
  119. package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
  120. package/dist/deck-builder/DeckLibrary.d.ts +0 -22
  121. package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
  122. package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
  123. package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
  124. package/dist/deck-builder/deck-api.d.ts +0 -76
  125. package/dist/deck-builder/deck-api.d.ts.map +0 -1
  126. package/dist/deck-builder/deck-chat-client.d.ts +0 -44
  127. package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
  128. package/dist/deck-builder/deck-chat-history.d.ts +0 -10
  129. package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
  130. package/dist/deck-builder/index.d.ts +0 -13
  131. package/dist/deck-builder/index.d.ts.map +0 -1
  132. package/dist/deck-builder/index.js +0 -1498
  133. package/dist/deck-builder/starter-deck.d.ts +0 -23
  134. package/dist/deck-builder/starter-deck.d.ts.map +0 -1
  135. package/dist/deck-builder/types.d.ts +0 -29
  136. package/dist/deck-builder/types.d.ts.map +0 -1
  137. package/dist/deck-builder/useSortableSlide.d.ts +0 -29
  138. package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
  139. package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  140. package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  141. package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  142. package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  143. package/dist/document/components/DocCoverPage.d.ts +0 -11
  144. package/dist/document/components/DocCoverPage.d.ts.map +0 -1
  145. package/dist/document/components/DocPage.d.ts +0 -10
  146. package/dist/document/components/DocPage.d.ts.map +0 -1
  147. package/dist/document/components/DocRail.d.ts +0 -28
  148. package/dist/document/components/DocRail.d.ts.map +0 -1
  149. package/dist/document/components/DocSectionPage.d.ts +0 -12
  150. package/dist/document/components/DocSectionPage.d.ts.map +0 -1
  151. package/dist/document/components/DocViewer.d.ts +0 -21
  152. package/dist/document/components/DocViewer.d.ts.map +0 -1
  153. package/dist/document/components/Wordmark.d.ts +0 -4
  154. package/dist/document/components/Wordmark.d.ts.map +0 -1
  155. package/dist/document/components/measure.d.ts +0 -7
  156. package/dist/document/components/measure.d.ts.map +0 -1
  157. package/dist/document/components/useDocPages.d.ts +0 -12
  158. package/dist/document/components/useDocPages.d.ts.map +0 -1
  159. package/dist/document/engine/blocks.d.ts +0 -2
  160. package/dist/document/engine/blocks.d.ts.map +0 -1
  161. package/dist/document/engine/harvest.d.ts +0 -3
  162. package/dist/document/engine/harvest.d.ts.map +0 -1
  163. package/dist/document/engine/masonry.d.ts +0 -26
  164. package/dist/document/engine/masonry.d.ts.map +0 -1
  165. package/dist/document/engine/paginate.d.ts +0 -43
  166. package/dist/document/engine/paginate.d.ts.map +0 -1
  167. package/dist/document/engine/settings.d.ts +0 -91
  168. package/dist/document/engine/settings.d.ts.map +0 -1
  169. package/dist/document/engine/types.d.ts +0 -44
  170. package/dist/document/engine/types.d.ts.map +0 -1
  171. package/dist/document/index.d.ts +0 -13
  172. package/dist/document/index.d.ts.map +0 -1
  173. package/dist/document/index.js +0 -1030
  174. package/dist/document/styles/document.css +0 -217
  175. package/dist/lib/deck-access.d.ts +0 -86
  176. package/dist/lib/deck-access.d.ts.map +0 -1
  177. package/dist/lib/deck-access.js +0 -58
  178. package/dist/lib/deck-dexie.d.ts +0 -88
  179. package/dist/lib/deck-dexie.d.ts.map +0 -1
  180. package/dist/lib/deck-dexie.js +0 -22
  181. package/dist/lib/deck-edit.d.ts +0 -67
  182. package/dist/lib/deck-edit.d.ts.map +0 -1
  183. package/dist/lib/deck-media-assets.d.ts +0 -27
  184. package/dist/lib/deck-media-assets.d.ts.map +0 -1
  185. package/dist/lib/deck-media-assets.js +0 -25
  186. package/dist/lib/deck-media-refs.d.ts +0 -35
  187. package/dist/lib/deck-media-refs.d.ts.map +0 -1
  188. package/dist/lib/deck-media-refs.js +0 -36
  189. package/dist/lib/deck-tool.d.ts +0 -25
  190. package/dist/lib/deck-tool.d.ts.map +0 -1
  191. package/dist/lib/deck-tool.js +0 -58
  192. package/dist/lib/document-input.d.ts +0 -25
  193. package/dist/lib/document-input.d.ts.map +0 -1
  194. package/dist/schemas/deck-meta.d.ts +0 -85
  195. package/dist/schemas/deck-meta.d.ts.map +0 -1
  196. package/dist/schemas/deck-meta.js +0 -64
  197. package/dist/schemas/deck.d.ts +0 -954
  198. package/dist/schemas/deck.d.ts.map +0 -1
  199. package/dist/schemas/deck.js +0 -65
  200. package/src/components/shell/DocInspector.tsx +0 -314
  201. package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
  202. package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
  203. package/src/components/shell/SiteSettingsDecks.tsx +0 -41
  204. package/src/deck/DeckComponentView.tsx +0 -28
  205. package/src/deck/DeckPlayer.tsx +0 -166
  206. package/src/deck/DeckSlideView.tsx +0 -38
  207. package/src/deck/DeckStage.tsx +0 -39
  208. package/src/deck/components/DeckImage.tsx +0 -38
  209. package/src/deck/components/DeckImageGrid.tsx +0 -254
  210. package/src/deck/components/DeckRichText.tsx +0 -111
  211. package/src/deck/components/DeckVideo.tsx +0 -81
  212. package/src/deck/editing.tsx +0 -201
  213. package/src/deck/flip.ts +0 -70
  214. package/src/deck/index.ts +0 -30
  215. package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
  216. package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
  217. package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
  218. package/src/deck/media.tsx +0 -46
  219. package/src/deck/tokens.ts +0 -57
  220. package/src/deck/useStageScale.ts +0 -44
  221. package/src/deck-builder/DeckBuilderApp.tsx +0 -572
  222. package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
  223. package/src/deck-builder/DeckChatPanel.tsx +0 -334
  224. package/src/deck-builder/DeckEditor.tsx +0 -258
  225. package/src/deck-builder/DeckLibrary.tsx +0 -195
  226. package/src/deck-builder/DeckSlideSurface.tsx +0 -55
  227. package/src/deck-builder/deck-api.ts +0 -177
  228. package/src/deck-builder/deck-chat-client.ts +0 -129
  229. package/src/deck-builder/deck-chat-history.ts +0 -62
  230. package/src/deck-builder/index.ts +0 -12
  231. package/src/deck-builder/starter-deck.ts +0 -86
  232. package/src/deck-builder/types.ts +0 -24
  233. package/src/deck-builder/useSortableSlide.ts +0 -102
  234. package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
  235. package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
  236. package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
  237. package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
  238. package/src/document/components/DocCoverPage.tsx +0 -45
  239. package/src/document/components/DocPage.tsx +0 -40
  240. package/src/document/components/DocRail.tsx +0 -189
  241. package/src/document/components/DocSectionPage.tsx +0 -48
  242. package/src/document/components/DocViewer.tsx +0 -313
  243. package/src/document/components/Wordmark.tsx +0 -18
  244. package/src/document/components/measure.tsx +0 -133
  245. package/src/document/components/useDocPages.ts +0 -95
  246. package/src/document/engine/blocks.ts +0 -116
  247. package/src/document/engine/harvest.ts +0 -95
  248. package/src/document/engine/masonry.ts +0 -158
  249. package/src/document/engine/paginate.ts +0 -232
  250. package/src/document/engine/settings.ts +0 -125
  251. package/src/document/engine/types.ts +0 -64
  252. package/src/document/index.ts +0 -18
  253. package/src/document/styles/document.css +0 -217
  254. package/src/lib/deck-access.ts +0 -132
  255. package/src/lib/deck-dexie.ts +0 -190
  256. package/src/lib/deck-edit.ts +0 -196
  257. package/src/lib/deck-media-assets.ts +0 -57
  258. package/src/lib/deck-media-refs.ts +0 -67
  259. package/src/lib/deck-tool.ts +0 -95
  260. package/src/lib/document-input.ts +0 -57
  261. package/src/schemas/deck-meta.ts +0 -88
  262. package/src/schemas/deck.ts +0 -478
  263. package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
@@ -1,116 +0,0 @@
1
- // Elements that are never allowed to survive into rendered output, wherever
2
- // they occur (top-level or nested).
3
- const DISALLOWED_TAGS = new Set([
4
- "script",
5
- "style",
6
- "iframe",
7
- "object",
8
- "embed",
9
- "link",
10
- "meta",
11
- "base",
12
- "form",
13
- // Raw-text / escapable-raw-text and declarative elements whose contents are
14
- // not parsed as normal markup. Left in place they enable serialize/reparse
15
- // (mXSS) bypasses — e.g. an <img onerror> smuggled inside <noscript> text.
16
- // Removing the element drops its contents too (including <template> content).
17
- "noscript",
18
- "xmp",
19
- "noembed",
20
- "noframes",
21
- "plaintext",
22
- "template",
23
- ]);
24
- const DISALLOWED_SELECTOR = Array.from(DISALLOWED_TAGS).join(",");
25
-
26
- // Attributes that can carry a URL and therefore need scheme scrutiny. `poster`
27
- // and `srcset` matter because <video>/<img>/<source> survive the scrub; the
28
- // rest belong to elements DISALLOWED_TAGS already removes, and are listed so a
29
- // future relaxation of that set doesn't quietly open a hole.
30
- const URL_ATTRS = new Set([
31
- "href",
32
- "src",
33
- "srcset",
34
- "poster",
35
- "xlink:href",
36
- "formaction",
37
- "action",
38
- "ping",
39
- "data",
40
- "background",
41
- ]);
42
-
43
- function isDangerousUrl(attrName: string, rawValue: string): boolean {
44
- // Browsers strip ASCII tab/CR/LF anywhere in a URL (and ignore leading
45
- // controls) before resolving its scheme, so "jav\tascript:" runs like
46
- // "javascript:". Over-stripping here only makes the check more conservative.
47
- const value = rawValue.toLowerCase().replace(/[\u0000-\u0020]/g, "");
48
- if (value.startsWith("javascript:") || value.startsWith("vbscript:")) return true;
49
- if (attrName === "src" && value.startsWith("data:text/html")) return true;
50
- return false;
51
- }
52
-
53
- // srcset is a comma-separated candidate list, so a startsWith check over the
54
- // whole value misses "a.png 1x, javascript:alert(1) 2x". Scrutinize each
55
- // candidate's URL part instead.
56
- function hasDangerousUrl(attrName: string, rawValue: string): boolean {
57
- if (attrName !== "srcset") return isDangerousUrl(attrName, rawValue);
58
- return rawValue
59
- .split(",")
60
- .some((candidate) => isDangerousUrl("src", candidate.trim().split(/\s+/)[0] ?? ""));
61
- }
62
-
63
- // Defense-in-depth scrub applied at the dangerouslySetInnerHTML/innerHTML
64
- // boundary. Upstream portal content is sanitized at authoring time, but
65
- // /print runs in an authed headless browser, so this engine (kept
66
- // dependency-free — no DOMPurify) removes known-dangerous elements and
67
- // attributes from the DOM it already parsed, in place.
68
- function scrubElement(root: Element): void {
69
- root.querySelectorAll(DISALLOWED_SELECTOR).forEach((el) => el.remove());
70
-
71
- const elements: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
72
- for (const el of elements) {
73
- for (const attr of Array.from(el.attributes)) {
74
- const name = attr.name.toLowerCase();
75
- if (name.startsWith("on") || name === "srcdoc") {
76
- el.removeAttribute(attr.name);
77
- continue;
78
- }
79
- if (URL_ATTRS.has(name) && hasDangerousUrl(name, attr.value)) {
80
- el.removeAttribute(attr.name);
81
- }
82
- }
83
- }
84
- }
85
-
86
- // Elements that carry meaning with no text of their own. The keep test used to
87
- // name <img> alone, so a rule, a video, an inline SVG diagram or a <picture>
88
- // was silently dropped from the printed document.
89
- const VISIBLE_EMPTY_SELECTOR = "img, hr, video, svg, picture, canvas";
90
-
91
- // Split sanitized rich-text HTML into atomic top-level blocks. Pagination
92
- // never splits inside a block — a block that doesn't fit moves wholesale to
93
- // the next page (spec: "pagination by block measurement").
94
- export function splitProseBlocks(html: string): string[] {
95
- if (!html.trim()) return [];
96
- const doc = new DOMParser().parseFromString(html, "text/html");
97
- const blocks: string[] = [];
98
- for (const node of Array.from(doc.body.childNodes)) {
99
- if (node.nodeType === Node.ELEMENT_NODE) {
100
- const el = node as Element;
101
- if (DISALLOWED_TAGS.has(el.tagName.toLowerCase())) continue;
102
- scrubElement(el);
103
- if ((el.textContent ?? "").trim() || el.matches(VISIBLE_EMPTY_SELECTOR) || el.querySelector(VISIBLE_EMPTY_SELECTOR)) {
104
- blocks.push(el.outerHTML);
105
- }
106
- } else if (node.nodeType === Node.TEXT_NODE) {
107
- const text = (node.textContent ?? "").trim();
108
- if (text) {
109
- const p = doc.createElement("p");
110
- p.textContent = text;
111
- blocks.push(p.outerHTML);
112
- }
113
- }
114
- }
115
- return blocks;
116
- }
@@ -1,95 +0,0 @@
1
- import { splitProseBlocks } from "./blocks";
2
- import { PREAMBLE_ID, type Chunk, type DocImage, type DocSectionInput, type SplitAt } from "./types";
3
-
4
- // Shape of the fields harvest reads, checked structurally at runtime — the
5
- // engine must tolerate any future portal section without crashing (allowlist).
6
- type Loose = Record<string, unknown>;
7
-
8
- function str(v: unknown): string {
9
- return typeof v === "string" ? v : "";
10
- }
11
-
12
- // Normalizes any runtime value (including null/undefined from malformed
13
- // portal data) into a safe-to-index object so field access never throws.
14
- function asRecord(v: unknown): Loose {
15
- return v && typeof v === "object" ? (v as Loose) : {};
16
- }
17
-
18
- // One section's contribution to the current chunk (containers recurse).
19
- function harvestInto(
20
- target: Chunk,
21
- type: string,
22
- content: unknown,
23
- imagesById: Record<string, DocImage>,
24
- ): void {
25
- const c = asRecord(content);
26
- switch (type) {
27
- case "sub_heading":
28
- case "link_heading": // nested (inside a container) — not a boundary here
29
- target.textBlocks.push({ kind: "heading", level: 2, text: str(c.heading) });
30
- break;
31
- case "sub_sub_heading":
32
- target.textBlocks.push({ kind: "heading", level: 3, text: str(c.heading) });
33
- break;
34
- case "prose":
35
- for (const html of splitProseBlocks(str(c.body))) {
36
- target.textBlocks.push({ kind: "prose", html });
37
- }
38
- break;
39
- case "media": {
40
- const ref = asRecord(c.ref);
41
- const image = imagesById[str(ref.imageId)];
42
- if (image) target.images.push(image);
43
- break;
44
- }
45
- case "container": {
46
- const children = Array.isArray(c.children) ? c.children : [];
47
- for (const child of children) {
48
- const cc = asRecord(child);
49
- if (typeof cc.type === "string") {
50
- harvestInto(target, cc.type, cc.content, imagesById);
51
- }
52
- }
53
- break;
54
- }
55
- default:
56
- break; // allowlist: video, button, spacer, colors, do/dont, icon list, unknown future types
57
- }
58
- }
59
-
60
- export function harvest(
61
- sections: DocSectionInput[],
62
- pageTitle: string,
63
- imagesById: Record<string, DocImage>,
64
- splitAtFor: (chunkId: string) => SplitAt = () => "none",
65
- ): Chunk[] {
66
- const chunks: Chunk[] = [];
67
- const preamble: Chunk = { id: PREAMBLE_ID, title: pageTitle, textBlocks: [], images: [] };
68
- let parent = preamble; // current top-level chunk — owns the splitAt setting
69
- let current = preamble; // chunk receiving harvested content
70
- let splitAt = splitAtFor(PREAMBLE_ID);
71
-
72
- for (const section of sections) {
73
- const heading = str(asRecord(section.content).heading);
74
- if (section.type === "link_heading") {
75
- parent = { id: section.id, title: heading, textBlocks: [], images: [] };
76
- current = parent;
77
- splitAt = splitAtFor(section.id);
78
- chunks.push(parent);
79
- } else if (section.type === "sub_heading" && splitAt === "sub_heading") {
80
- // Promotion: the boundary heading becomes the chunk title, not a TextBlock.
81
- current = { id: section.id, title: heading, textBlocks: [], images: [], parentId: parent.id, kicker: parent.title, depth: 1 };
82
- chunks.push(current);
83
- } else {
84
- harvestInto(current, section.type, section.content, imagesById);
85
- }
86
- }
87
-
88
- // Kept when it has own content OR spawned sub-chunks — an emptied-but-split
89
- // preamble must stay selectable (its ghost is the way back to splitAt).
90
- const keepPreamble =
91
- preamble.textBlocks.length > 0 ||
92
- preamble.images.length > 0 ||
93
- chunks.some((c) => c.parentId === PREAMBLE_ID);
94
- return keepPreamble ? [preamble, ...chunks] : chunks;
95
- }
@@ -1,158 +0,0 @@
1
- const EPS = 1e-6;
2
-
3
- export type MasonryMode = "rows" | "columns";
4
-
5
- export type Sized = { width: number; height: number };
6
-
7
- export interface MasonryOpts {
8
- mode: MasonryMode;
9
- columns: 2 | 3 | 4 | 5 | 6;
10
- bodyWIn: number;
11
- bodyHIn: number;
12
- gapIn: number;
13
- targetRowHeightIn: number;
14
- rows?: "auto" | 1 | 2 | 3 | 4;
15
- }
16
-
17
- export interface MasonryBox<T extends Sized = Sized> {
18
- asset: T;
19
- xIn: number;
20
- yIn: number;
21
- wIn: number;
22
- hIn: number;
23
- }
24
-
25
- // A "cover" photo is laid out in a SQUARE cell (effective aspect 1) and the
26
- // image crops to fill it; "contain" (default) uses the photo's natural aspect.
27
- const effectiveAspect = (im: Sized, fit: "cover" | "contain" | undefined) => {
28
- if (fit === "cover") return 1;
29
- const aspect = im.width / im.height;
30
- // A 0-height item (a legacy manifest row from before the decode guard) made
31
- // this Infinity or NaN, and every downstream width went with it — the whole
32
- // page laid out blank. Square is the same fallback the cover path uses.
33
- return Number.isFinite(aspect) && aspect > 0 ? aspect : 1;
34
- };
35
-
36
- function layoutRows<T extends Sized>(images: T[], o: MasonryOpts, fits: ("cover" | "contain")[]): { boxes: MasonryBox<T>[]; used: number; endY: number } {
37
- const asp = images.map((im, i) => effectiveAspect(im, fits[i]));
38
- const boxes: MasonryBox<T>[] = [];
39
- let y = 0, i = 0;
40
- let used: number | null = null;
41
- while (i < images.length) {
42
- const row: number[] = []; // indices in this row
43
- let natW = 0;
44
- while (i + row.length < images.length) {
45
- row.push(i + row.length);
46
- const gaps = o.gapIn * (row.length - 1);
47
- natW = row.reduce((s, idx) => s + asp[idx] * o.targetRowHeightIn, 0) + gaps;
48
- if (natW >= o.bodyWIn) break;
49
- }
50
- const gaps = o.gapIn * (row.length - 1);
51
- const sumAspect = row.reduce((s, idx) => s + asp[idx], 0);
52
- const isFull = natW >= o.bodyWIn;
53
- const rowH = isFull ? (o.bodyWIn - gaps) / sumAspect : o.targetRowHeightIn;
54
- if (used === null && y + rowH > o.bodyHIn + EPS && boxes.length > 0) used = boxes.length;
55
- let x = 0;
56
- for (const idx of row) {
57
- const w = asp[idx] * rowH;
58
- boxes.push({ asset: images[idx], xIn: x, yIn: y, wIn: w, hIn: rowH });
59
- x += w + o.gapIn;
60
- }
61
- y += rowH + o.gapIn;
62
- i += row.length;
63
- }
64
- return { boxes, used: used ?? images.length, endY: y };
65
- }
66
-
67
- // Forced row count: partition the images into exactly N rows (fewer only if
68
- // there are fewer images), aspect-balanced, each row scaled to full width —
69
- // unless the stack would exceed the region height, in which case the whole
70
- // block scales down uniformly to fit (never up), left-aligned. Unlike
71
- // layoutRows there is no target height — the row's share of the total aspect
72
- // determines its height.
73
- function layoutForcedRows<T extends Sized>(
74
- images: T[],
75
- o: MasonryOpts,
76
- fits: ("cover" | "contain")[],
77
- n: number,
78
- ): { boxes: MasonryBox<T>[]; used: number; endY: number } {
79
- const asp = images.map((im, i) => effectiveAspect(im, fits[i]));
80
- const count = Math.min(n, images.length);
81
- // Greedy balance: each row takes images until it reaches the average
82
- // aspect-sum of what's left, always leaving >=1 image per remaining row.
83
- const rows: number[][] = [];
84
- let i = 0;
85
- for (let r = 0; r < count; r++) {
86
- const remainingRows = count - r;
87
- const target = asp.slice(i).reduce((s, a) => s + a, 0) / remainingRows;
88
- const row: number[] = [];
89
- let sum = 0;
90
- while (i < asp.length) {
91
- if (row.length > 0) {
92
- if (asp.length - (i + 1) < remainingRows - 1) break; // taking i must leave >=1 image per remaining row
93
- if (sum >= target - EPS) break;
94
- }
95
- row.push(i);
96
- sum += asp[i];
97
- i++;
98
- }
99
- rows.push(row);
100
- }
101
-
102
- // Pass 1: full-width row heights from the aspect math.
103
- const rowMeta = rows.map((row) => {
104
- const gaps = o.gapIn * (row.length - 1);
105
- const sumAspect = row.reduce((s, a) => s + asp[a], 0);
106
- return { row, gaps, sumAspect, hIn: (o.bodyWIn - gaps) / sumAspect };
107
- });
108
- // Pass 2: if the stack exceeds the region, scale everything down uniformly —
109
- // natural aspects kept, gaps fixed, block left-aligned. Never upscale.
110
- const rowGaps = o.gapIn * (rowMeta.length - 1);
111
- const totalH = rowMeta.reduce((s, m) => s + m.hIn, 0);
112
- const scale = totalH > 0 ? Math.min(1, Math.max(0, o.bodyHIn - rowGaps) / totalH) : 1;
113
-
114
- const boxes: MasonryBox<T>[] = [];
115
- let y = 0;
116
- for (const m of rowMeta) {
117
- const rowH = m.hIn * scale;
118
- let x = 0;
119
- for (const idx of m.row) {
120
- const w = asp[idx] * rowH;
121
- boxes.push({ asset: images[idx], xIn: x, yIn: y, wIn: w, hIn: rowH });
122
- x += w + o.gapIn;
123
- }
124
- y += rowH + o.gapIn;
125
- }
126
- return { boxes, used: images.length, endY: y };
127
- }
128
-
129
- function layoutColumns<T extends Sized>(images: T[], o: MasonryOpts, fits: ("cover" | "contain")[]): { boxes: MasonryBox<T>[]; used: number; colHeights: number[]; colW: number } {
130
- const n = o.columns;
131
- const colW = (o.bodyWIn - o.gapIn * (n - 1)) / n;
132
- const colH = new Array(n).fill(0);
133
- const boxes: MasonryBox<T>[] = [];
134
- let used: number | null = null;
135
- images.forEach((im, idx) => {
136
- let c = 0;
137
- for (let k = 1; k < n; k++) if (colH[k] < colH[c]) c = k;
138
- const y = colH[c];
139
- const h = colW / effectiveAspect(im, fits[idx]);
140
- if (used === null && y + h > o.bodyHIn + EPS && idx > 0) used = idx;
141
- boxes.push({ asset: im, xIn: c * (colW + o.gapIn), yIn: y, wIn: colW, hIn: h });
142
- colH[c] = y + h + o.gapIn;
143
- });
144
- return { boxes, used: used ?? images.length, colHeights: colH, colW };
145
- }
146
-
147
- export function layoutMasonryPage<T extends Sized>(
148
- images: T[],
149
- o: MasonryOpts,
150
- fits: ("cover" | "contain")[] = [],
151
- ): { boxes: MasonryBox<T>[]; used: number } {
152
- if (o.mode === "columns") {
153
- const r = layoutColumns(images, o, fits);
154
- return { boxes: r.boxes, used: r.used };
155
- }
156
- const r = typeof o.rows === "number" ? layoutForcedRows(images, o, fits, o.rows) : layoutRows(images, o, fits);
157
- return { boxes: r.boxes, used: r.used };
158
- }
@@ -1,232 +0,0 @@
1
- import { layoutMasonryPage } from "./masonry";
2
- import type { SectionSettings } from "./settings";
3
- import type { Chunk, DocImage, TextBlock } from "./types";
4
-
5
- export interface DocGeom {
6
- bodyWIn: number;
7
- bodyHIn: number;
8
- titleBandIn: number;
9
- gapIn: number;
10
- leftColWIn: number;
11
- colGutterIn: number;
12
- minTextColIn: number;
13
- minImageRegionIn: number;
14
- masonryTargetRowIn: number;
15
- topTextMaxFraction: number;
16
- }
17
-
18
- // Mirrors .deck-body in deck.css (17×11 page, top 1.35in, sides/bottom 0.6in).
19
- export const DOC_GEOM: DocGeom = {
20
- bodyWIn: 15.8,
21
- bodyHIn: 9.05,
22
- titleBandIn: 0.9,
23
- gapIn: 0.15,
24
- leftColWIn: 4.6,
25
- colGutterIn: 0.4,
26
- minTextColIn: 1,
27
- minImageRegionIn: 1.2,
28
- masonryTargetRowIn: 2.6,
29
- // Top text fills at most this fraction of the body height below the title
30
- // band, so a text-heavy top section always leaves ≥ half the page for images.
31
- topTextMaxFraction: 0.5,
32
- };
33
-
34
- const EPS = 1e-6;
35
-
36
- export interface Region { xIn: number; yIn: number; wIn: number; hIn: number; }
37
- export interface ImageBox {
38
- image: DocImage;
39
- xIn: number; yIn: number; wIn: number; hIn: number;
40
- fit: "cover" | "contain";
41
- }
42
- export interface DerivedPage {
43
- id: string;
44
- chunkId: string;
45
- title: string;
46
- cont: boolean;
47
- // Sub-chunk context (absent on top-level pages) — see Chunk.
48
- kicker?: string;
49
- depth?: 1;
50
- textBlocks: TextBlock[];
51
- textRegion: Region | null;
52
- imageBoxes: ImageBox[];
53
- warnings: string[];
54
- }
55
-
56
- function fitImages(
57
- images: DocImage[],
58
- layout: SectionSettings["imageLayout"],
59
- region: Region,
60
- geom: DocGeom,
61
- ): { boxes: ImageBox[]; used: number } {
62
- if (images.length === 0 || region.hIn < geom.minImageRegionIn) return { boxes: [], used: 0 };
63
-
64
- if (layout.mode === "grid") {
65
- const used = Math.min(layout.rows * layout.columns, images.length);
66
- const cellW = (region.wIn - (layout.columns - 1) * geom.gapIn) / layout.columns;
67
- const cellH = (region.hIn - (layout.rows - 1) * geom.gapIn) / layout.rows;
68
- const boxes = images.slice(0, used).map((image, i) => {
69
- const r = Math.floor(i / layout.columns);
70
- const c = i % layout.columns;
71
- return {
72
- image,
73
- xIn: region.xIn + c * (cellW + geom.gapIn),
74
- yIn: region.yIn + r * (cellH + geom.gapIn),
75
- wIn: cellW,
76
- hIn: cellH,
77
- fit: layout.fit,
78
- };
79
- });
80
- return { boxes, used };
81
- }
82
-
83
- const r = layoutMasonryPage(images, {
84
- mode: layout.flow,
85
- columns: layout.columns,
86
- rows: layout.rows,
87
- bodyWIn: region.wIn,
88
- bodyHIn: region.hIn,
89
- gapIn: geom.gapIn,
90
- targetRowHeightIn: geom.masonryTargetRowIn,
91
- });
92
- const boxes = r.boxes.slice(0, r.used).map((b) => ({
93
- image: b.asset,
94
- xIn: region.xIn + b.xIn,
95
- yIn: region.yIn + b.yIn,
96
- wIn: b.wIn,
97
- hIn: b.hIn,
98
- fit: "contain" as const,
99
- }));
100
- return { boxes, used: r.used };
101
- }
102
-
103
- export function paginateChunk(
104
- chunk: Chunk,
105
- settings: SectionSettings,
106
- textHeightsIn: number[],
107
- geom: DocGeom = DOC_GEOM,
108
- naturalTextWIn?: number,
109
- ): DerivedPage[] {
110
- if (settings.excluded) return [];
111
- if (chunk.textBlocks.length !== textHeightsIn.length) {
112
- throw new Error("paginateChunk: textHeightsIn must match chunk.textBlocks");
113
- }
114
- if (chunk.textBlocks.length === 0 && chunk.images.length === 0) return [];
115
-
116
- // leftColumn shrinks to the widest natural text line (+ slack so sub-pixel
117
- // rounding can't re-wrap), floored/capped; absent measurement = legacy fixed
118
- // column. The shrunk width is >= every block's natural width by
119
- // construction, so measured heights remain valid.
120
- const colWIn =
121
- naturalTextWIn === undefined
122
- ? geom.leftColWIn
123
- : Math.min(geom.leftColWIn, Math.max(geom.minTextColIn, naturalTextWIn + 0.05));
124
-
125
- const pages: DerivedPage[] = [];
126
- let ti = 0;
127
- let ii = 0;
128
- const top = geom.titleBandIn;
129
-
130
- while (ti < chunk.textBlocks.length || ii < chunk.images.length) {
131
- const cont = pages.length > 0;
132
- const warnings: string[] = [];
133
- const textW = settings.textPlacement === "top" ? geom.bodyWIn : colWIn;
134
-
135
- // 1. Greedy text fill. Top text is capped at half the body so a text-heavy
136
- // section still leaves room for images below; leftColumn keeps full height.
137
- // The cap only earns its keep while images are still waiting for room. Held
138
- // on every page, it paginated a text-only section (or the text-only tail of
139
- // one) at half utilisation and roughly doubled its page count.
140
- const capY =
141
- settings.textPlacement === "top" && ii < chunk.images.length
142
- ? geom.titleBandIn + (geom.bodyHIn - geom.titleBandIn) * geom.topTextMaxFraction
143
- : geom.bodyHIn;
144
- const placed: TextBlock[] = [];
145
- let textEnd = top;
146
- while (ti < chunk.textBlocks.length) {
147
- const h = textHeightsIn[ti];
148
- const yAfter = placed.length === 0 ? top + h : textEnd + geom.gapIn + h;
149
- if (yAfter <= capY + EPS) {
150
- placed.push(chunk.textBlocks[ti]);
151
- textEnd = yAfter;
152
- ti++;
153
- } else if (placed.length === 0) {
154
- // Oversize block: it gets the page, clipped at the cap (masonry-notice style).
155
- placed.push(chunk.textBlocks[ti]);
156
- textEnd = capY;
157
- ti++;
158
- warnings.push("A text block is taller than the page and was clipped.");
159
- break;
160
- } else {
161
- break;
162
- }
163
- }
164
-
165
- // dropTextOverflow: on the FIRST page only, discard any text that didn't fit
166
- // rather than flowing it onto Cont. pages. Images still paginate as usual.
167
- if (!cont && settings.dropTextOverflow && ti < chunk.textBlocks.length) {
168
- const dropped = chunk.textBlocks.length - ti;
169
- ti = chunk.textBlocks.length;
170
- warnings.push(`${dropped} text block(s) hidden (overflow dropped)`);
171
- }
172
- const textRegion: Region | null =
173
- placed.length > 0 ? { xIn: 0, yIn: top, wIn: textW, hIn: textEnd - top } : null;
174
-
175
- // 2. Image region. leftColumn+right runs beside the text; every other
176
- // case — and any page whose text is exhausted — is full width below it.
177
- let region: Region;
178
- if (settings.textPlacement === "leftColumn" && settings.imagePlacement === "right" && placed.length > 0) {
179
- region = {
180
- xIn: colWIn + geom.colGutterIn,
181
- yIn: top,
182
- wIn: geom.bodyWIn - colWIn - geom.colGutterIn,
183
- hIn: geom.bodyHIn - top,
184
- };
185
- } else {
186
- const startY = placed.length > 0 ? textEnd + geom.gapIn : top;
187
- region = { xIn: 0, yIn: startY, wIn: geom.bodyWIn, hIn: Math.max(0, geom.bodyHIn - startY) };
188
- }
189
-
190
- // 3. Fit images into the region.
191
- const { boxes, used } = fitImages(chunk.images.slice(ii), settings.imageLayout, region, geom);
192
- ii += used;
193
- if (boxes.some((b) => b.yIn + b.hIn > region.yIn + region.hIn + 0.01)) {
194
- warnings.push("An image overflows the page and is clipped.");
195
- }
196
-
197
- // Progress guard: unreachable with sane geometry (a text-free page always
198
- // gets the full region, and both fitters place ≥1 there), but a config
199
- // mistake must not hang the browser.
200
- if (placed.length === 0 && used === 0) {
201
- warnings.push("Pagination could not place remaining content.");
202
- pages.push({
203
- id: `${chunk.id}#${pages.length}`,
204
- chunkId: chunk.id,
205
- title: cont ? `${chunk.title} Cont.` : chunk.title,
206
- cont,
207
- kicker: chunk.kicker,
208
- depth: chunk.depth,
209
- textBlocks: placed,
210
- textRegion,
211
- imageBoxes: boxes,
212
- warnings,
213
- });
214
- break;
215
- }
216
-
217
- pages.push({
218
- id: `${chunk.id}#${pages.length}`,
219
- chunkId: chunk.id,
220
- title: cont ? `${chunk.title} Cont.` : chunk.title,
221
- cont,
222
- kicker: chunk.kicker,
223
- depth: chunk.depth,
224
- textBlocks: placed,
225
- textRegion,
226
- imageBoxes: boxes,
227
- warnings,
228
- });
229
- }
230
-
231
- return pages;
232
- }