@beyondwork/docx-react-component 1.0.53 → 1.0.55

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 (99) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +125 -7
  3. package/src/index.ts +5 -0
  4. package/src/io/docx-session.ts +27 -3
  5. package/src/io/normalize/normalize-text.ts +1 -0
  6. package/src/io/ooxml/parse-field-switches.ts +134 -0
  7. package/src/io/ooxml/parse-fields.ts +28 -2
  8. package/src/model/canonical-document.ts +13 -2
  9. package/src/runtime/chart/chart-model-store.ts +88 -0
  10. package/src/runtime/chart/chart-snapshot.ts +239 -0
  11. package/src/runtime/collab/checkpoint-store.ts +1 -1
  12. package/src/runtime/collab/event-types.ts +4 -0
  13. package/src/runtime/collab/runtime-collab-sync.ts +1 -2
  14. package/src/runtime/document-runtime.ts +115 -13
  15. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  16. package/src/runtime/layout/layout-engine-version.ts +58 -1
  17. package/src/runtime/layout/layout-invalidation.ts +150 -30
  18. package/src/runtime/layout/page-graph.ts +19 -0
  19. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  20. package/src/runtime/layout/project-block-fragments.ts +27 -0
  21. package/src/runtime/layout/public-facet.ts +27 -0
  22. package/src/runtime/page-number-format.ts +207 -0
  23. package/src/runtime/render/render-frame-diff.ts +38 -2
  24. package/src/runtime/surface-projection.ts +32 -3
  25. package/src/ui/WordReviewEditor.tsx +57 -3
  26. package/src/ui/headless/comment-decoration-model.ts +60 -5
  27. package/src/ui/headless/revision-decoration-model.ts +94 -6
  28. package/src/ui/shared/revision-filters.ts +16 -6
  29. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  30. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  31. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  32. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  33. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  34. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  35. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  36. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  37. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  38. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  39. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  40. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  41. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  42. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  43. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  44. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  45. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  46. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  47. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  48. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  49. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  50. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  51. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  52. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  53. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  54. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  55. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  56. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  57. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  58. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  59. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  60. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  61. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  62. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  63. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  64. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  65. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  66. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  67. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  68. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  69. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  70. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  71. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  72. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  73. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  74. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  75. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
  76. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  77. package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
  78. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
  79. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  80. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
  81. package/src/ui-tailwind/index.ts +11 -0
  82. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
  83. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
  84. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
  85. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  86. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  87. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  88. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  89. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  90. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  91. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  92. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  93. package/src/ui-tailwind/theme/editor-theme.css +249 -22
  94. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  95. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  96. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  97. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  98. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  99. package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Public read-model for chart data. Projected from a `ChartModel` parsed during
