@beyondwork/docx-react-component 1.0.106 → 1.0.109

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 (190) 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 +2 -1
  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-time.ts +5 -0
  11. package/src/api/v3/ai/_pe2-evidence.ts +38 -0
  12. package/src/api/v3/ai/attach.ts +7 -2
  13. package/src/api/v3/ai/replacement.ts +101 -18
  14. package/src/api/v3/ai/resolve.ts +2 -2
  15. package/src/api/v3/ai/review.ts +177 -3
  16. package/src/api/v3/index.ts +1 -0
  17. package/src/api/v3/runtime/collab.ts +462 -0
  18. package/src/api/v3/runtime/document.ts +503 -20
  19. package/src/api/v3/runtime/geometry.ts +97 -0
  20. package/src/api/v3/runtime/layout.ts +744 -0
  21. package/src/api/v3/runtime/perf-probe.ts +14 -0
  22. package/src/api/v3/runtime/viewport.ts +9 -8
  23. package/src/api/v3/ui/_types.ts +149 -55
  24. package/src/api/v3/ui/chrome-preset-model.ts +5 -5
  25. package/src/api/v3/ui/debug.ts +115 -2
  26. package/src/api/v3/ui/index.ts +13 -0
  27. package/src/api/v3/ui/overlays.ts +0 -8
  28. package/src/api/v3/ui/surface.ts +56 -0
  29. package/src/api/v3/ui/viewport.ts +22 -9
  30. package/src/core/commands/image-commands.ts +1 -0
  31. package/src/core/commands/index.ts +6 -0
  32. package/src/core/schema/text-schema.ts +43 -5
  33. package/src/core/selection/mapping.ts +8 -1
  34. package/src/core/selection/review-anchors.ts +5 -1
  35. package/src/core/state/text-transaction.ts +8 -2
  36. package/src/io/export/serialize-revisions.ts +149 -1
  37. package/src/io/normalize/normalize-text.ts +6 -0
  38. package/src/io/ooxml/parse-bookmark-references.ts +55 -0
  39. package/src/io/ooxml/parse-fields.ts +24 -2
  40. package/src/io/ooxml/parse-headers-footers.ts +38 -5
  41. package/src/io/ooxml/parse-main-document.ts +153 -9
  42. package/src/io/ooxml/parse-numbering.ts +20 -0
  43. package/src/io/ooxml/parse-revisions.ts +19 -8
  44. package/src/io/opc/package-reader.ts +98 -8
  45. package/src/model/anchor.ts +4 -3
  46. package/src/model/canonical-document.ts +220 -2
  47. package/src/model/canonical-hash.ts +221 -0
  48. package/src/model/canonical-layout-inputs.ts +245 -6
  49. package/src/model/layout/index.ts +1 -0
  50. package/src/model/layout/page-graph-types.ts +118 -1
  51. package/src/model/review/revision-types.ts +14 -3
  52. package/src/preservation/store.ts +20 -4
  53. package/src/review/README.md +1 -1
  54. package/src/review/store/revision-actions.ts +14 -2
  55. package/src/runtime/collab/event-types.ts +67 -1
  56. package/src/runtime/collab/runtime-collab-sync.ts +177 -5
  57. package/src/runtime/diagnostics/layout-guard-warning.ts +18 -0
  58. package/src/runtime/document-heading-outline.ts +147 -0
  59. package/src/runtime/document-navigation.ts +8 -243
  60. package/src/runtime/document-runtime.ts +240 -97
  61. package/src/runtime/edit-dispatch/dispatch-text-command.ts +11 -0
  62. package/src/runtime/formatting/layout-inputs.ts +38 -5
  63. package/src/runtime/formatting/numbering/geometry.ts +28 -2
  64. package/src/runtime/geometry/adjacent-geometry-intake.ts +835 -0
  65. package/src/runtime/geometry/caret-geometry.ts +5 -6
  66. package/src/runtime/geometry/geometry-facet.ts +60 -10
  67. package/src/runtime/geometry/geometry-index.ts +591 -20
  68. package/src/runtime/geometry/geometry-types.ts +59 -0
  69. package/src/runtime/geometry/hit-test.ts +11 -1
  70. package/src/runtime/geometry/overlay-rects.ts +5 -3
  71. package/src/runtime/geometry/project-anchors.ts +1 -1
  72. package/src/runtime/geometry/word-layout-v2-line-intake.ts +323 -0
  73. package/src/runtime/layout/index.ts +6 -0
  74. package/src/runtime/layout/layout-engine-instance.ts +6 -1
  75. package/src/runtime/layout/layout-engine-version.ts +181 -16
  76. package/src/runtime/layout/layout-facet-types.ts +6 -0
  77. package/src/runtime/layout/page-graph.ts +21 -4
  78. package/src/runtime/layout/paginated-layout-engine.ts +139 -15
  79. package/src/runtime/layout/project-block-fragments.ts +265 -7
  80. package/src/runtime/layout/public-facet.ts +78 -24
  81. package/src/runtime/layout/table-row-continuation-contract.ts +107 -0
  82. package/src/runtime/layout/table-row-split.ts +92 -35
  83. package/src/runtime/prerender/cache-envelope.ts +2 -2
  84. package/src/runtime/prerender/cache-key.ts +5 -4
  85. package/src/runtime/prerender/customxml-cache.ts +0 -1
  86. package/src/runtime/render/render-kernel.ts +1 -1
  87. package/src/runtime/revision-runtime.ts +112 -10
  88. package/src/runtime/scopes/_scope-dependencies.ts +1 -0
  89. package/src/runtime/scopes/action-validation.ts +22 -2
  90. package/src/runtime/scopes/capabilities.ts +316 -0
  91. package/src/runtime/scopes/compile-scope-bundle.ts +14 -0
  92. package/src/runtime/scopes/compiler-service.ts +108 -4
  93. package/src/runtime/scopes/content-control-evidence.ts +79 -0
  94. package/src/runtime/scopes/create-issue.ts +5 -5
  95. package/src/runtime/scopes/evidence.ts +91 -0
  96. package/src/runtime/scopes/formatting/apply.ts +2 -0
  97. package/src/runtime/scopes/geometry-evidence.ts +130 -0
  98. package/src/runtime/scopes/index.ts +54 -0
  99. package/src/runtime/scopes/issue-lifecycle.ts +224 -0
  100. package/src/runtime/scopes/layout-evidence.ts +374 -0
  101. package/src/runtime/scopes/multi-paragraph-refusal.ts +37 -0
  102. package/src/runtime/scopes/preservation-boundary.ts +7 -1
  103. package/src/runtime/scopes/replacement/apply.ts +97 -34
  104. package/src/runtime/scopes/scope-kinds/paragraph.ts +108 -12
  105. package/src/runtime/scopes/semantic-scope-types.ts +242 -3
  106. package/src/runtime/scopes/visualization.ts +28 -0
  107. package/src/runtime/surface-projection.ts +44 -5
  108. package/src/runtime/telemetry/perf-probe.ts +216 -0
  109. package/src/runtime/virtualized-rendering.ts +36 -1
  110. package/src/runtime/workflow/ai-issue-lifecycle.ts +253 -0
  111. package/src/runtime/workflow/coordinator.ts +39 -11
  112. package/src/runtime/workflow/derived-scope-resolver.ts +63 -9
  113. package/src/runtime/workflow/index.ts +3 -0
  114. package/src/runtime/workflow/overlay-lane-types.ts +58 -0
  115. package/src/runtime/workflow/overlay-lanes.ts +168 -10
  116. package/src/runtime/workflow/overlay-store.ts +2 -2
  117. package/src/runtime/workflow/redline-posture-calibration.ts +257 -0
  118. package/src/runtime/workflow/word-field-matrix-calibration.ts +231 -0
  119. package/src/session/_sync-legacy.ts +17 -27
  120. package/src/session/import/loader.ts +6 -4
  121. package/src/session/import/source-package-evidence.ts +186 -2
  122. package/src/session/index.ts +5 -6
  123. package/src/session/session.ts +30 -56
  124. package/src/session/types.ts +8 -13
  125. package/src/shell/session-bootstrap.ts +155 -81
  126. package/src/ui/WordReviewEditor.tsx +520 -12
  127. package/src/ui/editor-shell-view.tsx +14 -4
  128. package/src/ui/editor-surface-controller.tsx +5 -3
  129. package/src/ui/headless/selection-tool-resolver.ts +1 -2
  130. package/src/ui/presence-overlay-lane.ts +0 -1
  131. package/src/ui/ui-controller-factory.ts +7 -0
  132. package/src/ui-tailwind/chrome/build-context-menu-entries.ts +5 -1
  133. package/src/ui-tailwind/chrome/editor-action-registry.ts +105 -5
  134. package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +7 -0
  135. package/src/ui-tailwind/chrome/layer-debug-contracts.ts +208 -0
  136. package/src/ui-tailwind/chrome/resolve-target-kind.ts +13 -0
  137. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +11 -3
  138. package/src/ui-tailwind/chrome/tw-command-palette.tsx +36 -6
  139. package/src/ui-tailwind/chrome/tw-context-menu.tsx +6 -1
  140. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +42 -109
  141. package/src/ui-tailwind/chrome/tw-inline-find-bar.tsx +26 -6
  142. package/src/ui-tailwind/chrome/tw-navigation-command-bar.tsx +328 -0
  143. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +8 -4
  144. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +129 -1
  145. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +19 -5
  146. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +5 -1
  147. package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +28 -12
  148. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -3
  149. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +116 -10
  150. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +223 -94
  151. package/src/ui-tailwind/chrome-overlay/tw-presence-overlay-lane.tsx +157 -0
  152. package/src/ui-tailwind/chrome-overlay/tw-review-overlay-lane-markers.tsx +259 -0
  153. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +5 -2
  154. package/src/ui-tailwind/chrome-overlay/tw-substrate-overlay-lanes.tsx +314 -0
  155. package/src/ui-tailwind/debug/README.md +4 -1
  156. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +272 -0
  157. package/src/ui-tailwind/debug/layer11-word-field-matrix-evidence.ts +160 -0
  158. package/src/ui-tailwind/editor-surface/perf-probe.ts +14 -215
  159. package/src/ui-tailwind/editor-surface/pm-decorations.ts +42 -0
  160. package/src/ui-tailwind/editor-surface/pm-position-map.ts +38 -2
  161. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -4
  162. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +34 -5
  163. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +9 -19
  164. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -2
  165. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +145 -0
  166. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +16 -11
  167. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +8 -10
  168. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +3 -0
  169. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +4 -2
  170. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +60 -20
  171. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +16 -11
  172. package/src/ui-tailwind/review/tw-health-panel.tsx +36 -17
  173. package/src/ui-tailwind/review/tw-review-rail.tsx +7 -4
  174. package/src/ui-tailwind/review-workspace/diagnostics-visibility.ts +44 -0
  175. package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +11 -0
  176. package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +16 -1
  177. package/src/ui-tailwind/review-workspace/types.ts +26 -12
  178. package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +40 -11
  179. package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +2 -1
  180. package/src/ui-tailwind/review-workspace/use-page-markers.ts +15 -26
  181. package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +35 -18
  182. package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +41 -32
  183. package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +2 -1
  184. package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +2 -1
  185. package/src/ui-tailwind/status/tw-status-bar.tsx +6 -5
  186. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +52 -80
  187. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +12 -48
  188. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +9 -4
  189. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +328 -361
  190. package/src/ui-tailwind/tw-review-workspace.tsx +152 -286
