@beyondwork/docx-react-component 1.0.37 → 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 (74) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +319 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +815 -55
  6. package/src/io/export/serialize-main-document.ts +2 -11
  7. package/src/io/export/serialize-numbering.ts +1 -2
  8. package/src/io/export/serialize-tables.ts +74 -0
  9. package/src/io/export/table-properties-xml.ts +139 -4
  10. package/src/io/normalize/normalize-text.ts +15 -0
  11. package/src/io/ooxml/parse-footnotes.ts +60 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  13. package/src/io/ooxml/parse-main-document.ts +137 -0
  14. package/src/io/ooxml/parse-tables.ts +249 -0
  15. package/src/model/canonical-document.ts +34 -0
  16. package/src/runtime/document-layout.ts +4 -2
  17. package/src/runtime/document-navigation.ts +1 -1
  18. package/src/runtime/document-runtime.ts +114 -0
  19. package/src/runtime/layout/default-page-format.ts +96 -0
  20. package/src/runtime/layout/index.ts +45 -0
  21. package/src/runtime/layout/inert-layout-facet.ts +14 -0
  22. package/src/runtime/layout/layout-engine-instance.ts +33 -23
  23. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  24. package/src/runtime/layout/page-format-catalog.ts +233 -0
  25. package/src/runtime/layout/page-graph.ts +19 -0
  26. package/src/runtime/layout/paginated-layout-engine.ts +142 -9
  27. package/src/runtime/layout/project-block-fragments.ts +91 -0
  28. package/src/runtime/layout/public-facet.ts +709 -16
  29. package/src/runtime/layout/table-render-plan.ts +229 -0
  30. package/src/runtime/render/block-fragment-projection.ts +35 -0
  31. package/src/runtime/render/decoration-resolver.ts +189 -0
  32. package/src/runtime/render/index.ts +57 -0
  33. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  34. package/src/runtime/render/render-frame-types.ts +317 -0
  35. package/src/runtime/render/render-kernel.ts +755 -0
  36. package/src/runtime/view-state.ts +67 -0
  37. package/src/runtime/workflow-markup.ts +1 -5
  38. package/src/runtime/workflow-rail-segments.ts +280 -0
  39. package/src/ui/WordReviewEditor.tsx +84 -15
  40. package/src/ui/editor-shell-view.tsx +6 -0
  41. package/src/ui/headless/chrome-registry.ts +280 -14
  42. package/src/ui/headless/scoped-chrome-policy.ts +20 -1
  43. package/src/ui/headless/selection-tool-types.ts +10 -0
  44. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  45. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  46. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  47. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  48. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  49. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  52. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  53. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  54. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  55. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  56. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  57. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  58. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  59. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  60. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
  61. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  62. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
  63. package/src/ui-tailwind/index.ts +33 -0
  64. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  65. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  66. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  68. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  69. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  70. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  71. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  72. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  73. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
  74. package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Table render plan (P3e).