3
+ * DOCX import. Agents consume this without re-parsing `rawXml`.
4
+ */
5
+
6
+ import type {
7
+ ChartModel,
8
+ BarChartModel,
9
+ LineChartModel,
10
+ PieChartModel,
11
+ AreaChartModel,
12
+ ScatterChartModel,
13
+ BubbleChartModel,
14
+ ComboChartModel,
15
+ } from "../../io/ooxml/chart/types.ts";
16
+
17
+ export interface ChartSnapshot {
18
+ chartId: string;
19
+ kind: ChartModel["kind"];
20
+ title?: string;
21
+ seriesCount: number;
22
+ categoryCount: number;
23
+ data: ChartSnapshotData;
24
+ }
25
+
26
+ export type ChartSnapshotData =
27
+ | {
28
+ kind: "bar";
29
+ direction: "bar" | "column";
30
+ grouping: "clustered" | "stacked" | "percentStacked" | "standard";
31
+ series: ChartSnapshotSeries[];
32
+ categories: string[];
33
+ }
34
+ | {
35
+ kind: "line";
36
+ grouping: "standard" | "stacked" | "percentStacked";
37
+ series: ChartSnapshotSeries[];
38
+ categories: string[];
39
+ }
40
+ | {
41
+ kind: "pie";
42
+ doughnut: boolean;
43
+ series: ChartSnapshotSeries[];
44
+ categories: string[];
45
+ }
46
+ | {
47
+ kind: "area";
48
+ grouping: "standard" | "stacked" | "percentStacked";
49
+ series: ChartSnapshotSeries[];
50
+ categories: string[];
51
+ }
52
+ | { kind: "scatter"; series: ChartSnapshotScatterSeries[] }
53
+ | { kind: "bubble"; series: ChartSnapshotBubbleSeries[] }
54
+ | {
55
+ kind: "combo";
56
+ groups: Array<{ kind: "bar" | "line" | "area"; series: ChartSnapshotSeries[] }>;
57
+ categories: string[];
58
+ }
59
+ | { kind: "unsupported"; reason: string };
60
+
61
+ export interface ChartSnapshotSeries {
62
+ name?: string;
63
+ values: Array<number | null>;
64
+ }
65
+
66
+ export interface ChartSnapshotScatterSeries {
67
+ name?: string;
68
+ xValues: Array<number | null>;
69
+ yValues: Array<number | null>;
70
+ }
71
+
72
+ export interface ChartSnapshotBubbleSeries {
73
+ name?: string;
74
+ xValues: Array<number | null>;
75
+ yValues: Array<number | null>;
76
+ sizes: Array<number | null>;
77
+ }
78
+
79
+ export function projectChartSnapshot(chartId: string, model: ChartModel): ChartSnapshot {
80
+ switch (model.kind) {
81
+ case "bar": {
82
+ const m = model as BarChartModel;
83
+ const categories = m.categoryAxis.kind === "category"
84
+ ? m.categoryAxis.categoryLabels
85
+ : (m.series[0]?.categories ?? []);
86
+ return {
87
+ chartId,
88
+ kind: "bar",
89
+ title: m.title?.text,
90
+ seriesCount: m.series.length,
91
+ categoryCount: categories.length,
92
+ data: {
93
+ kind: "bar",
94
+ direction: m.direction,
95
+ grouping: m.grouping,
96
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
97
+ categories,
98
+ },
99
+ };
100
+ }
101
+ case "line": {
102
+ const m = model as LineChartModel;
103
+ const categories = m.categoryAxis.kind === "category"
104
+ ? m.categoryAxis.categoryLabels
105
+ : (m.series[0]?.categories ?? []);
106
+ return {
107
+ chartId,
108
+ kind: "line",
109
+ title: m.title?.text,
110
+ seriesCount: m.series.length,
111
+ categoryCount: categories.length,
112
+ data: {
113
+ kind: "line",
114
+ grouping: m.grouping,
115
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
116
+ categories,
117
+ },
118
+ };
119
+ }
120
+ case "pie": {
121
+ const m = model as PieChartModel;
122
+ return {
123
+ chartId,
124
+ kind: "pie",
125
+ title: m.title?.text,
126
+ seriesCount: m.series.length,
127
+ categoryCount: m.categoryLabels.length,
128
+ data: {
129
+ kind: "pie",
130
+ doughnut: m.doughnut,
131
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
132
+ categories: m.categoryLabels,
133
+ },
134
+ };
135
+ }
136
+ case "area": {
137
+ const m = model as AreaChartModel;
138
+ const categories = m.categoryAxis.kind === "category"
139
+ ? m.categoryAxis.categoryLabels
140
+ : (m.series[0]?.categories ?? []);
141
+ return {
142
+ chartId,
143
+ kind: "area",
144
+ title: m.title?.text,
145
+ seriesCount: m.series.length,
146
+ categoryCount: categories.length,
147
+ data: {
148
+ kind: "area",
149
+ grouping: m.grouping,
150
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
151
+ categories,
152
+ },
153
+ };
154
+ }
155
+ case "scatter": {
156
+ const m = model as ScatterChartModel;
157
+ return {
158
+ chartId,
159
+ kind: "scatter",
160
+ seriesCount: m.series.length,
161
+ categoryCount: 0,
162
+ data: {
163
+ kind: "scatter",
164
+ series: m.series.map((s) => ({
165
+ name: s.name,
166
+ xValues: s.xValues,
167
+ yValues: s.yValues,
168
+ })),
169
+ },
170
+ };
171
+ }
172
+ case "bubble": {
173
+ const m = model as BubbleChartModel;
174
+ return {
175
+ chartId,
176
+ kind: "bubble",
177
+ seriesCount: m.series.length,
178
+ categoryCount: 0,
179
+ data: {
180
+ kind: "bubble",
181
+ series: m.series.map((s) => ({
182
+ name: s.name,
183
+ xValues: s.xValues,
184
+ yValues: s.yValues,
185
+ sizes: s.sizes,
186
+ })),
187
+ },
188
+ };
189
+ }
190
+ case "combo": {
191
+ const m = model as ComboChartModel;
192
+ const firstGroup = m.groups[0];
193
+ const categories =
194
+ firstGroup && "categoryAxis" in firstGroup &&
195
+ firstGroup.categoryAxis?.kind === "category"
196
+ ? firstGroup.categoryAxis.categoryLabels
197
+ : [];
198
+ return {
199
+ chartId,
200
+ kind: "combo",
201
+ title: m.title?.text,
202
+ seriesCount: m.groups.reduce((acc, g) => acc + g.series.length, 0),
203
+ categoryCount: categories.length,
204
+ data: {
205
+ kind: "combo",
206
+ groups: m.groups.map((g) => ({
207
+ kind: g.kind as "bar" | "line" | "area",
208
+ series: "series" in g
209
+ ? g.series.map((s) => ({
210
+ name: s.name,
211
+ values: "values" in s ? s.values : [],
212
+ }))
213
+ : [],
214
+ })),
215
+ categories,
216
+ },
217
+ };
218
+ }
219
+ case "unsupported": {
220
+ return {
221
+ chartId,
222
+ kind: "unsupported",
223
+ seriesCount: 0,
224
+ categoryCount: 0,
225
+ data: { kind: "unsupported", reason: model.detail },
226
+ };
227
+ }
228
+ default: {
229
+ const _exhaustive: never = model;
230
+ return {
231
+ chartId,
232
+ kind: (_exhaustive as ChartModel).kind,
233
+ seriesCount: 0,
234
+ categoryCount: 0,
235
+ data: { kind: "unsupported", reason: "unknown chart kind" },
236
+ };
237
+ }
238
+ }
239
+ }
@@ -30,7 +30,7 @@ export interface Checkpoint {
30
30
  authorClientId: number;
31
31
  }
