@beyondwork/docx-react-component 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Decoration resolver — single pass that turns canonical decoration sources
3
+ * (workflow scopes, comments, revisions, search, locked zones) into the
4
+ * five lanes of `DecorationIndex` that chrome consumes.
5
+ *
6
+ * Per runtime-rendering-and-chrome-phase.md §1, the kernel's MVP leaves
7
+ * `DecorationIndex` empty; chrome surfaces that need per-decoration anchor
8
+ * rects re-project from runtime offsets themselves. The resolver fixes
9
+ * that by walking each source once, resolving its body rect through the
10
+ * kernel's anchor index, and emitting a `RenderBlockDecoration` entry with
11
+ * the stable rect. Chrome reads the resolved lanes directly and never
12
+ * touches the DOM or re-runs anchor math.
13
+ *
14
+ * The resolver is pure: same inputs produce the same output. The kernel
15
+ * invokes it once per frame build, cached against the frame's revision.
16
+ */
17
+
18
+ import type {
19
+ CommentDecorationModel,
20
+ CommentDecorationThread,
21
+ } from "../../ui/headless/comment-decoration-model.ts";
22
+ import type {
23
+ RevisionDecorationModel,
24
+ RevisionDecorationEntry,
25
+ } from "../../ui/headless/revision-decoration-model.ts";
26
+ import type { ScopeRailSegment } from "../workflow-rail-segments.ts";
27
+ import type {
28
+ DecorationIndex,
29
+ RenderAnchorIndex,
30
+ RenderBlockDecoration,
31
+ RenderFrameRect,
32
+ } from "./render-frame-types.ts";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Input shapes
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface SearchMatchRange {
39
+ /** Stable identifier the search plugin emits for each match. */
40
+ matchId: string;
41
+ /** Inclusive runtime offset. */
42
+ from: number;
43
+ /** Exclusive runtime offset. */
44
+ to: number;
45
+ /** True when this match is the currently-focused "active" result. */
46
+ isActive?: boolean;
47
+ }
48
+
49
+ export interface LockedRangeInput {
50
+ /** Stable id of the lock (fragmentId for locked zones, scopeId for preserve-only scopes). */
51
+ lockId: string;
52
+ /** Inclusive runtime offset. */
53
+ from: number;
54
+ /** Exclusive runtime offset. */
55
+ to: number;
56
+ /** Optional human label. */
57
+ label?: string;
58
+ }
59
+
60
+ export interface ResolveDecorationIndexInput {
61
+ anchorIndex: RenderAnchorIndex;
62
+ /**
63
+ * Scope rail segments covering the active story. The resolver re-uses
64
+ * their anchor rects when present (`bodyTintRect`) and falls back to
65
+ * `anchorIndex.bySelection(from, to)` otherwise.
66
+ */
67
+ workflowSegments?: readonly ScopeRailSegment[];
68
+ comments?: CommentDecorationModel | null | undefined;
69
+ revisions?: RevisionDecorationModel | null | undefined;
70
+ searchMatches?: readonly SearchMatchRange[];
71
+ lockedRanges?: readonly LockedRangeInput[];
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Entry point
76
+ // ---------------------------------------------------------------------------
77
+
78
+ export function resolveDecorationIndex(
79
+ input: ResolveDecorationIndexInput,
80
+ ): DecorationIndex {
81
+ const workflow: RenderBlockDecoration[] = [];
82
+ const comments: RenderBlockDecoration[] = [];
83
+ const revisions: RenderBlockDecoration[] = [];
84
+ const search: RenderBlockDecoration[] = [];
85
+ const locked: RenderBlockDecoration[] = [];
86
+
87
+ if (input.workflowSegments) {
88
+ for (const segment of input.workflowSegments) {
89
+ const frame =
90
+ segment.bodyTintRect ??
91
+ input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset);
92
+ if (!frame) continue;
93
+ workflow.push({
94
+ kind: "workflow",
95
+ refId: segment.scopeId,
96
+ frame,
97
+ });
98
+ }
99
+ }
100
+
101
+ if (input.comments) {
102
+ for (const thread of input.comments.threads) {
103
+ const frame = resolveRange(
104
+ input.anchorIndex,
105
+ thread.from,
106
+ thread.to,
107
+ );
108
+ if (!frame) continue;
109
+ comments.push({
110
+ kind: "comment",
111
+ refId: thread.commentId,
112
+ frame,
113
+ });
114
+ // Track active thread separately so chrome can prioritize rendering.
115
+ // (We don't have a per-decoration "isActive" field today; chrome
116
+ // reads `CommentDecorationModel.activeCommentId` alongside the
117
+ // decoration lane and picks out the matching refId.)
118
+ void asCommentThread(thread);
119
+ }
120
+ }
121
+
122
+ if (input.revisions) {
123
+ for (const revision of input.revisions.revisions) {
124
+ const frame = resolveRange(
125
+ input.anchorIndex,
126
+ revision.from,
127
+ revision.to,
128
+ );
129
+ if (!frame) continue;
130
+ revisions.push({
131
+ kind: "revision",
132
+ refId: revision.revisionId,
133
+ frame,
134
+ });
135
+ void asRevisionEntry(revision);
136
+ }
137
+ }
138
+
139
+ if (input.searchMatches) {
140
+ for (const match of input.searchMatches) {
141
+ const frame = resolveRange(input.anchorIndex, match.from, match.to);
142
+ if (!frame) continue;
143
+ search.push({
144
+ kind: "search",
145
+ refId: match.matchId,
146
+ frame,
147
+ });
148
+ }
149
+ }
150
+
151
+ if (input.lockedRanges) {
152
+ for (const lock of input.lockedRanges) {
153
+ const frame = resolveRange(input.anchorIndex, lock.from, lock.to);
154
+ if (!frame) continue;
155
+ locked.push({
156
+ kind: "locked",
157
+ refId: lock.lockId,
158
+ frame,
159
+ });
160
+ }
161
+ }
162
+
163
+ return { workflow, comments, revisions, search, locked };
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Internals
168
+ // ---------------------------------------------------------------------------
169
+
170
+ function resolveRange(
171
+ anchorIndex: RenderAnchorIndex,
172
+ from: number,
173
+ to: number,
174
+ ): RenderFrameRect | null {
175
+ if (from >= to) {
176
+ return anchorIndex.byRuntimeOffset(from);
177
+ }
178
+ return anchorIndex.bySelection(from, to);
179
+ }
180
+
181
+ // Type preservation helpers so the input shapes stay referenced and
182
+ // `tsgo --noEmit` catches shape drift if the upstream models change.
183
+ function asCommentThread(thread: CommentDecorationThread): CommentDecorationThread {
184
+ return thread;
185
+ }
186
+
187
+ function asRevisionEntry(entry: RevisionDecorationEntry): RevisionDecorationEntry {
188
+ return entry;
189
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Runtime-owned render kernel — internal API surface.
3
+ *
4
+ * The kernel consumes the layout facet and emits `RenderFrame` objects
5
+ * that every chrome surface reads. Nothing in this module is part of
6
+ * the package's public API; consumers go through `ref.layout.getRenderFrame`
7
+ * (facet) rather than wiring the kernel directly.
8
+ */
9
+
10
+ export {
11
+ createRenderKernel,
12
+ sumDeltasBefore,
13
+ type RenderKernel,
14
+ type CreateRenderKernelInput,
15
+ type DecorationSources,
16
+ type PendingOpDelta,
17
+ } from "./render-kernel.ts";
18
+
19
+ export {
20
+ createPendingOpDeltaReader,
21
+ type PendingOpDeltaReader,
22
+ type PendingOpDeltaSnapshot,
23
+ type CreatePendingOpDeltaReaderInput,
24
+ } from "./pending-op-delta-reader.ts";
25
+
26
+ export {
27
+ resolveDecorationIndex,
28
+ type ResolveDecorationIndexInput,
29
+ type SearchMatchRange,
30
+ type LockedRangeInput,
31
+ } from "./decoration-resolver.ts";
32
+
33
+ export {
34
+ DEFAULT_PX_PER_TWIP,
35
+ EMPTY_DECORATION_INDEX,
36
+ defaultChromeReservations,
37
+ resolveDefaultZoom,
38
+ type DecorationIndex,
39
+ type PageChromeReservations,
40
+ type RenderAnchorIndex,
41
+ type RenderAnchorQuery,
42
+ type RenderBlock,
43
+ type RenderBlockDecoration,
44
+ type RenderFrame,
45
+ type RenderFrameQueryOptions,
46
+ type RenderFrameRect,
47
+ type RenderHitResult,
48
+ type RenderKernelEvent,
49
+ type RenderPoint,
50
+ type RenderLine,
51
+ type RenderLineAnchor,
52
+ type RenderPage,
53
+ type RenderPageRegions,
54
+ type RenderStoryRegion,
55
+ type RenderZoom,
56
+ type DefaultZoomInput,
57
+ } from "./render-frame-types.ts";
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Pending-op delta reader — thin accessor over `createRenderKernel`'s
3
+ * `getPendingOpDeltas` seam so chrome surfaces can reason about the
4
+ * predicted-dispatch window without reaching into the kernel's internals.
5
+ *
6
+ * Chrome must NOT mutate anchor rects during the predicted-dispatch window —
7
+ * per `bounded-local-first-interaction-architecture.md`, the rects settle
8
+ * when `TextCommandAck` reconciles. Chrome surfaces that want to *mask*
9
+ * during that window (disable buttons, show a spinner) read a flag from
10
+ * this module instead of polling the kernel directly.
11
+ *
12
+ * The reader is decoupled from the kernel so the predicted lane can be
13
+ * swapped or gated without touching chrome code.
14
+ */
15
+
16
+ import type { PendingOpDelta } from "./render-kernel.ts";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Reader contract
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Snapshot of the predicted-dispatch state at the moment it was read.
24
+ *
25
+ * `deltas` is the same shape the kernel's anchor index receives; chrome
26
+ * uses the summary fields below instead of walking the array in hot paths.
27
+ */
28
+ export interface PendingOpDeltaSnapshot {
29
+ /** Raw delta array, identical to what the kernel received. */
30
+ deltas: readonly PendingOpDelta[];
31
+ /** True when any pending op is outstanding. */
32
+ hasPending: boolean;
33
+ /** Count of pending ops (for perf probe sampling). */
34
+ pendingCount: number;
35
+ /** Net character delta summed across all pending ops. */
36
+ netCharacterDelta: number;
37
+ /** Lowest runtime offset touched by any pending op, or null when empty. */
38
+ earliestTouchedOffset: number | null;
39
+ /** Highest runtime offset touched by any pending op, or null when empty. */
40
+ latestTouchedOffset: number | null;
41
+ }
42
+
43
+ export interface PendingOpDeltaReader {
44
+ /** Read the current snapshot. Safe to call every render. */
45
+ read(): PendingOpDeltaSnapshot;
46
+ /**
47
+ * Predicate: is a given runtime offset inside the range touched by the
48
+ * pending window? Chrome uses this to decide whether a selection anchor
49
+ * is stable yet.
50
+ */
51
+ isOffsetPending(offset: number): boolean;
52
+ }
53
+
54
+ export interface CreatePendingOpDeltaReaderInput {
55
+ /**
56
+ * Reads the current pending-op deltas — typically
57
+ * `() => localEditSessionState.getPendingDeltas()` or the kernel's own
58
+ * `getPendingOpDeltas` accessor. When absent, the reader returns an
59
+ * empty snapshot.
60
+ */
61
+ getDeltas?: () => readonly PendingOpDelta[];
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Factory
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const EMPTY_SNAPSHOT: PendingOpDeltaSnapshot = Object.freeze({
69
+ deltas: [],
70
+ hasPending: false,
71
+ pendingCount: 0,
72
+ netCharacterDelta: 0,
73
+ earliestTouchedOffset: null,
74
+ latestTouchedOffset: null,
75
+ });
76
+
77
+ export function createPendingOpDeltaReader(
78
+ input: CreatePendingOpDeltaReaderInput = {},
79
+ ): PendingOpDeltaReader {
80
+ const getDeltas = input.getDeltas;
81
+
82
+ function read(): PendingOpDeltaSnapshot {
83
+ if (!getDeltas) {
84
+ return EMPTY_SNAPSHOT;
85
+ }
86
+ const deltas = getDeltas();
87
+ if (deltas.length === 0) {
88
+ return EMPTY_SNAPSHOT;
89
+ }
90
+ return summarize(deltas);
91
+ }
92
+
93
+ return {
94
+ read,
95
+ isOffsetPending(offset) {
96
+ const snapshot = read();
97
+ if (!snapshot.hasPending) return false;
98
+ const earliest = snapshot.earliestTouchedOffset;
99
+ const latest = snapshot.latestTouchedOffset;
100
+ if (earliest === null || latest === null) return false;
101
+ return offset >= earliest && offset <= latest;
102
+ },
103
+ };
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Internals
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function summarize(
111
+ deltas: readonly PendingOpDelta[],
112
+ ): PendingOpDeltaSnapshot {
113
+ let net = 0;
114
+ let earliest = Number.POSITIVE_INFINITY;
115
+ let latest = Number.NEGATIVE_INFINITY;
116
+ for (const delta of deltas) {
117
+ net += delta.delta;
118
+ if (delta.fromRuntime < earliest) earliest = delta.fromRuntime;
119
+ if (delta.fromRuntime > latest) latest = delta.fromRuntime;
120
+ }
121
+ return {
122
+ deltas,
123
+ hasPending: true,
124
+ pendingCount: deltas.length,
125
+ netCharacterDelta: net,
126
+ earliestTouchedOffset: Number.isFinite(earliest) ? earliest : null,
127
+ latestTouchedOffset: Number.isFinite(latest) ? latest : null,
128
+ };
129
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Shape definitions for the runtime-owned render kernel (Phase R1).
3
+ *
4
+ * Per runtime-rendering-and-chrome-phase.md §1 the render kernel projects the
5
+ * layout engine's page graph plus the decoration sources into a stable
6
+ * `RenderFrame` that every chrome surface reads. Today the kernel emits a
7
+ * minimum-viable frame so consumers can start wiring against the shape;
8
+ * richer decoration resolution, wrap rects, and table render plans land as
9
+ * the chrome phase continues.
10
+ */
11
+
12
+ import type {
13
+ EditorStoryTarget,
14
+ PageLayoutSnapshot,
15
+ } from "../../api/public-types";
16
+ import type {
17
+ PublicBlockFragment,
18
+ PublicLineBox,
19
+ PublicPageNode,
20
+ PublicPageRegion,
21
+ PublicMeasurementFidelity,
22
+ } from "../layout/public-facet.ts";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Core frame shape
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface RenderFrame {
29
+ /** Revision stamp the kernel used to build this frame. */
30
+ revision: number;
31
+ /** Measurement fidelity the frame was computed against. */
32
+ measurementFidelity: PublicMeasurementFidelity;
33
+ /** Story the mounted surface renders. */
34
+ activeStory: EditorStoryTarget;
35
+ /** Zoom the frame was composed for. */
36
+ zoom: RenderZoom;
37
+ /** Page frames in document order. */
38
+ pages: RenderPage[];
39
+ /** Decoration index (per-frame, keyed by canonical runtime ranges). */
40
+ decorationIndex: DecorationIndex;
41
+ /** Anchor index for chrome placement. */
42
+ anchorIndex: RenderAnchorIndex;
43
+ }
44
+
45
+ export interface RenderPage {
46
+ page: PublicPageNode;
47
+ /** Top-left in frame-local pixel coordinates (post-zoom). */
48
+ frame: RenderFrameRect;
49
+ regions: RenderPageRegions;
50
+ chromeReservations: PageChromeReservations;
51
+ }
52
+
53
+ export interface RenderPageRegions {
54
+ body: RenderStoryRegion;
55
+ header?: RenderStoryRegion;
56
+ footer?: RenderStoryRegion;
57
+ columns?: readonly RenderStoryRegion[];
58
+ footnoteArea?: RenderStoryRegion;
59
+ }
60
+
61
+ export interface RenderStoryRegion {
62
+ storyTarget: EditorStoryTarget;
63
+ region: PublicPageRegion;
64
+ frame: RenderFrameRect;
65
+ blocks: RenderBlock[];
66
+ }
67
+
68
+ export interface RenderBlock {
69
+ fragment: PublicBlockFragment;
70
+ frame: RenderFrameRect;
71
+ kind: "paragraph" | "table" | "opaque" | "image-float" | "synthetic";
72
+ lines: RenderLine[];
73
+ blockDecorations: RenderBlockDecoration[];
74
+ /**
75
+ * Table geometry + band classes, attached only when `kind === "table"`.
76
+ * Populated by the layout facet's `getTableRenderPlan(blockId)` during
77
+ * frame construction so chrome can read columns, merges, and resize-
78
+ * handle origins without walking the canonical model.
79
+ */
80
+ tablePlan?: import("../layout/table-render-plan.ts").TableRenderPlan | null;
81
+ }
82
+
83
+ export interface RenderLine {
84
+ line: PublicLineBox;
85
+ frame: RenderFrameRect;
86
+ /**
87
+ * Anchor metadata for chrome hit-testing and balloon placement. Today
88
+ * every line carries a single anchor covering its fragment; richer per-
89
+ * run anchors land with the kernel's full line-box projection.
90
+ */
91
+ anchors: RenderLineAnchor[];
92
+ }
93
+
94
+ export interface RenderLineAnchor {
95
+ /** Runtime offset this anchor represents. */
96
+ runtimeOffset: number;
97
+ /** Screen-relative rect the anchor occupies. */
98
+ frame: RenderFrameRect;
99
+ /** Optional fragment or block id hint. */
100
+ fragmentId?: string;
101
+ blockId?: string;
102
+ }
103
+
104
+ export interface RenderBlockDecoration {
105
+ kind: "workflow" | "comment" | "revision" | "search" | "locked";
106
+ /** Decoration identifier (scope id, comment id, revision id, etc.). */
107
+ refId: string;
108
+ frame: RenderFrameRect;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Geometry
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * All rects are in frame-local pixels at the kernel's chosen zoom. The
117
+ * kernel applies `RenderZoom.pxPerTwip` to the page graph's twip values
118
+ * before returning a frame.
119
+ */
120
+ export interface RenderFrameRect {
121
+ leftPx: number;
122
+ topPx: number;
123
+ widthPx: number;
124
+ heightPx: number;
125
+ }
126
+
127
+ export interface RenderZoom {
128
+ /** Pixels per twip (e.g. 96 dpi at 100% = 0.0667). */
129
+ pxPerTwip: number;
130
+ /** Viewport width in px. */
131
+ viewportWidthPx: number;
132
+ /** Fit mode the zoom was resolved from. */
133
+ fitMode: "fixed" | "page-width" | "one-page";
134
+ }
135
+
136
+ export interface PageChromeReservations {
137
+ /** Twips reserved to the left of the page frame for the workflow rail. */
138
+ railLaneTwips: number;
139
+ /** Twips reserved to the right for comment balloons. */
140
+ balloonLaneTwips: number;
141
+ /** Twips reserved at the bottom of the body for footnotes. */
142
+ footnoteAreaTwips: number;
143
+ /** Cached post-zoom page frame in pixels. */
144
+ pageFrameWidthPx: number;
145
+ pageFrameHeightPx: number;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Decoration and anchor indexes
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export interface DecorationIndex {
153
+ workflow: readonly RenderBlockDecoration[];
154
+ comments: readonly RenderBlockDecoration[];
155
+ revisions: readonly RenderBlockDecoration[];
156
+ search: readonly RenderBlockDecoration[];
157
+ locked: readonly RenderBlockDecoration[];
158
+ }
159
+
160
+ export interface RenderAnchorIndex {
161
+ byRuntimeOffset(
162
+ offset: number,
163
+ story?: EditorStoryTarget,
164
+ ): RenderFrameRect | null;
165
+ byBlockId(blockId: string): RenderFrameRect | null;
166
+ byFragmentId(fragmentId: string): RenderFrameRect | null;
167
+ byPageIndex(pageIndex: number): RenderFrameRect | null;
168
+ /**
169
+ * Union rect covering a selection range. Resolves one rect per runtime
170
+ * offset and unions them, so chrome surfaces anchoring on selections
171
+ * (selection toolbar, suggestion card, comment thread tool) get a single
172
+ * tight rect without repeating the union math per consumer. Returns
173
+ * `null` when no offset in the range resolves to an anchor.
174
+ *
175
+ * Inclusive `fromOffset`, exclusive `toOffset` — matches the selection
176
+ * model used by `SelectionSnapshot`. When `fromOffset === toOffset`,
177
+ * falls back to `byRuntimeOffset(fromOffset)` so collapsed carets
178
+ * resolve to their line-box rect.
179
+ */
180
+ bySelection(
181
+ fromOffset: number,
182
+ toOffset: number,
183
+ story?: EditorStoryTarget,
184
+ ): RenderFrameRect | null;
185
+ /**
186
+ * Locate a table cell's rect given its owning table block id and logical
187
+ * (rowIndex, columnIndex). Reads from the attached `TableRenderPlan` on
188
+ * the render block so chrome tools (border picker, cell context bar)
189
+ * position without re-measuring the DOM. Returns `null` when the block
190
+ * is not a table, the plan is absent, or the indices fall outside the
191
+ * cell matrix.
192
+ */
193
+ byTableCell(
194
+ tableBlockId: string,
195
+ rowIndex: number,
196
+ columnIndex: number,
197
+ ): RenderFrameRect | null;
198
+ /**
199
+ * Locate the vertical grip at the right edge of `columnIndex` on a
200
+ * table. Returns a zero-width rect whose `leftPx` is the grip origin
201
+ * and whose `heightPx` spans the table's visible height on the page.
202
+ */
203
+ byTableColumnEdge(
204
+ tableBlockId: string,
205
+ columnIndex: number,
206
+ ): RenderFrameRect | null;
207
+ /**
208
+ * Locate the horizontal grip at the bottom edge of `rowIndex`. Returns
209
+ * a zero-height rect whose `topPx` is the grip origin and whose
210
+ * `widthPx` spans the table's visible width.
211
+ */
212
+ byTableRowEdge(
213
+ tableBlockId: string,
214
+ rowIndex: number,
215
+ ): RenderFrameRect | null;
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Queries + events
220
+ // ---------------------------------------------------------------------------
221
+
222
+ export interface RenderFrameQueryOptions {
223
+ story?: EditorStoryTarget;
224
+ pageRange?: { fromPageIndex: number; toPageIndex: number };
225
+ includeDecorations?: boolean;
226
+ }
227
+
228
+ export interface RenderAnchorQuery {
229
+ kind:
230
+ | "runtime-offset"
231
+ | "block-id"
232
+ | "fragment-id"
233
+ | "scope-id"
234
+ | "comment-id"
235
+ | "revision-id"
236
+ | "page-index";
237
+ value: string | number;
238
+ story?: EditorStoryTarget;
239
+ }
240
+
241
+ export interface RenderHitResult {
242
+ pageIndex: number;
243
+ regionKind: PublicPageRegion["kind"];
244
+ blockId: string;
245
+ fragmentId: string;
246
+ lineIndex: number;
247
+ runtimeOffset: number;
248
+ }
249
+
250
+ /**
251
+ * Point in the shell's overlay coordinate space (not the viewport, not
252
+ * the document CSS). Chrome surfaces compute this from a pointer event
253
+ * via `chrome-overlay-projector.ts` (same projector that maps rects out
254
+ * of the overlay) and pass it into `facet.hitTest`.
255
+ */
256
+ export interface RenderPoint {
257
+ xPx: number;
258
+ yPx: number;
259
+ }
260
+
261
+ export type RenderKernelEvent =
262
+ | { kind: "frame_built"; revision: number; reason: "full" | "incremental" }
263
+ | {
264
+ kind: "frame_diff";
265
+ revision: number;
266
+ pageRange: { fromPageIndex: number; toPageIndex: number };
267
+ }
268
+ | { kind: "decoration_resolved"; revision: number };
269
+
270
+ export interface DefaultZoomInput {
271
+ pxPerTwip?: number;
272
+ viewportWidthPx?: number;
273
+ fitMode?: RenderZoom["fitMode"];
274
+ }
275
+
276
+ /**
277
+ * Conservative default zoom: 96 dpi at 100% = 1 inch per 96 pixels, and
278
+ * 1 inch = 1440 twips, so 1 twip = 96/1440 ≈ 0.0667 px. Consumers pass a
279
+ * real zoom when they have a viewport; this keeps the kernel testable
280
+ * without a DOM.
281
+ */
282
+ export const DEFAULT_PX_PER_TWIP = 96 / 1440;
283
+
284
+ export function resolveDefaultZoom(input: DefaultZoomInput = {}): RenderZoom {
285
+ return {
286
+ pxPerTwip: input.pxPerTwip ?? DEFAULT_PX_PER_TWIP,
287
+ viewportWidthPx: input.viewportWidthPx ?? 900,
288
+ fitMode: input.fitMode ?? "fixed",
289
+ };
290
+ }
291
+
292
+ export const EMPTY_DECORATION_INDEX: DecorationIndex = Object.freeze({
293
+ workflow: Object.freeze([]) as readonly RenderBlockDecoration[],
294
+ comments: Object.freeze([]) as readonly RenderBlockDecoration[],
295
+ revisions: Object.freeze([]) as readonly RenderBlockDecoration[],
296
+ search: Object.freeze([]) as readonly RenderBlockDecoration[],
297
+ locked: Object.freeze([]) as readonly RenderBlockDecoration[],
298
+ });
299
+
300
+ /**
301
+ * Default chrome reservations. Ship Letter-compatible defaults so the
302
+ * kernel can be instantiated without a host injecting chrome sizes; chrome
303
+ * consumers override via `RenderPage.chromeReservations` once they know
304
+ * their own layout.
305
+ */
306
+ export function defaultChromeReservations(
307
+ layout: PageLayoutSnapshot,
308
+ zoom: RenderZoom,
309
+ ): PageChromeReservations {
310
+ return {
311
+ railLaneTwips: 360,
312
+ balloonLaneTwips: 2160,
313
+ footnoteAreaTwips: 0,
314
+ pageFrameWidthPx: layout.pageWidth * zoom.pxPerTwip,
315
+ pageFrameHeightPx: layout.pageHeight * zoom.pxPerTwip,
316
+ };
317
+ }