3
+ *
4
+ * Per `docs/reference/docx/runtime-rendering-and-chrome-phase.md` §1, the
5
+ * render kernel consumes a `TableRenderPlan` per table per page so the
6
+ * chrome can:
7
+ * - render band-aware cell styling without pre-flattening into inline
8
+ * CSS strings,
9
+ * - position column-resize grips at logical column edges without
10
+ * re-measuring the DOM,
11
+ * - show repeated header rows on continuation pages (when row-level
12
+ * pagination lands; today the field is empty),
13
+ * - walk vertical-merge chains for merged-cell hit-testing.
14
+ *
15
+ * The plan is a pure function of the table block + resolved style
16
+ * resolution + gridColumns + the effective page column width. It does
17
+ * not read DOM or mutate state.
18
+ */
19
+
20
+ import type {
21
+ SurfaceBlockSnapshot,
22
+ SurfaceTableCellSnapshot,
23
+ SurfaceTableRowSnapshot,
24
+ } from "../../api/public-types";
25
+ import type { TableStyleConditionalRegion } from "../../model/canonical-document.ts";
26
+ import type { ResolvedTableStyleResolution } from "../table-style-resolver.ts";
27
+
28
+ // ─── Public shapes ───────────────────────────────────────────────────────────
29
+
30
+ export type TableBandRegion = TableStyleConditionalRegion;
31
+
32
+ export interface TableCellBandAssignment {
33
+ rowIndex: number;
34
+ columnIndex: number;
35
+ regions: readonly TableBandRegion[];
36
+ }
37
+
38
+ export interface TableRowBandAssignment {
39
+ rowIndex: number;
40
+ regions: readonly TableBandRegion[];
41
+ }
42
+
43
+ export interface TableBandClasses {
44
+ /** Active regions per row (firstRow, lastRow, band1Horz, band2Horz). */
45
+ rows: readonly TableRowBandAssignment[];
46
+ /** Active regions per cell (row regions ∪ column regions). */
47
+ cells: readonly TableCellBandAssignment[];
48
+ }
49
+
50
+ export interface VerticalMergeRef {
51
+ /** Logical column the chain lives in. */
52
+ columnIndex: number;
53
+ /** Row where the chain starts (verticalMerge: "restart"). */
54
+ startRowIndex: number;
55
+ /** Inclusive row where the chain ends. */
56
+ endRowIndex: number;
57
+ /** Horizontal span of the merged cell. */
58
+ columnSpan: number;
59
+ }
60
+
61
+ export interface RepeatedHeaderRowRef {
62
+ /** Row index in the source table whose header is repeated. */
63
+ sourceRowIndex: number;
64
+ /** Virtual fragment id the render kernel uses to address this repeat. */
65
+ virtualFragmentId: string;
66
+ }
67
+
68
+ export interface ColumnResizeHandle {
69
+ /** Logical column index the handle sits at (0-based, right edge of column i). */
70
+ columnIndex: number;
71
+ /** X-origin of the handle in twips, measured from the table's left edge. */
72
+ originTwips: number;
73
+ /** Handle height in twips (table visual height). */
74
+ heightTwips: number;
75
+ }
76
+
77
+ export interface TableRenderPlan {
78
+ blockId: string;
79
+ pageIndex: number;
80
+ /** Logical column widths in twips (may be scaled from canonical). */
81
+ columnsTwips: readonly number[];
82
+ /** Band-class assignments derived from resolved table style. */
83
+ bandClasses: TableBandClasses;
84
+ /** Vertical-merge chains in the visible table range. */
85
+ verticalMerges: readonly VerticalMergeRef[];
86
+ /** Header rows repeated on this page (empty until row-level split lands). */
87
+ repeatedHeaderRows: readonly RepeatedHeaderRowRef[];
88
+ /** Column-resize handle origins for chrome grip placement. */
89
+ columnResizeHandles: readonly ColumnResizeHandle[];
90
+ }
91
+
92
+ // ─── Builder ────────────────────────────────────────────────────────────────
93
+
94
+ export interface BuildTableRenderPlanInput {
95
+ blockId: string;
96
+ pageIndex: number;
97
+ block: Extract<SurfaceBlockSnapshot, { kind: "table" }>;
98
+ resolved: ResolvedTableStyleResolution;
99
+ /** Total height of the table in twips (for grip geometry). */
100
+ tableHeightTwips: number;
101
+ /** If `columnsTwips` was scaled to the canvas width, pass the scale
102
+ * factor here; otherwise leave undefined. */
103
+ columnsTwipsScale?: number;
104
+ }
105
+
106
+ export function buildTableRenderPlan(
107
+ input: BuildTableRenderPlanInput,
108
+ ): TableRenderPlan {
109
+ const { blockId, pageIndex, block, resolved, tableHeightTwips } = input;
110
+ const scale = input.columnsTwipsScale ?? 1;
111
+ const columnsTwips = block.gridColumns.map((w) => Math.round(w * scale));
112
+
113
+ const bandClasses = buildBandClasses(block.rows, resolved);
114
+ const verticalMerges = collectVerticalMerges(block.rows);
115
+ const repeatedHeaderRows: RepeatedHeaderRowRef[] = [];
116
+ const columnResizeHandles = buildColumnResizeHandles(columnsTwips, tableHeightTwips);
117
+
118
+ return {
119
+ blockId,
120
+ pageIndex,
121
+ columnsTwips,
122
+ bandClasses,
123
+ verticalMerges,
124
+ repeatedHeaderRows,
125
+ columnResizeHandles,
126
+ };
127
+ }
128
+
129
+ // ─── Internals ───────────────────────────────────────────────────────────────
130
+
131
+ function buildBandClasses(
132
+ rows: readonly SurfaceTableRowSnapshot[],
133
+ resolved: ResolvedTableStyleResolution,
134
+ ): TableBandClasses {
135
+ const rowAssignments: TableRowBandAssignment[] = [];
136
+ const cellAssignments: TableCellBandAssignment[] = [];
137
+
138
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
139
+ const resolvedRow = resolved.rows[rowIndex];
140
+ const rowRegions = resolvedRow?.style.activeConditionalRegions ?? [];
141
+ rowAssignments.push({ rowIndex, regions: rowRegions });
142
+
143
+ const resolvedCells = resolvedRow?.cells ?? [];
144
+ const sourceRow = rows[rowIndex]!;
145
+ let columnCursor = sourceRow.gridBefore ?? 0;
146
+ for (let cellIndex = 0; cellIndex < sourceRow.cells.length; cellIndex += 1) {
147
+ const cell = sourceRow.cells[cellIndex]!;
148
+ const columnSpan = Math.max(1, cell.colspan ?? 1);
149
+ const resolvedCell = resolvedCells[cellIndex];
150
+ const regions = resolvedCell?.activeConditionalRegions ?? [];
151
+ cellAssignments.push({
152
+ rowIndex,
153
+ columnIndex: columnCursor,
154
+ regions,
155
+ });
156
+ columnCursor += columnSpan;
157
+ }
158
+ }
159
+
160
+ return { rows: rowAssignments, cells: cellAssignments };
161
+ }
162
+
163
+ function collectVerticalMerges(
164
+ rows: readonly SurfaceTableRowSnapshot[],
165
+ ): VerticalMergeRef[] {
166
+ const merges: VerticalMergeRef[] = [];
167
+ // Track active chains by column: maps startColumn → { startRow, span }.
168
+ const active = new Map<number, { startRow: number; span: number }>();
169
+
170
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
171
+ const row = rows[rowIndex]!;
172
+ let columnCursor = row.gridBefore ?? 0;
173
+ const seenThisRow = new Set<number>();
174
+
175
+ for (const cell of row.cells) {
176
+ const span = Math.max(1, cell.colspan ?? 1);
177
+ if (cell.verticalMerge === "restart") {
178
+ active.set(columnCursor, { startRow: rowIndex, span });
179
+ seenThisRow.add(columnCursor);
180
+ } else if (cell.verticalMerge === "continue") {
181
+ seenThisRow.add(columnCursor);
182
+ // chain stays open; ended rowIndex extends via the flush below
183
+ }
184
+ columnCursor += span;
185
+ }
186
+
187
+ // Flush chains that did NOT appear as a continue in this row.
188
+ for (const [startColumn, chain] of [...active.entries()]) {
189
+ if (!seenThisRow.has(startColumn)) {
190
+ merges.push({
191
+ columnIndex: startColumn,
192
+ startRowIndex: chain.startRow,
193
+ endRowIndex: rowIndex - 1,
194
+ columnSpan: chain.span,
195
+ });
196
+ active.delete(startColumn);
197
+ }
198
+ }
199
+ }
200
+
201
+ // Flush any still-open chains at end of table.
202
+ for (const [startColumn, chain] of active.entries()) {
203
+ merges.push({
204
+ columnIndex: startColumn,
205
+ startRowIndex: chain.startRow,
206
+ endRowIndex: rows.length - 1,
207
+ columnSpan: chain.span,
208
+ });
209
+ }
210
+
211
+ return merges;
212
+ }
213
+
214
+ function buildColumnResizeHandles(
215
+ columnsTwips: readonly number[],
216
+ heightTwips: number,
217
+ ): ColumnResizeHandle[] {
218
+ const handles: ColumnResizeHandle[] = [];
219
+ let cursor = 0;
220
+ for (let i = 0; i < columnsTwips.length - 1; i += 1) {
221
+ cursor += columnsTwips[i] ?? 0;
222
+ handles.push({
223
+ columnIndex: i,
224
+ originTwips: cursor,
225
+ heightTwips,
226
+ });
227
+ }
228
+ return handles;
229
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Block fragment projection (P4b).
3
+ *
4
+ * Classifies each `PublicBlockFragment` produced by the layout facet into
5
+ * a `RenderBlock["kind"]` so the render kernel's output is accurate across
6
+ * paragraph / table / opaque / synthetic blocks. Previously the kernel's
7
+ * `classifyBlockKind` stub always returned `"paragraph"`.
8
+ *
9
+ * The classification is driven by the deterministic blockId prefix scheme
10
+ * emitted by `src/runtime/surface-projection.ts`:
11
+ * - `paragraph-*` → "paragraph"
12
+ * - `table-*` → "table"
13
+ * - `opaque-*` → "opaque"
14
+ * - `section-break-*` → "opaque" (read-only boundary marker)
15
+ * - `sdt-*`, `sdt-wrapper-*` → "opaque"
16
+ * - `custom-xml-*` → "opaque"
17
+ * - `alt-chunk-*` → "opaque"
18
+ * - `synthetic-*` → "synthetic"
19
+ * Unknown prefixes fall back to `"paragraph"` so the render frame stays
20
+ * usable while a new surface node type is in development.
21
+ */
22
+
23
+ import type { RenderBlock } from "./render-frame-types.ts";
24
+
25
+ export function classifyBlockKind(blockId: string): RenderBlock["kind"] {
26
+ if (blockId.startsWith("paragraph")) return "paragraph";
27
+ if (blockId.startsWith("table")) return "table";
28
+ if (blockId.startsWith("opaque")) return "opaque";
29
+ if (blockId.startsWith("section-break")) return "opaque";
30
+ if (blockId.startsWith("sdt")) return "opaque";
31
+ if (blockId.startsWith("custom-xml")) return "opaque";
32
+ if (blockId.startsWith("alt-chunk")) return "opaque";
33
+ if (blockId.startsWith("synthetic")) return "synthetic";
34
+ return "paragraph";
35
+ }
@@ -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
+ }