@peripheries/renderer 0.23.0 → 0.24.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 (105) hide show
  1. package/README.md +62 -34
  2. package/dist/catalog.js +2 -6
  3. package/dist/chunks/{forms-CCAE7sCu.js → forms-DxA9WzCt.js} +0 -6
  4. package/dist/chunks/{load-CjPvISzH.js → load-b38vUiZM.js} +0 -4
  5. package/dist/chunks/{page-bundles-DLRIBP8g.js → page-bundles-CPj_vx-J.js} +1 -5
  6. package/dist/chunks/{persistence-DCMakM9H.js → persistence-hurkUUQ0.js} +0 -6
  7. package/dist/chunks/{projection-yvYKzMOS.js → projection-zhu32_KJ.js} +1 -15
  8. package/dist/chunks/{query-LnTbQghF.js → query-D6xQbTFK.js} +0 -6
  9. package/dist/chunks/{refs-BLUokra5.js → refs-Csgh6rXq.js} +0 -4
  10. package/dist/chunks/{structure-BrKSgzRJ.js → structure-CKBtQHJq.js} +1 -7
  11. package/dist/chunks/{validate-B_g7719l.js → validate-BcB_PA1k.js} +2 -10
  12. package/dist/core.js +26 -26
  13. package/dist/forms.js +1 -1
  14. package/dist/print.js +0 -24
  15. package/dist/react.js +9 -27
  16. package/dist/runtime.js +2 -12
  17. package/dist/styles.css +9 -15
  18. package/dist/testing.js +5 -21
  19. package/dist/theme.js +0 -10
  20. package/dist/types/catalog/check.d.ts +0 -13
  21. package/dist/types/core/actions.d.ts +0 -24
  22. package/dist/types/core/bundles/load.d.ts +0 -23
  23. package/dist/types/core/bundles/page-bundles.d.ts +0 -9
  24. package/dist/types/core/bundles/refs.d.ts +1 -28
  25. package/dist/types/core/bundles/resolve.d.ts +0 -10
  26. package/dist/types/core/cell-props.d.ts +0 -20
  27. package/dist/types/core/cells.d.ts +0 -21
  28. package/dist/types/core/data-context.d.ts +0 -22
  29. package/dist/types/core/diagnostics.d.ts +1 -3
  30. package/dist/types/core/graph.d.ts +0 -54
  31. package/dist/types/core/index.d.ts +1 -0
  32. package/dist/types/core/layers.d.ts +8 -0
  33. package/dist/types/core/layers.gen.d.ts +12 -0
  34. package/dist/types/core/layout.d.ts +0 -9
  35. package/dist/types/core/load-page.d.ts +0 -111
  36. package/dist/types/core/query.d.ts +0 -64
  37. package/dist/types/core/references.d.ts +0 -21
  38. package/dist/types/core/release.d.ts +0 -10
  39. package/dist/types/core/rules.d.ts +0 -1
  40. package/dist/types/core/selected.d.ts +0 -5
  41. package/dist/types/core/sources.d.ts +5 -64
  42. package/dist/types/core/store/create-store.d.ts +0 -6
  43. package/dist/types/core/store/persistence.d.ts +0 -1
  44. package/dist/types/core/store/projection.d.ts +0 -5
  45. package/dist/types/core/store/scope-keys.d.ts +0 -1
  46. package/dist/types/core/store/types.d.ts +0 -6
  47. package/dist/types/core/structure.d.ts +0 -33
  48. package/dist/types/core/types.d.ts +0 -13
  49. package/dist/types/core/validate.d.ts +0 -12
  50. package/dist/types/core/values.d.ts +0 -6
  51. package/dist/types/forms/conditions.d.ts +0 -37
  52. package/dist/types/forms/index.d.ts +1 -0
  53. package/dist/types/forms/model.d.ts +4 -48
  54. package/dist/types/forms/port.d.ts +0 -22
  55. package/dist/types/forms/section.d.ts +22 -0
  56. package/dist/types/print/document-bindings.d.ts +0 -61
  57. package/dist/types/print/document-profile.d.ts +0 -137
  58. package/dist/types/print/map-staleness.d.ts +0 -66
  59. package/dist/types/print/page-map.d.ts +1 -334
  60. package/dist/types/print/paginate.d.ts +0 -190
  61. package/dist/types/print/print-layout.d.ts +0 -161
  62. package/dist/types/print/print-lint.d.ts +0 -109
  63. package/dist/types/print/print-policy.d.ts +0 -184
  64. package/dist/types/print/print-stylesheet.d.ts +0 -135
  65. package/dist/types/print/running-header.d.ts +0 -86
  66. package/dist/types/print/structure-print.d.ts +1 -243
  67. package/dist/types/react/cell-states.d.ts +0 -2
  68. package/dist/types/react/cell-view.d.ts +0 -16
  69. package/dist/types/react/page-renderer.d.ts +0 -37
  70. package/dist/types/react/structure-view.d.ts +0 -12
  71. package/dist/types/react/use-cell-store.d.ts +0 -5
  72. package/dist/types/react/use-container-size.d.ts +0 -5
  73. package/dist/types/react/use-forms-runtime.d.ts +2 -55
  74. package/dist/types/react/use-paging.d.ts +0 -9
  75. package/dist/types/runtime/boot.d.ts +0 -11
  76. package/dist/types/runtime/install.d.ts +0 -15
  77. package/dist/types/runtime/shims.d.ts +0 -8
  78. package/dist/types/runtime/store-export.d.ts +0 -7
  79. package/dist/types/testing/base-props.d.ts +0 -1
  80. package/dist/types/testing/components.d.ts +0 -8
  81. package/dist/types/testing/form-workbook.d.ts +0 -3
  82. package/dist/types/testing/golden.d.ts +0 -16
  83. package/dist/types/testing/ids.d.ts +0 -2
  84. package/dist/types/testing/sources.d.ts +0 -7
  85. package/dist/types/theme/check.d.ts +0 -2
  86. package/dist/types/theme/color.d.ts +0 -12
  87. package/dist/types/theme/derive.d.ts +0 -12
  88. package/dist/types/theme/parse.d.ts +0 -7
  89. package/package.json +2 -2
  90. package/dist/catalog.js.map +0 -1
  91. package/dist/chunks/forms-CCAE7sCu.js.map +0 -1
  92. package/dist/chunks/load-CjPvISzH.js.map +0 -1
  93. package/dist/chunks/page-bundles-DLRIBP8g.js.map +0 -1
  94. package/dist/chunks/persistence-DCMakM9H.js.map +0 -1
  95. package/dist/chunks/projection-yvYKzMOS.js.map +0 -1
  96. package/dist/chunks/query-LnTbQghF.js.map +0 -1
  97. package/dist/chunks/refs-BLUokra5.js.map +0 -1
  98. package/dist/chunks/structure-BrKSgzRJ.js.map +0 -1
  99. package/dist/chunks/validate-B_g7719l.js.map +0 -1
  100. package/dist/core.js.map +0 -1
  101. package/dist/print.js.map +0 -1
  102. package/dist/react.js.map +0 -1
  103. package/dist/runtime.js.map +0 -1
  104. package/dist/testing.js.map +0 -1
  105. package/dist/theme.js.map +0 -1