32
32
 
33
- const CHECKPOINTS_KEY = "checkpoints";
33
+ export const CHECKPOINTS_KEY = "checkpoints";
34
34
 
35
35
  export interface CreateCheckpointStoreOptions {
36
36
  ydoc: Y.Doc;
@@ -141,6 +141,10 @@ export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new S
141
141
  "section.set-header-footer-link",
142
142
  "content.insert-page-break",
143
143
  "content.insert-table",
144
+ // C1: Shift+Tab list/paragraph de-indent — produces a document mutation, must broadcast
145
+ "text.outdent-tab",
146
+ // C2: host insertFragment() API — routes through executeEditorCommand same as other mutations
147
+ "fragment.insert",
144
148
  ]);
145
149
 
146
150
  /**
@@ -22,7 +22,7 @@ import {
22
22
  type CommandEvent,
23
23
  } from "./event-types.ts";
24
24
  import { computeBaseDocFingerprint } from "./base-doc-fingerprint.ts";
25
- import type { Checkpoint } from "./checkpoint-store.ts";
25
+ import { CHECKPOINTS_KEY, type Checkpoint } from "./checkpoint-store.ts";
26
26
  import { createWorkflowShared, type WorkflowSharedHandle } from "./workflow-shared.ts";
27
27
 
28
28
  /** Shared Y.Map key — {@link SHARED_META_MAP_KEY}. */
