@beyondwork/docx-react-component 1.0.95 → 1.0.97

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +33 -19
  3. package/src/api/v3/ui/_types.ts +11 -21
  4. package/src/api/v3/ui/chrome.ts +8 -9
  5. package/src/api/v3/ui/debug.ts +15 -77
  6. package/src/api/v3/ui/overlays-visibility.ts +9 -10
  7. package/src/api/v3/ui/overlays.ts +8 -75
  8. package/src/io/ooxml/parse-main-document.ts +30 -0
  9. package/src/io/ooxml/parse-picture.ts +14 -0
  10. package/src/io/ooxml/parse-shapes.ts +41 -1
  11. package/src/model/canonical-document.ts +17 -0
  12. package/src/runtime/document-runtime.ts +46 -1
  13. package/src/runtime/layout/layout-engine-version.ts +8 -1
  14. package/src/runtime/layout/page-story-resolver.ts +1 -0
  15. package/src/runtime/layout/paginated-layout-engine.ts +26 -10
  16. package/src/runtime/surface-projection.ts +114 -12
  17. package/src/runtime/workflow/rail/compose.ts +5 -0
  18. package/src/ui/WordReviewEditor.tsx +6 -10
  19. package/src/ui/editor-command-bag.ts +2 -0
  20. package/src/ui/ui-controller-factory.ts +2 -2
  21. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +12 -41
  22. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +3 -7
  23. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
  24. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -228
  25. package/src/ui-tailwind/debug/README.md +12 -50
  26. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
  27. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
  28. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
  29. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
  30. package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
  31. package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
  32. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
  33. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
  34. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +53 -53
  35. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
  36. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
  37. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
  38. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
  39. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
  40. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  41. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
  42. package/src/ui-tailwind/theme/editor-theme.css +82 -84
  43. package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
@@ -14,7 +14,7 @@
14
14
 
15
15
  import * as React from "react";
16
16
  import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
