@beyondwork/docx-react-component 1.0.106 → 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 (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
@@ -2,6 +2,10 @@ import { useMemo } from "react";
2
2
 
3
3
  import type { RuntimeRenderSnapshot } from "../../api/public-types.ts";
4
4
  import type { SessionCapabilities } from "../../api/public-types";
5
+ import {
6
+ filterProductWarnings,
7
+ isDebugOnlyCompatibilityFeature,
8
+ } from "./diagnostics-visibility.ts";
5
9
 
6
10
  export interface DiagnosticsSignal {
7
11
  severity: "none" | "info" | "warning" | "blocked";
@@ -13,13 +17,20 @@ export interface UseDiagnosticsSignalOptions {
13
17
  caps: SessionCapabilities | undefined;
14
18
  preserveOnlyCount: number;
15
19
  blockedReasonsCount: number;
20
+ /**
21
+ * Preserve-only / opaque diagnostics are debug/operator UX. Default
22
+ * product chrome should not open Health or badge the toolbar solely
23
+ * because content is preserved for export.
24
+ */
25
+ includePreservationDiagnostics?: boolean;
16
26
  }
17
27
 
18
28
  /**
19
29
  * Phase E.1 — diagnostics signal feeds `resolveChromeComposition` so the
20
30
  * `health` rail tab can enter `visibleTabs`. Severity ladder mirrors
21
31
  * `TwAlertBanner`'s precedence (§6.17): blocked export → workflow blocked
22
- * → preserve-only / warnings info.
32
+ * → warnings → info. Preserve-only / opaque counts are included only
33
+ * for debug/operator surfaces.
23
34
  *
24
35
  * Perf — I1 fix: the workspace re-renders on every PM transaction
25
36
  * (inverted-truth architecture — `view.updateState()` replaces PM state
@@ -32,25 +43,42 @@ export interface UseDiagnosticsSignalOptions {
32
43
  export function useDiagnosticsSignal(
33
44
  options: UseDiagnosticsSignalOptions,
34
45
  ): DiagnosticsSignal {
35
- const { snapshot, caps, preserveOnlyCount, blockedReasonsCount } = options;
46
+ const {
47
+ snapshot,
48
+ caps,
49
+ preserveOnlyCount,
50
+ blockedReasonsCount,
51
+ includePreservationDiagnostics = false,
52
+ } = options;
36
53
 
37
54
  return useMemo(() => {
38
- const featureEntries = snapshot.compatibility.featureEntries;
39
- const unsupportedFatalCount =
40
- caps?.unsupportedFatalCount ??
41
- featureEntries.filter((e) => e.featureClass === "unsupported-fatal").length;
55
+ const featureEntries = includePreservationDiagnostics
56
+ ? snapshot.compatibility.featureEntries
57
+ : snapshot.compatibility.featureEntries.filter(
58
+ (entry) => !isDebugOnlyCompatibilityFeature(entry),
59
+ );
60
+ const visibleWarnings = includePreservationDiagnostics
61
+ ? snapshot.warnings
62
+ : filterProductWarnings(snapshot.warnings);
63
+ const unsupportedFatalCount = includePreservationDiagnostics
64
+ ? caps?.unsupportedFatalCount ??
65
+ featureEntries.filter((e) => e.featureClass === "unsupported-fatal").length
66
+ : featureEntries.filter((e) => e.featureClass === "unsupported-fatal").length;
42
67
  let infoWarningCount = 0;
43
- for (const w of snapshot.warnings) {
68
+ for (const w of visibleWarnings) {
44
69
  if (w.severity === "info") infoWarningCount += 1;
45
70
  }
46
- const blockingWarningCount = snapshot.warnings.length - infoWarningCount;
71
+ const blockingWarningCount = visibleWarnings.length - infoWarningCount;
72
+ const preservationCount = includePreservationDiagnostics
73
+ ? preserveOnlyCount
74
+ : 0;
47
75
  const count =
48
- caps?.healthIssueCount ??
49
- preserveOnlyCount + unsupportedFatalCount + snapshot.warnings.length;
76
+ (includePreservationDiagnostics ? caps?.healthIssueCount : undefined) ??
77
+ preservationCount + unsupportedFatalCount + visibleWarnings.length;
50
78
  const severity: "none" | "info" | "warning" | "blocked" =
51
79
  snapshot.compatibility.blockExport || unsupportedFatalCount > 0
52
80
  ? "blocked"
53
- : preserveOnlyCount > 0 ||
81
+ : preservationCount > 0 ||
54
82
  blockingWarningCount > 0 ||
55
83
  blockedReasonsCount > 0
56
84
  ? "warning"
@@ -66,5 +94,6 @@ export function useDiagnosticsSignal(
66
94
  caps?.healthIssueCount,
67
95
  preserveOnlyCount,
68
96
  blockedReasonsCount,
97
+ includePreservationDiagnostics,
69
98
  ]);
70
99
  }
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
+ import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
2
3
 
3
4
  /**
4
5
  * P3.b + P5b + P14.b — subscribe to layout facet events that should
@@ -12,7 +13,7 @@ import { useEffect, useState } from "react";
12
13
  * the same synchronous tick fold into one re-render.
13
14
  */
14
15
  export function useLayoutFacetRenderSignal(
15
- layoutFacet: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet | undefined,
16
+ layoutFacet: WordReviewEditorLayoutFacet | undefined,
16
17
  ): number {
17
18
  const [renderFrameRevision, setRenderFrameRevision] = useState(0);
18
19
 
@@ -1,11 +1,14 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
 
3
- import type { GeometryFacet, RuntimeRenderSnapshot } from "../../api/public-types.ts";
3
+ import type {
4
+ GeometryFacet,
5
+ RuntimeRenderSnapshot,
6
+ WordReviewEditorLayoutFacet,
7
+ } from "../../api/public-types.ts";
4
8
  import {
5
9
  resolveVisibleBlockRangesFromPageOffsets,
6
10
  resolveVisibleBlockRangesFromPageRange,
7
11
  resolveVisiblePageIndexRangeFromViewport,
8
- useVisibleBlockRange,
9
12
  useVisibleBlockRanges,
10
13
  useVisiblePageIndexRange,
11
14
  type VisiblePageIndexRange,
@@ -14,7 +17,7 @@ import {
14
17
  export interface UsePageMarkersOptions {
15
18
  pageStackScrollRoot: HTMLElement | null;
16
19
  snapshot: RuntimeRenderSnapshot;
17
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
20
+ layoutFacet?: WordReviewEditorLayoutFacet;
18
21
  geometryFacet?: GeometryFacet;
19
22
  renderFrameRevision?: number;
20
23
  /** CSS zoom applied to the document surface; scroll pixels are normalized by this. */
@@ -23,13 +26,6 @@ export interface UsePageMarkersOptions {
23
26
 
24
27
  export interface PageMarkersResult {
25
28
  pageMarkers: readonly HTMLElement[];
26
- /**
27
- * Bounding hull of {@link visibleBlockRanges}. Kept on the result for
28
- * back-compat with call sites that only consumed a single range; the
29
- * runtime now receives the disjoint ranges directly via
30
- * `setVisibleBlockRanges`.
31
- */
32
- visibleBlockRange: ReturnType<typeof useVisibleBlockRange>;
33
29
  /** The canonical multi-interval realization set. */
34
30
  visibleBlockRanges: ReturnType<typeof useVisibleBlockRanges>;
35
31
  visiblePageIndexRange: ReturnType<typeof useVisiblePageIndexRange>;
@@ -81,7 +77,7 @@ function pageMarkersEqual(
81
77
  function useGeometryVisiblePageIndexRange(input: {
82
78
  scrollRoot: HTMLElement | null;
83
79
  geometryFacet?: GeometryFacet;
84
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
80
+ layoutFacet?: WordReviewEditorLayoutFacet;
85
81
  pageMarkerCount: number;
86
82
  overscanPages: number;
87
83
  renderFrameRevision?: number;
@@ -180,6 +176,13 @@ function useGeometryVisiblePageIndexRange(input: {
180
176
  return range;
181
177
  }
182
178
 
179
+ export function shouldUseCompatibilityPageMarkerScan(input: {
180
+ geometryFacet?: GeometryFacet;
181
+ layoutFacet?: WordReviewEditorLayoutFacet;
182
+ }): boolean {
183
+ return !input.geometryFacet || !input.layoutFacet;
184
+ }
185
+
183
186
  /**
184
187
  * L7 Phase 2 Task 2.2.4a — viewport-scroll wiring.
185
188
  *
@@ -212,7 +215,7 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
212
215
  const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
213
216
 
214
217
  useEffect(() => {
215
- if (geometryFacet && layoutFacet) {
218
+ if (!shouldUseCompatibilityPageMarkerScan({ geometryFacet, layoutFacet })) {
216
219
  // Warm path: render-kernel geometry gives us page frames and the layout
217
220
  // facet gives us page offsets, so DOM page-break markers are only legacy
218
221
  // fallback. Skipping the marker scan avoids querySelectorAll +
@@ -385,19 +388,6 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
385
388
  visibleBlockRangesFromPageRange ??
386
389
  markerVisibleBlockRanges;
387
390
 
388
- // Bounding hull of the disjoint ranges for the back-compat `visibleBlockRange`
389
- // result field + for the effect's dep key (below).
390
- const visibleBlockRange = useMemo(() => {
391
- if (visibleBlockRanges.length === 0) return { start: 0, end: 0 };
392
- let start = visibleBlockRanges[0]!.start;
393
- let end = visibleBlockRanges[0]!.end;
394
- for (let i = 1; i < visibleBlockRanges.length; i += 1) {
395
- if (visibleBlockRanges[i]!.start < start) start = visibleBlockRanges[i]!.start;
396
- if (visibleBlockRanges[i]!.end > end) end = visibleBlockRanges[i]!.end;
397
- }
398
- return { start, end };
399
- }, [visibleBlockRanges]);
400
-
401
391
  // Stable fingerprint of the current disjoint-range set; used as the effect
402
392
  // dep so identity-preserving recomputes (same intervals, new memo array
403
393
  // reference) don't re-fire the viewport refresh.
@@ -423,7 +413,6 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
423
413
 
424
414
  return {
425
415
  pageMarkers,
426
- visibleBlockRange,
427
416
  visibleBlockRanges,
428
417
  visiblePageIndexRange,
429
418
  };
@@ -3,8 +3,11 @@ import { useCallback, useEffect, useState } from "react";
3
3
  import type {
4
4
  EditorAnchorProjection,
5
5
  ScopeIssueAction,
6
+ ScopeCardModel,
7
+ WorkflowFacet,
6
8
  WorkflowScopeMode,
7
9
  } from "../../api/public-types.ts";
10
+ import type { ApiV3Ui } from "../../api/v3/ui";
8
11
  import {
9
12
  cycleScopeIndex,
10
13
  shouldHandleScopeNavKey,
@@ -12,12 +15,12 @@ import {
12
15
  import { useUiApi } from "../ui-api-context.tsx";
13
16
 
14
17
  export interface UseScopeCardStateOptions {
15
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
16
18
  /**
17
- * Layer-06 workflow facet no-provider fallback for scope card models.
18
- * Mounted paths prefer `api.ui.scope.rail/card`.
19
+ * Compatibility-only fallback for no-provider tests/headless mounts.
20
+ * Mounted editor paths must read scope identity + card models through
21
+ * `api.ui.scope.rail/card`.
19
22
  */
20
- workflowFacet?: import("../../runtime/workflow/rail/types.ts").WorkflowFacet;
23
+ workflowFacet?: WorkflowFacet;
21
24
  onScopeModeChangeRequested?: (payload: {
22
25
  scopeId: string;
23
26
  mode: WorkflowScopeMode;
@@ -68,7 +71,6 @@ export interface ScopeCardState {
68
71
  */
69
72
  export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardState {
70
73
  const {
71
- layoutFacet,
72
74
  workflowFacet,
73
75
  onScopeModeChangeRequested,
74
76
  onScopeIssueActionRequested,
@@ -76,31 +78,21 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
76
78
  onScopeRejectSuggestionGroup,
77
79
  onScopeAskAgent,
78
80
  } = options;
79
- void layoutFacet;
80
81
 
81
82
  const [activeScopeId, setActiveScopeId] = useState<string | null>(null);
82
83
  const ui = useUiApi();
83
84
 
84
85
  const readScopeIds = useCallback((): string[] => {
85
86
  if (ui) {
86
- const ids: string[] = [];
87
- const seen = new Set<string>();
88
- for (const segment of ui.scope.rail().segments) {
89
- if (seen.has(segment.scopeId)) continue;
90
- seen.add(segment.scopeId);
91
- ids.push(segment.scopeId);
92
- }
93
- return ids;
87
+ return readMountedScopeIdsFromUi(ui);
94
88
  }
95
- return workflowFacet?.getAllScopeCardModels().map((model) => model.scopeId) ?? [];
89
+ return readCompatibilityScopeIdsFromWorkflowFacet(workflowFacet);
96
90
  }, [ui, workflowFacet]);
97
91
 
98
92
  const readScopeCard = useCallback(
99
93
  (scopeId: string) => {
100
94
  if (ui) return ui.scope.card(scopeId);
101
- return workflowFacet
102
- ?.getAllScopeCardModels()
103
- .find((m) => m.scopeId === scopeId) ?? null;
95
+ return readCompatibilityScopeCardFromWorkflowFacet(workflowFacet, scopeId);
104
96
  },
105
97
  [ui, workflowFacet],
106
98
  );
@@ -190,3 +182,28 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
190
182
  handleScopeCardAskAgent,
191
183
  };
192
184
  }
185
+
186
+ function readMountedScopeIdsFromUi(ui: ApiV3Ui): string[] {
187
+ const ids: string[] = [];
188
+ const seen = new Set<string>();
189
+ for (const scope of ui.scope.list()) {
190
+ const scopeId = scope.handle.scopeId;
191
+ if (seen.has(scopeId)) continue;
192
+ seen.add(scopeId);
193
+ ids.push(scopeId);
194
+ }
195
+ return ids;
196
+ }
197
+
198
+ function readCompatibilityScopeIdsFromWorkflowFacet(
199
+ workflowFacet: WorkflowFacet | undefined,
200
+ ): string[] {
201
+ return workflowFacet?.getAllScopeCardModels().map((model) => model.scopeId) ?? [];
202
+ }
203
+
204
+ function readCompatibilityScopeCardFromWorkflowFacet(
205
+ workflowFacet: WorkflowFacet | undefined,
206
+ scopeId: string,
207
+ ): ScopeCardModel | null {
208
+ return workflowFacet?.getAllScopeCardModels().find((m) => m.scopeId === scopeId) ?? null;
209
+ }
@@ -22,33 +22,24 @@ export interface UseSelectionToolbarPlacementOptions {
22
22
  /**
23
23
  * Compute placement for the floating selection toolbar.
24
24
  *
25
- * Resolution order (flicker-remediation 2026-04-22):
25
+ * Resolution order:
26
26
  *
27
- * 1. Legacy `resolveSelectionToolbarPlacement` driven by the
28
- * host-supplied `selectionToolAnchor` prop + the toolbar root's
29
- * `getBoundingClientRect()`. This path correctly handles
30
- * viewport-to-container translation + zoom scaling. It was the
31
- * original pre-DS-C1 path, is demonstrably correct, and is
32
- * populated every render by the shell — so it does not race with
33
- * bridge publication order.
27
+ * 1. `ui.overlays.getAnchor({ kind: "selection" })` when the mounted
28
+ * `UiApiProvider` and shell bridge can resolve a frame-local
29
+ * selection anchor. This is the DS-C1 steady-state path.
34
30
  *
35
- * 2. `ui.overlays.getAnchor({ kind: "selection" })` fallback when
36
- * the host hasn't populated `selectionToolAnchor`. The UI API
37
- * returns a frame-local rect; projecting it into container-local
38
- * coordinates is a documented TODO (the bridge rect's
39
- * `space: "frame"` is tracked but not yet translated in this
40
- * placement path). Until that projection lands, this branch is
41
- * best-effort for consumers that don't supply the legacy prop —
42
- * e.g. future headless / Playwright drivers.
31
+ * 2. Legacy `resolveSelectionToolbarPlacement` driven by the
32
+ * host-supplied `selectionToolAnchor` prop + toolbar root
33
+ * `getBoundingClientRect()`. This remains the compatibility
34
+ * fallback for unmounted/headless paths, cold bridge frames, and
35
+ * legacy callers that have not adopted the UI API seam.
43
36
  *
44
- * DS-C1 (designsystem.md §8.8.1 "Selection toolbar" row) still routes
45
- * anchor READS through the UI API in principle; the shell bridge
46
- * (`useShellSelectionAnchorBridge`) continues publishing tool-aware
47
- * rects so `ui.overlays.getAnchor({ kind: "selection" })` returns a
48
- * meaningful rect for the debug service, Playwright, and the UI-API
49
- * consumer tests. The placement hook's priority change is strictly a
50
- * flicker-remediation concession: same source of truth, correct
51
- * coordinate space.
37
+ * DS-C1 (designsystem.md §8.8.1 "Selection toolbar" row) routes anchor
38
+ * reads through the UI API. The shell bridge
39
+ * (`useShellSelectionAnchorBridge`) publishes tool-aware frame rects on
40
+ * every render; the toolbar root is the same frame-local positioning
41
+ * container, so `resolveSelectionToolPlacement` can consume the rect
42
+ * without a DOM anchor re-measure.
52
43
  *
53
44
  * The memo re-runs when the geometry facet bumps `renderFrameRevision`
54
45
  * so placement tracks new rects without a separate subscription here.
@@ -67,19 +58,19 @@ export function useSelectionToolbarPlacement(
67
58
  const ui = useUiApi();
68
59
 
69
60
  return useMemo(() => {
70
- // Primary: legacy DOM-anchor path. Correct coord space; no bridge
71
- // timing dependency.
72
61
  const legacy = resolveSelectionToolbarPlacement(
73
62
  selectionToolAnchor,
74
63
  selectionToolbarRootRef.current,
75
64
  zoomScale,
76
65
  );
77
- if (legacy) return legacy;
78
66
 
79
- // Fallback: UI API anchor for consumers that don't populate
80
- // `selectionToolAnchor`. Coord-space projection TODO the bridge
81
- // rect is frame-local, `resolveSelectionToolPlacement` assumes
82
- // container-local. Best-effort until projection lands.
67
+ // Primary: mounted UI API anchor seam. The bridge returns frame-local
68
+ // px and the toolbar root is the frame-local absolute positioning
69
+ // container, so only the container size comes from DOM. If the bridge
70
+ // returns a broad frame anchor that would force above/below placement
71
+ // while the legacy measured text rect can still side-place the toolbar,
72
+ // keep the side placement as the precision fallback until all selection
73
+ // anchors are per-range precise.
83
74
  if (ui && gatedSelectionTool) {
84
75
  const anchorRect = ui.overlays.getAnchor({ kind: "selection" });
85
76
  if (anchorRect && selectionToolbarRootRef.current) {
@@ -97,9 +88,27 @@ export function useSelectionToolbarPlacement(
97
88
  heightPx: containerRect.height,
98
89
  },
99
90
  });
100
- if (result) return result;
91
+ if (result) {
92
+ if (
93
+ legacy &&
94
+ (result.placement === "above" || result.placement === "below") &&
95
+ (legacy.placement === "right" || legacy.placement === "left")
96
+ ) {
97
+ return legacy;
98
+ }
99
+ return result;
100
+ }
101
101
  }
102
102
  }
103
+
104
+ // Compatibility fallback: legacy DOM-anchor prop for bridge-cold
105
+ // mounted frames and non-UI-API consumers. Removal trigger:
106
+ // delete this path once `UiApiProvider` is mandatory for every
107
+ // production WordReviewEditor mount and the shell selection anchor
108
+ // bridge has no cold-frame gap (tracked in L10 backlog follow-up
109
+ // "selection-toolbar legacy DOM-anchor fallback").
110
+ if (legacy) return legacy;
111
+
103
112
  return null;
104
113
  // eslint-disable-next-line react-hooks/exhaustive-deps
105
114
  }, [
@@ -3,6 +3,7 @@ import { useMemo } from "react";
3
3
  import type {
4
4
  PublicMeasurementFidelity,
5
5
  RuntimeRenderSnapshot,
6
+ WordReviewEditorLayoutFacet,
6
7
  } from "../../api/public-types.ts";
7
8
 
8
9
  export interface StatusBarPageFacts {
@@ -12,7 +13,7 @@ export interface StatusBarPageFacts {
12
13
  }
13
14
 
14
15
  export interface UseStatusBarPageFactsOptions {
15
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
16
+ layoutFacet?: WordReviewEditorLayoutFacet;
16
17
  selectionPosition: number;
17
18
  activeStory: RuntimeRenderSnapshot["activeStory"];
18
19
  renderFrameRevision: number;
@@ -2,6 +2,7 @@ import { useCallback, useEffect } from "react";
2
2
  import type { Dispatch, SetStateAction } from "react";
3
3
 
4
4
  import { createCanvasBackend } from "../../api/public-types.ts";
5
+ import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
5
6
  import {
6
7
  incrementInvalidationCounter,
7
8
  recordPerfSample,
@@ -12,7 +13,7 @@ import type { PageChromeModel } from "./page-chrome.ts";
12
13
  import type { PageShellMetrics } from "./page-shell-metrics.ts";
13
14
 
14
15
  export interface UseWorkspaceSideEffectsOptions {
15
- layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
16
+ layoutFacet?: WordReviewEditorLayoutFacet;
16
17
  activeParagraphLayout: ActiveParagraphLayout | null;
17
18
  pageChromeModel: PageChromeModel;
18
19
  pageShellMetrics: PageShellMetrics;
@@ -35,9 +35,9 @@ export interface TwStatusBarProps {
35
35
  measurementFidelity?: PublicMeasurementFidelity;
36
36
  /**
37
37
  * Lane 6b §6b.S4: opt-in diagnostics flag. When `true`, the status
38
- * bar reveals the measurement-fidelity badge on the right zone.
39
- * Hosts must pair this with `debugMode` in production — the fidelity
40
- * badge is a maintainer affordance, not a user-facing surface.
38
+ * bar may reveal debug/operator-only diagnostics such as measurement
39
+ * fidelity and preserve-only export warnings. Default product chrome
40
+ * keeps opaque/deferred preservation silent.
41
41
  */
42
42
  debugMode?: boolean;
43
43
  /**
@@ -67,9 +67,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
67
67
  : props.isDirty
68
68
  ? "Unsaved"
69
69
  : "Ready";
70
+ const showPreservationDiagnostics = props.debugMode === true;
70
71
  const exportState = props.isExportBlocked
71
72
  ? "Blocked"
72
- : props.preserveOnlyCount > 0
73
+ : showPreservationDiagnostics && props.preserveOnlyCount > 0
73
74
  ? "Warnings"
74
75
  : "Ready";
75
76
 
@@ -80,7 +81,7 @@ export function TwStatusBar(props: TwStatusBarProps) {
80
81
  : "bg-[var(--color-status-ready)]";
81
82
  const exportDotColor = props.isExportBlocked
82
83
  ? "bg-[var(--color-status-blocked)]"
83
- : props.preserveOnlyCount > 0
84
+ : showPreservationDiagnostics && props.preserveOnlyCount > 0
84
85
  ? "bg-[var(--color-semantic-warning)]"
85
86
  : "bg-[var(--color-status-ready)]";
86
87
 
@@ -13,8 +13,8 @@
13
13
  * traversal + claim/skip/complete.
14
14
  */
15
15
 
16
- import React, { useLayoutEffect, useRef, useState, type CSSProperties } from "react";
17
- import { createPortal } from "react-dom";
16
+ import React, { useState } from "react";
17
+ import * as Popover from "@radix-ui/react-popover";
18
18
  import * as Toggle from "@radix-ui/react-toggle";
19
19
  import * as Tooltip from "@radix-ui/react-tooltip";
20
20
  import {
@@ -154,6 +154,9 @@ export function TwRoleActionRegion(
154
154
  data-testid={props["data-testid"] ?? `role-action-region-${props.role}`}
155
155
  data-role={props.role}
156
156
  >
157
+ {props.role === "workflow" && props.workflowItem !== undefined ? (
158
+ <WorkflowActiveLabel item={props.workflowItem} />
159
+ ) : null}
157
160
  {inlineIds.map((id) => (
158
161
  <RoleActionButton
159
162
  key={id}
@@ -177,13 +180,14 @@ function isRoleActionRenderable(
177
180
  switch (id) {
178
181
  case "review-queue-prev":
179
182
  case "review-queue-next":
180
- case "review-queue-counts":
181
- case "review-queue-active-label":
182
183
  case "review-accept":
183
184
  case "review-reject":
184
185
  case "review-accept-all":
185
186
  case "review-reject-all":
186
187
  return reviewQueueTotal > 0;
188
+ case "review-queue-counts":
189
+ case "review-queue-active-label":
190
+ return props.reviewQueue !== undefined;
187
191
  case "workflow-prev":
188
192
  case "workflow-next":
189
193
  case "workflow-mark-complete":
@@ -433,100 +437,38 @@ function RoleActionOverflow({
433
437
  props,
434
438
  }: RoleActionOverflowProps): React.JSX.Element {
435
439
  const [open, setOpen] = useState(false);
436
- const triggerRef = useRef<HTMLButtonElement>(null);
437
440
 
438
441
  return (
439
- <>
442
+ <Popover.Root open={open} onOpenChange={setOpen}>
443
+ <Popover.Trigger asChild>
440
444
  <button
441
- ref={triggerRef}
442
445
  type="button"
443
446
  aria-label="More role actions"
444
447
  aria-expanded={open}
445
- aria-haspopup="menu"
446
448
  onMouseDown={preserveEditorSelectionMouseDown}
447
- onClick={(event) => {
448
- event.preventDefault();
449
- setOpen((value) => !value);
450
- }}
451
449
  title="More role actions"
452
- className={`inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas ${
453
- open ? "text-accent ring-1 ring-accent/30 shadow-sm" : ""
454
- }`}
450
+ className="inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
455
451
  data-testid="role-action-overflow-trigger"
456
452
  >
457
453
  <MoreHorizontal className="h-3.5 w-3.5 text-tertiary" aria-hidden="true" />
458
454
  </button>
459
- <RoleActionPortalMenu anchorRef={triggerRef} open={open}>
455
+ </Popover.Trigger>
456
+ <Popover.Portal>
457
+ <Popover.Content
458
+ className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
459
+ sideOffset={8}
460
+ align="start"
461
+ data-testid="role-action-overflow-content"
462
+ >
460
463
  {ids.map((id) => (
461
464
  <OverflowAction key={id} id={id} props={props} onClose={() => setOpen(false)} />
462
465
  ))}
463
- </RoleActionPortalMenu>
464
- </>
466
+ </Popover.Content>
467
+ </Popover.Portal>
468
+ </Popover.Root>
465
469
  );
466
470
  }
467
471
 
468
- function RoleActionPortalMenu(props: {
469
- anchorRef: React.RefObject<HTMLButtonElement | null>;
470
- children: React.ReactNode;
471
- open: boolean;
472
- }): React.ReactPortal | null {
473
- const style = useRoleActionPortalPosition(props.anchorRef, props.open);
474
- const body = props.anchorRef.current?.ownerDocument?.body;
475
- if (!props.open || !body) return null;
476
- return createPortal(
477
- <div
478
- className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
479
- data-testid="role-action-overflow-content"
480
- style={style}
481
- >
482
- {props.children}
483
- </div>,
484
- body,
485
- );
486
- }
487
-
488
- function useRoleActionPortalPosition(
489
- anchorRef: React.RefObject<HTMLButtonElement | null>,
490
- open: boolean,
491
- ): CSSProperties {
492
- const [style, setStyle] = useState<CSSProperties>({
493
- left: 8,
494
- position: "fixed",
495
- top: 8,
496
- zIndex: 50,
497
- });
498
-
499
- useLayoutEffect(() => {
500
- if (!open) return;
501
- const anchor = anchorRef.current;
502
- const ownerWindow = anchor?.ownerDocument?.defaultView;
503
- if (!anchor || !ownerWindow) return;
504
- const update = () => {
505
- const rect = anchor.getBoundingClientRect();
506
- const width = 220;
507
- const left = Math.min(
508
- Math.max(8, rect.left),
509
- Math.max(8, (ownerWindow.innerWidth || width + 16) - width - 8),
510
- );
511
- setStyle({
512
- left,
513
- position: "fixed",
514
- top: Math.max(8, rect.bottom + 8),
515
- zIndex: 50,
516
- });
517
- };
518
- update();
519
- ownerWindow.addEventListener("resize", update);
520
- ownerWindow.addEventListener("scroll", update, true);
521
- return () => {
522
- ownerWindow.removeEventListener("resize", update);
523
- ownerWindow.removeEventListener("scroll", update, true);
524
- };
525
- }, [anchorRef, open]);
526
-
527
- return style;
528
- }
529
-
530
472
  function OverflowAction(arg: {
531
473
  id: ToolbarChromeItemId;
532
474
  props: TwRoleActionRegionProps;
@@ -755,4 +697,34 @@ function ReviewActiveLabel({
755
697
  );
756
698
  }
757
699
 
700
+ function WorkflowActiveLabel({
701
+ item,
702
+ }: {
703
+ item: WorkflowWorkItemSnapshot | null;
704
+ }): React.JSX.Element {
705
+ const status = item?.status ?? "idle";
706
+ const statusLabel =
707
+ status === "done"
708
+ ? "Done"
709
+ : status === "blocked"
710
+ ? "Blocked"
711
+ : status === "active"
712
+ ? "Active"
713
+ : status === "pending"
714
+ ? "Pending"
715
+ : "Idle";
716
+
717
+ return (
718
+ <span
719
+ className="inline-flex min-w-0 items-center gap-1.5 truncate text-[11px] text-primary"
720
+ data-testid="role-workflow-active-label"
721
+ >
722
+ <span className="shrink-0 rounded-full bg-canvas px-1.5 py-0.5 text-[9px] font-medium text-secondary ring-1 ring-border/50">
723
+ {statusLabel}
724
+ </span>
725
+ <span className="truncate">{item?.title ?? "No active work item"}</span>
726
+ </span>
727
+ );
728
+ }
729
+
758
730
  export default TwRoleActionRegion;