@@ -12,8 +12,8 @@
12
12
  * Band placement
13
13
  * --------------
14
14
  * For each measured page rect the layer emits one
15
- * `<div data-page-chrome-frame data-page-index={i}>` positioned at the
16
- * rect's top/height. Inside the frame, four children conditionally
15
+ * `<div data-page-chrome-frame data-page-anchor data-page-index={i}>`
16
+ * positioned at the rect's top/height. Inside the frame, four children conditionally
17
17
  * mount:
18
18
  *
19
19
  * 1. `<TwPageHeaderBand>` — when the page's `regions.header` + the
@@ -73,13 +73,17 @@ import type {
73
73
  GeometryFacet,
74
74
  WordReviewEditorLayoutFacet,
75
75
  } from "../../api/public-types.ts";
76
+ import { buildPageAnchorSelector } from "../../api/v3/_page-anchor-id.ts";
76
77
  import {
77
78
  measureWidgetsViaOffsetChain,
78
79
  measureWidgetsViaBoundingRect,
80
+ resolvePageOverlayRectsFromUiApi,
79
81
  resolvePageOverlayRectsFromGeometry,
80
82
  resolvePageOverlayRects,
83
+ reconcilePageStackRectsWithFlow,
81
84
  type PageOverlayRect,
82
85
  } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