17
- import type { ScopeRailPosture, ScopeRailSegment } from "../../api/public-types.ts";
17
+ import type { ScopeRailSegment } from "../../api/public-types.ts";
18
18
  import type {
19
19
  EditorRole,
20
20
  EditorStoryTarget,
@@ -23,12 +23,10 @@ import type {
23
23
  WordReviewEditorLayoutFacet,
24
24
  WorkflowScopeMode,
25
25
  } from "../../api/public-types";
26
- import { TwScopeRailLayer } from "./tw-scope-rail-layer";
27
26
  import { TwScopeCardLayer } from "./tw-scope-card-layer";
28
27
  import { TwPageStackChromeLayer, type PmPortalView } from "../page-stack/tw-page-stack-chrome-layer";
29
28
  import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
30
29
  import { TwObjectSelectionOverlay } from "./tw-object-selection-overlay";
31
- import { useUiApi } from "../ui-api-context";
32
30
 
33
31
  export interface TwChromeOverlayProps {
34
32
  /** Layout facet the overlay layers read from (layout-semantic data). */
@@ -49,17 +47,13 @@ export interface TwChromeOverlayProps {
49
47
  space?: OverlayCoordinateSpace;
50
48
  /** Active scope id (for emphasis + rail tab sync). */
51
49
  activeScopeId?: string | null;
52
- /** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
53
- visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
54
50
  /**
55
- * Click handler fired when the user clicks a scope rail stripe.
56
- * P0 wires this to open the scope card (P1 ships the card layer).
51
+ * Deprecated no-op. Scope rails are no longer visible in the product
52
+ * overlay; scoped text itself is the visible affordance.
57
53
  */
58
54
  onScopeStripeClick?: (segment: ScopeRailSegment) => void;
59
55
  /**
60
- * Legacy click handler kept for compatibility with consumers that
61
- * subscribed before the stripe affordance existed. Called alongside
62
- * `onScopeStripeClick` on a stripe click.
56
+ * Deprecated no-op kept for prop compatibility with pre-inline-scope hosts.
63
57
  */
64
58
  onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
65
59
  /**
@@ -185,6 +179,12 @@ export interface TwChromeOverlayProps {
185
179
  * See `useVisiblePageIndexRange` in `src/ui-tailwind/page-stack/use-visible-block-range.ts`.
186
180
  */
187
181
  visiblePageIndexRange?: { start: number; end: number } | null;
182
+ /**
183
+ * Visual-fidelity/chrome-less hosts still need page-stack measurement and
184
+ * story content, but they should not paint editor-only header/footer band
185
+ * tints over the captured document.
186
+ */
187
+ plainPageBands?: boolean;
188
188
  /** Preview catalog threaded into the page-stack chrome so header /
189
189
  * footer / footnote / endnote regions render real <img>s. */
190
190
  mediaPreviews?: Record<string, import("../editor-surface/pm-state-from-snapshot").MediaPreviewDescriptor>;
@@ -215,7 +215,6 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
215
215
  geometryFacet,
216
216
  space,
217
217
  activeScopeId,
218
- visibleScopePostures,
219
218
  onScopeStripeClick,
220
219
  onScopeSegmentClick,
221
220
  onScopeCardClose,
@@ -242,28 +241,10 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
242
241
  pmSurfaceElement,
243
242
  pmView,
244
243
  visiblePageIndexRange,
244
+ plainPageBands,
245
245
  mediaPreviews,
246
246
  activeBandRibbonProps,
247
247
  }) => {
248
- const ui = useUiApi();
249
- const scopeRailSegments = React.useMemo(
250
- () =>
251
- ui?.scope.rail().segments ??
252
- workflowFacet?.getAllRailSegments() ??
253
- [],
254
- [ui, workflowFacet, renderFrameRevision],
255
- );
256
- const visibleScopeIds = React.useMemo(() => {
257
- if (!visibleScopePostures) return undefined;
258
- const ids = new Set<string>();
259
- for (const segment of scopeRailSegments) {
260
- if (visibleScopePostures.has(segment.posture)) {
261
- ids.add(segment.scopeId);
262
- }
263
- }
264
- return ids;
265
- }, [scopeRailSegments, visibleScopePostures]);
266
-
267
248
  return (
268
249
  <div
269
250
  className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
@@ -281,25 +262,15 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
281
262
  pmSurfaceElement={pmSurfaceElement}
282
263
  pmView={pmView}
283
264
  visiblePageIndexRange={visiblePageIndexRange ?? null}
265
+ plainPageBands={plainPageBands ?? false}
284
266
  mediaPreviews={mediaPreviews}
285
267
  activeBandRibbonProps={activeBandRibbonProps ?? null}
286
268
  />
287
269
  ) : null}
288
- <TwScopeRailLayer
289
- geometryFacet={geometryFacet}
290
- workflowFacet={workflowFacet}
291
- scopeRailSegments={scopeRailSegments}
292
- space={space}
293
- activeScopeId={activeScopeId}
294
- visibleScopePostures={visibleScopePostures}
295
- onStripeClick={onScopeStripeClick}
296
- onSegmentClick={onScopeSegmentClick}
297
- />
298
270
  <TwScopeCardLayer
299
271
  facet={facet}
300
272
  workflowFacet={workflowFacet}
301
273
  activeScopeId={activeScopeId ?? null}
302
- visibleScopeIds={visibleScopeIds}
303
274
  onClose={onScopeCardClose ?? noop}
304
275
  onModeChange={onScopeCardModeChange ?? noopModeChange}
305
276
  onIssueAction={onScopeCardIssueAction ?? noopIssueAction}
@@ -3,8 +3,8 @@
3
3
  * layer resolves which card is visible from two inputs:
4
4
  *
5
5
  * 1. `activeScopeId` — the currently opened scope, set by the
6
- * workspace when the user clicks a rail stripe. Resets on
7
- * click-outside / Escape / close button.
6
+ * workspace or host workflow chrome. Resets on click-outside /
7
+ * Escape / close button.
8
8
  * 2. internal `pinnedScopeId` — when the user clicks the pin button
9
9
  * on a card, that scope persists across `activeScopeId` changes
10
10
  * until explicitly unpinned or the scope disappears from the
@@ -53,8 +53,6 @@ export interface TwScopeCardLayerProps {
53
53
  */
54
54
  workflowFacet: WorkflowFacet | null;
55
55
  activeScopeId: string | null;
56
- /** Scope ids currently visible under the Workflow rail layer filters. */
57
- visibleScopeIds?: ReadonlySet<string>;
58
56
  onClose: () => void;
59
57
  onModeChange: (scopeId: string, mode: WorkflowScopeMode) => void;
60
58
  onIssueAction: (
@@ -94,7 +92,6 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
94
92
  facet,
95
93
  workflowFacet,
96
94
  activeScopeId,
97
- visibleScopeIds,
98
95
  onClose,
99
96
  onModeChange,
100
97
  onIssueAction,
@@ -131,11 +128,10 @@ export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
131
128
  const getVisibleScopeCardModel = React.useCallback(
132
129
  (scopeId: string | null): ScopeCardModel | null => {
133
130
  if (!scopeId) return null;
134
- if (visibleScopeIds && !visibleScopeIds.has(scopeId)) return null;
135
131
  if (ui) return ui.scope.card(scopeId);
136
132
  return getWorkflowScopeCardModel(scopeId);
137
133
  },
138
- [getWorkflowScopeCardModel, ui, visibleScopeIds],
134
+ [getWorkflowScopeCardModel, ui],
139
135
  );
140
136
 
141
137
  // The effective scope is the pinned one if it still resolves to a
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * TwScopeCard — inline floating card shown above a scoped region when
3
- * the user activates the rail stripe. Displays scope label + mode
3
+ * the user activates that scope. Displays scope label + mode
4
4
  * selector + (when an `IssueMetadataValue` is attached via
5
5
  * `ScopeCardModel.issue`) the R2 issue severity, owner, title, and
6
6
  * resolve/waive/escalate actions.
@@ -1,25 +1,23 @@
1
1
  /**
2
- * Scope rail layer — renders workflow scopes as a thin color stripe in
3
- * the reserved left-gutter lane plus a per-line flat tint behind the
4
- * scoped text runs.
2
+ * Scope rail layer — intentionally non-rendering in product chrome.
3
+ *
4
+ * Scoped text is now the only visible rest-state affordance for a set
5
+ * scope. The old gutter rail created duplicate markers, drifted away
6
+ * from the scoped text, and surfaced invisible/implementation scopes in
7
+ * production. Keep this component as a compatibility shim plus a home
8
+ * for the pure geometry helpers used by tests.
5
9
  *
6
10
  * Per runtime-rendering-and-chrome-phase.md §5 and
7
- * docs/plans/scope-card-overlay.md P0, the rail is a projection over
8
- * canonical workflow scopes; it never lives inside the PM NodeView
9
- * tree. Positions come from the render kernel's per-line block data
10
- * (walked directly from `RenderFrame.pages[].regions.body.blocks[]`)
11
- * so multi-line scopes produce one tight tint per line rather than a
12
- * fat bounding-box union.
11
+ * docs/plans/scope-card-overlay.md P0, historical rail geometry came
12
+ * from the render kernel's per-line block data. The helpers remain for
13
+ * callers that need geometry, but the React layer no longer paints or
14
+ * reads scope data.
13
15
  */
14
16
 
15
17
  import * as React from "react";
16
- import {
17
- projectRectToOverlay,
18
- type OverlayCoordinateSpace,
19
- } from "./chrome-overlay-projector";
20
- import { useUiApi } from "../ui-api-context";
18
+ import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
21
19
  import type { RenderFrame, RenderFrameRect } from "../../api/public-types.ts";
22
- import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types.ts";
20
+ import type { ScopeRailSegment } from "../../api/public-types.ts";
23
21
  import type { WorkflowFacet } from "../../api/public-types.ts";
24
22
  import type { GeometryFacet } from "../../api/public-types";
25
23
 
@@ -29,21 +27,18 @@ import type { GeometryFacet } from "../../api/public-types";
29
27
 
30
28
  export interface TwScopeRailLayerProps {
31
29
  /**
32
- * Geometry facet used for `getRenderFrame()`. Migrated from
33
- * `facet: WordReviewEditorLayoutFacet` in refactor/05 cross-lane-coord
34
- * §8.4 pass. Mounted rail/card data flows through `api.ui.scope.*`.
30
+ * Legacy prop kept for compatibility. The component no longer reads
31
+ * geometry because it does not paint the rail.
35
32
  */
36
33
  geometryFacet: GeometryFacet;
37
34
  /**
38
- * Workflow facet no-provider fallback for scope rail/card reads.
39
- * Mounted editor paths prefer `api.ui.scope.*`; passing `null` makes
40
- * fallback reads no-op.
35
+ * Legacy prop kept for compatibility. The component intentionally does
36
+ * not read workflow scope data.
41
37
  */
42
38
  workflowFacet: WorkflowFacet | null;
43
39
  /**
44
- * Optional pre-read rail snapshot from `ui.scope.rail()`. When omitted,
45
- * the layer reads the mounted UI API itself, then falls back to the
46
- * workflow facet for no-provider paths.
40
+ * Legacy pre-read snapshot. Ignored so invisible implementation scopes
41
+ * cannot surface through the old rail path.
47
42
  */
48
43
  scopeRailSegments?: readonly ScopeRailSegment[];
49
44
  /** Overlay's coordinate space. Defaults to the overlay's own origin. */
@@ -52,224 +47,23 @@ export interface TwScopeRailLayerProps {
52
47
  railLaneWidthPx?: number;
53
48
  /** Scope id that should render with the `active` emphasis. */
54
49
  activeScopeId?: string | null;
55
- /** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
56
- visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
57
50
  /**
58
- * Fires when the user clicks the rail stripe — opens the scope card.
59
- * P0 wires this directly; P1 replaces with card-layer-aware routing.
51
+ * Deprecated no-op. Scoped text selection/card flows replace rail clicks.
60
52
  */
61
53
  onStripeClick?: (segment: ScopeRailSegment) => void;
62
54
  /**
63
- * Legacy click handler kept for existing consumers. Called alongside
64
- * `onStripeClick` so host apps that subscribed to the pre-stripe API
65
- * continue to receive clicks.
55
+ * Deprecated no-op kept for existing consumers.
66
56
  */
67
57
  onSegmentClick?: (segment: ScopeRailSegment) => void;
68
58
  /** Test id applied to the layer root. */
69
59
  "data-testid"?: string;
70
60
  }
71
61
 
72
- // ---------------------------------------------------------------------------
73
- // Posture → visual grammar
74
- // ---------------------------------------------------------------------------
75
-
76
- interface PostureStyle {
77
- labelText: string;
78
- icon: string;
79
- railToken: string;
80
- tintToken: string;
81
- }
82
-
83
- const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
84
- edit: { labelText: "EDIT", icon: "pencil", railToken: "accent", tintToken: "accent" },
85
- suggest: { labelText: "SUGGEST", icon: "sparkles", railToken: "warning", tintToken: "warning" },
86
- comment: { labelText: "COMMENT", icon: "message", railToken: "insert", tintToken: "insert" },
87
- view: { labelText: "IN SCOPE", icon: "eye", railToken: "secondary", tintToken: "secondary" },
88
- candidate: { labelText: "PROPOSED", icon: "flag", railToken: "warning", tintToken: "warning" },
89
- "preserve-only": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
90
- "blocked-import": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
91
- };
92
-
93
62
  // ---------------------------------------------------------------------------
94
63
  // Component
95
64
  // ---------------------------------------------------------------------------
96
65
 
97
- const DEFAULT_RAIL_LANE_PX = 44;
98
- const STRIPE_WIDTH_PX = 6;
99
- const LABEL_WIDTH_PX = 58;
100
- const STACK_OFFSET_PX = 6;
101
-
102
- export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
103
- geometryFacet,
104
- workflowFacet,
105
- scopeRailSegments,
106
- space,
107
- railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
108
- activeScopeId,
109
- visibleScopePostures,
110
- onStripeClick,
111
- onSegmentClick,
112
- "data-testid": testId,
113
- }) => {
114
- const ui = useUiApi();
115
- const frame = geometryFacet.getRenderFrame() ?? null;
116
- const railSegments =
117
- scopeRailSegments ??
118
- ui?.scope.rail().segments ??
119
- workflowFacet?.getAllRailSegments() ??
120
- [];
121
- const segments = railSegments.filter((segment) =>
122
- visibleScopePostures ? visibleScopePostures.has(segment.posture) : true,
123
- );
124
-
125
- if (!frame || segments.length === 0) {
126
- return null;
127
- }
128
-
129
- // P2: which scopes currently have a `source: "ai"` candidate
130
- // overlapping — drives the agent-pending shimmer class on their
131
- // tints. Mounted surfaces read card projections through ui.scope.card;
132
- // no-provider paths fall back to the workflow facet.
133
- const cardModels = ui ? [] : workflowFacet?.getAllScopeCardModels() ?? [];
134
- const agentPendingByScope = new Map<string, boolean>();
135
- for (const model of cardModels) {
136
- if (model.agentPending) {
137
- agentPendingByScope.set(model.scopeId, true);
138
- }
139
- }
140
- if (ui) {
141
- for (const segment of segments) {
142
- const model = ui.scope.card(segment.scopeId);
143
- if (model?.agentPending) {
144
- agentPendingByScope.set(segment.scopeId, true);
145
- }
146
- }
147
- }
148
-
149
- // P3c: stack offsets for overlapping scopes. Two scopes whose
150
- // offset ranges intersect on the same page render as stacked
151
- // stripes in the gutter; the inner stripe shifts STACK_OFFSET_PX
152
- // further right per overlap count so all are clickable.
153
- const stackIndexByScope = computeStackIndices(segments);
154
-
155
- const projectorSpace: OverlayCoordinateSpace = space ?? { originLeftPx: 0, originTopPx: 0 };
156
-
157
- return (
158
- <div
159
- className="wre-scope-rail-layer pointer-events-none absolute inset-0 z-20"
160
- data-testid={testId ?? "scope-rail-layer"}
161
- aria-hidden="false"
162
- role="group"
163
- aria-label="Workflow scope rail"
164
- >
165
- {segments.map((segment) => {
166
- const style = POSTURE_STYLES[segment.posture];
167
- const lineRects = collectLineRectsForSegment(frame, segment);
168
- if (lineRects.length === 0) return null;
169
-
170
- const isActive =
171
- activeScopeId === segment.scopeId || segment.isActiveWorkItem;
172
-
173
- // Stripe + label span the vertical range of the scope's lines;
174
- // they live in the gutter lane to the left of the first line.
175
- const firstLine = lineRects[0];
176
- const lastLine = lineRects[lineRects.length - 1];
177
- const stripeTopPx = firstLine.topPx;
178
- const stripeHeightPx =
179
- lastLine.topPx + lastLine.heightPx - firstLine.topPx;
180
- const stackIndex = stackIndexByScope.get(segment.scopeId) ?? 0;
181
- const stackOffset = stackIndex * STACK_OFFSET_PX;
182
- const stripeRect: RenderFrameRect = {
183
- leftPx:
184
- firstLine.leftPx
185
- - railLaneWidthPx
186
- + (railLaneWidthPx - STRIPE_WIDTH_PX) / 2
187
- + stackOffset,
188
- topPx: stripeTopPx,
189
- widthPx: STRIPE_WIDTH_PX,
190
- heightPx: Math.max(stripeHeightPx, 14),
191
- };
192
- const labelRect: RenderFrameRect = {
193
- leftPx: firstLine.leftPx - railLaneWidthPx + stackOffset,
194
- topPx: stripeTopPx,
195
- widthPx: LABEL_WIDTH_PX,
196
- heightPx: 20,
197
- };
198
-
199
- const handleActivate = () => {
200
- onStripeClick?.(segment);
201
- onSegmentClick?.(segment);
202
- };
203
- const handleStripeKey = (event: React.KeyboardEvent<HTMLButtonElement>) => {
204
- if (event.key === "Enter" || event.key === " ") {
205
- event.preventDefault();
206
- handleActivate();
207
- }
208
- };
209
-
210
- return (
211
- <React.Fragment key={`${segment.scopeId}:${segment.pageIndex}:${segment.fromOffset}`}>
212
- {/* Per-line tint behind the scoped text runs. */}
213
- {lineRects.map((lineRect, index) => {
214
- const agentPending = agentPendingByScope.get(segment.scopeId) === true;
215
- const tintClassList = [
216
- "wre-scope-rail-tint",
217
- `wre-scope-rail-tint-${style.tintToken}`,
218
- ];
219
- if (isActive) tintClassList.push("wre-scope-rail-tint-active");
220
- if (agentPending) {
221
- tintClassList.push("wre-scope-rail-tint-agent-pending");
222
- }
223
- return (
224
- <div
225
- key={`tint:${index}`}
226
- className={tintClassList.join(" ")}
227
- data-scope-id={segment.scopeId}
228
- data-posture={segment.posture}
229
- data-line-index={index}
230
- data-agent-pending={agentPending ? "true" : undefined}
231
- style={projectRectToOverlay(lineRect, projectorSpace)}
232
- />
233
- );
234
- })}
235
- {/* Rail stripe in the gutter. */}
236
- <button
237
- type="button"
238
- className={`wre-scope-rail-stripe wre-scope-rail-label-${style.railToken} ${
239
- isActive ? "wre-scope-rail-stripe-active" : ""
240
- }`}
241
- data-scope-id={segment.scopeId}
242
- data-posture={segment.posture}
243
- data-stack-index={stackIndex}
244
- data-testid="scope-rail-stripe"
245
- aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
246
- aria-expanded={isActive ? "true" : "false"}
247
- onClick={handleActivate}
248
- onKeyDown={handleStripeKey}
249
- style={projectRectToOverlay(stripeRect, projectorSpace)}
250
- />
251
- {/* Label pill — revealed on stripe hover via CSS. */}
252
- <button
253
- type="button"
254
- tabIndex={-1}
255
- className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken} ${
256
- isActive ? "wre-scope-rail-label-active" : ""
257
- }`}
258
- data-scope-id={segment.scopeId}
259
- data-posture={segment.posture}
260
- aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
261
- onClick={handleActivate}
262
- style={projectRectToOverlay(labelRect, projectorSpace)}
263
- >
264
- <span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
265
- <span className="wre-scope-rail-label-text">{style.labelText}</span>
266
- </button>
267
- </React.Fragment>
268
- );
269
- })}
270
- </div>
271
- );
272
- };
66
+ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = (_props) => null;
273
67
 
274
68
  // ---------------------------------------------------------------------------
275
69
  // Internals
@@ -1,57 +1,19 @@
1
- # Phase Q — Debug UX (reserved)
1
+ # Debug UX (internal only)
2
2
 
3
- Target for the runtime-embedded debug UX consumed through `ui.debug.attach()`
4
- (layer 10, refactor/10 Slice 5). This directory is **reserved** — the Slice 5
5
- ship includes the UI API substrate (`src/api/v3/ui/debug.ts` +
6
- `UiController.attachDebug` hook), while the React presentation components +
7
- the public `debugMode` prop + the visibility invariant test are a **Phase Q
8
- follow-up** per `docs/plans/refactor/10-ui-api.md` Risk #3:
3
+ Runtime debug chrome is not a production API surface. `WordReviewEditor` does
4
+ not expose a `debugMode` prop, `api.ui.debug.attach()` is a hard-disabled
5
+ runtime API call, and `ChromePosture.debugMode` is production-forced to
6
+ `"off"`.
9
7
 
10
- > **Phase Q UX surface area.** `src/ui-tailwind/debug/**` is a new substantial
11
- > UI surface. *Mitigation:* ship substrate in M4 Slice 5; polish in follow-up.
8
+ The supported local diagnostic surface is the mounted runtime REPL keyboard
9
+ shortcut. These components remain in the tree for internal harness/story
10
+ experiments only; do not wire them into `WordReviewEditor` or public API paths.
12
11
 
13
- ## Planned files (follow-up)
12
+ ## Files
14
13
 
15
14
  | File | Purpose |
16
15
  |---|---|
17
- | `tw-debug-top-bar.tsx` | Minimal top bar when `debugMode: "top-bar"` — document hash, scope count, invalidation counter |
18
- | `tw-debug-overlay.tsx` | Full overlay when `debugMode: "full"` — renders `DebugInspectorSnapshot` sections |
19
- | `tw-debug-repl.tsx` | REPL with `api`, `runtime`, `debug` in scope; localStorage-persisted history |
16
+ | `tw-debug-top-bar.tsx` | Internal minimal top bar for harness/story experiments |
17
+ | `tw-debug-overlay.tsx` | Internal overlay rendering `DebugInspectorSnapshot` sections |
18
+ | `tw-debug-repl.tsx` | Legacy placeholder; production diagnostics use `tw-runtime-repl-dialog.tsx` |
20
19
  | `tw-debug-event-tail.tsx` | Tail of the last N events on active telemetry channels |
21
- | `keybindings.ts` | Cmd/Ctrl+Shift+D toggles top-bar ↔ full |
22
-
23
- ## Planned prop (follow-up)
24
-
25
- Add `debugMode?: "off" | "top-bar" | "full"` to `WordReviewEditorProps`.
26
- **Default MUST be `"off"`** per CLAUDE.md Protected Invariants § "Phase Q
27
- placeholder" — this default has regressed multiple times in predecessor
28
- surfaces (`showUnsupportedObjectPreviews`, `unsupportedPreviewsPolicy`).
29
-
30
- ## Planned test (follow-up)
31
-
32
- `test/ui/debug-mode-visibility-invariant.test.ts` — asserts:
33
- - `debugMode: "off"` → no debug UI renders
34
- - `debugMode: "top-bar"` → only the top bar renders
35
- - `debugMode: "full"` → overlay renders
36
-
37
- ## How Slice 5 consumers wire today
38
-
39
- Until the Phase Q React components land, Slice 5 ships only the contract:
40
-
41
- ```ts
42
- import { createUiApi } from "@beyondwork/docx-react-component/api/v3/ui";
43
-
44
- const ui = createUiApi(handle);
45
- ui.session.bind({
46
- kind: "runtime-direct",
47
- id: "my-mount",
48
- attachDebug(session) {
49
- // bind-side: wire runtime.debug.bus + getSnapshot to your debug surface
50
- const off = runtime.debug.bus.on(/* ... */, (evt) => {/* render */});
51
- return () => off();
52
- },
53
- });
54
- const attachment = ui.debug.attach({ id: "s1", channels: ["api", "render"] });
55
- // later:
56
- attachment.detach();
57
- ```
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Phase Q — Debug overlay.
3
3
  *
4
- * Full-screen tabbed panel shown when
5
- * `WordReviewEditorProps.debugMode === "full"`. Renders the top bar
6
- * (reused from `tw-debug-top-bar.tsx`) + three content panes:
4
+ * Internal full-screen tabbed panel for harness/story experiments.
5
+ * Production `WordReviewEditor` does not mount this component. Renders
6
+ * the top bar (reused from `tw-debug-top-bar.tsx`) + three content panes:
7
7
  *
8
8
  * - **Inspector** — pretty-prints the bound `DebugInspectorSnapshot`
9
9
  * (canonical / layout / geometry / scope / review / diagnostics
@@ -32,8 +32,8 @@ export interface TwDebugOverlayProps {
32
32
  sessionId: string | null;
33
33
  snapshot: TwDebugTopBarProps["snapshot"];
34
34
  /**
35
- * Raw `DebugInspectorSnapshot` the host wired through
36
- * `ui.debug.attach()`. Rendered as pretty-printed JSON in the
35
+ * Raw `DebugInspectorSnapshot` supplied by an internal harness/story.
36
+ * Rendered as pretty-printed JSON in the
37
37
  * Inspector pane. Large snapshots are clipped to the first 16 KB
38
38
  * to keep the DOM responsive.
39
39
  */
@@ -126,7 +126,7 @@ export function TwDebugOverlay(props: TwDebugOverlayProps): React.ReactElement {
126
126
  <ul className="flex flex-col gap-0.5 font-mono text-[10px] text-[var(--color-text-secondary)]">
127
127
  {(props.events ?? []).length === 0 ? (
128
128
  <li className="italic text-[var(--color-text-tertiary)]">
129
- (no events — wire `ui.debug.attach()` with a DebugBus listener)
129
+ (no events)
130
130
  </li>
131
131
  ) : (
132
132
  props.events!.slice(-200).map((e, i) => (
@@ -1,34 +1,24 @@
1
1
  /**
2
2
  * Phase Q — DebugMode router.
3
3
  *
4
- * Single mount point `WordReviewEditor.tsx` consumes. Picks between
5
- * the minimal top-bar, the full overlay, or nothing depending on the
6
- * public `debugMode` prop. Keeping the router in one file means the
7
- * invariant test + CI guard can target a single surface.
8
- *
9
- * Data flow (consumers wire through `ui.debug.attach()` per refactor/10
10
- * Slice 5):
11
- *
12
- * host → `ui.debug.attach({ id, channels })` →
13
- * controller.attachDebug(session) →
14
- * { sessionId, snapshot, events } → this component
15
- *
16
- * Slice 7 ships the visibility scaffold; the full REPL + event-tail
17
- * wiring to `runtime.debug.bus` + `DebugInspectorSnapshot` lives in a
18
- * follow-up (see `src/ui-tailwind/debug/README.md`).
4
+ * Internal-only debug presentation router. Production `WordReviewEditor`
5
+ * does not mount this component and does not expose a prop or runtime API
6
+ * that can enable it; the supported local diagnostic surface is the runtime
7
+ * REPL keyboard shortcut.
19
8
  */
20
9
 
21
10
  import React from "react";
22
11
 
23
- import type { DebugMode } from "../../api/public-types.ts";
24
12
  import { TwDebugTopBar, type TwDebugTopBarProps } from "./tw-debug-top-bar.tsx";
25
13
  import { TwDebugOverlay, type TwDebugOverlayProps } from "./tw-debug-overlay.tsx";
26
14
 
15
+ export type DebugMode = "off" | "top-bar" | "full";
16
+
27
17
  export interface TwDebugPresentationProps {
28
18
  mode: DebugMode;
29
19
  /**
30
- * Debug session id mirrors what the host passed into
31
- * `ui.debug.attach({ id })`. `null` when no attachment is active.
20
+ * Debug session id for internal harness/storybook experiments.
21
+ * Production runtime mounting never supplies an active attachment.
32
22
  */
33
23
  sessionId?: string | null;
34
24
  snapshot?: TwDebugTopBarProps["snapshot"];
@@ -36,8 +26,7 @@ export interface TwDebugPresentationProps {
36
26
  events?: TwDebugOverlayProps["events"];
37
27
  onReplEval?: TwDebugOverlayProps["onReplEval"];
38
28
  /**
39
- * Optional override for the mode toggle — host supplies when it
40
- * owns the `debugMode` state (e.g. harness with a keyboard shortcut).
29
+ * Optional override for internal harness/storybook mode toggles.
41
30
  */
42
31
  onModeChange?: (next: DebugMode) => void;
43
32
  }
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Phase Q — Debug top bar.
3
3
  *
4
- * Minimal always-on status strip shown when
5
- * `WordReviewEditorProps.debugMode === "top-bar"` (or as the header of
6
- * the "full" overlay). Renders the bound debug session id + a small
7
- * set of counters derived from the `DebugInspectorSnapshot` the host
8
- * supplies through `ui.debug.attach()` wiring.
4
+ * Internal status strip for harness/story experiments. Production
5
+ * `WordReviewEditor` does not mount this component or expose a public
6
+ * debugMode prop/API. Renders the bound debug session id + a small set
7
+ * of counters derived from a supplied `DebugInspectorSnapshot`.
9
8
  *
10
9
  * Slice 7 ships the minimal surface; the REPL + event-tail panes live
11
10
  * in `tw-debug-overlay.tsx` (full mode only). Future telemetry (scope
@@ -17,7 +16,7 @@ import React from "react";
17
16
 
18
17
  export interface TwDebugTopBarProps {
19
18
  /**
20
- * The debug-session id the host passed into `ui.debug.attach()`.
19
+ * The debug-session id the internal harness/story surface supplies.
21
20
  * Shown verbatim so operators can correlate with Railway debug-
22
21
  * service logs. `null` when no attachment is active.
23
22
  */