@peripheries/renderer 0.16.0 → 0.19.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 (46) hide show
  1. package/README.md +1 -0
  2. package/dist/catalog.js +2 -2
  3. package/dist/chunks/{projection-BTt_LUza.js → projection-CNtMyJ-p.js} +2 -2
  4. package/dist/chunks/projection-CNtMyJ-p.js.map +1 -0
  5. package/dist/chunks/{query-D5QSXRBC.js → query-BPuCJC1C.js} +111 -22
  6. package/dist/chunks/query-BPuCJC1C.js.map +1 -0
  7. package/dist/chunks/{structure-Dfk3Y6SN.js → structure-o9pYoDjD.js} +29 -3
  8. package/dist/chunks/{structure-Dfk3Y6SN.js.map → structure-o9pYoDjD.js.map} +1 -1
  9. package/dist/chunks/{validate-CYJf4bNS.js → validate-LRaUUXIm.js} +3 -3
  10. package/dist/chunks/{validate-CYJf4bNS.js.map → validate-LRaUUXIm.js.map} +1 -1
  11. package/dist/core.js +130 -17
  12. package/dist/core.js.map +1 -1
  13. package/dist/print.js +1391 -0
  14. package/dist/print.js.map +1 -0
  15. package/dist/react.js +21 -9
  16. package/dist/react.js.map +1 -1
  17. package/dist/runtime.js +5 -4
  18. package/dist/runtime.js.map +1 -1
  19. package/dist/testing.js +1 -1
  20. package/dist/types/core/cell-props.d.ts +3 -1
  21. package/dist/types/core/data-context.d.ts +4 -0
  22. package/dist/types/core/index.d.ts +2 -1
  23. package/dist/types/core/load-page.d.ts +15 -1
  24. package/dist/types/core/references.d.ts +8 -2
  25. package/dist/types/core/sources.d.ts +97 -0
  26. package/dist/types/core/structure.d.ts +16 -0
  27. package/dist/types/core/types.d.ts +34 -0
  28. package/dist/types/print/document-bindings.d.ts +76 -0
  29. package/dist/types/print/document-profile.d.ts +203 -0
  30. package/dist/types/print/index.d.ts +11 -0
  31. package/dist/types/print/map-staleness.d.ts +75 -0
  32. package/dist/types/print/page-map.d.ts +427 -0
  33. package/dist/types/print/paginate.d.ts +231 -0
  34. package/dist/types/print/print-layout.d.ts +212 -0
  35. package/dist/types/print/print-lint.d.ts +124 -0
  36. package/dist/types/print/print-policy.d.ts +210 -0
  37. package/dist/types/print/print-stylesheet.d.ts +168 -0
  38. package/dist/types/print/running-header.d.ts +105 -0
  39. package/dist/types/print/structure-print.d.ts +302 -0
  40. package/dist/types/react/cell-view.d.ts +3 -0
  41. package/dist/types/react/page-renderer.d.ts +6 -0
  42. package/dist/types/runtime/install.d.ts +9 -3
  43. package/dist/types/testing/components.d.ts +1 -1
  44. package/package.json +6 -2
  45. package/dist/chunks/projection-BTt_LUza.js.map +0 -1
  46. package/dist/chunks/query-D5QSXRBC.js.map +0 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * `@page` geometry, generated from a `DocumentProfile`.
