@beyondwork/docx-react-component 1.0.105 → 1.0.108

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 (193) hide show
  1. package/package.json +19 -5
  2. package/src/api/geometry-overlay-rects.ts +5 -0
  3. package/src/api/package-version.ts +1 -1
  4. package/src/api/page-anchor-id.ts +5 -0
  5. package/src/api/public-types.ts +16 -9
  6. package/src/api/table-node-specs.ts +6 -0
  7. package/src/api/v3/_create.ts +10 -2
  8. package/src/api/v3/_page-anchor-id.ts +52 -0
  9. package/src/api/v3/_runtime-handle.ts +92 -1
  10. package/src/api/v3/ai/_audit-reference.ts +28 -0
  11. package/src/api/v3/ai/_audit-time.ts +5 -0
  12. package/src/api/v3/ai/_pe2-evidence.ts +310 -6
  13. package/src/api/v3/ai/attach.ts +29 -4
  14. package/src/api/v3/ai/bundle.ts +6 -2
  15. package/src/api/v3/ai/inspect.ts +6 -2
  16. package/src/api/v3/ai/replacement.ts +112 -18
  17. package/src/api/v3/ai/resolve.ts +2 -2
  18. package/src/api/v3/ai/review.ts +177 -3
  19. package/src/api/v3/index.ts +8 -0
  20. package/src/api/v3/runtime/collab.ts +462 -0
  21. package/src/api/v3/runtime/document.ts +503 -20
  22. package/src/api/v3/runtime/geometry.ts +97 -0
  23. package/src/api/v3/runtime/layout.ts +744 -0
  24. package/src/api/v3/runtime/perf-probe.ts +14 -0
  25. package/src/api/v3/runtime/viewport.ts +9 -8
  26. package/src/api/v3/ui/_types.ts +202 -55
  27. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  28. package/src/api/v3/ui/debug.ts +115 -2
  29. package/src/api/v3/ui/index.ts +17 -0
  30. package/src/api/v3/ui/overlays.ts +0 -8
  31. package/src/api/v3/ui/surface.ts +56 -0
  32. package/src/api/v3/ui/viewport.ts +119 -9
  33. package/src/core/commands/image-commands.ts +1 -0
  34. package/src/core/commands/index.ts +6 -0
  35. package/src/core/schema/text-schema.ts +43 -5
  36. package/src/core/selection/mapping.ts +8 -1
  37. package/src/core/selection/review-anchors.ts +5 -1
  38. package/src/core/state/text-transaction.ts +8 -2
  39. package/src/io/export/serialize-revisions.ts +149 -1
  40. package/src/io/normalize/normalize-text.ts +6 -0
  41. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  42. package/src/io/ooxml/parse-fields.ts +24 -2
  43. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  44. package/src/io/ooxml/parse-main-document.ts +153 -9
  45. package/src/io/ooxml/parse-numbering.ts +20 -0
  46. package/src/io/ooxml/parse-revisions.ts +19 -8
  47. package/src/io/opc/package-reader.ts +98 -8
  48. package/src/model/anchor.ts +4 -3
  49. package/src/model/canonical-document.ts +220 -2
  50. package/src/model/canonical-hash.ts +221 -0
  51. package/src/model/canonical-layout-inputs.ts +245 -6
  52. package/src/model/layout/index.ts +1 -0
  53. package/src/model/layout/page-graph-types.ts +147 -1
  54. package/src/model/review/revision-types.ts +14 -3
  55. package/src/preservation/store.ts +20 -4
  56. package/src/review/README.md +1 -1
  57. package/src/review/store/revision-actions.ts +14 -2
  58. package/src/runtime/collab/event-types.ts +67 -1
  59. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  60. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  61. package/src/runtime/document-heading-outline.ts +147 -0
  62. package/src/runtime/document-navigation.ts +8 -243
  63. package/src/runtime/document-runtime.ts +279 -115
  64. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  65. package/src/runtime/formatting/layout-inputs.ts +38 -5
  66. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  67. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  68. package/src/runtime/geometry/caret-geometry.ts +5 -6
  69. package/src/runtime/geometry/geometry-facet.ts +60 -10
  70. package/src/runtime/geometry/geometry-index.ts +661 -16
  71. package/src/runtime/geometry/geometry-types.ts +59 -0
  72. package/src/runtime/geometry/hit-test.ts +11 -1
  73. package/src/runtime/geometry/overlay-rects.ts +5 -3
  74. package/src/runtime/geometry/project-anchors.ts +1 -1
  75. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  76. package/src/runtime/layout/index.ts +6 -0
  77. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  78. package/src/runtime/layout/layout-engine-version.ts +188 -16
  79. package/src/runtime/layout/layout-facet-types.ts +6 -0
  80. package/src/runtime/layout/page-graph.ts +23 -4
  81. package/src/runtime/layout/paginated-layout-engine.ts +149 -15
  82. package/src/runtime/layout/project-block-fragments.ts +351 -14
  83. package/src/runtime/layout/public-facet.ts +162 -24
  84. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  85. package/src/runtime/layout/table-row-split.ts +92 -35
  86. package/src/runtime/prerender/cache-envelope.ts +2 -2
  87. package/src/runtime/prerender/cache-key.ts +5 -4
  88. package/src/runtime/prerender/customxml-cache.ts +0 -1
  89. package/src/runtime/render/render-kernel.ts +1 -1
  90. package/src/runtime/revision-runtime.ts +112 -10
  91. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  92. package/src/runtime/scopes/action-validation.ts +22 -2
  93. package/src/runtime/scopes/capabilities.ts +316 -0
  94. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  95. package/src/runtime/scopes/compiler-service.ts +108 -4
  96. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  97. package/src/runtime/scopes/create-issue.ts +5 -5
  98. package/src/runtime/scopes/evidence.ts +91 -0
  99. package/src/runtime/scopes/formatting/apply.ts +2 -0
  100. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  101. package/src/runtime/scopes/index.ts +54 -0
  102. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  103. package/src/runtime/scopes/layout-evidence.ts +374 -0
  104. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  105. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  106. package/src/runtime/scopes/replacement/apply.ts +97 -34
  107. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  108. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  109. package/src/runtime/scopes/visualization.ts +28 -0
  110. package/src/runtime/surface-projection.ts +44 -5
  111. package/src/runtime/telemetry/perf-probe.ts +216 -0
  112. package/src/runtime/virtualized-rendering.ts +36 -1
  113. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  114. package/src/runtime/workflow/coordinator.ts +39 -11
  115. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  116. package/src/runtime/workflow/index.ts +4 -0
  117. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  118. package/src/runtime/workflow/overlay-lanes.ts +386 -0
  119. package/src/runtime/workflow/overlay-store.ts +2 -2
  120. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  121. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  122. package/src/session/_sync-legacy.ts +17 -27
  123. package/src/session/import/loader.ts +6 -4
  124. package/src/session/import/source-package-evidence.ts +186 -2
  125. package/src/session/index.ts +5 -6
  126. package/src/session/session.ts +30 -56
  127. package/src/session/types.ts +8 -13
  128. package/src/shell/session-bootstrap.ts +155 -81
  129. package/src/ui/WordReviewEditor.tsx +520 -12
  130. package/src/ui/editor-shell-view.tsx +14 -4
  131. package/src/ui/editor-surface-controller.tsx +5 -3
  132. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  133. package/src/ui/presence-overlay-lane.ts +130 -0
  134. package/src/ui/ui-controller-factory.ts +17 -0
  135. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  136. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  137. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  138. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  139. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  140. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  141. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  142. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  143. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  144. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  145. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  146. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  147. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  148. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  149. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  150. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  151. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  152. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  153. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  154. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  155. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  156. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  157. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  158. package/src/ui-tailwind/debug/README.md +4 -1
  159. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  160. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  161. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  162. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  163. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  164. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  165. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  166. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  167. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  168. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  169. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  170. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  171. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  172. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  173. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  174. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  175. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  176. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  177. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  178. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  179. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  180. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  181. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  182. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  183. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  184. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  185. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  186. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  187. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  188. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  189. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  190. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  191. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  192. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  193. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -28,13 +28,21 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
