@beyondwork/docx-react-component 1.0.103 → 1.0.105

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +66 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/_pe2-evidence.ts +153 -0
  5. package/src/api/v3/ai/bundle.ts +13 -5
  6. package/src/api/v3/ai/inspect.ts +7 -1
  7. package/src/api/v3/ai/outline.ts +2 -7
  8. package/src/api/v3/ai/replacement.ts +113 -0
  9. package/src/api/v3/runtime/geometry.ts +79 -0
  10. package/src/api/v3/ui/_types.ts +86 -0
  11. package/src/api/v3/ui/index.ts +5 -0
  12. package/src/api/v3/ui/overlays.ts +104 -0
  13. package/src/io/ooxml/parse-drawing.ts +99 -1
  14. package/src/io/ooxml/parse-fields.ts +27 -6
  15. package/src/io/ooxml/parse-shapes.ts +130 -0
  16. package/src/model/canonical-document.ts +34 -3
  17. package/src/model/canonical-layout-inputs.ts +979 -0
  18. package/src/model/layout/index.ts +9 -0
  19. package/src/model/layout/page-graph-types.ts +150 -0
  20. package/src/model/layout/runtime-page-graph-types.ts +23 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  22. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  23. package/src/runtime/document-runtime.ts +30 -14
  24. package/src/runtime/event-refresh-hints.ts +35 -5
  25. package/src/runtime/formatting/formatting-context.ts +110 -9
  26. package/src/runtime/formatting/index.ts +2 -0
  27. package/src/runtime/formatting/layout-inputs.ts +67 -3
  28. package/src/runtime/geometry/caret-geometry.ts +82 -10
  29. package/src/runtime/geometry/geometry-facet.ts +44 -0
  30. package/src/runtime/geometry/geometry-index.ts +1268 -0
  31. package/src/runtime/geometry/geometry-types.ts +227 -1
  32. package/src/runtime/geometry/index.ts +26 -0
  33. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  34. package/src/runtime/geometry/object-handles.ts +7 -4
  35. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  36. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  37. package/src/runtime/layout/layout-engine-version.ts +44 -1
  38. package/src/runtime/layout/page-graph.ts +877 -2
  39. package/src/runtime/layout/project-block-fragments.ts +101 -1
  40. package/src/runtime/layout/public-facet.ts +152 -0
  41. package/src/runtime/prerender/graph-canonicalize.ts +44 -0
  42. package/src/runtime/surface-projection.ts +43 -3
  43. package/src/runtime/workflow/coordinator.ts +57 -11
  44. package/src/ui/ui-controller-factory.ts +11 -0
  45. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
@@ -7,6 +7,10 @@ import type {
7
7
  FieldRegistryEntry,
8
8
  TabStop,
9
9
  } from "../../model/canonical-document.ts";
10
+ import {
11
+ MAIN_STORY_KEY,
12
+ type CanonicalFieldRegionIdentity,
13
+ } from "../../model/canonical-layout-inputs.ts";
10
14
  import type { ResolvedField } from "./field/resolver.ts";
