@beyondwork/docx-react-component 1.0.37 → 1.0.39

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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -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 +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  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/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Resolve style chains for paragraph and character styles by walking the
3
+ * `basedOn` reference, with cycle detection.
4
+ *
5
+ * Returned chains are leaf-first (most-specific style at index 0, root at the
6
+ * end). Cycles break at the first re-encounter. Dangling `basedOn` references
7
+ * stop the walk silently (the leaf is still returned).
8
+ *
9
+ * Tasks 9 and 10 extend this module with `resolveEffective*Formatting`
10
+ * helpers that cascade the chain into a flat formatting record.
11
+ */
12
+
13
+ import type {
14
+ CanonicalParagraphFormatting,
15
+ CanonicalRunFormatting,
16
+ CharacterStyleDefinition,
17
+ ParagraphStyleDefinition,
18
+ StylesCatalog,
19
+ } from "../model/canonical-document.ts";
20
+
21
+ export function resolveParagraphStyleChain(
22
+ styleId: string,
23
+ catalog: StylesCatalog | undefined,
24
+ ): string[] {
25
+ const chain: string[] = [];
26
+ if (!catalog) return chain;
27
+ const visited = new Set<string>();
28
+ let current: string | undefined = styleId;
29
+ while (current && !visited.has(current)) {
30
+ visited.add(current);
31
+ const def: ParagraphStyleDefinition | undefined = catalog.paragraphs[current];
32
+ if (!def) break;
33
+ chain.push(current);
34
+ current = def.basedOn;
35
+ }
36
+ return chain;
37
+ }
38
+
39
+ export function resolveCharacterStyleChain(
40
+ styleId: string,
41
+ catalog: StylesCatalog | undefined,
42
+ ): string[] {
43
+ const chain: string[] = [];
44
+ if (!catalog) return chain;
45
+ const visited = new Set<string>();
46
+ let current: string | undefined = styleId;
47
+ while (current && !visited.has(current)) {
48
+ visited.add(current);
49
+ const def: CharacterStyleDefinition | undefined = catalog.characters[current];
50
+ if (!def) break;
51
+ chain.push(current);
52
+ current = def.basedOn;
53
+ }
54
+ return chain;
55
+ }
56
+
57
+ function mergeRun(
58
+ base: CanonicalRunFormatting | undefined,
59
+ over: CanonicalRunFormatting | undefined,
60
+ ): CanonicalRunFormatting | undefined {
61
+ if (!base && !over) return undefined;
62
+ return { ...(base ?? {}), ...(over ?? {}) };
63
+ }
64
+
65
+ function mergeParagraph(
66
+ base: CanonicalParagraphFormatting | undefined,
67
+ over: CanonicalParagraphFormatting | undefined,
68
+ ): CanonicalParagraphFormatting | undefined {
69
+ if (!base && !over) return undefined;
70
+ const merged: CanonicalParagraphFormatting = { ...(base ?? {}), ...(over ?? {}) };
71
+ if (base?.spacing || over?.spacing) {
72
+ merged.spacing = { ...(base?.spacing ?? {}), ...(over?.spacing ?? {}) };
73
+ }
74
+ if (base?.indentation || over?.indentation) {
75
+ merged.indentation = { ...(base?.indentation ?? {}), ...(over?.indentation ?? {}) };
76
+ }
77
+ if (base?.borders || over?.borders) {
78
+ merged.borders = { ...(base?.borders ?? {}), ...(over?.borders ?? {}) };
79
+ }
80
+ if (base?.shading || over?.shading) {
81
+ merged.shading = { ...(base?.shading ?? {}), ...(over?.shading ?? {}) };
82
+ }
83
+ if (base?.paragraphMarkRunProperties || over?.paragraphMarkRunProperties) {
84
+ merged.paragraphMarkRunProperties = mergeRun(
85
+ base?.paragraphMarkRunProperties,
86
+ over?.paragraphMarkRunProperties,
87
+ );
88
+ }
89
+ return merged;
90
+ }
91
+
92
+ export interface ParagraphResolveInput {
93
+ styleId: string | undefined;
94
+ direct: CanonicalParagraphFormatting | undefined;
95
+ }
96
+
97
+ /**
98
+ * Resolve paragraph formatting cascade: docDefaults.paragraph → basedOn chain
99
+ * (root-to-leaf) → direct. Returns `{}` when nothing is present.
100
+ */
101
+ export function resolveEffectiveParagraphFormatting(
102
+ input: ParagraphResolveInput,
103
+ catalog: StylesCatalog | undefined,
104
+ ): CanonicalParagraphFormatting {
105
+ let acc: CanonicalParagraphFormatting | undefined = catalog?.docDefaults?.paragraph;
106
+ if (catalog && input.styleId) {
107
+ const chain = resolveParagraphStyleChain(input.styleId, catalog);
108
+ // Walk root-to-leaf (most-general first) so the most-specific style wins
109
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
110
+ const styleId = chain[i]!;
111
+ acc = mergeParagraph(acc, catalog.paragraphs[styleId]?.paragraphProperties);
112
+ }
113
+ }
114
+ acc = mergeParagraph(acc, input.direct);
115
+ return acc ?? {};
116
+ }
117
+
118
+ export interface RunResolveInput {
119
+ paragraphStyleId: string | undefined;
120
+ characterStyleId: string | undefined;
121
+ direct: CanonicalRunFormatting | undefined;
122
+ }
123
+
124
+ /**
125
+ * Resolve run formatting cascade: docDefaults.run → paragraph-style-chain rPr
126
+ * (root-to-leaf) → character-style-chain (root-to-leaf) → direct. Returns
127
+ * `{}` when nothing is present.
128
+ */
129
+ export function resolveEffectiveRunFormatting(
130
+ input: RunResolveInput,
131
+ catalog: StylesCatalog | undefined,
132
+ ): CanonicalRunFormatting {
133
+ let acc: CanonicalRunFormatting | undefined = catalog?.docDefaults?.run;
134
+ if (catalog && input.paragraphStyleId) {
135
+ const chain = resolveParagraphStyleChain(input.paragraphStyleId, catalog);
136
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
137
+ const styleId = chain[i]!;
138
+ acc = mergeRun(acc, catalog.paragraphs[styleId]?.runProperties);
139
+ }
140
+ }
141
+ if (catalog && input.characterStyleId) {
142
+ const chain = resolveCharacterStyleChain(input.characterStyleId, catalog);
143
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
144
+ const styleId = chain[i]!;
145
+ acc = mergeRun(acc, catalog.characters[styleId]?.runProperties);
146
+ }
147
+ }
148
+ acc = mergeRun(acc, input.direct);
149
+ return acc ?? {};
150
+ }
151
+
152
+ export interface MarkerResolveInput {
153
+ paragraphStyleId: string | undefined;
154
+ levelRunProperties: CanonicalRunFormatting | undefined;
155
+ }
156
+
157
+ /**
158
+ * Resolve the numbering marker's character formatting — the rPr that styles
159
+ * the number/bullet glyph itself (bold, color, font, size, etc.).
160
+ *
161
+ * Cascade order (lowest to highest priority):
162
+ * 1. `docDefaults.run` — Word's baseline run formatting.
163
+ * 2. Paragraph style chain's `runProperties` — walked root-to-leaf; the
164
+ * numbered paragraph's styleId contributes each ancestor's rPr.
165
+ * 3. The leaf paragraph style's `paragraphProperties.paragraphMarkRunProperties`
166
+ * — Word stores the paragraph-mark rPr separately from the body rPr,
167
+ * and the mark's formatting is what the numbering marker inherits from.
168
+ * 4. `levelRunProperties` — `<w:lvl><w:rPr>` on the numbering level. Per
169
+ * ECMA-376, this is the most specific formatting for the marker and
170
+ * overrides the paragraph's run formatting entirely for the marker.
171
+ */
172
+ export function resolveNumberingMarkerRunFormatting(
173
+ input: MarkerResolveInput,
174
+ catalog: StylesCatalog | undefined,
175
+ ): CanonicalRunFormatting {
176
+ // Start with docDefaults baseline
177
+ let acc: CanonicalRunFormatting | undefined = catalog?.docDefaults?.run;
178
+
179
+ // Walk the paragraph style chain's rPr (root-to-leaf, most-specific wins)
180
+ if (catalog && input.paragraphStyleId) {
181
+ const chain = resolveParagraphStyleChain(input.paragraphStyleId, catalog);
182
+ for (let i = chain.length - 1; i >= 0; i -= 1) {
183
+ const styleId = chain[i]!;
184
+ acc = mergeRun(acc, catalog.paragraphs[styleId]?.runProperties);
185
+ }
186
+ // Leaf paragraph style's paragraph-mark rPr layers on top of the chain
187
+ const leaf = catalog.paragraphs[input.paragraphStyleId];
188
+ acc = mergeRun(acc, leaf?.paragraphProperties?.paragraphMarkRunProperties);
189
+ }
190
+
191
+ // Level rPr has the final say (per ECMA-376 17.9)
192
+ acc = mergeRun(acc, input.levelRunProperties);
193
+ return acc ?? {};
194
+ }
@@ -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
+ }