28
28
  return entries[entries.length - 1]?.pmEnd ?? pmDocSize - 1;
29
29
  }
30
30
 
31
+ let previous: MapEntry | null = null;
31
32
  for (const entry of entries) {
33
+ if (
34
+ entry.runtimeStart === entry.runtimeEnd &&
35
+ runtimePos === entry.runtimeStart
36
+ ) {
37
+ return entry.pmStart;
38
+ }
32
39
  if (runtimePos >= entry.runtimeStart && runtimePos < entry.runtimeEnd) {
33
40
  return entry.pmStart + (runtimePos - entry.runtimeStart);
34
41
  }
35
42
  if (runtimePos < entry.runtimeStart) {
36
- return entry.pmStart;
43
+ return nearestRuntimeGapPm(runtimePos, previous, entry);
37
44
  }
45
+ previous = entry;
38
46
  }
39
47
 
40
48
  return entries[entries.length - 1]?.pmEnd ?? 1;
@@ -48,13 +56,15 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
48
56
  return runtimeStorySize;
49
57
  }
50
58
 
59
+ let previous: MapEntry | null = null;
51
60
  for (const entry of entries) {
52
61
  if (pmPos >= entry.pmStart && pmPos <= entry.pmEnd) {
53
62
  return entry.runtimeStart + (pmPos - entry.pmStart);
54
63
  }
55
64
  if (pmPos < entry.pmStart) {
56
- return entry.runtimeStart;
65
+ return nearestPmGapRuntime(pmPos, previous, entry);
57
66
  }
67
+ previous = entry;
58
68
  }