3
+ *
4
+ * ── One source, and it is the profile ───────────────────────────────────────
5
+ *
6
+ * `03-print-strategy.md` §2 specified a `PaperConfig` on the layout contract.
7
+ * That is superseded: `DocumentProfile` already carries `sheet`, `margins`,
8
+ * `measure`, `floorPx` and `regions`, it is authored where the design lives, and
9
+ * it is already lifted off the bundle barrel. A second geometry source would be
10
+ * the exact thing §2 argued against — *"it distributes the page definition
11
+ * across every file that has an opinion"* — with the added twist that the two
12
+ * sources would disagree in a way only a printed page could reveal.
13
+ *
14
+ * ── Why the host injects margin-box content as literals ─────────────────────
15
+ *
16
+ * MEASURED, Chromium 151, this session:
17
+ *
18
+ * CSS.supports('string-set', 'docCode content(text)') -> false
19
+ * CSS.supports('content', 'string(docCode)') -> false
20
+ *
21
+ * and, inserted into a real stylesheet, both are **dropped by the parser**:
22
+ * `h1 { string-set: … }` round-trips as `h1 { }`, and
23
+ * `@page { @bottom-left { content: string(docCode) } }` as `@bottom-left { }`.
24
+ * Literal strings and `counter(page)`/`counter(pages)` both survive.
25
+ *
26
+ * So Chromium does not implement the GCPM `string()` mechanism, and a footer
27
+ * cannot pull text out of the document. **This is a third silent-failure mode**
28
+ * beside the two `03` §4 already lists (`preferCSSPageSize`, margin boxes not
29
+ * inheriting the body font): the declaration is dropped at parse time, the
30
+ * margin box renders empty, and nothing anywhere reports it.
31
+ *
32
+ * The host therefore resolves each region slot to a literal at build time —
33
+ * which it can, because it knows the document — and refuses to emit a slot it
34
+ * cannot resolve rather than emitting one that will silently render blank.
35
+ */
36
+ import type { DocumentProfile } from './document-profile.js';
37
+ /**
38
+ * Values for the region slots a profile names, keyed by slot name.
39
+ *
40
+ * `instrument` asks for `execution-state` bottom-left and `page-x-of-y`
41
+ * bottom-right. The second is a built-in; the first is document-derived and has
42
+ * to be supplied here.
43
+ */
44
+ export type PageRegionValues = Readonly<Record<string, string>>;
45
+ /** Test seam. Nothing in the app should need this. */
46
+ export declare function __clearPrintStylesheetWarnings(): void;
47
+ /**
48
+ * Cross-check the bundle's paginator vocabulary against what the host can render.
49
+ *
50
+ * A document bundle exports `PAGINATOR_BINDINGS` — the bindings the host must NOT try to
51
+ * supply, because CSS computes them during pagination. The host holds the other
52
+ * half: the CSS expression each one becomes. Today the two lists agree exactly
53
+ * (`page-x-of-y`, `page-number`, `page-count`), arrived at independently on both
54
+ * sides — which is reassuring and completely unenforced.
55
+ *
56
+ * The divergence that matters is one-directional. If the bundle adds a fourth binding
57
+ * — `page-of-section`, say — the host has no expression for it, `values` will
58
+ * never contain it (correctly: nobody may supply a paginator binding), and the
59
+ * margin box is **silently omitted**. The document loses a footer and nothing
60
+ * reports it, which is the failure this whole module is built to refuse.
61
+ *
62
+ * So the check runs at bundle load, once, and names the gap. The host cannot fix
63
+ * it at runtime — an expression has to be written — but a named gap is a
64
+ * two-minute change and a silent one is a reprint.
65
+ */
66
+ export declare function checkPaginatorBindings(bindings: unknown, debugLabel: string): string[];
67
+ /**
68
+ * Host fallback geometry, used when no profile resolves. US Letter.
69
+ *
70
+ * **Exported because the page map needs the same numbers.** Most pages declare
71
+ * no profile — the proof document's `layout` is `columns, shell, structure, type`
72
+ * with no `profile` key — so these, not a profile, are what the live `@page`
73
+ * rule is built from. A page map that fell back to its own copy of "US Letter"
74
+ * would be a second source for the geometry the paginator measures against, and
75
+ * the two agreeing today is not the same as them being one number.
76
+ */
77
+ export declare const DEFAULT_SHEET: {
78
+ width: string;
79
+ height: string;
80
+ };
81
+ export declare const DEFAULT_MARGINS: {
82
+ top: string;
83
+ right: string;
84
+ bottom: string;
85
+ left: string;
86
+ };
87
+ /**
88
+ * Quote a value for use in a CSS `content` string.
89
+ *
90
+ * Not decoration: a document code carrying an apostrophe — or a member name
91
+ * carrying one, which is common — would otherwise terminate the string early
92
+ * and produce a stylesheet that fails to parse from that point on, taking the
93
+ * rest of the `@page` block with it. Backslash first, or it re-escapes the
94
+ * escapes.
95
+ */
96
+ export declare function cssString(value: string): string;
97
+ /**
98
+ * A profile value (a length, a colour, a rule weight) as a CSS token, or undefined when it isn't
99
+ * one. These come from the bundle's profile, never from document data, but a value that could
100
+ * end a declaration or the style element is dropped all the same: letters, digits, `#`, `.`, `,`,
101
+ * `%`, `-`, `_`, `/`, spaces and parentheses, at most 120 characters.
102
+ */
103
+ export declare function cssValue(value: unknown): string | undefined;
104
+ export interface BuildPageStylesheetOptions {
105
+ values?: PageRegionValues;
106
+ /** Named in warnings so a blank footer is traceable to a document. */
107
+ debugLabel?: string;
108
+ /**
109
+ * Throw when a binding in `profile.requiredBindings` cannot be filled.
110
+ *
111
+ * **Defaults to `true`, and the default is the whole point.** A caller that
112
+ * forgets this option gets the refusal; a caller that wants to render anyway
113
+ * has to say so at the call site, where a reader can see it and ask why. The
114
+ * inverse default would make "produce a legal instrument that does not say it
115
+ * is a draft" the thing you get by not thinking about it.
116
+ *
117
+ * Screen rendering passes `false` deliberately: `@page` does not apply on
118
+ * screen, so a missing footer binding misrepresents nothing there. The
119
+ * artifact path — where the document becomes paper — must not.
120
+ */
121
+ strict?: boolean;
122
+ }
123
+ /**
124
+ * Thrown when a document cannot be printed without misrepresenting itself.
125
+ *
126
+ * A distinct class rather than a bare `Error` so an artifact endpoint can
127
+ * answer 422-with-a-reason instead of 500-with-a-stack: this is a document
128
+ * that is not printable yet, not a server that broke.
129
+ */
130
+ export declare class MissingRequiredBindingError extends Error {
131
+ readonly profileId: string | undefined;
132
+ readonly missing: string[];
133
+ constructor(profileId: string | undefined, missing: string[]);
134
+ }
135
+ export interface PageStylesheet {
136
+ css: string;
137
+ /**
138
+ * Unfilled bindings the profile marked required.
139
+ *
140
+ * Only ever non-empty when `strict: false` — strict mode throws instead. It
141
+ * exists so a lenient (screen) caller can still surface the problem before
142
+ * someone hits print, rather than discovering it on paper.
143
+ */
144
+ missingRequired: string[];
145
+ /**
146
+ * Slots the profile named and the host could not fill.
147
+ *
148
+ * Returned rather than only logged so a caller — a print preflight, or the
149
+ * parity test in stage 9 — can assert on them. A blank margin box is invisible
150
+ * on screen and only wrong on paper, which is the worst place to find it.
151
+ */
152
+ unresolvedSlots: string[];
153
+ }
154
+ /**
155
+ * Build the `@page` block for one document.
156
+ *
157
+ * @param profile Resolved profile, or `null` for host defaults.
158
+ */
159
+ export declare function buildPageStylesheet(profile: DocumentProfile | null, options?: BuildPageStylesheetOptions): PageStylesheet;
160
+ /**
161
+ * Profile-derived custom properties for the printed body.
162
+ *
163
+ * Separate from the `@page` block because they cascade to elements, and `@page`
164
+ * is not part of the document tree — a custom property declared inside `@page`
165
+ * is not visible to the body, and one declared on `:root` is not visible to a
166
+ * margin box. Both halves are needed and neither can be dropped into the other.
167
+ */
168
+ export declare function buildPageVariables(profile: DocumentProfile | null): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Running headers and footers that know where they are in the document.
3
+ *
4
+ * ── What this is for ───────────────────────────────────────────────────────
5
+ *
6
+ * The owner: *"I'd like to be able to create a custom print header and footer
7
+ * that updates based on the page layout i.e. page number section continuation
8
+ * etc."*
9
+ *
10
+ * Page numbers were already possible — `counter(page)` in a margin box, and
11
+ * MEASURED to work in Chrome. *"Financial Summary (continued)"* was not, and the
12
+ * reason is recorded in `print-stylesheet.ts`: Chromium drops the GCPM
13
+ * `string()` mechanism at parse time, so a margin box cannot pull text out of
14
+ * the document. Every engine that does this natively (Prince, WeasyPrint) has
15
+ * `string-set`; Chrome has neither.
16
+ *
17
+ * ── Why the host can do it anyway ──────────────────────────────────────────
18
+ *
19
+ * Because the host now knows the pagination. `paginate` assigns every row to a
20
+ * page, and MEASURED this session, Chrome supports **named pages**:
21
+ *
22
+ * @page secA { @top-left { content: "Financial Summary" } }
23
+ * .row { page: secA; }
24
+ *
25
+ * with `counter(page)` staying continuous across them and margin boxes
26
+ * cascading **per box** — a named rule overrides only the slots it redefines,
27
+ * leaving the rest of the default `@page` intact.
28
+ *
29
+ * So the host resolves the running content per page itself and emits one named
30
+ * `@page` rule for each. Nothing is pulled from the document at print time; it
31
+ * is all decided before the stylesheet is written.
32
+ *
33
+ * ── Where a "section" comes from, and why not from config ──────────────────
34
+ *
35
+ * Not from config, because it is not there. The proof page's `layout.structure` is a flat
36
+ * list of `{ cellId }` with no sections and no `sectionId`, and a survey
37
+ * measured 3 `sectionId`s across 2,279 structure nodes with **none** on that
38
+ * document. Its cells are thin refs, so even the component name lives in DSNS
39
+ * rather than the page.
40
+ *
41
+ * It comes from the **rendered document**, on a signal FEC already authors:
42
+ * a row that declares `break-after: avoid` is a row bound to what follows —
43
+ * which is what a section heading is. Measured on the proof document, that selects
44
+ * **exactly** the seven `*-header` rows, all `<h2>`, with no false positives:
45
+ *
46
+ * summary-header · retirement-header · networth-header · tax-header
47
+ * insurance-header · decumulation-header · decisions-header
48
+ *
49
+ * Requiring a heading element as well excludes the cover (`<h1>`, and not bound
50
+ * to anything) and the `<h3>`/`<h4>` sub-headings that live *inside* content
51
+ * rows — those are 6 more heading elements that are emphatically not sections.
52
+ *
53
+ * This module takes the markers already resolved. Finding them in the DOM is
54
+ * the caller's job, for the same reason `collectBlocks` takes an injected
55
+ * measurer: a pure function is testable and a DOM walk is not.
56
+ */
57
+ import type { PageMap } from './paginate.js';
58
+ import type { Band } from './page-map.js';
59
+ /** A row that begins a section, and the title it carries. */
60
+ export interface SectionMarker {
61
+ /** The cell id of the heading row. */
62
+ cellId: string;
63
+ title: string;
64
+ }
65
+ export interface RunningValues {
66
+ /** 1-based page number. */
67
+ page: number;
68
+ /** The section this page sits in, or undefined before the first heading. */
69
+ sectionTitle?: string;
70
+ /**
71
+ * True when the section started on an EARLIER page.
72
+ *
73
+ * The distinction a reader needs: a page headed *"Retirement Analysis"* that
74
+ * begins the section reads differently from one that carries it on, and
75
+ * without the marker a reader flipping through cannot tell whether they have
76
+ * missed the start.
77
+ */
78
+ continued: boolean;
79
+ }
80
+ export interface ResolveRunningOptions {
81
+ /** Appended when a section carries over. `' (continued)'` by default. */
82
+ continuedSuffix?: string;
83
+ }
84
+ /**
85
+ * Which section each page is in, and whether it carries over.
86
+ *
87
+ * @param map From `paginate`, whose pages hold band ids in reading order.
88
+ * @param bands From `collectBlocks`, to resolve a band id to its member cells.
89
+ * @param markers The section headings, in document order.
90
+ */
91
+ export declare function resolveRunningValues(map: PageMap, bands: readonly Band[], markers: readonly SectionMarker[]): RunningValues[];
92
+ /**
93
+ * The per-page slot values, keyed by slot name.
94
+ *
95
+ * Two names, deliberately few. `section-title` is the bare title;
96
+ * `section-title-continued` is the same title with the suffix when it carries
97
+ * over — a profile chooses which behaviour it wants rather than the host
98
+ * deciding for it, because a footer that says *"Retirement Analysis
99
+ * (continued)"* is right for a report and wrong for a form.
100
+ */
101
+ export declare const RUNNING_SLOTS: readonly ["section-title", "section-title-continued"];
102
+ export type RunningSlot = (typeof RUNNING_SLOTS)[number];
103
+ export declare function isRunningSlot(name: string): name is RunningSlot;
104
+ /** Resolve one page's running slots to literal strings. */
105
+ export declare function runningSlotValues(values: RunningValues, { continuedSuffix }?: ResolveRunningOptions): Partial<Record<RunningSlot, string>>;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Writing a person's page-break decision back into `layout.structure`.
3
+ *
4
+ * ── What this is for ───────────────────────────────────────────────────────
5
+ *
6
+ * The owner: *"hover buttons that allow the user to move components either up or
7
+ * down in the print map … do page mappings get persisted so the next time it
8
+ * respects those changes."*
9
+ *
10
+ * They do, and this is the write. Two things had to be settled before it could
11
+ * exist, and both were settled by the people who own them rather than here:
12
+ *
13
+ * - **The vocabulary is FEC's**. `keepWithPrevious`, not
14
+ * `breakBefore: false`, because an absent boolean and a false one are the same
15
+ * value across a JSON round-trip — so "no opinion" and "explicitly do not
16
+ * break" would have been indistinguishable, which is the declared-vs-default
17
+ * flattening that split a section heading in half.
18
+ * - **The storage is `layout.structure[].print`** (agreed with the agency side, checked
19
+ * against the database). A template re-sync does not touch an instance page's
20
+ * `layout`; the standing rule is never to call sync with `rebuild`.
21
+ *
22
+ * ── Why an intent and not a page number ────────────────────────────────────
23
+ *
24
+ * A stored page ASSIGNMENT — "page 7 holds these four rows" — is wrong the
25
+ * instant any height changes: a data refresh, a webfont, a longer client name.
26
+ * A stored INTENT — "start a page before this row" — survives all of them,
27
+ * because it is a statement about a boundary rather than about a layout.
28
+ *
29
+ * ── Why it is a pure function over the structure array ─────────────────────
30
+ *
31
+ * Because the array is the awkward part, not the write. Entries are strings OR
32
+ * objects, sections nest children arbitrarily deep, and the same cell id must not
33
+ * be matched twice. All of that is testable without a database, and none of it is
34
+ * testable through a mutation hook.
35
+ */
36
+ import type { LayoutNode as RuntimeLayoutNode } from '@peripheries/runtime';
37
+ import type { PrintConstraints } from './print-policy.js';
38
+ /**
39
+ * Where a structure row renders outside the flow. The runtime types `slot` as an
40
+ * open string; the print writer only knows this one.
41
+ */
42
+ export type StructureSlot = 'marginalia';
43
+ /**
44
+ * The runtime's `LayoutNode`, with the print block's `blankPageBefore` (a
45
+ * deliberate blank page before the row), which the runtime type does not
46
+ * declare yet. Everything else is the runtime's.
47
+ */
48
+ export type LayoutNode = Omit<RuntimeLayoutNode, 'print' | 'children'> & {
49
+ children?: Array<LayoutNode | string>;
50
+ print?: NonNullable<RuntimeLayoutNode['print']> & {
51
+ blankPageBefore?: boolean;
52
+ };
53
+ };
54
+ /** What a person asked for about a row's position in the page map. */
55
+ export type PageIntent =
56
+ /** Start a new page at this row. */
57
+ 'break-before'
58
+ /** Keep this row on the page above — it may not open a page. */
59
+ | 'keep-with-previous'
60
+ /** No opinion: the packer decides. Clears any previous intent. */
61
+ | 'auto';
62
+ /** A structure entry, as authored. A bare string is shorthand for `{ cellId }`. */
63
+ export type StructureEntry = string | StructureNode;
64
+ /**
65
+ * A `layout.structure` entry, as the print writer sees it.
66
+ *
67
+ * **Not a second declaration.** Every field lives on `LayoutNode`
68
+ * (`@peripheries/runtime`, plus `blankPageBefore` above) — its header records why there
69
+ * is now one. This alias adds exactly two things, both about writing rather
70
+ * than rendering:
71
+ *
72
+ * - **an index signature**, so an authored key the host has never heard of
73
+ * survives a read-modify-write instead of becoming an excess-property error.
74
+ * The renderer must not have it: it would switch off excess-property checking
75
+ * for every node literal there.
76
+ * - **`children` narrowed to `StructureEntry[]`**, so the recursive walks below
77
+ * stay typed. `LayoutNode.children` is `Array<LayoutNode | string>`, and a
78
+ * `LayoutNode` is not assignable to a type carrying an index signature.
79
+ *
80
+ * **Add a field to `LayoutNode`, never here.** A field added here is invisible
81
+ * to the renderer — which is exactly how `slot` shipped into one of two types
82
+ * and was found by FEC's drift check rather than by either of us.
83
+ */
84
+ export type StructureNode = Omit<LayoutNode, 'children'> & {
85
+ children?: StructureEntry[];
86
+ [key: string]: unknown;
87
+ };
88
+ export interface SetIntentResult {
89
+ structure: StructureEntry[];
90
+ /** False when no entry named this cell — the caller must not save a no-op. */
91
+ changed: boolean;
92
+ }
93
+ /**
94
+ * Set one row's page intent, returning a new structure.
95
+ *
96
+ * @param structure The page's `layout.structure`, untouched by this call.
97
+ * @param cellId The row to set it on.
98
+ * @param intent What the person asked for.
99
+ */
100
+ export declare function setRowPageIntent(structure: readonly StructureEntry[] | undefined, cellId: string, intent: PageIntent): SetIntentResult;
101
+ /**
102
+ * The `print` block an intent produces, or `undefined` when it produces none.
103
+ *
104
+ * ── Why this is exported ────────────────────────────────────────────────────
105
+ *
106
+ * Because the same intent now has to land in TWO places, per the owner's ruling
107
+ * on where print configuration lives: *"The honest answer is both. If there is
108
+ * an associated process node synced to the config, then it lives in DSNS. But
109
+ * this should update the project_page record anyways."*
110
+ *
111
+ * So `setRowPageIntent` writes the page row's `layout.structure[].print` and
112
+ * `recordCellPrintIntent` writes the cell's DSNS `content.layout.print`. Two
113
+ * call sites, one set of rules — because two copies of these three lines is
114
+ * exactly how a mirror comes to disagree with its source, which is the defect
115
+ * this whole channel keeps meeting.
116
+ *
117
+ * The rules, all three load-bearing:
118
+ *
119
+ * 1. **Both keys are deleted first.** Leaving a stale `breakBefore: true` under
120
+ * a new `keepWithPrevious: true` produces a node that asks for a break and
121
+ * asks not to break. `StructureRenderer` resolves that to the break and
122
+ * warns, so a person pressing "move up" would watch nothing happen and be
123
+ * told nothing.
124
+ * 2. **`auto` is the ABSENCE of both**, never either set `false`. The
125
+ * vocabulary is FEC's and `nodePrintStyle` reads a `false` as no
126
+ * declaration at all — indistinguishable from working.
127
+ * 3. **An empty block is `undefined`, not `{}`.** To a reader `{}` says
128
+ * "someone had an opinion here", and it would survive every future diff.
129
+ *
130
+ * `breakInside` is never touched: a different decision, authored by someone
131
+ * else, and this function has no opinion about it.
132
+ */
133
+ export declare function printBlockForIntent(existing: StructureNode['print'] | undefined, intent: PageIntent): StructureNode['print'] | undefined;
134
+ /** What intent a `print` block expresses. The inverse of `printBlockForIntent`. */
135
+ export declare function intentOfPrintBlock(print: StructureNode['print'] | undefined): PageIntent;
136
+ /** What intent a row currently carries, for a control that shows its state. */
137
+ export declare function rowPageIntent(structure: readonly StructureEntry[] | undefined, cellId: string): PageIntent;
138
+ /**
139
+ * Every cell's extrinsic `print` block, keyed by cell id.
140
+ *
141
+ * ── Why this exists, and what it cost not to have it ───────────────────────
142
+ *
143
+ * `layout.structure[].print` was honoured by `StructureRenderer` and by nothing
144
+ * else. `usesStructureLayout` routes a page there only for `type: 'document'`
145
+ * or a section-shaped node — **2 of 198** structured pages — so on every other
146
+ * page the extrinsic half was written, mirrored, and dropped at render.
147
+ *
148
+ * Measured end to end on the proof page: a client pressed *"Start a new page here"*,
149
+ * the intent landed in the cell DSNS AND the page row (both verified by query),
150
+ * and the document did not break there. The button worked, the write worked,
151
+ * and the paper never heard about it — which is indistinguishable from a broken
152
+ * feature and is worse, because two records now claim something the reader
153
+ * cannot see.
154
+ *
155
+ * It is the exact mirror of an earlier finding: that found `StructureRenderer`
156
+ * dropping the INTRINSIC `@printPolicy`; this is `CellsRenderer` dropping the
157
+ * EXTRINSIC `node.print`. Two renderers, each honouring one half.
158
+ *
159
+ * Walks children, because a section's rows carry their own intents and a
160
+ * document-type page routes to the other renderer but a `grid` page with
161
+ * children does not.
162
+ */
163
+ export declare function printByCellId(structure: readonly StructureEntry[] | undefined): ReadonlyMap<string, NonNullable<StructureNode['print']>>;
164
+ /**
165
+ * A structure node's extrinsic declaration, as `PrintConstraints`.
166
+ *
167
+ * ── Why this is in lib rather than beside the renderer that had it ─────────
168
+ *
169
+ * It lived in `StructureRenderer`, which is fine while one renderer needs it.
170
+ * **Both do:** `CellsRenderer` renders 196 of 198 structured pages and was
171
+ * dropping the extrinsic half entirely. `StructureRenderer` imports
172
+ * `CellRenderer`, so importing back would make the pair circular — and a
173
+ * second copy is how the two renderers would come to disagree about what a
174
+ * node declared, which is the defect this whole file exists to prevent.
175
+ *
176
+ * **It can only ever TIGHTEN**, which is the component repo's invariant for the extrinsic
177
+ * half (`a node may ADD a break, never RELAX one`). `breakInside` is emitted
178
+ * only for `'avoid'`, never `'auto'`, so composing this after a component's
179
+ * intrinsic policy cannot turn an atomic component splittable — a page author
180
+ * cannot switch off what a component declared about itself.
181
+ *
182
+ * `keepWithPrevious` becomes `breakBefore: 'avoid'`, and a `breakBefore`
183
+ * declared alongside it wins, because CSS Fragmentation §3.2 resolves a forced
184
+ * break over an avoid and the composer follows the enforcer rather than
185
+ * inventing a rule.
186
+ */
187
+ export declare function nodePrintConstraints(node: {
188
+ print?: StructureNode['print'];
189
+ }): PrintConstraints | undefined;
190
+ /**
191
+ * Add or remove a deliberate blank page before a row.
192
+ *
193
+ * Deliberately NOT folded into `setRowPageIntent`: that function owns the
194
+ * break-vocabulary keys and clears the ones it does not set, which is right for
195
+ * three mutually exclusive intents and wrong here. A blank page is an
196
+ * independent decision — a row can both open a page and have a blank one before
197
+ * it — and a person moving a row must not silently delete the blank page they
198
+ * put in front of it.
199
+ *
200
+ * `changed` is false when the row already says this, so a caller can refuse to
201
+ * burn a revision recording nothing.
202
+ */
203
+ export declare function setRowBlankPage(structure: readonly StructureEntry[] | undefined, cellId: string, present: boolean): {
204
+ structure: StructureEntry[];
205
+ changed: boolean;
206
+ };
207
+ /** Does a deliberate blank page precede this row? */
208
+ export declare function rowHasBlankPage(structure: readonly StructureEntry[] | undefined, cellId: string): boolean;
209
+ /**
210
+ * What one write actually changed, per cell.
211
+ *
212
+ * ── Why a write must carry a diff and not a document ────────────────────────
213
+ *
214
+ * Every layout gesture computes a whole new `layout` from the copy in the
215
+ * client's cache and hands it to the writer, which `update`s the row with it.
216
+ * That is a blind overwrite, and it loses every change the cache had not caught
217
+ * up with — the owner: *"if I don't close the layout mode after adding or
218
+ * deleting a blank page any edits I made to other layout configurations get
219
+ * reverted."*
220
+ *
221
+ * The invalidation bug that made the cache stale is fixed separately, and
222
+ * fixing it is not enough: two clicks land inside one refetch, and the second
223
+ * still reverts the first. The durable answer is for the write to state its
224
+ * own scope. Then the row's untouched entries come from the row as read at
225
+ * submit, and a concurrent change to a different cell survives — the same rule
226
+ * `record-state-patch` follows for authored scalars.
227
+ *
228
+ * `undefined` print means REMOVE the block, which is a real gesture (deleting a
229
+ * page break, deleting a blank page) and must not be confused with "unchanged".
230
+ * That is why the result is a list of explicit entries rather than a partial
231
+ * map: absence here means *not part of this write*, and only `print:
232
+ * undefined` means *take it away*.
233
+ */
234
+ export interface RowPrintChange {
235
+ cellId: string;
236
+ /** The new block, or `undefined` to remove it. */
237
+ print?: NonNullable<StructureNode['print']>;
238
+ }
239
+ export declare function diffRowPrint(base: readonly StructureEntry[] | undefined, next: readonly StructureEntry[] | undefined): RowPrintChange[];
240
+ /**
241
+ * Apply a diff onto a structure read at submit.
242
+ *
243
+ * `missing` names changes whose cell has no entry in the structure being
244
+ * written to — a renamed cell, or a change aimed at a page that has moved on.
245
+ * Reported rather than silently dropped: a write that lands partially and says
246
+ * nothing is the shape this whole area keeps producing.
247
+ */
248
+ export declare function applyRowPrint(structure: readonly StructureEntry[] | undefined, changes: readonly RowPrintChange[]): {
249
+ structure: StructureEntry[];
250
+ changed: boolean;
251
+ missing: string[];
252
+ };
253
+ /**
254
+ * The cell ids a structure hoists out of the flow, by slot.
255
+ *
256
+ * Walks nested entries, because a document structure nests
257
+ * (`{sectionId, children: [{cellId}]}`) and a flat scan finds nothing at depth
258
+ * — the exact false negative a review hit and recorded, where a flat
259
+ * comparison reported 7 and 10 cells "missing" from pages that were complete.
260
+ *
261
+ * Returns ids in document order and de-duplicated. Order matters only if a
262
+ * profile ever allows more than one occupant; today the caller takes the first
263
+ * and the rest are a finding, not a layout.
264
+ */
265
+ export declare function findSlottedCellIds(structure: readonly StructureEntry[] | undefined, slot: StructureSlot): string[];
266
+ /** A page whose cells and structure can be split. Structural minimum, not the full row. */
267
+ type SplittablePage = {
268
+ content?: {
269
+ cells?: Array<{
270
+ id?: string;
271
+ }>;
272
+ } | null;
273
+ layout?: {
274
+ structure?: StructureEntry[];
275
+ } | null;
276
+ } & Record<string, unknown>;
277
+ export interface MarginaliaSplit<P> {
278
+ /** What sets in the measure. The SAME reference as the input when nothing moved. */
279
+ bodyPage: P;
280
+ /** What sets beside it, or null when nothing is hoisted. */
281
+ marginaliaPage: P | null;
282
+ /**
283
+ * Why nothing was hoisted, when nothing was. `null` when the split happened.
284
+ * `'dangling'` is the one worth surfacing: the row declares a slot naming a
285
+ * cell the page does not have, which is the dangling-reference class.
286
+ */
287
+ reason: null | 'no-slot' | 'no-track' | 'dangling';
288
+ }
289
+ /**
290
+ * Split a page into what sets in the measure and what sets in the marginalia.
291
+ *
292
+ * Pure, and separated from the view for one reason: it is the half that can be
293
+ * wrong in a way nobody sees. The DOM placement is visible the moment you open
294
+ * the page; *which cell moved, whether the rest survived, and whether a page
295
+ * that declares nothing is returned untouched* are not.
296
+ *
297
+ * **`bodyPage` is the input reference when nothing moves**, not a shallow copy.
298
+ * A copy would be equal and would still invalidate every downstream memo keyed
299
+ * on identity, on every page in the product — all of which declare no slot.
300
+ */
301
+ export declare function splitMarginalia<P extends SplittablePage>(page: P | null | undefined, marginaliaCellId: string | null | undefined, hasReservedTrack: boolean): MarginaliaSplit<P>;
302
+ export {};
@@ -8,6 +8,7 @@ import type { CellData } from '../core/load-page.js';
8
8
  import { type ResolutionContext } from '../core/references.js';
