@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
@@ -0,0 +1,259 @@
1
+ import * as React from "react";
2
+
3
+ import type {
4
+ ApiV3Ui,
5
+ GeometryRect,
6
+ UiOverlayLaneEntry,
7
+ UiOverlayLaneKind,
8
+ UiOverlayLaneSnapshot,
9
+ } from "../../api/v3/ui/index.ts";
10
+ import { AUTHOR_PALETTE } from "../../ui/headless/revision-decoration-model";
11
+ import { useUiApi } from "../ui-api-context.tsx";
12
+ import type { OverlayCoordinateSpace } from "./chrome-overlay-projector.ts";
13
+
14
+ type ReviewLaneKind = Extract<UiOverlayLaneKind, "comments" | "issues" | "redlines">;
15
+
16
+ const REVIEW_LANE_KINDS: readonly ReviewLaneKind[] = ["comments", "issues", "redlines"];
17
+ const ZERO_SPACE: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 };
18
+ const REDLINE_BAR_WIDTH_PX = 3;
19
+ const REDLINE_BAR_LEFT_OFFSET_PX = 8;
20
+
21
+ export interface TwReviewOverlayLaneMarkersProps {
22
+ space?: OverlayCoordinateSpace;
23
+ }
24
+
25
+ /**
26
+ * L11 painter for review overlay lanes surfaced by L10.
27
+ *
28
+ * L06 owns comment/issue/redline semantics and L05/L10 own anchor geometry.
29
+ * This layer only joins resolved lane entries to `ui.overlays.getAnchor(...)`
30
+ * rects and paints non-interactive markers; unavailable or rehydration-only
31
+ * entries stay invisible.
32
+ */
33
+ export function TwReviewOverlayLaneMarkers({
34
+ space = ZERO_SPACE,
35
+ }: TwReviewOverlayLaneMarkersProps): React.ReactElement | null {
36
+ const ui = useUiApi();
37
+ const [lanes, setLanes] = React.useState<Readonly<Record<ReviewLaneKind, UiOverlayLaneSnapshot | null>>>(() =>
38
+ readReviewLanes(ui),
39
+ );
40
+ const [overlayRevision, setOverlayRevision] = React.useState(0);
41
+
42
+ React.useEffect(() => {
43
+ if (!ui) {
44
+ setLanes(readReviewLanes(null));
45
+ return undefined;
46
+ }
47
+
48
+ setLanes(readReviewLanes(ui));
49
+ const unsubscribers: Array<() => void> = [];
50
+
51
+ for (const kind of REVIEW_LANE_KINDS) {
52
+ try {
53
+ unsubscribers.push(
54
+ ui.overlays.subscribeLane(kind, (lane) => {
55
+ setLanes((current) => ({ ...current, [kind]: lane }));
56
+ }),
57
+ );
58
+ } catch {
59
+ // A cold controller already reports `unavailable` through getLane.
60
+ }
61
+ }
62
+
63
+ try {
64
+ unsubscribers.push(
65
+ ui.overlays.subscribe(() => {
66
+ setOverlayRevision((revision) => revision + 1);
67
+ }),
68
+ );
69
+ } catch {
70
+ // Overlay invalidation is best-effort; lane updates still repaint.
71
+ }
72
+
73
+ return () => {
74
+ for (const unsubscribe of unsubscribers) unsubscribe();
75
+ };
76
+ }, [ui]);
77
+
78
+ const markers = React.useMemo(
79
+ () => resolveReviewLaneMarkers(ui, lanes, space),
80
+ [ui, lanes, overlayRevision, space],
81
+ );
82
+
83
+ if (markers.length === 0) return null;
84
+
85
+ return (
86
+ <div
87
+ aria-hidden="true"
88
+ className="wre-review-overlay-lane-markers pointer-events-none absolute inset-0"
89
+ data-review-overlay-lane-markers=""
90
+ data-overlay-revision={overlayRevision}
91
+ >
92
+ {markers.map((marker) => (
93
+ <div
94
+ className="wre-review-overlay-lane-marker pointer-events-none absolute flex items-center justify-center rounded-sm border bg-white text-[10px] font-semibold leading-none shadow-sm"
95
+ data-review-lane-kind={marker.kind}
96
+ data-review-lane-entry-id={marker.entryId}
97
+ data-review-lane-marker-shape={marker.shape}
98
+ key={`${marker.kind}:${marker.entryId}`}
99
+ style={marker.style}
100
+ title={marker.title}
101
+ >
102
+ {marker.label}
103
+ </div>
104
+ ))}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ interface ReviewLaneMarker {
110
+ kind: ReviewLaneKind;
111
+ entryId: string;
112
+ label: string;
113
+ shape: "badge" | "bar";
114
+ title: string;
115
+ style: React.CSSProperties;
116
+ }
117
+
118
+ export function resolveReviewLaneMarkers(
119
+ ui: ApiV3Ui | null,
120
+ lanes: Readonly<Record<ReviewLaneKind, UiOverlayLaneSnapshot | null>>,
121
+ space: OverlayCoordinateSpace = ZERO_SPACE,
122
+ ): ReviewLaneMarker[] {
123
+ if (!ui) return [];
124
+
125
+ const markers: ReviewLaneMarker[] = [];
126
+ for (const kind of REVIEW_LANE_KINDS) {
127
+ const lane = lanes[kind];
128
+ if (!lane || lane.kind !== kind || lane.status === "unavailable") continue;
129
+
130
+ for (const entry of lane.entries) {
131
+ if (entry.status !== "resolved" || !entry.anchor) continue;
132
+ const rect = ui.overlays.getAnchor(entry.anchor);
133
+ if (!rect) continue;
134
+ markers.push(markerFromEntry(kind, entry, rect, space));
135
+ }
136
+ }
137
+ return markers;
138
+ }
139
+
140
+ function readReviewLanes(
141
+ ui: ApiV3Ui | null,
142
+ ): Readonly<Record<ReviewLaneKind, UiOverlayLaneSnapshot | null>> {
143
+ if (!ui) {
144
+ return { comments: null, issues: null, redlines: null };
145
+ }
146
+ return {
147
+ comments: ui.overlays.getLane("comments"),
148
+ issues: ui.overlays.getLane("issues"),
149
+ redlines: ui.overlays.getLane("redlines"),
150
+ };
151
+ }
152
+
153
+ function markerFromEntry(
154
+ kind: ReviewLaneKind,
155
+ entry: UiOverlayLaneEntry,
156
+ rect: GeometryRect,
157
+ space: OverlayCoordinateSpace,
158
+ ): ReviewLaneMarker {
159
+ if (kind === "redlines") {
160
+ return redlineMarkerFromEntry(entry, rect, space);
161
+ }
162
+
163
+ const issueSeverity = stringValue(entry.data?.severity);
164
+ const color =
165
+ kind === "issues"
166
+ ? issueColor(issueSeverity)
167
+ : "var(--color-semantic-info, #2563eb)";
168
+ const title =
169
+ kind === "comments"
170
+ ? stringValue(entry.data?.excerpt) ?? stringValue(entry.data?.anchorLabel) ?? entry.id
171
+ : stringValue(entry.data?.title) ?? stringValue(entry.data?.label) ?? entry.id;
172
+
173
+ return {
174
+ kind,
175
+ entryId: entry.id,
176
+ label: kind === "comments" ? "C" : "!",
177
+ shape: "badge",
178
+ title,
179
+ style: {
180
+ left: `${rect.leftPx - space.originLeftPx + Math.max(rect.widthPx, 0) + 4}px`,
181
+ top: `${rect.topPx - space.originTopPx}px`,
182
+ width: "18px",
183
+ height: "18px",
184
+ color,
185
+ borderColor: color,
186
+ zIndex: 9,
187
+ },
188
+ };
189
+ }
190
+
191
+ function redlineMarkerFromEntry(
192
+ entry: UiOverlayLaneEntry,
193
+ rect: GeometryRect,
194
+ space: OverlayCoordinateSpace,
195
+ ): ReviewLaneMarker {
196
+ const color = revisionColor(entry);
197
+ return {
198
+ kind: "redlines",
199
+ entryId: entry.id,
200
+ label: "",
201
+ shape: "bar",
202
+ title:
203
+ stringValue(entry.data?.label) ??
204
+ stringValue(entry.data?.excerpt) ??
205
+ stringValue(entry.data?.revisionKind) ??
206
+ entry.id,
207
+ style: {
208
+ left: `${rect.leftPx - space.originLeftPx - REDLINE_BAR_LEFT_OFFSET_PX - REDLINE_BAR_WIDTH_PX}px`,
209
+ top: `${rect.topPx - space.originTopPx}px`,
210
+ width: `${REDLINE_BAR_WIDTH_PX}px`,
211
+ height: `${Math.max(rect.heightPx, 12)}px`,
212
+ backgroundColor: color,
213
+ borderColor: color,
214
+ borderRadius: "1px",
215
+ zIndex: 8,
216
+ },
217
+ };
218
+ }
219
+
220
+ function revisionColor(entry: UiOverlayLaneEntry): string {
221
+ const paletteIndex = numberValue(entry.data?.authorPaletteIndex);
222
+ if (paletteIndex !== null) {
223
+ return AUTHOR_PALETTE[paletteIndex % AUTHOR_PALETTE.length];
224
+ }
225
+ const seed = stringValue(entry.data?.authorId) ?? stringValue(entry.data?.revisionId) ?? entry.id;
226
+ return AUTHOR_PALETTE[hashString(seed) % AUTHOR_PALETTE.length];
227
+ }
228
+
229
+ function issueColor(severity: string | null): string {
230
+ switch (severity) {
231
+ case "high":
232
+ case "critical":
233
+ case "fatal":
234
+ return "var(--color-semantic-error, #dc2626)";
235
+ case "medium":
236
+ case "warning":
237
+ return "var(--color-semantic-warning, #d97706)";
238
+ default:
239
+ return "var(--color-semantic-info, #2563eb)";
240
+ }
241
+ }
242
+
243
+ function stringValue(value: unknown): string | null {
244
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
245
+ }
246
+
247
+ function numberValue(value: unknown): number | null {
248
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
249
+ }
250
+
251
+ function hashString(value: string): number {
252
+ let hash = 0;
253
+ for (let index = 0; index < value.length; index += 1) {
254
+ hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
255
+ }
256
+ return hash;
257
+ }
258
+
259
+ export default TwReviewOverlayLaneMarkers;
@@ -120,6 +120,9 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
120
120
 
121
121
  const getWorkflowScopeCardModel = React.useCallback(
122
122
  (scopeId: string): ScopeCardModel | null =>
123
+ // Compatibility fallback for no-provider tests/headless mounts.
124
+ // Mounted editor surfaces must prefer `api.ui.scope.card(...)`
125
+ // through `getVisibleScopeCardModel`.
123
126
  workflowFacet?.getAllScopeCardModels().find((m) => m.scopeId === scopeId) ??
124
127
  null,
125
128
  [workflowFacet],
@@ -177,7 +180,7 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
177
180
 
178
181
  const projectorSpace: OverlayCoordinateSpace =
179
182
  space ?? { originLeftPx: 0, originTopPx: 0 };
180
- const positioned = resolveCardPosition(
183
+ const positioned = resolveScopeCardPosition(
181
184
  model.primaryAnchorRect,
182
185
  projectorSpace,
183
186
  viewportTopPx,
@@ -246,7 +249,7 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
246
249
  * own shadow token tweaks) can key off to verify which placement the
247
250
  * layer chose.
248
251
  */
249
- function resolveCardPosition(
252
+ export function resolveScopeCardPosition(
250
253
  anchor: RenderFrameRect | null,
251
254
  space: OverlayCoordinateSpace,
252
255
  viewportTopPx: number,
@@ -0,0 +1,314 @@
1
+ import * as React from "react";
2
+
3
+ import type {
4
+ ApiV3Ui,
5
+ GeometryRect,
6
+ UiOverlayLaneEntry,
7
+ UiOverlayLaneKind,
8
+ UiOverlayLaneSnapshot,
9
+ } from "../../api/v3/ui/index.ts";
10
+ import { useUiApi } from "../ui-api-context.tsx";
11
+ import type { OverlayCoordinateSpace } from "./chrome-overlay-projector.ts";
12
+
13
+ type SubstrateLaneKind = Extract<
14
+ UiOverlayLaneKind,
15
+ | "selection"
16
+ | "caret"
17
+ | "field-scopes"
18
+ | "broad-scopes"
19
+ | "tables"
20
+ | "objects"
21
+ | "search"
22
+ >;
23
+
24
+ const SUBSTRATE_LANE_KINDS: readonly SubstrateLaneKind[] = [
25
+ "selection",
26
+ "caret",
27
+ "field-scopes",
28
+ "broad-scopes",
29
+ "tables",
30
+ "objects",
31
+ "search",
32
+ ];
33
+ const ZERO_SPACE: OverlayCoordinateSpace = { originLeftPx: 0, originTopPx: 0 };
34
+
35
+ export interface TwSubstrateOverlayLanesProps {
36
+ space?: OverlayCoordinateSpace;
37
+ }
38
+
39
+ /**
40
+ * L11 painter for the non-review L10 overlay lanes.
41
+ *
42
+ * L10 owns lane snapshots and L05 owns resolved rects/anchors. This component
43
+ * only paints entries already exposed through `ui.overlays`; unresolved,
44
+ * unavailable, anchorless, and unprojected entries remain invisible.
45
+ */
46
+ export function TwSubstrateOverlayLanes({
47
+ space = ZERO_SPACE,
48
+ }: TwSubstrateOverlayLanesProps): React.ReactElement | null {
49
+ const ui = useUiApi();
50
+ const [lanes, setLanes] = React.useState<Readonly<Record<SubstrateLaneKind, UiOverlayLaneSnapshot | null>>>(() =>
51
+ readSubstrateLanes(ui),
52
+ );
53
+ const [overlayRevision, setOverlayRevision] = React.useState(0);
54
+
55
+ React.useEffect(() => {
56
+ if (!ui) {
57
+ setLanes(readSubstrateLanes(null));
58
+ return undefined;
59
+ }
60
+
61
+ setLanes(readSubstrateLanes(ui));
62
+ const unsubscribers: Array<() => void> = [];
63
+
64
+ for (const kind of SUBSTRATE_LANE_KINDS) {
65
+ try {
66
+ unsubscribers.push(
67
+ ui.overlays.subscribeLane(kind, (lane) => {
68
+ setLanes((current) => ({ ...current, [kind]: lane }));
69
+ }),
70
+ );
71
+ } catch {
72
+ // Cold controllers report unavailable lanes through getLane.
73
+ }
74
+ }
75
+
76
+ try {
77
+ unsubscribers.push(
78
+ ui.overlays.subscribe(() => {
79
+ setOverlayRevision((revision) => revision + 1);
80
+ }),
81
+ );
82
+ } catch {
83
+ // Lane updates still repaint; coarse overlay ticks are best-effort.
84
+ }
85
+
86
+ return () => {
87
+ for (const unsubscribe of unsubscribers) unsubscribe();
88
+ };
89
+ }, [ui]);
90
+
91
+ const markers = React.useMemo(
92
+ () => resolveSubstrateLaneMarkers(ui, lanes, space),
93
+ [ui, lanes, overlayRevision, space],
94
+ );
95
+
96
+ if (markers.length === 0) return null;
97
+
98
+ return (
99
+ <div
100
+ aria-hidden="true"
101
+ className="wre-substrate-overlay-lanes pointer-events-none absolute inset-0"
102
+ data-substrate-overlay-lanes=""
103
+ data-overlay-revision={overlayRevision}
104
+ >
105
+ {markers.map((marker) => (
106
+ <div
107
+ className="wre-substrate-overlay-lane-marker pointer-events-none absolute"
108
+ data-substrate-lane-kind={marker.kind}
109
+ data-substrate-lane-entry-id={marker.entryId}
110
+ data-substrate-lane-shape={marker.shape}
111
+ key={marker.key}
112
+ style={marker.style}
113
+ title={marker.title}
114
+ />
115
+ ))}
116
+ </div>
117
+ );
118
+ }
119
+
120
+ interface SubstrateLaneMarker {
121
+ kind: SubstrateLaneKind;
122
+ entryId: string;
123
+ key: string;
124
+ shape: "rect" | "caret" | "outline" | "handle";
125
+ title: string;
126
+ style: React.CSSProperties;
127
+ }
128
+
129
+ export function resolveSubstrateLaneMarkers(
130
+ ui: ApiV3Ui | null,
131
+ lanes: Readonly<Record<SubstrateLaneKind, UiOverlayLaneSnapshot | null>>,
132
+ space: OverlayCoordinateSpace = ZERO_SPACE,
133
+ ): SubstrateLaneMarker[] {
134
+ if (!ui) return [];
135
+
136
+ const markers: SubstrateLaneMarker[] = [];
137
+ for (const kind of SUBSTRATE_LANE_KINDS) {
138
+ const lane = lanes[kind];
139
+ if (!lane || lane.kind !== kind || lane.status === "unavailable") continue;
140
+
141
+ for (const entry of lane.entries) {
142
+ if (entry.status !== "resolved") continue;
143
+ const rects = resolveEntryRects(ui, entry);
144
+ rects.forEach((rect, index) => {
145
+ markers.push(markerFromEntry(kind, entry, rect, index, space));
146
+ });
147
+ }
148
+ }
149
+ return markers;
150
+ }
151
+
152
+ function readSubstrateLanes(
153
+ ui: ApiV3Ui | null,
154
+ ): Readonly<Record<SubstrateLaneKind, UiOverlayLaneSnapshot | null>> {
155
+ if (!ui) {
156
+ return {
157
+ selection: null,
158
+ caret: null,
159
+ "field-scopes": null,
160
+ "broad-scopes": null,
161
+ tables: null,
162
+ objects: null,
163
+ search: null,
164
+ };
165
+ }
166
+ return {
167
+ selection: ui.overlays.getLane("selection"),
168
+ caret: ui.overlays.getLane("caret"),
169
+ "field-scopes": ui.overlays.getLane("field-scopes"),
170
+ "broad-scopes": ui.overlays.getLane("broad-scopes"),
171
+ tables: ui.overlays.getLane("tables"),
172
+ objects: ui.overlays.getLane("objects"),
173
+ search: ui.overlays.getLane("search"),
174
+ };
175
+ }
176
+
177
+ function resolveEntryRects(ui: ApiV3Ui, entry: UiOverlayLaneEntry): readonly GeometryRect[] {
178
+ if (entry.rects && entry.rects.length > 0) return entry.rects;
179
+ if (!entry.anchor) return [];
180
+ const rect = ui.overlays.getAnchor(entry.anchor);
181
+ return rect ? [rect] : [];
182
+ }
183
+
184
+ function markerFromEntry(
185
+ kind: SubstrateLaneKind,
186
+ entry: UiOverlayLaneEntry,
187
+ rect: GeometryRect,
188
+ index: number,
189
+ space: OverlayCoordinateSpace,
190
+ ): SubstrateLaneMarker {
191
+ const left = rect.leftPx - space.originLeftPx;
192
+ const top = rect.topPx - space.originTopPx;
193
+ const width = Math.max(rect.widthPx, kind === "caret" ? 2 : 1);
194
+ const height = Math.max(rect.heightPx, kind === "caret" ? 12 : 1);
195
+ const shape = markerShape(kind);
196
+ const color = markerColor(kind);
197
+ const title = stringValue(entry.data?.label) ?? stringValue(entry.data?.title) ?? entry.id;
198
+
199
+ return {
200
+ kind,
201
+ entryId: entry.id,
202
+ key: `${kind}:${entry.id}:${index}`,
203
+ shape,
204
+ title,
205
+ style: markerStyle({ kind, shape, left, top, width, height, color }),
206
+ };
207
+ }
208
+
209
+ function markerShape(kind: SubstrateLaneKind): SubstrateLaneMarker["shape"] {
210
+ switch (kind) {
211
+ case "caret":
212
+ return "caret";
213
+ case "field-scopes":
214
+ case "broad-scopes":
215
+ case "tables":
216
+ return "outline";
217
+ case "objects":
218
+ return "handle";
219
+ default:
220
+ return "rect";
221
+ }
222
+ }
223
+
224
+ function markerColor(kind: SubstrateLaneKind): string {
225
+ switch (kind) {
226
+ case "selection":
227
+ return "var(--color-selection, #93c5fd)";
228
+ case "caret":
229
+ return "var(--color-accent-primary, #2563eb)";
230
+ case "field-scopes":
231
+ return "var(--color-semantic-info, #2563eb)";
232
+ case "broad-scopes":
233
+ return "var(--color-semantic-success, #16a34a)";
234
+ case "tables":
235
+ return "var(--color-semantic-warning, #d97706)";
236
+ case "objects":
237
+ return "var(--color-accent-primary, #2563eb)";
238
+ case "search":
239
+ return "var(--color-search-highlight, #facc15)";
240
+ }
241
+ }
242
+
243
+ function markerStyle(input: {
244
+ kind: SubstrateLaneKind;
245
+ shape: SubstrateLaneMarker["shape"];
246
+ left: number;
247
+ top: number;
248
+ width: number;
249
+ height: number;
250
+ color: string;
251
+ }): React.CSSProperties {
252
+ const base: React.CSSProperties = {
253
+ left: `${input.left}px`,
254
+ top: `${input.top}px`,
255
+ width: `${input.width}px`,
256
+ height: `${input.height}px`,
257
+ zIndex: zIndexForKind(input.kind),
258
+ };
259
+
260
+ switch (input.shape) {
261
+ case "caret":
262
+ return {
263
+ ...base,
264
+ width: "2px",
265
+ minHeight: "12px",
266
+ backgroundColor: input.color,
267
+ borderRadius: "1px",
268
+ };
269
+ case "outline":
270
+ return {
271
+ ...base,
272
+ border: `1px solid ${input.color}`,
273
+ borderRadius: input.kind === "tables" ? "2px" : "3px",
274
+ backgroundColor: "transparent",
275
+ };
276
+ case "handle":
277
+ return {
278
+ ...base,
279
+ border: `1px solid ${input.color}`,
280
+ backgroundColor: "color-mix(in srgb, var(--color-bg-canvas, #ffffff) 75%, transparent)",
281
+ boxShadow: `0 0 0 1px ${input.color}`,
282
+ };
283
+ case "rect":
284
+ return {
285
+ ...base,
286
+ backgroundColor:
287
+ input.kind === "search"
288
+ ? "color-mix(in srgb, var(--color-search-highlight, #facc15) 45%, transparent)"
289
+ : "color-mix(in srgb, var(--color-selection, #93c5fd) 35%, transparent)",
290
+ borderRadius: "2px",
291
+ };
292
+ }
293
+ }
294
+
295
+ function zIndexForKind(kind: SubstrateLaneKind): number {
296
+ switch (kind) {
297
+ case "caret":
298
+ return 12;
299
+ case "objects":
300
+ return 11;
301
+ case "selection":
302
+ return 5;
303
+ case "search":
304
+ return 4;
305
+ default:
306
+ return 7;
307
+ }
308
+ }
309
+
310
+ function stringValue(value: unknown): string | null {
311
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
312
+ }
313
+
314
+ export default TwSubstrateOverlayLanes;
@@ -6,7 +6,10 @@ runtime API call, and `ChromePosture.debugMode` is production-forced to
6
6
  `"off"`.
7
7
 
8
8
  The supported local diagnostic surface is the mounted runtime REPL keyboard
9
- shortcut. These components remain in the tree for internal harness/story
9
+ shortcut. Open it with `Cmd/Ctrl+Alt+P`; its `Agent layer view` shares the
10
+ L01-L11 layer map with the harness debug pane, including `Alt+1`...`Alt+9`,
11
+ `Alt+0`, and `Alt+-` navigation plus snippets that evaluate against
12
+ `runtime`/`ref`. These components remain in the tree for internal harness/story
10
13
  experiments only; do not wire them into `WordReviewEditor` or public API paths.
11
14
 
12
15
  ## Files