59
69
 
60
70
  return runtimeStorySize;
@@ -65,6 +75,32 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
65
75
  };
66
76
  }
67
77
 
78
+ function nearestRuntimeGapPm(
79
+ runtimePos: number,
80
+ previous: MapEntry | null,
81
+ next: MapEntry,
82
+ ): number {
83
+ if (!previous) {
84
+ return next.pmStart;
85
+ }
86
+ const distanceToPrevious = Math.abs(runtimePos - previous.runtimeEnd);
87
+ const distanceToNext = Math.abs(next.runtimeStart - runtimePos);
88
+ return distanceToPrevious <= distanceToNext ? previous.pmEnd : next.pmStart;
89
+ }
90
+
91
+ function nearestPmGapRuntime(
92
+ pmPos: number,
93
+ previous: MapEntry | null,
94
+ next: MapEntry,
95
+ ): number {
96
+ if (!previous) {
97
+ return next.runtimeStart;
98
+ }
99
+ const distanceToPrevious = Math.abs(pmPos - previous.pmEnd);
100
+ const distanceToNext = Math.abs(next.pmStart - pmPos);
101
+ return distanceToPrevious <= distanceToNext ? previous.runtimeEnd : next.runtimeStart;
102
+ }
103
+
68
104
  function walkBlocks(
69
105
  blocks: SurfaceBlockSnapshot[],
70
106
  pmCursor: number,
@@ -4,7 +4,7 @@ import {
4
4
  tableRowNodeSpec,
5
5
  tableCellNodeSpec,
6
6
  tableHeaderCellNodeSpec,
7
- } from "../../runtime/table-schema.ts";
7
+ } from "../../api/table-node-specs.ts";
8
8
  import {
9
9
  isSupportedShapeGeometry,
10
10
  renderShapeSvg,
@@ -199,6 +199,7 @@ export const editorSchema = new Schema({
199
199
  bidi: { default: null },
200
200
  pageBreakBefore: { default: null },
201
201
  hiddenTextOnly: { default: null },
202
+ tabLayout: { default: null },
202
203
  placeholderCulled: { default: null },
203
204
  /**
204
205
  * Rendered height (in twips) of the block that this placeholder
@@ -541,11 +542,17 @@ export const editorSchema = new Schema({
541
542
  : (numberingPrefix ?? ""),
542
543
  ]);
543
544
  }
545
+ const contentAttrs: Record<string, string> = {
546
+ class: "pm-paragraph-content",
547
+ };
548
+ if (node.attrs.tabLayout === "right") {
549
+ contentAttrs["data-tab-layout"] = "right";
550
+ contentAttrs.style =
551
+ "display: inline-flex; align-items: baseline; width: 100%; min-width: 0; white-space: nowrap";
552
+ }
544
553
  children.push([
545
554
  "span",
546
- {
547
- class: "pm-paragraph-content",
548
- },
555
+ contentAttrs,
549
556
  0,
550
557
  ]);
551
558
  return ["p", attrs, ...children];
@@ -585,6 +592,9 @@ export const editorSchema = new Schema({
585
592
  `width: ${width && width > 0 ? width : 32}px`,
586
593
  `min-width: 8px`,
587
594
  ];
595
+ if (align === "right" || align === "end") {
596
+ styles.push(`flex: 1 1 auto`);
597
+ }
588
598
  if (leader === "dot" || leader === "middleDot") {
589
599
  styles.push(
590
600
  `background-image: radial-gradient(circle, currentColor 1px, transparent 1.25px)`,
@@ -343,8 +343,14 @@ export function createPMSelectionFromSnapshot(
343
343
  );
344
344
  } catch {
345
345
  // If the mapped runtime selection is invalid or lands in a non-text block,
346
- // let ProseMirror choose the nearest valid starting selection.
347
- return Selection.atStart(doc);
346
+ // keep the cursor near the mapped anchor. Falling back to doc start makes
347
+ // table-boundary mapping failures show up as visible cursor jumps.
348
+ try {
349
+ const safeAnchor = clamp(pmAnchor, 0, doc.content.size);
350
+ return Selection.near(doc.resolve(safeAnchor), selection.head >= selection.anchor ? 1 : -1);
351
+ } catch {
352
+ return Selection.atStart(doc);
353
+ }
348
354
  }
349
355
  }
350
356
 
@@ -512,7 +518,7 @@ function buildParagraph(
512
518
  : 0;
513
519
  const widthPx = Math.round((stopPos - prevPos) / 15);
514
520
  const leader = (stop as { leader?: string }).leader ?? null;
515
- const align = (stop as { val?: string }).val ?? null;
521
+ const align = (stop as { val?: string; align?: string }).val ?? (stop as { align?: string }).align ?? null;
516
522
  content.push(
517
523
  editorSchema.nodes.tab_char.create({
518
524
  tabWidth: widthPx > 8 ? widthPx : null,
@@ -594,6 +600,7 @@ function buildParagraph(
594
600
  bidi: block.bidi ?? cascade?.bidi ?? null,
595
601
  pageBreakBefore: block.pageBreakBefore ?? cascade?.pageBreakBefore ?? null,
596
602
  hiddenTextOnly: fullyVanishedParagraph || null,
603
+ tabLayout: hasRightAlignedTabStop(block, paragraphLayout.tabStops) ? "right" : null,
597
604
  // `<w:framePr>` out-of-flow frame — forward to the PM paragraph node
598
605
  // so `pm-schema.ts::paragraph.toDOM` emits the absolute positioning
599
606
  // that matches the static `buildParagraphStyle` path (L04
@@ -605,12 +612,30 @@ function buildParagraph(
605
612
  );
606
613
  }
607
614
 
615
+ function readTabStopAlign(stop: { val?: string; align?: string }): string | undefined {
616
+ return stop.val ?? stop.align;
617
+ }
618
+
619
+ function hasRightAlignedTabStop(
620
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
621
+ tabStops: Array<{ val?: string; align?: string }>,
622
+ ): boolean {
623
+ let tabIndex = 0;
624
+ for (const segment of block.segments) {
625
+ if (segment.kind !== "tab") continue;
626
+ const align = readTabStopAlign(tabStops[tabIndex] ?? {});
627
+ if (align === "right" || align === "end") return true;
628
+ tabIndex += 1;
629
+ }
630
+ return false;
631
+ }
632
+
608
633
  function resolveParagraphLayout(
609
634
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
610
635
  ): {
611
636
  spacing: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["spacing"];
612
637
  indentation: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["indentation"];
613
- tabStops: Array<{ pos: number; val?: string; leader?: string }>;
638
+ tabStops: Array<{ pos?: number; position?: number; val?: string; align?: string; leader?: string }>;
614
639
  markerLane: NonNullable<
615
640
  NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"]>["geometry"]["markerLane"]
616
641
  > | undefined;
@@ -628,7 +653,11 @@ function resolveParagraphLayout(
628
653
  block.resolvedNumbering?.geometry.indentation ??
629
654
  block.indentation ??
630
655
  cascadeFormatting?.indentation,
631
- tabStops: block.resolvedNumbering?.geometry.tabStops ?? block.tabStops ?? [],
656
+ tabStops:
657
+ block.resolvedNumbering?.geometry.tabStops ??
658
+ block.tabStops ??
659
+ cascadeFormatting?.tabStops ??
660
+ [],
632
661
  markerLane: block.resolvedNumbering?.geometry.markerLane,
633
662
  markerJustification: block.resolvedNumbering?.geometry.markerJustification,
634
663
  };
@@ -51,7 +51,8 @@ import type {
51
51
  RevisionDecorationModel,
52
52
  RevisionDisplayFlags,
53
53
  } from "../../ui/headless/revision-decoration-model";
54
- import { buildDecorations } from "./pm-decorations";
54
+ import { buildRuntimeDecorations } from "./pm-decorations";
55
+ import { recordPerfSample } from "./perf-probe";
55
56
  import type { PositionMap } from "./pm-position-map";
56
57
 
57
58
  /** Inputs that drive every call to `buildDecorations`, plus an opaque
@@ -116,24 +117,7 @@ export function __resetRuntimeDecorationCountersForTests(): void {
116
117
  }
117
118
 
118
119
  function rebuildSet(doc: PMNode, inputs: RuntimeDecorationInputs): DecorationSet {
119
- const base = buildDecorations(
120
- doc,
121
- inputs.positionMap,
122
- inputs.commentModel,
123
- inputs.revisionModel,
124
- inputs.markupDisplay,
125
- inputs.showTrackedChanges,
126
- inputs.suggestionsEnabled,
127
- inputs.workflowScopes,
128
- inputs.activeStory,
129
- inputs.workflowCandidates,
130
- inputs.workflowBlockedReasons,
131
- inputs.workflowLockedZones,
132
- inputs.activeWorkflowWorkItemId,
133
- inputs.activeWorkflowScopeIds,
134
- inputs.workflowMetadata,
135
- inputs.revisionDisplayByOffset,
136
- );
120
+ const base = buildRuntimeDecorations({ doc, ...inputs });
137
121
  const extras = inputs.extraDecorations ? inputs.extraDecorations(doc) : [];
138
122
  if (extras.length === 0) return base;
139
123
  // Layer extras on top of the base set. `DecorationSet.add` is the
@@ -186,5 +170,11 @@ export function applyRuntimeDecorationInputs(
186
170
  const tr: Transaction = view.state.tr.setMeta(runtimeDecorationPluginKey, {
187
171
  inputs,
188
172
  });
173
+ const startedAt = nowMs();
189
174
  view.dispatch(tr);
175
+ recordPerfSample("pm.decorations.apply", nowMs() - startedAt);
176
+ }
177
+
178
+ function nowMs(): number {
179
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
190
180
  }
@@ -21,7 +21,7 @@ export function createSurfaceDocumentBuildKey(input: {
21
21
  surface: EditorSurfaceSnapshot | null | undefined;
22
22
  activeStory: EditorStoryTarget;
23
23
  mediaPreviewKey: string;
24
- showUnsupportedObjectPreviews?: boolean;
24
+ unsupportedObjectPreviewsVisible?: boolean;
25
25
  isPageWorkspace?: boolean;
26
26
  }): string {
27
27
  const ranges = input.surface?.viewportBlockRanges ?? null;
@@ -32,7 +32,7 @@ export function createSurfaceDocumentBuildKey(input: {
32
32
  : getSurfaceIdentity(input.surface),
33
33
  activeStory: input.activeStory,
34
34
  mediaPreviewKey: input.mediaPreviewKey,
35
- showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? false,
35
+ unsupportedObjectPreviewsVisible: input.unsupportedObjectPreviewsVisible ?? false,
36
36
  isPageWorkspace: input.isPageWorkspace ?? false,
37
37
  // Serialize all intervals (sorted by start) — disjoint viewport+caret
38
38
  // ranges must key distinctly from a single merged range so PM rebuilds
@@ -136,6 +136,151 @@ export function computeTabWidthsInPoints(
136
136
  return widths;
137
137
  }
138
138
 
139
+ export interface TabRenderInfo {
140
+ widthPt: number;
141
+ align?: string;
142
+ leader?: string;
143
+ }
144
+
145
+ function readTabStop(
146
+ stop: {
147
+ pos?: number;
148
+ position?: number;
149
+ val?: string;
150
+ align?: string;
151
+ leader?: string;
152
+ },
153
+ ): { pos: number; align?: string; leader?: string } {
154
+ return {
155
+ pos: stop.pos ?? stop.position ?? 0,
156
+ align: stop.val ?? stop.align,
157
+ leader: stop.leader,
158
+ };
159
+ }
160
+
161
+ function resolveTabStops(
162
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
163
+ ):
164
+ | Array<{
165
+ pos?: number;
166
+ position?: number;
167
+ val?: string;
168
+ align?: string;
169
+ leader?: string;
170
+ }>
171
+ | undefined {
172
+ return (
173
+ block.resolvedNumbering?.geometry.tabStops ??
174
+ block.tabStops ??
175
+ block.resolvedParagraphFormatting?.tabStops
176
+ );
177
+ }
178
+
179
+ function isRightAlignedTab(align: string | undefined): boolean {
180
+ return align === "right" || align === "end";
181
+ }
182
+
183
+ export function hasRightAlignedTabStop(
184
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
185
+ ): boolean {
186
+ const rawStops = resolveTabStops(block);
187
+ if (!rawStops || rawStops.length === 0) return false;
188
+
189
+ let tabIndex = 0;
190
+ for (const seg of block.segments) {
191
+ if (seg.kind !== "tab") continue;
192
+ const stop = rawStops[tabIndex];
193
+ if (stop && isRightAlignedTab(readTabStop(stop).align)) return true;
194
+ tabIndex += 1;
195
+ }
196
+ return false;
197
+ }
198
+
199
+ export function buildParagraphContentStyle(
200
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
201
+ ): React.CSSProperties | undefined {
202
+ if (!hasRightAlignedTabStop(block)) return undefined;
203
+ return {
204
+ display: "inline-flex",
205
+ alignItems: "baseline",
206
+ width: "100%",
207
+ minWidth: 0,
208
+ whiteSpace: "nowrap",
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Precompute per-tab render data from paragraph tab stops.
214
+ *
215
+ * Mirrors `computeTabWidthsInPoints`, but keeps the tab stop's alignment and
216
+ * leader metadata so static body and region renderers can match the PM
217
+ * `tab_char.toDOM` path for TOC leader dots and related leader styles.
218
+ */
219
+ export function computeTabRenderInfoBySegment(
220
+ block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
221
+ ): Map<string, TabRenderInfo> {
222
+ const infos = new Map<string, TabRenderInfo>();
223
+ const rawStops = resolveTabStops(block);
224
+ if (!rawStops || rawStops.length === 0) return infos;
225
+
226
+ let tabIndex = 0;
227
+ for (const seg of block.segments) {
228
+ if (seg.kind !== "tab") continue;
229
+ const stop = rawStops[tabIndex];
230
+ if (stop) {
231
+ const current = readTabStop(stop);
232
+ const prevPos =
233
+ tabIndex > 0 ? readTabStop(rawStops[tabIndex - 1]!).pos : 0;
234
+ const widthTwips = current.pos - prevPos;
235
+ if (widthTwips > 0) {
236
+ infos.set(seg.segmentId, {
237
+ widthPt: widthTwips / 20,
238
+ ...(current.align ? { align: current.align } : {}),
239
+ ...(current.leader ? { leader: current.leader } : {}),
240
+ });
241
+ }
242
+ }
243
+ tabIndex += 1;
244
+ }
245
+ return infos;
246
+ }
247
+
248
+ export function buildTabStyle(
249
+ info: TabRenderInfo | undefined,
250
+ ): React.CSSProperties {
251
+ const style: React.CSSProperties =
252
+ typeof info?.widthPt === "number"
253
+ ? { display: "inline-block", width: `${info.widthPt}pt`, minWidth: "8px" }
254
+ : { display: "inline-block", width: "32px", minWidth: "8px" };
255
+
256
+ if (isRightAlignedTab(info?.align)) {
257
+ style.flex = "1 1 auto";
258
+ }
259
+
260
+ switch (info?.leader) {
261
+ case "dot":
262
+ case "middleDot":
263
+ style.backgroundImage =
264
+ "radial-gradient(circle, currentColor 1px, transparent 1.25px)";
265
+ style.backgroundSize = "6px 3px";
266
+ style.backgroundRepeat = "repeat-x";
267
+ style.backgroundPosition = "left calc(100% - 2px)";
268
+ style.opacity = 0.55;
269
+ break;
270
+ case "hyphen":
271
+ style.borderBottom = "1px dashed rgba(107,107,107,0.65)";
272
+ break;
273
+ case "underscore":
274
+ style.borderBottom = "1px solid rgba(107,107,107,0.65)";
275
+ break;
276
+ case "heavy":
277
+ style.borderBottom = "2px solid rgba(107,107,107,0.75)";
278
+ break;
279
+ }
280
+
281
+ return style;
282
+ }
283
+
139
284
  /** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
140
285
  export function buildParagraphStyle(
141
286
  block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
@@ -6,12 +6,15 @@ import type {
6
6
  } from "../../api/public-types.ts";
7
7
  import {
8
8
  buildMarkerStyle,
9
+ buildParagraphContentStyle,
9
10
  buildParagraphStyle,
10
11
  buildSegmentStyle,
11
- computeTabWidthsInPoints,
12
+ buildTabStyle,
13
+ computeTabRenderInfoBySegment,
12
14
  hasStyleEntries,
13
15
  headingClassList,
14
16
  resolveHeadingLevel,
17
+ type TabRenderInfo,
15
18
  } from "./tw-page-block-view.helpers.ts";
16
19
 
17
20
  // ---------------------------------------------------------------------------
@@ -19,7 +22,7 @@ import {
19
22
  // ---------------------------------------------------------------------------
20
23
 
21
24
  /** Render a single inline segment. */
22
- function renderSegment(seg: SurfaceInlineSegment, tabWidthsPt: Map<string, number>): React.ReactNode {
25
+ function renderSegment(seg: SurfaceInlineSegment, tabInfoBySegment: Map<string, TabRenderInfo>): React.ReactNode {
23
26
  switch (seg.kind) {
24
27
  case "text": {
25
28
  const style = buildSegmentStyle(seg.marks, seg.markAttrs);
@@ -34,16 +37,13 @@ function renderSegment(seg: SurfaceInlineSegment, tabWidthsPt: Map<string, numbe
34
37
  );
35
38
  }
36
39
  case "tab": {
37
- const widthPt = tabWidthsPt.get(seg.segmentId);
38
- const tabStyle: React.CSSProperties =
39
- typeof widthPt === "number"
40
- ? { display: "inline-block", width: `${widthPt}pt`, minWidth: "8px" }
41
- : { display: "inline-block", width: "32px", minWidth: "8px" };
40
+ const tabInfo = tabInfoBySegment.get(seg.segmentId);
42
41
  return (
43
42
  <span
44
43
  key={seg.segmentId}
45
44
  data-node-type="tab"
46
- style={tabStyle}
45
+ style={buildTabStyle(tabInfo)}
46
+ title={tabInfo?.align ? `Tab stop · ${tabInfo.align}` : "Tab stop"}
47
47
  >
48
48
  {"\u00A0"}
49
49
  </span>
@@ -121,7 +121,8 @@ function ParagraphBlock({
121
121
  }
122
122
 
123
123
  const pStyle = buildParagraphStyle(block);
124
- const tabWidthsPt = computeTabWidthsInPoints(block);
124
+ const tabInfoBySegment = computeTabRenderInfoBySegment(block);
125
+ const contentStyle = buildParagraphContentStyle(block);
125
126
  const attrs: React.HTMLAttributes<HTMLParagraphElement> & {
126
127
  "data-heading-level"?: string;
127
128
  "data-numbered"?: string;
@@ -199,8 +200,12 @@ function ParagraphBlock({
199
200
  return (
200
201
  <p {...attrs}>
201
202
  {prefixSpan}
202
- <span className="pm-paragraph-content">
203
- {block.segments.map((seg) => renderSegment(seg, tabWidthsPt))}
203
+ <span
204
+ className="pm-paragraph-content"
205
+ data-tab-layout={contentStyle ? "right" : undefined}
206
+ style={contentStyle}
207
+ >
208
+ {block.segments.map((seg) => renderSegment(seg, tabInfoBySegment))}
204
209
  </span>
205
210
  </p>
206
211
  );
@@ -13,6 +13,7 @@ import type {
13
13
  DocumentNavigationSnapshot,
14
14
  EditorStoryTarget,
15
15
  EditorUser,
16
+ GeometryFacet,
16
17
  RuntimeRenderSnapshot,
17
18
  SearchOptions,
18
19
  SearchResultSnapshot,
@@ -105,13 +106,12 @@ const BOUNDED_TYPING_SCROLL_RESTORE_MAX_DELTA_PX = 256;
105
106
  export function shouldPreserveScrollAnchorForRebuild(options: {
106
107
  policy: RebuildScrollAnchorPolicy | null;
107
108
  view: Pick<EditorView, "hasFocus"> | null;
108
- geometryFacet?: import("../../runtime/geometry/index.ts").GeometryFacet;
109
+ geometryFacet?: GeometryFacet;
109
110
  previousStory: EditorStoryTarget | null;
110
111
  nextStory: EditorStoryTarget;
111
112
  }): boolean {
112
113
  if (options.policy !== "bounded-same-story") return false;
113
114
  if (!options.view?.hasFocus()) return false;
114
- if (!options.geometryFacet) return false;
115
115
  if (!options.previousStory) return false;
116
116
  return storyTargetsEqual(options.previousStory, options.nextStory);
117
117
  }
@@ -128,9 +128,7 @@ export function shouldPreserveScrollAnchorForRebuild(options: {
128
128
  * between adjacent blocks.
129
129
  */
130
130
  function buildPageBreakDecorationsFromProps(
131
- geometryFacet:
132
- | import("../../runtime/geometry/index.ts").GeometryFacet
133
- | undefined,
131
+ geometryFacet: GeometryFacet | undefined,
134
132
  isMainStory: boolean,
135
133
  positionMap: PositionMap,
136
134
  posture: "canvas" | "page",
@@ -232,7 +230,7 @@ export interface TwProseMirrorSurfaceProps {
232
230
  documentNavigation: DocumentNavigationSnapshot;
233
231
  reviewMode: "editing" | "review";
234
232
  markupDisplay: MarkupDisplay;
235
- showUnsupportedObjectPreviews?: boolean;
233
+ unsupportedObjectPreviewsVisible?: boolean;
236
234
  activeRevisionId?: string;
237
235
  activeSelectionToolKind?: ActiveSelectionToolModel["kind"] | null;
238
236
  showTrackedChanges?: boolean;
@@ -340,7 +338,7 @@ export interface TwProseMirrorSurfaceProps {
340
338
  * render-frame access; layout-facet's `getRenderFrame` is being
341
339
  * deleted.
342
340
  */
343
- geometryFacet?: import("../../runtime/geometry/index.ts").GeometryFacet;
341
+ geometryFacet?: GeometryFacet;
344
342
  /** Height in px of each page's header band. Default 32. */
345
343
  pageChromeHeaderBandPx?: number;
346
344
  /** Height in px of each page's footer band. Default 32. */
@@ -544,13 +542,13 @@ export const TwProseMirrorSurface = forwardRef<
544
542
  surface,
545
543
  activeStory: snapshot.activeStory,
546
544
  mediaPreviewKey,
547
- showUnsupportedObjectPreviews: props.showUnsupportedObjectPreviews,
545
+ unsupportedObjectPreviewsVisible: props.unsupportedObjectPreviewsVisible,
548
546
  isPageWorkspace: props.isPageWorkspace,
549
547
  }),
550
548
  [
551
549
  mediaPreviewKey,
552
550
  props.isPageWorkspace,
553
- props.showUnsupportedObjectPreviews,
551
+ props.unsupportedObjectPreviewsVisible,
554
552
  snapshot.activeStory,
555
553
  surface,
556
554
  ],
@@ -924,7 +922,7 @@ export const TwProseMirrorSurface = forwardRef<
924
922
  snapshot.selection,
925
923
  plugins,
926
924
  props.mediaPreviews,
927
- props.showUnsupportedObjectPreviews,
925
+ props.unsupportedObjectPreviewsVisible ?? false,
928
926
  props.isPageWorkspace,
929
927
  );
930
928
  positionMapRef.current = positionMap;
@@ -69,6 +69,9 @@ function createPaddingCell(colSpan: number, widthTwips: number): HTMLTableCellEl
69
69
  const cell = document.createElement("td");
70
70
  cell.setAttribute("data-row-padding", "true");
71
71
  cell.setAttribute("aria-hidden", "true");
72
+ cell.setAttribute("contenteditable", "false");
73
+ cell.setAttribute("role", "presentation");
74
+ cell.tabIndex = -1;
72
75
  cell.colSpan = Math.max(1, colSpan);
73
76
  cell.style.border = "none";
74
77
  cell.style.padding = "0";
@@ -20,6 +20,7 @@ import type {
20
20
  SurfaceTableRowSnapshot,
21
21
  WordReviewEditorLayoutFacet,
22
22
  } from "../../api/public-types.ts";
23
+ import { buildPageAnchorAttributes } from "../../api/v3/_page-anchor-id.ts";
23
24
  import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
24
25
  import { TwTableContinuationHeader } from "../chrome-overlay/tw-table-continuation-header.tsx";
25
26
  import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
@@ -144,6 +145,7 @@ function TwPageChromeEntryInner({
144
145
  );
145
146
 
146
147
  const frameHeightPx = rect.bottomPx - rect.topPx;
148
+ const pageAnchorAttributes = buildPageAnchorAttributes(rect.pageId, pageIndex);
147
149
 
148
150
  // Viewport cull — lightweight placeholder outside the visible range.
149
151
  const isCulled =
@@ -154,8 +156,8 @@ function TwPageChromeEntryInner({
154
156
  if (isCulled) {
155
157
  return (
156
158
  <div
159
+ {...pageAnchorAttributes}
157
160
  data-page-chrome-frame=""
158
- data-page-index={pageIndex}
159
161
  data-page-chrome-culled=""
160
162
  style={{
161
163
  position: "absolute",
@@ -190,8 +192,8 @@ function TwPageChromeEntryInner({
190
192
 
191
193
  return (
192
194
  <div
195
+ {...pageAnchorAttributes}
193
196
  data-page-chrome-frame=""
194
- data-page-index={pageIndex}
195
197
  style={{
196
198
  position: "absolute",
197
199
  top: `${rect.topPx}px`,