@@ -1,104 +1,5 @@
1
- /**
2
- * Reading measured blocks out of the rendered DOM, for `paginate()`.
3
- *
4
- * ── Why this is separate from the packer AND from the hook ──────────────────
5
- *
6
- * `paginate()` is arithmetic and pure. The React hook owns re-measurement and
7
- * lifecycle. This middle layer is the part that touches the DOM, and it is a
8
- * plain function taking injected measurers so it can be tested without a
9
- * browser — jsdom reports every height as 0, so a collector that called
10
- * `getBoundingClientRect` directly would be untestable in exactly the way that
11
- * matters.
12
- *
13
- * ── The unit of pagination is a BAND, not a cell ───────────────────────────
14
- *
15
- * This module's first version treated a document as a vertical stack of cells,
16
- * one `[data-cell-id]` per block. That is wrong, and it was wrong in a way that
17
- * produced credible numbers for a whole day.
18
- *
19
- * A `/doc` page is a **12-column grid**. Cells sit side by side: two `span 6`
20
- * profile cards share one row, a `span 8` chart shares a row with a `span 4`
21
- * stat strip. You cannot put a page break between two cells that are beside
22
- * each other — the break goes between grid ROWS. So the unit is a *band*: a
23
- * maximal set of cells whose vertical extents connect.
24
- *
25
- * **How many bands the proof document has depends on HOW the width is narrowed**,
26
- * and getting that wrong is the third measurement error of this same family:
27
- *
28
- * - Narrow the grid element alone and it stays a 12-column grid, because the
29
- * `container-type: inline-size` ancestor is still 976px wide and the container
30
- * query has not fired. Six pairs share rows → **27 bands**.
31
- * - Narrow that ANCESTOR — which is what layout mode does, and what printing
32
- * does to the whole page — and the query fires, the template collapses to a
33
- * single 672px column, every `span` becomes full width, and nothing shares a
34
- * row → **33 bands**.
35
- *
36
- * The second is the truth about paper. A hand-measurement that sets a width on
37
- * the grid and reads the result is measuring a layout that will never print.
38
- * The banding code stays necessary regardless: a wider sheet, a landscape
39
- * profile, or a template whose query threshold sits below 672px all keep the
40
- * multi-column layout, and then a break between two side-by-side cells is
41
- * exactly the error this prevents.
42
- *
43
- * Two further corrections came out of the same measurement, both from picking
44
- * the wrong element:
45
- *
46
- * 1. **The fragmentation box is the cell's PARENT.** `[data-cell-id]` is the
47
- * `.cell-renderer` inside the grid item; the grid item is the element
48
- * `CellsRenderer` puts `break-inside`/`break-after` on, and the element the
49
- * browser would actually split. Measuring the child loses the constraint on
50
- * **28 of 33 cells** and gets a different height on **10 of 33** (a
51
- * `SectionHeader` cell is 104px, its grid item 152px).
52
- * 2. **Constraints are read from computed style, not resolved by name.** The
53
- * renderer has already resolved `@printPolicy` through the bundle cascade
54
- * and written it onto that grid item. Re-resolving it here would be a second
55
- * implementation of a lookup that can disagree with the first; reading the
56
- * computed value cannot.
57
- *
58
- * ── `reconciles` is the check that would have caught all of this ────────────
59
- *
60
- * Everything above the first band, plus every band, plus the gaps between them,
61
- * must equal the measured height of the container. The old per-cell model
62
- * summed to 9,973px against a 9,629px container and nothing in the code
63
- * noticed; the first version of THIS model read `row-gap` off the wrong
64
- * element, lost all 32 gaps, and came 512px short — caught by the check on its
65
- * first run against a real document rather than by a third day of measuring.
66
- *
67
- * So the map now carries that arithmetic as a field. A map that does not
68
- * reconcile is not slightly wrong — it is a different document — and a consumer
69
- * must be able to refuse it rather than draw page edges from it.
70
- *
71
- * ── The width problem, and why the map admits it ────────────────────────────
72
- *
73
- * A page map is only true at the width it was measured at. Measured on the
74
- * proof document: at the screen's 976px the document has **no** unpageable blocks;
75
- * with the grid constrained to the true 672px, `ReportCoverHero` becomes
76
- * unpageable. A screen-width map is not a slightly-wrong print map — it is
77
- * missing the overflows.
78
- *
79
- * Forcing print width on a document someone is reading reflows it under them,
80
- * so this module does **not** do it: it measures where the DOM already is and
81
- * reports `approximate` when that is not print width. Exact maps come from a
82
- * caller already laying out at print width — layout mode.
83
- *
84
- * ── What this still cannot see ─────────────────────────────────────────────
85
- *
86
- * `@media print` rules are inert unless the browser is actually printing.
87
- * Constraining a container to 672px does not activate them. So a
88
- * map taken this way reflects screen-media geometry at paper width, which is
89
- * close but not identical — on the proof cover the difference was 261px until the
90
- * print rule stopped being defeated by an inline style. A consumer that needs
91
- * the print-media truth needs real print emulation, which this module has no
92
- * way to request.
93
- *
94
- * Charts also settle asynchronously, and measuring before they draw yields
95
- * heights no assertion here can catch. What this offers against that is
96
- * `zeroHeightIds` — the boxes that measured 0, the observable signature of "not
97
- * yet rendered". Re-measurement on mutation belongs to the hook.
98
- */
99
1
  import { type PrintConstraints } from './print-policy.js';