11
15
  import type {
12
16
  EffectiveParagraphFormatting,
@@ -43,7 +47,11 @@ export interface NumberingLayoutInput {
43
47
 
44
48
  export interface FieldLayoutInput {
45
49
  readonly fieldId: string;
50
+ readonly regionId: string;
51
+ readonly regionKind: "field" | "toc-region";
52
+ readonly storyKey: string;
46
53
  readonly fieldIndex: number;
54
+ readonly paragraphIndex: number;
47
55
  readonly family: FieldFamily;
48
56
  readonly instruction: string;
49
57
  readonly cachedText: string;
@@ -58,6 +66,11 @@ export interface FieldLayoutInput {
58
66
 
59
67
  export interface RevisionLayoutPosture {
60
68
  readonly mode: "clean" | "simple" | "all" | "original" | "no-markup";
69
+ /**
70
+ * True only when the active revision display changes measured text
71
+ * for the paragraph. Paint-only redline posture is compositor data,
72
+ * not an L04 measurement input.
73
+ */
61
74
  readonly affectsMeasuredText: boolean;
62
75
  readonly hiddenRevisionIds: readonly string[];
63
76
  readonly visibleRevisionIds: readonly string[];
@@ -100,6 +113,27 @@ export function toLayoutTabStops(
100
113
  .sort((a, b) => a.positionTwips - b.positionTwips);
101
114
  }
102
115
 
116
+ export function mergeLayoutTabStops(
117
+ ...groups: readonly (readonly LayoutTabStopInput[] | undefined)[]
118
+ ): readonly LayoutTabStopInput[] {
119
+ const byKey = new Map<string, LayoutTabStopInput>();
120
+ for (const group of groups) {
121
+ for (const tabStop of group ?? []) {
122
+ const key = [
123
+ tabStop.source,
124
+ tabStop.positionTwips,
125
+ tabStop.align,
126
+ tabStop.leader ?? "",
127
+ ].join(":");
128
+ byKey.set(key, tabStop);
129
+ }
130
+ }
131
+ return [...byKey.values()].sort((a, b) => {
132
+ if (a.positionTwips !== b.positionTwips) return a.positionTwips - b.positionTwips;
133
+ return a.source.localeCompare(b.source);
134
+ });
135
+ }
136
+
103
137
  export function toNumberingLayoutInput(
104
138
  numbering: NumberingPrefixResult | null | undefined,
105
139
  ): NumberingLayoutInput | undefined {
@@ -138,10 +172,15 @@ export function toNumberingLayoutInput(
138
172
  export function toFieldLayoutInput(
139
173
  entry: FieldRegistryEntry,
140
174
  resolved?: ResolvedField,
175
+ region?: CanonicalFieldRegionIdentity,
141
176
  ): FieldLayoutInput {
142
177
  return {
143
178
  fieldId: `field-${entry.fieldIndex}`,
179
+ regionId: region?.regionId ?? `field:${entry.fieldIndex}`,
180
+ regionKind: region?.kind ?? "field",
181
+ storyKey: region?.storyKey ?? entry.storyKey ?? MAIN_STORY_KEY,
144
182
  fieldIndex: entry.fieldIndex,
183
+ paragraphIndex: region?.paragraphIndex ?? entry.paragraphIndex,
145
184
  family: entry.fieldFamily,
146
185
  instruction: entry.instruction,
147
186
  cachedText: entry.displayText,
@@ -156,20 +195,32 @@ export function toFieldLayoutInput(
156
195
  export function collectFieldLayoutInputs(
157
196
  registry: FieldRegistry | undefined,
158
197
  resolve?: (entry: FieldRegistryEntry) => ResolvedField | undefined,
198
+ regions: readonly CanonicalFieldRegionIdentity[] = [],
159
199
  ): readonly FieldLayoutInput[] {
160
200
  if (!registry) return [];
161
201
  const inputs: FieldLayoutInput[] = [];
162
202
  const entries = [...registry.supported, ...registry.preserveOnly];
203
+ const fieldRegions = new Map(
204
+ regions
205
+ .filter((region) => region.kind === "field")
206
+ .map((region) => [region.fieldIndex, region]),
207
+ );
208
+ const tocRegions = new Map(
209
+ regions
210
+ .filter((region) => region.kind === "toc-region" && region.tocId !== undefined)
211
+ .map((region) => [region.tocId!, region]),
212
+ );
163
213
  for (const entry of entries) {
164
- inputs.push(toFieldLayoutInput(entry, resolve?.(entry)));
214
+ inputs.push(toFieldLayoutInput(entry, resolve?.(entry), fieldRegions.get(entry.fieldIndex)));
165
215
  }
166
216
  for (const region of registry.tocRegions ?? []) {
167
217
  const source = registry.supported.find(
168
218
  (entry) => entry.fieldIndex === region.sourceFieldIndex,
169
219
  );
170
220
  if (!source) continue;
221
+ const identity = tocRegions.get(region.tocId);
171
222
  inputs.push({
172
- ...toFieldLayoutInput(source, resolve?.(source)),
223
+ ...toFieldLayoutInput(source, resolve?.(source), identity),
173
224
  fieldId: `toc-${region.tocId}`,
174
225
  family: "TOC",
175
226
  instruction: region.instruction.raw,
@@ -200,6 +251,18 @@ export function buildRevisionLayoutPosture(
200
251
  };
201
252
  }
202
253
 
254
+ export function toMeasuredRevisionLayoutPosture(
255
+ posture: RevisionLayoutPosture | undefined,
256
+ ): RevisionLayoutPosture | undefined {
257
+ if (!posture?.affectsMeasuredText || posture.hiddenRevisionIds.length === 0) {
258
+ return undefined;
259
+ }
260
+ return {
261
+ ...posture,
262
+ visibleRevisionIds: [],
263
+ };
264
+ }
265
+
203
266
  export function buildEffectiveLayoutFormatting(input: {
204
267
  readonly paragraph?: EffectiveParagraphFormatting;
205
268
  readonly runs?: readonly EffectiveRunFormatting[];
@@ -213,6 +276,7 @@ export function buildEffectiveLayoutFormatting(input: {
213
276
  readonly compatFlags?: readonly string[];
214
277
  readonly revisionPosture?: RevisionLayoutPosture;
215
278
  }): EffectiveLayoutFormatting {
279
+ const revisionPosture = toMeasuredRevisionLayoutPosture(input.revisionPosture);
216
280
  const withoutHash = {
217
281
  ...(input.paragraph ? { paragraph: input.paragraph } : {}),
218
282
  runs: input.runs ?? [],
@@ -221,7 +285,7 @@ export function buildEffectiveLayoutFormatting(input: {
221
285
  ...(input.numbering ? { numbering: input.numbering } : {}),
222
286
  tabs: input.tabs ?? [],
223
287
  compatFlags: input.compatFlags ?? [],
224
- ...(input.revisionPosture ? { revisionPosture: input.revisionPosture } : {}),
288
+ ...(revisionPosture ? { revisionPosture } : {}),
225
289
  };
226
290
  return {
227
291
  ...withoutHash,
@@ -37,6 +37,7 @@ import type { EditorStoryTarget } from "../../api/public-types";
37
37
  import type {
38
38
  RenderFrame,
39
39
  RenderFrameRect,
40
+ RenderStoryRegion,
40
41
  } from "../render/index.ts";
41
42
  import type {
42
43
  CaretGeometry,
@@ -86,12 +87,14 @@ export function resolveCaretGeometry(
86
87
  * Resolve `GeometryRect[]` for a selection range. Slice 4 produces:
87
88
  * - an empty list when either endpoint can't be resolved
88
89
  * - a single zero-width caret rect when `from === to`
90
+ * - one line rect per covered render line when line ranges are available
89
91
  * - a single union rect otherwise (via `frame.anchorIndex.bySelection`)
90
92
  *
91
- * When the kernel's per-line anchor projection matures (Slice 5+), this
92
- * function flips to emitting one rect per line. The single-rect fallback
93
- * keeps chrome surfaces that currently call `getAnchorRects({ kind:
94
- * "runtime-offset", … })[0]` in sync with the new selection API.
93
+ * The line-aware path still tags rects `within-tolerance`: without per-run
94
+ * anchors we can identify covered lines but not the first/last glyph bounds.
95
+ * The single-rect fallback keeps chrome surfaces that currently call
96
+ * `getAnchorRects({ kind: "runtime-offset", … })[0]` in sync with the
97
+ * selection API when a frame lacks line boxes.
95
98
  */
96
99
  export function resolveSelectionRects(
97
100
  frame: RenderFrame | null,
@@ -119,14 +122,11 @@ export function resolveSelectionRects(
119
122
  return [caret.rect];
120
123
  }
121
124
 
125
+ const lineRects = resolveLineSelectionRects(frame, lo, hi, range.story);
126
+ if (lineRects.length > 0) return lineRects;
127
+
122
128
  const union = frame.anchorIndex.bySelection(lo, hi, range.story);
123
129
  if (!union) return [];
124
- // Slice 7b (2026-04-22): the substrate returns ONE union rect for any
125
- // non-collapsed range. That rect is geometrically correct at the
126
- // block level but loses per-line detail — tag it `within-tolerance`
127
- // so callers can gate per-line chrome work. When the render kernel
128
- // ships per-run anchors (refactor/05 Slice 7 Task 2), this site
129
- // upgrades to multiple `"exact"` rects automatically.
130
130
  return [toGeometryRect(union, "within-tolerance")];
131
131
  }
132
132
 
@@ -151,6 +151,78 @@ function toGeometryRect(
151
151
  return out;
152
152
  }
153
153
 
154
+ function resolveLineSelectionRects(
155
+ frame: RenderFrame,
156
+ from: number,
157
+ to: number,
158
+ story?: EditorStoryTarget,
159
+ ): readonly GeometryRect[] {
160
+ if (!Array.isArray(frame.pages)) return [];
161
+ const rects: GeometryRect[] = [];
162
+ for (const page of frame.pages) {
163
+ for (const region of collectRegions(page.regions)) {
164
+ if (!storyMatches(region.storyTarget, story)) continue;
165
+ for (const block of region.blocks) {
166
+ const blockFrom = block.fragment.from;
167
+ const blockTo = block.fragment.to;
168
+ if (blockTo <= from || blockFrom >= to) continue;
169
+ if (block.lines.length === 0) continue;
170
+ const lineCount = block.lines.length;
171
+ const span = Math.max(1, blockTo - blockFrom);
172
+ for (let i = 0; i < lineCount; i += 1) {
173
+ const line = block.lines[i]!;
174
+ const lineFrom =
175
+ blockFrom + Math.floor((span * i) / Math.max(1, lineCount));
176
+ const lineTo =
177
+ i === lineCount - 1
178
+ ? blockTo
179
+ : blockFrom +
180
+ Math.floor((span * (i + 1)) / Math.max(1, lineCount));
181
+ if (lineTo <= from || lineFrom >= to) continue;
182
+ rects.push(toGeometryRect(line.frame, "within-tolerance"));
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return rects;
188
+ }
189
+
190
+ function collectRegions(
191
+ regions: RenderFrame["pages"][number]["regions"],
192
+ ): readonly RenderStoryRegion[] {
193
+ return [
194
+ regions.body,
195
+ regions.header,
196
+ regions.footer,
197
+ ...(regions.columns ?? []),
198
+ regions.footnoteArea,
199
+ ...(regions.footnotes ?? []),
200
+ ].filter((region): region is RenderStoryRegion => Boolean(region));
201
+ }
202
+
203
+ function storyMatches(
204
+ actual: EditorStoryTarget,
205
+ expected: EditorStoryTarget | undefined,
206
+ ): boolean {
207
+ if (!expected) return true;
208
+ if (actual.kind !== expected.kind) return false;
209
+ switch (expected.kind) {
210
+ case "main":
211
+ return true;
212
+ case "header":
213
+ case "footer":
214
+ return (
215
+ "relationshipId" in actual &&
216
+ actual.relationshipId === expected.relationshipId &&
217
+ actual.variant === expected.variant &&
218
+ actual.sectionIndex === expected.sectionIndex
219
+ );
220
+ case "footnote":
221
+ case "endnote":
222
+ return "noteId" in actual && actual.noteId === expected.noteId;
223
+ }
224
+ }
225
+
154
226
  /**
155
227
  * Approximate the baseline offset within a line rect. The render frame
156
228
  * does not yet carry per-anchor baseline metadata, so Slice 4 uses a
@@ -37,6 +37,8 @@ import type {
37
37
  BlockGeometry,
38
38
  CaretGeometry,
39
39
  EnvelopeBundle,
40
+ GeometryIndex,
41
+ GeometryIndexCoverage,
40
42
  GeometryRect,
41
43
  HitTestPoint,
42
44
  HitTestResult,
@@ -51,6 +53,11 @@ import {
51
53
  import { resolveHitTest } from "./hit-test.ts";
52
54
  import { resolveObjectHandles } from "./object-handles.ts";
53
55
  import { resolveAnchorRects } from "./project-anchors.ts";
56
+ import {
57
+ createUnavailableGeometryCoverage,
58
+ projectGeometryIndexFromFrame,
59
+ summarizeGeometryCoverageFromFrame,
60
+ } from "./geometry-index.ts";
54
61
  import {
55
62
  resolveReplacementEnvelope,
56
63
  type ReplacementScope,
@@ -67,6 +74,21 @@ import { createViewport, type ViewportHandle } from "./viewport.ts";
67
74
  // ---------------------------------------------------------------------------
68
75
 
69
76
  export interface GeometryFacet {
77
+ // PE2 index / coverage -------------------------------------------------
78
+ /**
79
+ * Renderer-neutral geometry index for the current frame. This is the PE2
80
+ * Slice-1 substrate: pages, regions, slices, lines, anchors, hit targets,
81
+ * and coverage metadata. Returns null when no render kernel/frame is warm.
82
+ */
83
+ getGeometryIndex(): GeometryIndex | null;
84
+
85
+ /**
86
+ * Lightweight coverage summary for evidence/debug consumers. Unlike
87
+ * `getGeometryIndex`, this always returns a value; unwired/pre-paint states
88
+ * report `status: "unavailable"` with zero counts.
89
+ */
90
+ getGeometryCoverage(): GeometryIndexCoverage;
91
+
70
92
  // Hit-test -------------------------------------------------------------
71
93
  /**
72
94
  * Resolve a point in the shell's overlay coordinate space to a canonical
@@ -226,6 +248,28 @@ export function createGeometryFacet(
226
248
  const viewport = input.viewport ?? createViewport();
227
249
 
228
250
  return {
251
+ getGeometryIndex() {
252
+ const kernel = input.renderKernel?.();
253
+ if (!kernel) return null;
254
+ return projectGeometryIndexFromFrame(kernel.getRenderFrame(), {
255
+ canonicalDocument: input.getCanonicalDocument?.() ?? null,
256
+ });
257
+ },
258
+
259
+ getGeometryCoverage() {
260
+ const kernel = input.renderKernel?.();
261
+ if (!kernel) return createUnavailableGeometryCoverage();
262
+ const frame = kernel.getRenderFrame();
263
+ const canonicalDocument = input.getCanonicalDocument?.() ?? null;
264
+ if (canonicalDocument) {
265
+ return (
266
+ projectGeometryIndexFromFrame(frame, { canonicalDocument })?.coverage ??
267
+ createUnavailableGeometryCoverage()
268
+ );
269
+ }
270
+ return summarizeGeometryCoverageFromFrame(frame);
271
+ },
272
+
229
273
  hitTest(point) {
230
274
  // Slice 6 wrapper-deletion (2026-04-22): the layout-facet fallback
231
275
  // was deleted. `hitTest` now resolves only through the render-kernel