86
+ import { useUiApi } from "../ui-api-context.tsx";
83
87
  import { TwEndnoteArea } from "./tw-endnote-area.tsx";
84
88
  import { TwPageChromeEntry } from "./tw-page-chrome-entry.tsx";
85
89
  import type { TwActiveBandRibbonProps } from "./tw-active-band-ribbon.tsx";
@@ -101,7 +105,7 @@ export interface TwPageStackChromeLayerProps {
101
105
  facet: WordReviewEditorLayoutFacet;
102
106
  /** Geometry facet — warm-path source of per-page frame rects. */
103
107
  geometryFacet?: GeometryFacet;
104
- /** Scroll root whose `[data-page-frame-*]` markers drive per-page rects. */
108
+ /** Scroll root used only by the page-rect cold fallback before geometry is warm. */
105
109
  scrollRoot: HTMLElement | null;
106
110
  /**
107
111
  * Render-frame revision tick incremented on `layout_recomputed`,
@@ -137,7 +141,7 @@ export interface TwPageStackChromeLayerProps {
137
141
  * pages whose sequential index falls inside this range (with overscan
138
142
  * already applied by the caller). Pages outside the range render a
139
143
  * lightweight wrapper `div` carrying the `data-page-chrome-frame` +
140
- * `data-page-index` attributes — every position read (portal-slot
144
+ * shared `data-page-anchor` attributes — every position read (portal-slot
141
145
  * query, per-page measurement) continues to resolve deterministically,
142
146
  * but the four child React subtrees (Header/Footer/Footnote bands)
143
147
  * never mount, eliminating the measured 412 ms Layout + 229 ms
@@ -183,9 +187,29 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
183
187
  mediaPreviews,
184
188
  activeBandRibbonProps,
185
189
  }) => {
190
+ const ui = useUiApi();
191
+ const resolveUiPageRects = React.useCallback(
192
+ (pageCount: number): readonly PageOverlayRect[] | null => {
193
+ if (!ui) return null;
194
+ const pageIds = Array.from(
195
+ { length: pageCount },
196
+ (_, pageIndex) => facet.getPage(pageIndex)?.pageId ?? `page-${pageIndex}`,
197
+ );
198
+ return resolvePageOverlayRectsFromUiApi(
199
+ ui,
200
+ pageCount,
201
+ visiblePageIndexRange,
202
+ pageIds,
203
+ );
204
+ },
205
+ [facet, ui, visiblePageIndexRange],
206
+ );
207
+
186
208
  const [rects, setRects] = React.useState<readonly PageOverlayRect[]>(() => {
187
- if (!geometryFacet) return [];
188
209
  const pageCount = facet.getPageCount();
210
+ const uiRects = resolveUiPageRects(pageCount);
211
+ if (uiRects !== null) return uiRects;
212
+ if (!geometryFacet) return [];
189
213
  const warm = resolvePageOverlayRectsFromGeometry(
190
214
  geometryFacet,
191
215
  pageCount,
@@ -206,6 +230,20 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
206
230
 
207
231
  const refreshRectsNow = React.useCallback(() => {
208
232
  const pageCount = facet.getPageCount();
233
+ const reconcileRects = (baseRects: readonly PageOverlayRect[]) =>
234
+ reconcilePageStackRectsWithFlow({
235
+ baseRects,
236
+ pageCount,
237
+ scrollRoot,
238
+ originElement: overlayRootRef.current,
239
+ });
240
+
241
+ const uiRects = resolveUiPageRects(pageCount);
242
+ if (uiRects !== null) {
243
+ setRects(reconcileRects(uiRects));
244
+ return;
245
+ }
246
+
209
247
  if (geometryFacet) {
210
248
  const geometryRects = resolvePageOverlayRectsFromGeometry(
211
249
  geometryFacet,
@@ -213,7 +251,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
213
251
  visiblePageIndexRange,
214
252
  );
215
253
  if (geometryRects !== null) {
216
- setRects(geometryRects);
254
+ setRects(reconcileRects(geometryRects));
217
255
  return;
218
256
  }
219
257
  }
@@ -240,29 +278,31 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
240
278
  : originRect.height > 0
241
279
  ? originRect.height
242
280
  : scrollRoot.clientHeight;
281
+ const domRects = resolvePageOverlayRects({
282
+ widgets,
283
+ pageCount,
284
+ scrollHeight,
285
+ visiblePageIndexRange,
286
+ });
243
287
  setRects(
244
- resolvePageOverlayRects({
245
- widgets,
246
- pageCount,
247
- scrollHeight,
248
- visiblePageIndexRange,
249
- }),
288
+ reconcileRects(domRects),
250
289
  );
251
290
  } else {
252
291
  const widgets = measureWidgetsViaOffsetChain(scrollRoot, {
253
292
  pageCount,
254
293
  visiblePageIndexRange,
255
294
  });
295
+ const domRects = resolvePageOverlayRects({
296
+ widgets,
297
+ pageCount,
298
+ scrollHeight: scrollRoot.clientHeight,
299
+ visiblePageIndexRange,
300
+ });
256
301
  setRects(
257
- resolvePageOverlayRects({
258
- widgets,
259
- pageCount,
260
- scrollHeight: scrollRoot.clientHeight,
261
- visiblePageIndexRange,
262
- }),
302
+ reconcileRects(domRects),
263
303
  );
264
304
  }
265
- }, [facet, geometryFacet, scrollRoot, visiblePageIndexRange]);
305
+ }, [facet, geometryFacet, resolveUiPageRects, scrollRoot, visiblePageIndexRange]);
266
306
 
267
307
  const refreshRects = React.useCallback(() => {
268
308
  if (!scrollRoot) {
@@ -384,7 +424,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
384
424
  const pageScopedSelector =
385
425
  activeStoryPageIndex == null
386
426
  ? null
387
- : `[data-pm-portal-slot][data-page-index="${activeStoryPageIndex}"]`;
427
+ : `${buildPageAnchorSelector(activeStoryPageIndex)} [data-pm-portal-slot]`;
388
428
  const activeSlot =
389
429
  (pageScopedSelector
390
430
  ? overlay?.querySelector<HTMLElement>(pageScopedSelector)
@@ -7,12 +7,15 @@ import type {
7
7
  import type { MediaPreviewDescriptor } from "../editor-surface/pm-state-from-snapshot.ts";
8
8
  import {
9
9
  buildMarkerStyle,
10
+ buildParagraphContentStyle,
10
11
  buildParagraphStyle,
11
12
  buildSegmentStyle,
12
- computeTabWidthsInPoints,
13
+ buildTabStyle,
14
+ computeTabRenderInfoBySegment,
13
15
  hasStyleEntries,
14
16
  headingClassList,
15
17
  resolveHeadingLevel,
18
+ type TabRenderInfo,
16
19
  } from "../editor-surface/tw-page-block-view.helpers.ts";
17
20
  import { shouldRenderAbsoluteFloatingImageInPageOverlay } from "./floating-image-overlay-model.ts";
18
21
 
@@ -61,7 +64,7 @@ function renderSegment(
61
64
  seg: SurfaceInlineSegment,
62
65
  mediaPreviews: Record<string, MediaPreviewDescriptor>,
63
66
  fallbackDisplay: RegionImageFallbackDisplay,
64
- tabWidthsPt: Map<string, number>,
67
+ tabInfoBySegment: Map<string, TabRenderInfo>,
65
68
  ): React.ReactNode {
66
69
  switch (seg.kind) {
67
70
  case "text": {
@@ -77,16 +80,13 @@ function renderSegment(
77
80
  );
78
81
  }
79
82
  case "tab": {
80
- const widthPt = tabWidthsPt.get(seg.segmentId);
81
- const tabStyle: React.CSSProperties =
82
- typeof widthPt === "number"
83
- ? { display: "inline-block", width: `${widthPt}pt`, minWidth: "8px" }
84
- : { display: "inline-block", width: "32px", minWidth: "8px" };
83
+ const tabInfo = tabInfoBySegment.get(seg.segmentId);
85
84
  return (
86
85
  <span
87
86
  key={seg.segmentId}
88
87
  data-node-type="tab"
89
- style={tabStyle}
88
+ style={buildTabStyle(tabInfo)}
89
+ title={tabInfo?.align ? `Tab stop · ${tabInfo.align}` : "Tab stop"}
90
90
  >
91
91
  {"\u00A0"}
92
92
  </span>
@@ -248,7 +248,8 @@ function RegionParagraph({
248
248
  }
249
249
 
250
250
  const pStyle = buildParagraphStyle(block);
251
- const tabWidthsPt = computeTabWidthsInPoints(block);
251
+ const tabInfoBySegment = computeTabRenderInfoBySegment(block);
252
+ const contentStyle = buildParagraphContentStyle(block);
252
253
 
253
254
  // Numbering prefix span — matches tw-page-block-view so region content that
254
255
  // happens to carry numbering (e.g. footnote bodies authored as lists) shows
@@ -312,8 +313,12 @@ function RegionParagraph({
312
313
  return (
313
314
  <div {...attrs}>
314
315
  {prefixSpan}
315
- <span className="pm-paragraph-content">
316
- {block.segments.map((seg) => renderSegment(seg, mediaPreviews, fallbackDisplay, tabWidthsPt))}
316
+ <span
317
+ className="pm-paragraph-content"
318
+ data-tab-layout={contentStyle ? "right" : undefined}
319
+ style={contentStyle}
320
+ >
321
+ {block.segments.map((seg) => renderSegment(seg, mediaPreviews, fallbackDisplay, tabInfoBySegment))}
317
322
  </span>
318
323
  </div>
319
324
  );
@@ -2,17 +2,26 @@ import React from "react";
2
2
  import { AlertTriangle, Info, Shield, ShieldAlert, ShieldCheck } from "lucide-react";
3
3
 
4
4
  import type {
5
- CompatibilityFeatureEntry,
6
5
  CompatibilityPanelSnapshot,
7
6
  EditorWarning,
8
7
  WorkflowBlockedCommandReason,
9
8
  } from "../../api/public-types";
10
9
  import { TwEmptyState } from "../chrome/tw-empty-state";
10
+ import {
11
+ filterProductCompatibility,
12
+ filterProductWarnings,
13
+ } from "../review-workspace/diagnostics-visibility.ts";
11
14
 
12
15
  export interface TwHealthPanelProps {
13
16
  compatibility: CompatibilityPanelSnapshot;
14
17
  warnings: EditorWarning[];
15
18
  blockedReasons?: WorkflowBlockedCommandReason[];
19
+ /**
20
+ * Preserve-only / opaque diagnostics are debug/operator-only. Default
21
+ * product chrome shows export blockers and actionable warnings, not
22
+ * preservation internals.
23
+ */
24
+ showPreservationDiagnostics?: boolean;
16
25
  }