100
2
  import type { PaginationBlock } from './paginate.js';
101
- /** The part of a DOMRect this module needs, in a common coordinate space. */
102
3
  export interface ElementRect {
103
4
  top: number;
104
5
  bottom: number;
@@ -106,175 +7,43 @@ export interface ElementRect {
106
7
  width: number;
107
8
  }
108
9
  export interface CollectOptions {
109
- /**
110
- * Rect of an element. Injected so this is testable: pass
111
- * `(el) => el.getBoundingClientRect()` in a browser.
112
- *
113
- * `top` and `bottom` must be in one coordinate space across all calls —
114
- * banding compares them against each other. Viewport coordinates are fine.
115
- */
116
10
  measureRect: (el: Element) => ElementRect;
117
- /**
118
- * The fragmentation properties the browser has resolved for this element.
119
- * Injected for the same reason as `measureRect`, and because a real
120
- * implementation needs `getComputedStyle`. See `constraintsFromComputedStyle`.
121
- */
122
11
  readConstraints?: (el: Element) => PrintConstraints | undefined;
123
- /**
124
- * The fragmentation box for a matched cell. Defaults to the cell's parent,
125
- * which is the grid item — see the header. Overridable because a caller with
126
- * a different cell wrapper should not have to reimplement banding.
127
- */
128
12
  boxFor?: (cell: Element) => Element;
129
- /** Selector for a cell. Defaults to the host's cell marker. */
130
13
  selector?: string;
131
- /**
132
- * Does this cell OPEN a section, and what is the section called?
133
- *
134
- * Returns the title, or `undefined` for a cell that opens nothing. The
135
- * section's id is the cell's id, so a caller never invents one.
136
- *
137
- * ── Why this is injected, and why it lives HERE rather than beside the ────
138
- * ── running header, which also needs it ──────────────────────────────────
139
- *
140
- * Because it needs to be inferred exactly once. Two places already wanted
141
- * "which rows are section headings": the running header, and now the
142
- * specification's pagination rule 01. Two inferences of one fact drift, and
143
- * this project has spent a week on exactly that failure in three other
144
- * shapes. So the walk that already visits every cell answers it, and both
145
- * consumers read the answer.
146
- *
147
- * Injected rather than hard-coded because the signal is not settled.
148
- * Today it is FEC's own `break-after: avoid` plus a heading element, which
149
- * selects exactly the seven `*-header` rows on the proof document. If the bundle
150
- * adopts the specification's stable section ids it becomes a
151
- * config read, and only the caller changes.
152
- */
153
14
  sectionStart?: (cell: Element, constraints: PrintConstraints | undefined) => string | undefined;
154
- /**
155
- * Does a DELIBERATE blank page precede this cell?
156
- *
157
- * Injected like `sectionStart`, and for the same reason: it comes from the
158
- * page config, which this module has no business reading. `paginate` needs it
159
- * because a blank page changes the page count and therefore where every
160
- * subsequent break falls — it cannot be added after the map the way the
161
- * hover controls are.
162
- */
163
15
  blankBefore?: (cellId: string) => boolean;
164
16
  }
165
- /** A row of the document: the cells that share it, and its geometry. */
166
17
  export interface Band {
167
- /** Member cell ids joined by `+`. Stable, and readable in a page listing. */
168
18
  id: string;
169
19
  members: string[];
170
- /** Offset of the band's top from the container's top, in CSS px. */
171
20
  topPx: number;
172
- /** Band height. The row gap is NOT included — see `blocks`. */
173
21
  heightPx: number;
174
22
  constraints?: PrintConstraints;
175
23
  }
176
24
  export interface CollectedBlocks {
177
- /**
178
- * One block per band, in reading order, with the row gap folded into every
179
- * band but the last — a gap sits below its band in the flow.
180
- */
181
25
  blocks: PaginationBlock[];
182
26
  bands: Band[];
183
- /**
184
- * The sections found, in document order — `cellId` is the row that opens the
185
- * section and doubles as its id.
186
- *
187
- * Structurally a `SectionMarker` (`running-header.ts`), and deliberately not
188
- * imported as one: that module imports `Band` from here, and a type import
189
- * back would make the pair circular for the sake of a two-field object.
190
- */
191
27
  sections: Array<{
192
28
  cellId: string;
193
29
  title: string;
194
30
  }>;
195
- /** Width the measurement ran at, in CSS px. The map is only true here. */
196
31
  measuredWidthPx: number;
197
- /**
198
- * Set when `measuredWidthPx` is not the print text-block width, so the map is
199
- * indicative rather than exact.
200
- */
201
32
  approximate?: boolean;
202
- /** Boxes whose measured height was 0 — usually "not yet rendered". */
203
33
  zeroHeightIds: string[];
204
- /**
205
- * Does the band model account for the container's height?
206
- *
207
- * **False means the map is describing a different document** and a consumer
208
- * must refuse to present it as precise. This is the assertion whose absence
209
- * let a per-cell model over-count the proof document by 344px for a day.
210
- */
211
34
  reconciles: boolean;
212
35
  reconciliation: {
213
- /** Anything above the first band, plus every band and the gaps between. */
214
36
  modelPx: number;
215
- /** The container's own measured height. */
216
37
  containerPx: number;
217
- /** Space above the first band — unmeasured, and it displaces every edge. */
218
38
  leadingPx: number;
219
39
  };
220
40
  }
221
- /**
222
- * Read the bands of a container.
223
- *
224
- * @param root The element that lays the cells out.
225
- * @param printWidthPx The print text-block width, for the accuracy comparison.
226
- */
227
41
  export declare function collectBlocks(root: Element | null, printWidthPx: number, { measureRect, readConstraints, boxFor, selector, sectionStart, blankBefore }: CollectOptions): CollectedBlocks;
228
- /**
229
- * `readConstraints` from a `CSSStyleDeclaration` — what the browser resolved.
230
- *
231
- * **Only PROHIBITIONS and FORCED breaks are read. Never a permission.**
232
- *
233
- * `orphans` and `widows` were already excluded, and the reason given was right:
234
- * their computed value is always present (`2` in Chrome, declared or not), so a
235
- * computed style cannot distinguish "this component declared a threshold" from
236
- * "nobody has ever mentioned this".
237
- *
238
- * What this function's own header then got wrong, for a fortnight: it claimed
239
- * `break-*` do not have that problem, because their initial value `auto` is
240
- * *"both the absence of a declaration and a true statement about what the
241
- * browser will do"*. The second half is true and the first half is the bug.
242
- * `mayNotSplit` does not ask what the browser will do — it asks **what was
243
- * declared**, and reads an explicit `auto` as PERMISSION to split. Its own
244
- * header states the rule: *"absence is not permission"*.
245
- *
246
- * So every element on every document reported `breakInside: 'auto'`, because
247
- * that is what an unstyled element computes to, and the packer read every one
248
- * of them as a component that had asked to be splittable. The conservative
249
- * default for the 44 untagged components — *"untagged implies unbreakable"* —
250
- * was unreachable through this path from the day it was written.
251
- *
252
- * The declaration now travels as an attribute the renderer stamps
253
- * (`PRINT_POLICY_ATTR`), and this function is the fallback for a DOM nobody
254
- * stamped. In that role it must be conservative in the same direction as
255
- * absence: `avoid` is a prohibition and `page` an instruction, and both are
256
- * safe to believe from a computed value because neither is what an element gets
257
- * for free.
258
- */
259
42
  export declare function constraintsFromComputedStyle(cs: {
260
43
  breakInside?: string;
261
44
  breakAfter?: string;
262
45
  breakBefore?: string;
263
46
  }): PrintConstraints | undefined;
264
- /**
265
- * The constraints on a fragmentation box: the DECLARATION where there is one,
266
- * the computed style where there is not.
267
- *
268
- * Not a compose of the two. The attribute already carries the composed
269
- * intrinsic-plus-extrinsic answer — the renderer composed it before writing it
270
- * — and folding the computed style back in would re-add the `auto` this exists
271
- * to keep out.
272
- *
273
- * The fallback matters and is not defensive padding: `/portal/page/[pageId]`,
274
- * a print harness and any future headless render reach `collectBlocks` through
275
- * elements these renderers did not write, and those keep the behaviour they
276
- * have always had.
277
- */
278
47
  export declare function constraintsFromBox(el: {
279
48
  getAttribute(name: string): string | null;
280
49
  }, computed: () => {
@@ -282,18 +51,9 @@ export declare function constraintsFromBox(el: {
282
51
  breakAfter?: string;
283
52
  breakBefore?: string;
284
53
  }): PrintConstraints | undefined;
285
- /** What `forcedBreaksWithin` needs of an element: its element children, in order. */
286
54
  type BoxNode = {
287
55
  children: ArrayLike<BoxNode>;
288
56
  };
289
- /**
290
- * A forced break a box's content asks for at its edges (CSS Fragmentation §3.1: a `break-before`
291
- * on a box's first in-flow child, and a `break-after` on its last, propagate to the box). A
292
- * component's own page break (core's `PageBreak` is `break-after: page` on its root) sits
293
- * inside the cell's frame, so the frame's computed style doesn't show it, but Chrome breaks
294
- * there. Walks the first-child chain for `before` and the last-child chain for `after`; only
295
- * forced breaks (`page`) propagate.
296
- */
297
57
  export declare function forcedBreaksWithin<T extends BoxNode>(box: T, styleOf: (el: T) => {
298
58
  breakAfter?: string;
299
59
  breakBefore?: string;
@@ -301,107 +61,20 @@ export declare function forcedBreaksWithin<T extends BoxNode>(box: T, styleOf: (
301
61
  breakBefore?: 'page';
302
62
  breakAfter?: 'page';
303
63
  };
304
- /**
305
- * The constraints on a box with the forced breaks its content propagates (forcedBreaksWithin)
306
- * folded in: a forced break wins over `avoid` on the same edge, as it does in CSS.
307
- */
308
64
  export declare function withForcedBreaks(constraints: PrintConstraints | undefined, forced: {
309
65
  breakBefore?: 'page';
310
66
  breakAfter?: 'page';
311
67
  }): PrintConstraints | undefined;
312
- /**
313
- * Build a `readConstraints` from a component-name → policy lookup.
314
- *
315
- * The **contract-derived** path, kept for a caller that has no rendered DOM to
316
- * read (a headless render service resolving policy before layout). Prefer
317
- * `constraintsFromComputedStyle` where the document is on screen: the renderer
318
- * has already done this resolution and the DOM cannot disagree with itself.
319
- *
320
- * `composeConstraints` is called even for a single link, deliberately: it
321
- * promotes `atomic` to `breakInside: 'avoid'`, and the render path's failure to
322
- * call it is exactly how `atomic` once emitted no CSS at all.
323
- */
324
68
  export declare function constraintsResolver(policyFor: (componentName: string) => PrintConstraints | undefined): (componentName: string | null) => PrintConstraints | undefined;
325
- /**
326
- * The paper geometry a page map needs, in CSS pixels.
327
- *
328
- * The document profile stores lengths as CSS strings (`'8.5in'`, `'0.75in'`) —
329
- * the right shape for a stylesheet and useless to a packer that works in
330
- * numbers. This converts once, at the boundary.
331
- *
332
- * **Only absolute units are accepted.** `em`, `%` and `vh` are all legal CSS
333
- * and none of them has a fixed pixel value on paper, so accepting them would
334
- * mean converting against a font size or a viewport that has nothing to do with
335
- * the sheet. An unparseable length yields `null` rather than a plausible
336
- * default: the geometry falling back silently to US Letter is how a document
337
- * configured for something else would paginate correctly and print wrong.
338
- */
339
69
  export interface PaperGeometryPx {
340
- /** Usable height per page — sheet height minus top and bottom margins. */
341
70
  textBlockPx: number;
342
- /**
343
- * The width the CONTENT column lays out at — the profile's `measure` where it
344
- * declares one, otherwise the full page box.
345
- *
346
- * ── Why this is the measure and not sheet-minus-margins ──────────────────
347
- *
348
- * It was sheet-minus-margins, and the component repo measured what that costs on a
349
- * profile whose measure is narrower than its page box. The bundle's `instrument`
350
- * states its page as a single sum:
351
- *
352
- * 1.25in margin · 4.50in measure · 0.25in gutter · 1.50in marginalia · 1.00in margin
353
- *
354
- * The page box is `8.5 − 1.25 − 1.00 = 6.25in`, and the text column is
355
- * specified at **4.50in**. Laying content out across the whole box set it
356
- * **39% over measure** and never established the marginalia column at all.
357
- *
358
- * **`workbook` and `report` are unaffected, and that is arithmetic rather
359
- * than a special case:** workbook's measure is `6.85in` and its page box is
360
- * `8.5 − 0.95 − 0.70 = 6.85in`. Identical. `report` extends workbook. So
361
- * reading the measure changes nothing for either, which is what the
362
- * component repo asked for.
363
- */
364
71
  printWidthPx: number;
365
- /**
366
- * The full width between the left and right margins.
367
- *
368
- * Equal to `printWidthPx` unless the profile declares a narrower measure. The
369
- * content column occupies `printWidthPx` of it and the remainder is the
370
- * gutter plus the marginalia column.
371
- */
372
72
  pageBoxWidthPx: number;
373
- /** The marginalia column reserved beside the content. 0 when there is none. */
374
73
  railPx: number;
375
- /** Space between the content column and the marginalia column. 0 when none. */
376
74
  gutterPx: number;
377
- /**
378
- * Does the profile's own column arithmetic add up?
379
- *
380
- * `printWidthPx + gutterPx + railPx` against `pageBoxWidthPx`. False means the
381
- * profile's stated sum disagrees with its own sheet and margins, and the
382
- * caller is reserving a column from a page that has no room for it.
383
- *
384
- * Reported rather than clamped, for `reconciles`' reason: a geometry that
385
- * does not add up is not slightly wrong, it is describing a different page,
386
- * and silently absorbing the difference is how a wrong number survives.
387
- * `true` when no rail column is declared — there is nothing to add up.
388
- */
389
75
  columnsReconcile: boolean;
390
76
  }
391
- /**
392
- * Parse one CSS absolute length to px, or `null`.
393
- *
394
- * Returns `null` for a relative unit, a unitless number that is not zero, or
395
- * anything unparseable. `0` alone is legal CSS with no unit and means zero.
396
- */
397
77
  export declare function cssLengthToPx(value: string | undefined | null): number | null;
398
- /**
399
- * Compute the usable text block from a profile's sheet and margins.
400
- *
401
- * Returns `null` when any length is unparseable or the result is not a usable
402
- * box — the caller then has no geometry, which `paginate` reports as
403
- * `degenerateGeometry` rather than guessing.
404
- */
405
78
  export declare function paperGeometryPx(sheet: {
406
79
  width?: string;
407
80
  height?: string;
@@ -410,13 +83,7 @@ export declare function paperGeometryPx(sheet: {
410
83
  right?: string;
411
84
  bottom?: string;
412
85
  left?: string;
413
- } | undefined,
414
- /**
415
- * The profile's column declarations. Optional, and absent means "the content
416
- * fills the page box", which is what every profile did before columns existed and
417
- * what the host default still does.
418
- */
419
- columns?: {
86
+ } | undefined, columns?: {
420
87
  measure?: string;
421
88
  rail?: {
422
89
  placement?: string;
@@ -1,231 +1,41 @@
1
- /**
2
- * Page boundaries, computed by the host.
3
- *
4
- * ── Why the host paginates at all ───────────────────────────────────────────
5
- *
6
- * Layers 1–3 of print mode hand rules to the browser and let it paginate:
7
- * `@page` geometry from the document profile, `@printPolicy` from each
8
- * component onto the cell wrapper, `node.print` from the page author. That is
9
- * enough to *print* well and useless for anything else, because **the browser
10
- * never reports where the pages fell.** Approved by the repo owner 2026-09-03
11
- * (*"2. Yes to pagination"*).
12
- *
13
- * Everything the owner asked for beyond printing needs page boundaries as data:
14
- * showing page edges on screen, a layout mode that moves a block between pages,
15
- * knowing which page the cursor is on, and reflowing when a chart no longer
16
- * fits. None of it is expressible against a browser that paginates privately.
17
- *
18
- * ── The split this file exists to make ─────────────────────────────────────
19
- *
20
- * **Measuring is DOM work; packing is arithmetic.** They are separated because
21
- * only one of them can be tested without a browser, and the browser run is
22
- * exactly where a regression goes unnoticed — the spike that produced the
23
- * numbers below was a one-off `evaluate_script` whose result no CI could
24
- * reproduce. So this module takes measured heights as *input* and is pure.
25
- *
26
- * ── Measured on the proof document, 2026-09-03 ──────────────────────────────
27
- *
28
- * 33 cells, US Letter, 0.75in margins, 96 CSS px/in → text block **912 × 672**.
29
- * Heights measured with the `display: grid` container constrained to 672px:
30
- *
31
- * total content 9,973 px
32
- * perfect packing 10.94 pages (the floor)
33
- * every block atomic 14 pages (the conservative worst case)
34
- * blocks that cannot fit 2 — `ReportCoverHero` 1,150 px,
35
- * `DecumulationSimulator` 952 px
36
- *
37
- * **The first version of these numbers was measured wrong** and was corrected:
38
- * the width was applied to `firstCell.parentElement`, which is the
39
- * first cell's own wrapper, so one of 33 cells reflowed. That produced 9,215 px,
40
- * 10.1 pages and a single overflow — and a conclusion that content barely grows
41
- * at print width, drawn from the one element that had actually been narrowed.
42
- * Twelve cells change; the document grows 13.4%.
43
- *
44
- * Two consequences that shape the rules below:
45
- *
46
- * 1. **The 3-page spread between 10.94 and 14 is what accurate `@printPolicy`
47
- * tagging buys.** So `breakInside: 'auto'` has to be honoured as a positive
48
- * assertion, not folded into the default — otherwise the packer cannot
49
- * realise the tagging it is being given.
50
- * 2. **A block can exceed the page.** `ReportCoverHero` at 1,150px does not fit
51
- * on its own page, so "give an unplaceable atomic block its own page" is not
52
- * a complete answer and this module must be able to say *"this does not fit"*
53
- * rather than silently clipping it.
54
- */
55
1
  import type { PrintConstraints } from './print-policy.js';
56
- /** One measured, packable thing. Usually a cell; the packer does not care. */
57
2
  export interface PaginationBlock {
58
3
  id: string;
59
- /** Rendered height at the PRINT text-block width, not the screen width. */
60
4
  heightPx: number;
61
- /**
62
- * The block's effective constraints — already composed down its nesting
63
- * chain by `composeConstraints`. This module does not compose: composition is
64
- * a property of the tree and packing is a property of the sequence, and
65
- * conflating them is how `atomic` came to mean two things.
66
- */
67
5
  constraints?: PrintConstraints;
68
- /**
69
- * A DELIBERATE blank page immediately before this block.
70
- *
71
- * Distinct from the empty page `breakPage` refuses to emit: that guard exists
72
- * because an ACCIDENTAL blank sheet is a defect. This one is asked for, so it
73
- * is carried as data and emitted as its own page with no blocks.
74
- */
75
6
  blankPageBefore?: boolean;
76
- /**
77
- * The section this block belongs to, for the specification's pagination
78
- * rule 01. Blocks sharing an id must be CONTIGUOUS — a section is a run of
79
- * the document, not a tag scattered through it.
80
- *
81
- * Optional, and absent means "no section": rule 01 simply does not fire, and
82
- * a document with no sections paginates exactly as it did before. That is the
83
- * right default while the host still INFERS sections from the DOM rather than
84
- * reading declared ids.
85
- */
86
7
  sectionId?: string;
87
8
  }
88
9
  export interface PaginateOptions {
89
- /** Usable height per page: sheet height minus top and bottom margins. */
90
10
  textBlockPx: number;
91
- /**
92
- * How far an over-tall block may be squeezed to keep its heading with it.
93
- *
94
- * When a block too tall for a page is preceded by a `break-after: avoid`
95
- * heading, the heading is brought onto its page and the block gets what is
96
- * left. That only helps if what is left is still enough — below this ratio
97
- * the heading is left behind instead, because a stranded heading is a
98
- * typographic fault and an illegible chart is a useless one.
99
- *
100
- * 0.6 by default, matching `buildPrintLayout`'s `scaleFloor`: this decides
101
- * whether the row CAN be scaled and that decides how far. Two numbers that
102
- * must agree, and the default is the same literal in both — if you change one,
103
- * pass the other.
104
- */
105
11
  keepWithNextScaleFloor?: number;
106
- /**
107
- * Rule 01: *"A section that fills half a page or more starts a new page."*
108
- *
109
- * The specification's own words (`Document System.pdf` p11), and its own
110
- * reason: *"Members flip and file by section, and a section that begins
111
- * mid-page is hard to find again."* It is the one pagination rule that is
112
- * unambiguously the host's — a component cannot know what half a page is, and
113
- * neither can a page config; only the packer holds the geometry and the
114
- * measured height of the run.
115
- *
116
- * Expressed as a RATIO rather than the literal `0.5`, because the threshold
117
- * is a judgement about findability and not a fact about paper. `0` disables
118
- * the rule; `1` would mean "only a section that fills a whole page".
119
- *
120
- * **This is the one number in this file that came from the specification
121
- * rather than from a measurement**, which is worth saying out loud: if it
122
- * turns out to be wrong, it is wrong upstream and the fix is a conversation,
123
- * not a tuning pass.
124
- */
125
12
  sectionFillRatio?: number;
126
13
  }
127
14
  export interface PlacedBlock {
128
15
  id: string;
129
16
  heightPx: number;
130
- /**
131
- * This entry is the CONTINUATION of a block that began on an earlier page,
132
- * not a second block with the same id. `heightPx` here is the portion on
133
- * this page, so summing a page's `heightPx` gives its real occupancy.
134
- */
135
17
  continues?: boolean;
136
- /**
137
- * The block's FULL measured height, present only when it spans pages.
138
- *
139
- * `heightPx` is always this page's share, so the invariant *"a page's block
140
- * heights sum to its `usedPx`"* holds everywhere. Without this the whole size
141
- * would be unrecoverable from the map.
142
- */
143
18
  totalHeightPx?: number;
144
- /**
145
- * True when the block is taller than the space remaining and is *permitted*
146
- * to split, so it continues onto the following page.
147
- *
148
- * **The host does not know WHERE inside the block the break falls** — that is
149
- * the browser's line-breaking, and this packer sees an opaque height. So a
150
- * spanning block reports which pages it touches and no more. A page map that
151
- * claimed a split position it cannot compute would be worse than one that
152
- * admits the gap.
153
- */
154
19
  spansPages?: number;
155
20
  }
156
21
  export interface PaginatedPage {
157
- /**
158
- * This page is a deliberate blank — no blocks, and it prints the notice.
159
- *
160
- * The ONLY page in a map that may have an empty `blocks` array. Every other
161
- * empty page is a bug, which is why this is a positive marker rather than a
162
- * count of the blocks.
163
- */
164
22
  blank?: boolean;
165
- /** 1-based, because it is a page number shown to a person. */
166
23
  index: number;
167
24
  blocks: PlacedBlock[];
168
25
  usedPx: number;
169
- /** Never negative: a page that overflows reports the overflow separately. */
170
26
  wastePx: number;
171
27
  }
172
28
  export interface PaginationOverflow {
173
29
  id: string;
174
30
  heightPx: number;
175
31
  textBlockPx: number;
176
- /** How much taller than a whole empty page this block is. */
177
32
  excessPx: number;
178
- /**
179
- * The height this block actually gets, which is NOT always a whole page.
180
- *
181
- * A block too tall for a page is given its own page — unless it was preceded
182
- * by a heading that declares `break-after: avoid`, in which case the heading
183
- * comes with it and the block gets what is left. The consumer that scales the
184
- * row has to scale it to THIS, not to the sheet, or it will scale to a height
185
- * that its heading is already occupying part of.
186
- */
187
33
  availablePx: number;
188
34
  }
189
35
  export interface PageMap {
190
36
  pages: PaginatedPage[];
191
- /**
192
- * Blocks that cannot fit a page even alone AND are not permitted to split.
193
- *
194
- * Reported rather than thrown, and rather than clipped. `ReportCoverHero` is
195
- * a real instance (1,150px against 912px), and the only useful thing to do
196
- * with it is tell somebody — the fix is a tagging or component decision, not
197
- * a packing one.
198
- */
199
37
  overflows: PaginationOverflow[];
200
- /**
201
- * Total content height ÷ page height. A lower bound, and **unreachable in
202
- * principle** — do not read it as a target.
203
- *
204
- * It assumes blocks may be packed in any order. A document may not: **page
205
- * order is reading order**, so a chart cannot be moved ahead of its heading
206
- * to fill a gap. For a fixed sequence of unsplittable blocks, placing each
207
- * one on the current page if it fits and starting a new page otherwise
208
- * already minimises the page count — so the greedy pass in `paginate` is
209
- * optimal, and the gap between it and `idealPages` is not slack a better
210
- * algorithm can recover.
211
- *
212
- * On the proof document that gap is **14 vs 10.94**, and I twice described it as
213
- * something tagging or better packing would close.
214
- * Neither can. The only levers are fewer atomic blocks (a tagging decision,
215
- * now made and exhausted — it bought zero pages), scaling a block to fit (a
216
- * component decision), or less content.
217
- *
218
- * Kept because it is a useful *diagnostic*: a large gap says the document is
219
- * made of big indivisible blocks, which is a content fact worth surfacing in
220
- * layout mode. It is not a performance figure.
221
- */
222
38
  idealPages: number;
223
- /**
224
- * Set when `textBlockPx` was not a usable page height, so nothing was
225
- * paginated and every block sits on one page. Present rather than silent
226
- * because a one-page map is otherwise indistinguishable from a short
227
- * document.
228
- */
229
39
  degenerateGeometry?: boolean;
230
40
  }
231
41
  export declare function paginate(blocks: readonly PaginationBlock[], { textBlockPx, keepWithNextScaleFloor, sectionFillRatio }: PaginateOptions): PageMap;