@@ -30,7 +30,6 @@ const SHARED_META_MAP_KEY = "meta";
30
30
  const META_BASE_DOC_HASH_KEY = "baseDocHash";
31
31
  const META_SCHEMA_VERSION_KEY = "schemaVersion";
32
32
  const META_CREATED_AT_KEY = "createdAt";
33
- const CHECKPOINTS_KEY = "checkpoints";
34
33
 
35
34
  /**
36
35
  * Lifecycle + correctness events surfaced by a
@@ -242,6 +242,7 @@ import type {
242
242
  import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
243
243
  import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
244
244
  import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
245
+ import { formatPageNumber } from "./page-number-format.ts";
245
246
 
246
247
  /** Internal extension of ExportDocxOptions that threads the collected
247
248
  * editorState payload from the runtime to the docx serializer. */
@@ -5196,8 +5197,34 @@ function refreshDocumentTableOfContents(
5196
5197
  protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
5197
5198
  } {
5198
5199
  const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
5200
+ // Build a single O(N) map from paragraph offset → bookmark name so the
5201
+ // per-heading lookup below is O(1) instead of O(N) per heading.
5202
+ const bookmarkNameByOffset = new Map<number, string>();
5203
+ {
5204
+ let runningOffset = 0;
5205
+ for (const block of document.content.children) {
5206
+ if (block.type !== "paragraph") {
5207
+ continue;
5208
+ }
5209
+ const paragraphStart = runningOffset;
5210
+ for (const child of block.children) {
5211
+ if (child.type === "text") {
5212
+ runningOffset += child.text.length;
5213
+ } else if (child.type === "tab" || child.type === "hard_break") {
5214
+ runningOffset += 1;
5215
+ }
5216
+ }
5217
+ const bookmarkStart = block.children.find(
5218
+ (child): child is Extract<typeof child, { type: "bookmark_start" }> =>
5219
+ child.type === "bookmark_start" && Boolean(child.name),
5220
+ );
5221
+ if (bookmarkStart?.name) {
5222
+ bookmarkNameByOffset.set(paragraphStart, bookmarkStart.name);
5223
+ }
5224
+ }
5225
+ }
5199
5226
  let changed = false;
5200
- let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
5227
+ let resultEntries: Array<{ level: number; text: string; pageIndex: number; bookmarkName?: string }> = [];
5201
5228
  let changedFrom: number | undefined;
5202
5229
  let changedTo: number | undefined;
5203
5230
  const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
@@ -5209,11 +5236,15 @@ function refreshDocumentTableOfContents(
5209
5236
  : parseTocLevelRange(field.instruction);
5210
5237
  const entries = navigation.headings
5211
5238
  .filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
5212
- .map((heading) => ({
5213
- level: heading.level,
5214
- text: heading.text,
5215
- pageIndex: heading.pageIndex,
5216
- }));
5239
+ .map((heading) => {
5240
+ const bookmarkName = bookmarkNameByOffset.get(heading.offset);
5241
+ return {
5242
+ level: heading.level,
5243
+ text: heading.text,
5244
+ pageIndex: heading.pageIndex,
5245
+ ...(bookmarkName ? { bookmarkName } : {}),
5246
+ };
5247
+ });
5217
5248
  if (resultEntries.length === 0) {
5218
5249
  resultEntries = entries;
5219
5250
  }
@@ -5410,12 +5441,20 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
5410
5441
  * resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
5411
5442
  */
5412
5443
  function buildTocInlineNodes(
5413
- entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
5444
+ entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
5414
5445
  resolveDisplayPageNumber?: (pageIndex: number) => number | null,
5415
5446
  ): InlineNode[] {
5416
5447
  const children: InlineNode[] = [];
5417
5448
  entries.forEach((entry, index) => {
5418
- children.push({ type: "text", text: entry.text });
5449
+ if (entry.bookmarkName) {
5450
+ children.push({
5451
+ type: "hyperlink",
5452
+ href: `#${entry.bookmarkName}`,
5453
+ children: [{ type: "text", text: entry.text }],
5454
+ });
5455
+ } else {
5456
+ children.push({ type: "text", text: entry.text });
5457
+ }
5419
5458
  children.push({ type: "tab" });
5420
5459
  const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
5421
5460
  children.push({
@@ -5431,7 +5470,7 @@ function buildTocInlineNodes(
5431
5470
 
5432
5471
  /** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
5433
5472
  export function __buildTocInlineNodes(
5434
- entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
5473
+ entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
5435
5474
  resolveDisplayPageNumber?: (pageIndex: number) => number | null,
5436
5475
  ): InlineNode[] {
5437
5476
  return buildTocInlineNodes(entries, resolveDisplayPageNumber);
@@ -5471,6 +5510,38 @@ function collectFieldsFromSubParts(
5471
5510
  return nextIndex;
5472
5511
  }
5473
5512
 
5513
+ function resolveStyleRefFieldText(
5514
+ styleQuery: string,
5515
+ paragraphs: readonly ParagraphContext[],
5516
+ styles: CanonicalDocumentEnvelope["styles"],
5517
+ ): { text: string; refreshStatus: FieldRefreshStatus } {
5518
+ const normalized = styleQuery.trim().toLowerCase();
5519
+
5520
+ // Look up styleId: first by direct id match, then by displayName
5521
+ const styleId = (() => {
5522
+ const byId = Object.keys(styles.paragraphs).find(
5523
+ (id) => id.toLowerCase() === normalized,
5524
+ );
5525
+ if (byId) return byId;
5526
+ const byName = Object.values(styles.paragraphs).find(
5527
+ (s) => s.displayName?.toLowerCase() === normalized,
5528
+ );
5529
+ return byName?.styleId;
5530
+ })();
5531
+
5532
+ if (!styleId) return { text: "", refreshStatus: "unresolvable" };
5533
+
5534
+ // Walk paragraphs top-down for the first matching styleId
5535
+ for (const ctx of paragraphs) {
5536
+ if (ctx.paragraph.styleId === styleId) {
5537
+ const text = flattenInlineDisplayText(ctx.paragraph.children);
5538
+ if (text.trim()) return { text: text.trim(), refreshStatus: "current" };
5539
+ }
5540
+ }
5541
+
5542
+ return { text: "", refreshStatus: "unresolvable" };
5543
+ }
5544
+
5474
5545
  function resolveSupportedFieldDisplay(
5475
5546
  field: FieldNode,
5476
5547
  document: CanonicalDocumentEnvelope,
@@ -5485,13 +5556,30 @@ function resolveSupportedFieldDisplay(
5485
5556
  if (field.fieldFamily === "TOC") {
5486
5557
  return undefined;
5487
5558
  }
5559
+ if (field.fieldFamily === "STYLEREF") {
5560
+ if (!field.fieldTarget) return { displayText: "", refreshStatus: "unresolvable" };
5561
+ const result = resolveStyleRefFieldText(field.fieldTarget, paragraphs, document.styles);
5562
+ return { displayText: result.text, refreshStatus: result.refreshStatus };
5563
+ }
5564
+ if (field.fieldFamily === "SECTIONPAGES") {
5565
+ const sectionIndex = "sectionIndex" in storyTarget && typeof storyTarget.sectionIndex === "number"
5566
+ ? storyTarget.sectionIndex
5567
+ : navigation.activeSectionIndex;
5568
+ const sectionPages = navigation.pages.filter((p) => p.sectionIndex === sectionIndex);
5569
+ if (sectionPages.length === 0) return { displayText: "", refreshStatus: "unresolvable" };
5570
+ const fmt = sectionPages[0]?.layout.pageNumbering?.format;
5571
+ return {
5572
+ displayText: formatPageNumber(sectionPages.length, fmt),
5573
+ refreshStatus: "current",
5574
+ };
5575
+ }
5488
5576
  if (field.fieldFamily === "PAGE") {
5489
5577
  const page = resolveRepresentativePageForStory(navigation, storyTarget);
5490
5578
  if (!page) {
5491
5579
  return { displayText: "", refreshStatus: "unresolvable" };
5492
5580
  }
5493
5581
  return {
5494
- displayText: String(resolveDisplayedPageNumber(page)),
5582
+ displayText: resolveDisplayedPageNumber(page),
5495
5583
  refreshStatus: "current",
5496
5584
  };
5497
5585
  }
@@ -5522,10 +5610,23 @@ function resolveSupportedFieldDisplay(
5522
5610
  if (!paragraph) {
5523
5611
  return { displayText: "", refreshStatus: "unresolvable" };
5524
5612
  }
5613
+
5614
+ // \p switch: emit relative position text ("above" / "below" / "on this page")
5615
+ if (field.switches?.relativePosition === true) {
5616
+ const fieldPage = resolveRepresentativePageForStory(navigation, storyTarget);
5617
+ const targetPageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
5618
+ const fieldPageIndex = fieldPage
5619
+ ? navigation.pages.indexOf(fieldPage)
5620
+ : navigation.activePageIndex;
5621
+ if (targetPageIndex < fieldPageIndex) return { displayText: "above", refreshStatus: "current" };
5622
+ if (targetPageIndex > fieldPageIndex) return { displayText: "below", refreshStatus: "current" };
5623
+ return { displayText: "on this page", refreshStatus: "current" };
5624
+ }
5625
+
5525
5626
  const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
5526
5627
  const page = navigation.pages[pageIndex] ?? navigation.pages[0];
5527
5628
  return page
5528
- ? { displayText: String(resolveDisplayedPageNumber(page)), refreshStatus: "current" }
5629
+ ? { displayText: resolveDisplayedPageNumber(page), refreshStatus: "current" }
5529
5630
  : { displayText: "", refreshStatus: "unresolvable" };
5530
5631
  }
5531
5632
  if (field.fieldFamily === "NOTEREF") {
@@ -5584,8 +5685,9 @@ function isDefaultHeaderFooterPage(
5584
5685
 
5585
5686
  function resolveDisplayedPageNumber(
5586
5687
  page: DocumentNavigationSnapshot["pages"][number],
5587
- ): number {
5588
- return (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
5688
+ ): string {
5689
+ const n = (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
5690
+ return formatPageNumber(n, page.layout.pageNumbering?.format);
5589
5691
  }
5590
5692
 
5591
5693
  interface ParagraphContext {
@@ -59,6 +59,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
59
59
  swapMeasurementProvider: () => undefined,
60
60
  invalidateMeasurementCache: () => undefined,
61
61
  getTableRenderPlan: () => null,
62
+ getTableBodyYOffsetOnPage: () => null,
62
63
  getDirtyFieldFamilies: () => [],
63
64
  getFieldDirtinessReport: () => emptyReport,
64
65
  setVisibleBlockRange: () => undefined,
@@ -113,6 +113,13 @@
113
113
  * pages so chrome can prepend header rows visually. No
114
114
  * pixel-geometry change; cache envelopes from v11 invalidate
115
115
  * because the table-render-plan contract changed.
116
+ * 13 — Lane 6d.U2 canvas-seam pill polish: the canvas-posture page-break
117
+ * widget's "N / M" badge is promoted from transparent text over the
118
+ * dotted seam to a true pill with `--radius-pill` geometry, hairline
119
+ * `--color-border-default` border, and `--shadow-soft`. Widget DOM
120
+ * shape changed (new `data-variant="pill"` attribute; additional
121
+ * inline style declarations on the badge). Cache envelopes from v12
122
+ * invalidate because the decoration's cacheable DOM shape changed.
116
123
  * 13 — Lane 3a P14.c: render-kernel gains a single-slot `DecorationIndex`
117
124
  * cache keyed on (revision, activeStory.kind, zoom.pxPerTwip, and
118
125
  * reference equality on each decoration source). When layout
@@ -122,8 +129,58 @@
122
129
  * rebuild path (on every keystroke that triggers a layout event).
123
130
  * No pixel-geometry change; cache envelopes from v12 invalidate
124
131
  * because the render-kernel source changed.
132
+ * 14 — Lane 3a Slice 5: `RuntimeBlockFragment` gains `resolvedStyleChainRef`
133
+ * (block's styleId) and `numberingInstanceId` (block's list-instance id).
134
+ * `analyzeInvalidation` for `styles-change` (when `dirtyStyleIds` is
135
+ * supplied) and `numbering-change` (when `numberingInstanceId` is
136
+ * supplied) now return `scope: "bounded"` starting from the first page
137
+ * whose fragments reference the dirty style / instance. Fallback to
138
+ * `scope: "full"` when payload is absent or no match found. No
139
+ * pixel-geometry change; cache envelopes from v13 invalidate because
140
+ * the fragment shape and invalidation-scope contract changed.
141
+ * 15 — Bug fixes: `pageNodesStructurallyEqual` now compares
142
+ * `lineBoxes.length` and `noteAllocations.length` as structural
143
+ * proxies to prevent stale-node reuse when line geometry changes
144
+ * with stable fragment IDs (L1). `analyzeSectionChange` normalizes
145
+ * `dirtySectionRange` to guarantee from ≤ to for all graph states
146
+ * including empty-sections fallback (L2).
147
+ * 16 — Bug fixes: `diffRenderFrames` now flags pages whose physical frame
148
+ * changed (but block regions are stable) with `pageFrameChanged: true`
149
+ * in `changedPages` so consumers can re-project without a block-region
150
+ * signal (R1). Chrome reservation changes (`railLaneTwips`,
151
+ * `balloonLaneTwips`, `footnoteAreaTwips`, `pageFrameWidthPx`,
152
+ * `pageFrameHeightPx`) now trigger `changedPages` so overlay
153
+ * re-projection is not silently skipped (R2).
154
+ * 17 — Lane 3a Slice 2 + R4: `WordReviewEditorLayoutFacet` gains
155
+ * `getTableBodyYOffsetOnPage(blockId, pageIndex)` which returns the
156
+ * Y offset (in twips from body top) of the table's first fragment on
157
+ * a given page by summing prior body-fragment heights. Used by the
158
+ * new `TwTableContinuationHeader` chrome overlay to position repeated
159
+ * header rows on continuation pages of multi-page tables — no DOM
160
+ * measurement, layout-engine fragment heights only. No cached-geometry
161
+ * change; cache envelopes from v16 invalidate because the facet
162
+ * interface changed.
163
+ * 18 — Lane 3a Slice 6: `buildPageStackFromWithSplits` no longer discards
164
+ * `resumeAt.startOffset`. When `startOffset > 0` and no block
165
+ * straddles the dirty section boundary, only sections at and after
166
+ * the first dirty section are paginated; the resulting page indices
167
+ * are shifted by `startPageIndex` so they align with the global graph.
168
+ * Full-paginate + tail-slice fallback used when a block straddles the
169
+ * section boundary (safety guard). This eliminates re-paginating
170
+ * settled head sections on every bounded-invalidation relayout.
171
+ * No pixel-geometry change; cache envelopes from v17 invalidate
172
+ * because `buildPageStackFromWithSplits` output contract changed.
173
+ * 19 — Slice 5 bug-fix: `analyzeNumberingChange` now honors its own
174
+ * "Fallback to full rebuild when absent or no match" contract. When
175
+ * `numberingInstanceId` is supplied but no materialized fragment
176
+ * matches it, the analyzer returns `scope: "full"` +
177
+ * `requiresFullRecompute: true` instead of the prior "bounded over
178
+ * full range" shortcut, which bypassed the safety guard and could
179
+ * leak stale field-family projections. No pixel-geometry change;
180
+ * cache envelopes from v18 invalidate because the invalidation
181
+ * classifier's contract corrected.
125
182
  */
126
- export const LAYOUT_ENGINE_VERSION = 13 as const;
183
+ export const LAYOUT_ENGINE_VERSION = 19 as const;
127
184
 
128
185
  /**
129
186
  * Serialization schema version for the LayCache payload (the cache envelope