17
26
 
18
27
  type SeverityKey = "error" | "warning" | "info";
@@ -91,7 +100,16 @@ function renderGroup(
91
100
  }
92
101
 
93
102
  export function TwHealthPanel(props: TwHealthPanelProps) {
94
- const { compatibility, warnings, blockedReasons = [] } = props;
103
+ const {
104
+ blockedReasons = [],
105
+ showPreservationDiagnostics = false,
106
+ } = props;
107
+ const compatibility = showPreservationDiagnostics
108
+ ? props.compatibility
109
+ : filterProductCompatibility(props.compatibility);
110
+ const warnings = showPreservationDiagnostics
111
+ ? props.warnings
112
+ : filterProductWarnings(props.warnings);
95
113
 
96
114
  // Blocked export group: unsupported-fatal entries
97
115
  const blockedExportItems: IssueRow[] = compatibility.featureEntries
@@ -111,20 +129,22 @@ export function TwHealthPanel(props: TwHealthPanelProps) {
111
129
 
112
130
  // Warning group: preserve-only entries + workflow blocked reasons + "warning"-severity EditorWarnings
113
131
  const warningItems: IssueRow[] = [
114
- ...compatibility.featureEntries
115
- .filter((e) => e.featureClass === "preserve-only")
116
- .map((e) => ({
117
- id: e.featureEntryId,
118
- message: e.message,
119
- detail: e.featureKey,
120
- badge: "preserve-only",
121
- icon: (
122
- <Shield
123
- className="h-4 w-4 shrink-0 mt-0.5"
124
- style={{ color: "var(--color-semantic-warning)" }}
125
- />
126
- ),
127
- })),
132
+ ...(showPreservationDiagnostics
133
+ ? compatibility.featureEntries
134
+ .filter((e) => e.featureClass === "preserve-only")
135
+ .map((e) => ({
136
+ id: e.featureEntryId,
137
+ message: e.message,
138
+ detail: e.featureKey,
139
+ badge: "preserve-only",
140
+ icon: (
141
+ <Shield
142
+ className="h-4 w-4 shrink-0 mt-0.5"
143
+ style={{ color: "var(--color-semantic-warning)" }}
144
+ />
145
+ ),
146
+ }))
147
+ : []),
128
148
  ...blockedReasons.map((r, index) => ({
129
149
  id: `blocked-reason-${index}`,
130
150
  message: r.message,
@@ -186,4 +206,3 @@ export function TwHealthPanel(props: TwHealthPanelProps) {
186
206
  </div>
187
207
  );
188
208
  }
189
-
@@ -31,8 +31,8 @@ import {
31
31
  * to a signal-only chip. When `showHealthTab` is absent the rail retains the
32
32
  * shipped three-tab layout for back-compat.
33
33
  *
34
- * The Workflow tab reads `scopeRailSegments` from the runtime facet that
35
- * stays the default path. For hosts that need to override the Workflow card
34
+ * The Workflow tab reads `scopeRailSegments` from the mounted UI API seam by
35
+ * default. For hosts that need to override the Workflow card
36
36
  * content (CLM, BW, agent integrations that derive their own model), pass
37
37
  * `workflowTab` as a ReactNode and it will replace the built-in projection.
38
38
  * Either way policy stays host-side; the runtime rail remains projection-only.
@@ -60,8 +60,8 @@ export interface TwReviewRailProps {
60
60
  activeCommentId?: string;
61
61
  activeRevisionId?: string;
62
62
  /**
63
- * Scope rail segments used by the Workflow tab. Consumers typically
64
- * pass `ref.layout.getAllScopeRailSegments()` here. When omitted the
63
+ * Scope rail segments used by the Workflow tab. Mounted consumers pass
64
+ * `api.ui.scope.rail().segments` through the workspace. When omitted the
65
65
  * Workflow tab renders an empty state (unless `workflowTab` is set).
66
66
  */
67
67
  scopeRailSegments?: readonly ScopeRailSegment[];
@@ -103,6 +103,8 @@ export interface TwReviewRailProps {
103
103
  healthSeverity?: "info" | "warning" | "blocked";
104
104
  /** Forwarded to the Health tab's `TwHealthPanel` to surface blocked reasons. */
105
105
  workflowBlockedReasons?: WorkflowBlockedCommandReason[];
106
+ /** Debug/operator-only opt-in for preserve-only / opaque diagnostics. */
107
+ showPreservationDiagnostics?: boolean;
106
108
 
107
109
  onActiveTabChange: (tab: ReviewRailTab) => void;
108
110
  onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
@@ -299,6 +301,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
299
301
  <TwHealthPanel
300
302
  compatibility={props.compatibility}
301
303
  warnings={props.warnings}
304
+ showPreservationDiagnostics={props.showPreservationDiagnostics}
302
305
  {...(props.workflowBlockedReasons
303
306
  ? { blockedReasons: props.workflowBlockedReasons }
304
307
  : {})}
@@ -0,0 +1,44 @@
1
+ import type {
2
+ CompatibilityFeatureEntry,
3
+ CompatibilityPanelSnapshot,
4
+ EditorWarning,
5
+ } from "../../api/public-types.ts";
6
+
7
+ const DEBUG_ONLY_WARNING_CODES = new Set<EditorWarning["code"]>([
8
+ "unsupported_ooxml_preserved",
9
+ "unsupported_ooxml_locked",
10
+ ]);
11
+
12
+ export function isDebugOnlyCompatibilityFeature(
13
+ entry: CompatibilityFeatureEntry,
14
+ ): boolean {
15
+ return entry.featureClass === "preserve-only";
16
+ }
17
+
18
+ export function isDebugOnlyWarning(warning: EditorWarning): boolean {
19
+ return (
20
+ warning.source === "preservation" ||
21
+ DEBUG_ONLY_WARNING_CODES.has(warning.code)
22
+ );
23
+ }
24
+
25
+ export function filterProductWarnings(
26
+ warnings: readonly EditorWarning[] | undefined,
27
+ ): EditorWarning[] {
28
+ return (warnings ?? []).filter((warning) => !isDebugOnlyWarning(warning));
29
+ }
30
+
31
+ export function filterProductCompatibility(
32
+ compatibility: CompatibilityPanelSnapshot,
33
+ ): CompatibilityPanelSnapshot {
34
+ const featureEntries = compatibility.featureEntries.filter(
35
+ (entry) => !isDebugOnlyCompatibilityFeature(entry),
36
+ );
37
+ if (featureEntries.length === compatibility.featureEntries.length) {
38
+ return compatibility;
39
+ }
40
+ return {
41
+ ...compatibility,
42
+ featureEntries,
43
+ };
44
+ }
@@ -12,6 +12,9 @@ export const FRAME_PX_PER_TWIP_AT_96DPI = 96 / 1440;
12
12
  /** Floor on header/footer band heights so empty bands stay clickable. */
13
13
  export const MIN_BAND_HEIGHT_PX = 20;
14
14
 
15
+ /** Word's default Letter body width: 8.5in page minus 1in margins at 96dpi. */
16
+ export const DEFAULT_CANVAS_BODY_WIDTH_PX = 624;
17
+
15
18
  const FIT_WIDTH_CHROME_RESERVATION_PX = 96;
16
19
  const FIT_HEIGHT_CHROME_RESERVATION_PX = 180;
17
20
  const MIN_FIT_ZOOM = 0.5;
@@ -22,6 +25,8 @@ export interface PageShellMetrics {
22
25
  frameWidthPx?: number;
23
26
  /** P2.a — page frame CSS px height = `pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`. */
24
27
  frameHeightPx?: number;
28
+ /** Word body column width after subtracting left/right section margins. */
29
+ contentWidthPx?: number;
25
30
  contentInsetStyle: CSSProperties;
26
31
  pageFrameStyle: CSSProperties;
27
32
  }
@@ -35,6 +40,7 @@ export function buildPageShellMetrics(
35
40
  pageFrameStyle: {},
36
41
  frameWidthPx: 0,
37
42
  frameHeightPx: 0,
43
+ contentWidthPx: 0,
38
44
  };
39
45
  }
40
46
 
@@ -43,6 +49,10 @@ export function buildPageShellMetrics(
43
49
  const frameHeightPx = Math.round(pageLayout.pageHeight * pxPerTwip);
44
50
  const horizontalInsetPx = Math.round(pageLayout.marginLeft * pxPerTwip);
45
51
  const horizontalInsetRightPx = Math.round(pageLayout.marginRight * pxPerTwip);
52
+ const contentWidthPx = Math.max(
53
+ 0,
54
+ frameWidthPx - horizontalInsetPx - horizontalInsetRightPx,
55
+ );
46
56
 
47
57
  return {
48
58
  contentInsetStyle: {
@@ -58,6 +68,7 @@ export function buildPageShellMetrics(
58
68
  },
59
69
  frameWidthPx,
60
70
  frameHeightPx,
71
+ contentWidthPx,
61
72
  };
62
73
  }
63
74
 
@@ -7,6 +7,8 @@ import { TwReviewRail, type TwReviewRailProps } from "../review/tw-review-rail";
7
7
  *
8
8
  * - `docked` — render the rail inline as a sibling of the document
9
9
  * column (standard desktop width).
10
+ * - `overlay` — render the rail over the canvas, so opening the rail never
11
+ * shrinks or left-shifts the continuous editor column.
10
12
  * - `drawer` — render the rail behind a dismiss-overlay button so the
11
13
  * narrow-viewport scrim can close it.
12
14
  * - `hidden` — render nothing (the parent already decided the rail
@@ -16,7 +18,7 @@ import { TwReviewRail, type TwReviewRailProps } from "../review/tw-review-rail";
16
18
  * `rail`; the wrapper owns the `variant` slot so the parent doesn't have
17
19
  * to branch twice on posture.
18
20
  */
19
- export type TwReviewWorkspaceRailMode = "docked" | "drawer" | "hidden";
21
+ export type TwReviewWorkspaceRailMode = "docked" | "overlay" | "drawer" | "hidden";
20
22
 
21
23
  export interface TwReviewWorkspaceRailProps {
22
24
  mode: TwReviewWorkspaceRailMode;
@@ -40,6 +42,19 @@ export function TwReviewWorkspaceRail({
40
42
  return <TwReviewRail {...rail} />;
41
43
  }
42
44
 
45
+ if (mode === "overlay") {
46
+ return (
47
+ <div
48
+ className="pointer-events-none absolute inset-y-0 right-0 z-30 flex justify-end"
49
+ data-testid="review-rail-overlay"
50
+ >
51
+ <div className="pointer-events-auto h-full">
52
+ <TwReviewRail variant="drawer" {...rail} />
53
+ </div>
54
+ </div>
55
+ );
56
+ }
57
+
43
58
  return (
44
59
  <div
45
60
  className="pointer-events-none absolute inset-0 z-30 flex justify-end"
@@ -9,6 +9,7 @@ import type {
9
9
  EditorViewStateSnapshot,
10
10
  FormattingStateSnapshot,
11
11
  FormattingAlignment,
12
+ GeometryFacet,
12
13
  HeaderFooterLinkPatch,
13
14
  InteractionGuardSnapshot,
14
15
  InsertImageOptions,
@@ -17,11 +18,14 @@ import type {
17
18
  ReviewQueueSnapshot,
18
19
  SectionPageNumberingPatch,
19
20
  SectionBreakType,
21
+ CollabSession,
20
22
  StyleCatalogSnapshot,
21
23
  TrackedChangeEntrySnapshot,
24
+ WordReviewEditorLayoutFacet,
22
25
  WordReviewEditorChromeOptions,
23
26
  WordReviewEditorChromePreset,
24
27
  WordReviewEditorChromeVisibility,
28
+ WorkflowFacet,
25
29
  WorkflowScopeSnapshot,
26
30
  WorkspaceMode,
27
31
  ZoomLevel,
@@ -31,6 +35,10 @@ import type {
31
35
  ActiveSelectionToolModel,
32
36
  SelectionToolAnchor,
33
37
  } from "../../ui/headless/selection-tool-types";
38
+ import type {
39
+ UiObjectDragSession,
40
+ UiObjectDragStartInput,
41
+ } from "../../api/v3/ui/_types.ts";
34
42
  import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
35
43
  import type { EditorCommandBag } from "../../ui/editor-command-bag.ts";
36
44
  import type { EditorActionHostCallbacks } from "../chrome/editor-action-registry";
@@ -54,7 +62,7 @@ export interface TwReviewWorkspaceProps {
54
62
  * supplied, the ChromeOverlay plane (scope rail, workflow dock, etc.)
55
63
  * renders over the document column.
56
64
  */
57
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
65
+ layoutFacet?: WordReviewEditorLayoutFacet;
58
66
  /**
59
67
  * Layer-05 geometry facet (`runtime.geometry`). Required by chrome
60
68
  * overlay layers (scope rail, object selection, table grip, page-break
@@ -63,7 +71,7 @@ export interface TwReviewWorkspaceProps {
63
71
  * through the now-`@deprecated` `layoutFacet.getRenderFrame` /
64
72
  * `.getRenderZoom` methods.
65
73
  */
66
- geometryFacet?: import("../../runtime/geometry/index.ts").GeometryFacet;
74
+ geometryFacet?: GeometryFacet;
67
75
  /**
68
76
  * Layer-06 workflow facet — canonical source of scope rail segments
69
77
  * and scope card models (`runtime.workflow`). Required by the scope
@@ -71,19 +79,16 @@ export interface TwReviewWorkspaceProps {
71
79
  * removed those methods from `WordReviewEditorLayoutFacet`. When
72
80
  * absent, those layers render nothing.
73
81
  */
74
- workflowFacet?: import("../../runtime/workflow/rail/types.ts").WorkflowFacet;
82
+ workflowFacet?: WorkflowFacet;
75
83
  /**
76
84
  * Optional shell header override. Mounted above the formatting toolbar.
77
85
  *
78
- * Per docs/plans/chrome-composition-audit.md §2.4 the shell is always
79
- * present in default composition. When this prop is omitted the
80
- * workspace mounts a default `<TwShellHeader />` with the 4-mode
81
- * switcher (edit / review / workflow / more) so mode authority is
82
- * always visible and centralized.
86
+ * The default product workspace no longer mounts a top mode-strip shell.
87
+ * Edit / Review / Workflow mode authority lives in the main toolbar.
83
88
  *
84
- * Hosts that need a custom brand, mode handlers, or primary CTA pass a
89
+ * Hosts that need a custom brand, actions, or primary CTA pass a
85
90
  * pre-assembled `<TwShellHeader />` here; the custom node replaces the
86
- * default entirely.
91
+ * empty default shell entirely.
87
92
  */
88
93
  shellHeader?: ReactNode;
89
94
  /**
@@ -121,7 +126,7 @@ export interface TwReviewWorkspaceProps {
121
126
  /**
122
127
  * Optional host-provided Workflow-tab override for the review rail.
123
128
  * When unset the rail renders the built-in `TwWorkflowTab` sourced from
124
- * `layoutFacet.getAllScopeRailSegments()`.
129
+ * `api.ui.scope.rail()` on mounted paths.
125
130
  */
126
131
  reviewRailWorkflowTab?: ReactNode;
127
132
  reviewRailWorkflowCount?: number;
@@ -136,6 +141,13 @@ export interface TwReviewWorkspaceProps {
136
141
  searchLabel?: string;
137
142
  helpLabel?: string;
138
143
  };
144
+ /**
145
+ * Internal debug/operator switch for opaque/preserve-only diagnostics.
146
+ * Default product chrome must leave this unset so the shipping component
147
+ * never exposes preservation internals, health badges, or debug detail by
148
+ * default.
149
+ */
150
+ showDebugDiagnostics?: boolean;
139
151
  /** Opens the built-in inline find surface from More/Search and command palette. */
140
152
  onOpenInlineFind?: () => void;
141
153
  document: ReactNode;
@@ -157,7 +169,7 @@ export interface TwReviewWorkspaceProps {
157
169
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
158
170
  density?: "compact" | "standard" | "comfortable";
159
171
  /** P9g — live collab session for the `"collab"` chrome preset's top nav. */
160
- collabSession?: import("../../runtime/collab-session.ts").CollabSession;
172
+ collabSession?: CollabSession;
161
173
  collabTransportStatus?: import("../../api/awareness-identity-types.ts").TransportStatus;
162
174
  collabActorId?: string;
163
175
  collabSendBaseline?: {
@@ -179,6 +191,8 @@ export interface TwReviewWorkspaceProps {
179
191
  }) => void;
180
192
  /** N6 — release the grabbed image/shape. Wired to `runtime.deselectObject()` by the host. */
181
193
  onDeselectObject?: () => void;
194
+ /** Object handle drag lifecycle; routed through the mounted UI API seam. */
195
+ onBeginObjectDrag?: (input: UiObjectDragStartInput) => UiObjectDragSession;
182
196
  activeSelectionTool?: ActiveSelectionToolModel | null;
183
197
  selectionToolAnchor?: SelectionToolAnchor | null;
184
198
  documentNavigation?: DocumentNavigationSnapshot;