9
9
  import type { StackStoreApi } from '../core/store/create-store.js';
10
10
  import type { ScopeContext } from '../core/store/scope-keys.js';
11
+ import type { EditCell, PropSourceEntry } from '../core/sources.js';
11
12
  import type { PageCell } from '../core/types.js';
12
13
  import { type LoadPage } from './use-paging.js';
13
14
  export type CellEnvironment = {
@@ -40,6 +41,8 @@ export type CellEnvironment = {
40
41
  loadPage?: LoadPage;
41
42
  onCellError?: (cellId: string, error: Error) => void;
42
43
  onUnresolvedReference?: (cellId: string, ref: string) => void;
44
+ /** Each rendered cell's props, by prop path, with where each value came from (RendererEnvironment.onPropSources). */
45
+ onPropSources?: (cell: EditCell, sources: PropSourceEntry[]) => void;
43
46
  /** An embed cell's page (PageRuntime.embeds), rendered by the page renderer. */
44
47
  renderEmbed?: (cellId: string) => ReactNode;
45
48
  /** Where a render-time diagnostic goes (`COMPONENT_UNKNOWN` from `renderComponent`). */
@@ -4,6 +4,7 @@ import type { PageBundles } from '../core/bundles/page-bundles.js';
4
4
  import type { CoreRegistry } from '../core/bundles/resolve.js';
5
5
  import type { Diagnostic } from '../core/graph.js';
6
6
  import type { PageRuntime } from '../core/load-page.js';
7
+ import type { EditCell, PropSourceEntry } from '../core/sources.js';
7
8
  import type { StackStoreApi } from '../core/store/create-store.js';
8
9
  import type { PageCell } from '../core/types.js';
9
10
  import { type FrameProps } from './cell-view.js';
@@ -38,6 +39,11 @@ export type RendererEnvironment = {
38
39
  onCellError?: (cellId: string, error: Error) => void;
39
40
  /** A cell's prop reference resolved to nothing (a binding the page gets wrong). */
40
41
  onUnresolvedReference?: (cellId: string, ref: string) => void;
42
+ /**
43
+ * Each rendered cell's props, leaf by leaf, with where each value came from (INV-DOC-6): what
44
+ * the edit layer maps to a write function with `editTarget`. Called as each cell renders.
45
+ */
46
+ onPropSources?: (cell: EditCell, sources: PropSourceEntry[]) => void;
41
47
  /** Reads the page again: a cell whose data or bundle failed offers it as "Try again" (shell audit S1). */
42
48
  retry?: () => void;
43
49
  /** Extra attributes and style for each cell's frame, e.g. a print policy the host doesn't apply yet. */
@@ -1,12 +1,18 @@
1
1
  import { READY_GLOBAL, RESOLVE_GLOBAL, RUNTIME_GLOBAL, type RuntimeSpecifier } from '@peripheries/runtime';
2
- export type StackRuntime = Record<RuntimeSpecifier, unknown> & Record<string, unknown>;
2
+ /** `@peripheries/ui` is lent by the host (`extra`), not installed by the renderer. */
3
+ export type StackRuntime = Record<Exclude<RuntimeSpecifier, '@peripheries/ui'>, unknown> & Record<string, unknown>;
3
4
  type RuntimeGlobals = {
4
5
  [RUNTIME_GLOBAL]?: StackRuntime;
5
6
  [READY_GLOBAL]?: Promise<void>;
6
7
  [RESOLVE_GLOBAL]?: () => void;
7
8
  require?: (specifier: string) => unknown;
8
9
  };
9
- export declare function buildRuntime(): StackRuntime;
10
+ /**
11
+ * The host's module instances by specifier. `extra` is what the host lends beyond the
12
+ * renderer's own (`@peripheries/ui`), so the published renderer depends on nothing it
13
+ * doesn't install itself.
14
+ */
15
+ export declare function buildRuntime(extra?: Record<string, unknown>): StackRuntime;
10
16
  /**
11
17
  * Puts the host's module instances where the `/_shared/*.js` shims look for
12
18
  * them, and releases every shim waiting on the boot promise. Idempotent.
@@ -16,5 +22,5 @@ export declare function buildRuntime(): StackRuntime;
16
22
  * at module init, which throws in a browser without it. The runtime modules
17
23
  * are already loaded, so it answers synchronously.
18
24
  */
19
- export declare function installRuntime(g?: RuntimeGlobals): StackRuntime;
25
+ export declare function installRuntime(g?: RuntimeGlobals, extra?: Record<string, unknown>): StackRuntime;
20
26
  export { runtimeReady } from '../core/bundles/load.js';
@@ -14,7 +14,7 @@ type SectionMaps = {
14
14
  export declare function Masthead({ title, subtitle }: Props): import("react").JSX.Element;
15
15
  /** Authored text arrives as `children` unless the cell has child cells. */
16
16
  export declare function Prose({ children }: Props): import("react").JSX.Element;
17
- /** A section body in the style of acrc's DocumentSection: it owns its section's anchor. */
17
+ /** A section body in the style of an org bundle's DocumentSection: it owns its section's anchor. */
18
18
  export declare function DocumentSection({ sectionId, title, body, blocks, children, sectionNumbers, sectionAnchors }: Props & SectionMaps): import("react").JSX.Element;
19
19
  /** A same-page citation: `§<number> <title>`, plus the clause's letter when it names one. */
20
20
  export declare function SectionReference({ to, sectionReferences }: Props & SectionMaps): import("react").JSX.Element;