@beyondwork/docx-react-component 1.0.96 → 1.0.98
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.
- package/package.json +1 -1
- package/src/api/public-types.ts +47 -19
- package/src/api/v3/ui/_types.ts +11 -21
- package/src/api/v3/ui/chrome.ts +8 -9
- package/src/api/v3/ui/debug.ts +15 -77
- package/src/api/v3/ui/overlays-visibility.ts +9 -10
- package/src/api/v3/ui/overlays.ts +8 -75
- package/src/io/ooxml/parse-main-document.ts +30 -0
- package/src/io/ooxml/parse-picture.ts +14 -0
- package/src/io/ooxml/parse-shapes.ts +41 -1
- package/src/io/ooxml/payload-signature.ts +101 -0
- package/src/model/canonical-document.ts +17 -0
- package/src/runtime/layout/layout-engine-version.ts +14 -1
- package/src/runtime/layout/page-story-resolver.ts +1 -0
- package/src/runtime/layout/paginated-layout-engine.ts +26 -10
- package/src/runtime/surface-projection.ts +114 -12
- package/src/session/export/stateful-export-pipeline.ts +18 -2
- package/src/session/export/stateful-export.ts +21 -1
- package/src/ui/WordReviewEditor.tsx +6 -10
- package/src/ui/editor-command-bag.ts +2 -0
- package/src/ui/ui-controller-factory.ts +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +11 -25
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -220
- package/src/ui-tailwind/debug/README.md +12 -50
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
- package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
- package/src/ui-tailwind/editor-surface/preserve-position.ts +12 -2
- package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +67 -56
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
- package/src/ui-tailwind/theme/editor-theme.css +18 -11
- package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
|
@@ -187,7 +187,6 @@ import {
|
|
|
187
187
|
withExportDelivery,
|
|
188
188
|
} from "./browser-export";
|
|
189
189
|
import { EditorShellView } from "./editor-shell-view.tsx";
|
|
190
|
-
import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
|
|
191
190
|
import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
|
|
192
191
|
import { EditorSurfaceController } from "./editor-surface-controller.tsx";
|
|
193
192
|
import type { EditorActionHostCallbacks } from "../ui-tailwind/chrome/editor-action-registry";
|
|
@@ -1061,7 +1060,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1061
1060
|
chromeControllerRef,
|
|
1062
1061
|
commandPaletteDisabled,
|
|
1063
1062
|
customSelectionTools,
|
|
1064
|
-
debugMode = "off",
|
|
1065
1063
|
} = props;
|
|
1066
1064
|
|
|
1067
1065
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
@@ -1140,7 +1138,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1140
1138
|
hostPosture: {
|
|
1141
1139
|
reviewMode: reviewMode === "review" ? "reviewer" : "author",
|
|
1142
1140
|
markupDisplay: normalizeHostMarkupDisplay(markupDisplay),
|
|
1143
|
-
debugMode,
|
|
1144
1141
|
chromePreset,
|
|
1145
1142
|
},
|
|
1146
1143
|
chromePresetInput: {
|
|
@@ -1261,7 +1258,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1261
1258
|
api,
|
|
1262
1259
|
reviewMode,
|
|
1263
1260
|
markupDisplay,
|
|
1264
|
-
debugMode,
|
|
1265
1261
|
chromePreset,
|
|
1266
1262
|
readOnly,
|
|
1267
1263
|
]);
|
|
@@ -3395,6 +3391,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3395
3391
|
activeRuntime.rejectAllChanges();
|
|
3396
3392
|
setActiveRailTab("changes");
|
|
3397
3393
|
},
|
|
3394
|
+
onAcceptSuggestionGroup: (groupId: string) =>
|
|
3395
|
+
applySuggestionGroupAction(activeRuntime, groupId, "accept"),
|
|
3396
|
+
onRejectSuggestionGroup: (groupId: string) =>
|
|
3397
|
+
applySuggestionGroupAction(activeRuntime, groupId, "reject"),
|
|
3398
3398
|
onCloseStory: () => {
|
|
3399
3399
|
activeRuntime.closeStory();
|
|
3400
3400
|
},
|
|
@@ -3907,10 +3907,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3907
3907
|
});
|
|
3908
3908
|
}}
|
|
3909
3909
|
onScopeAcceptSuggestionGroup={(payload) => {
|
|
3910
|
-
|
|
3910
|
+
commands.onAcceptSuggestionGroup?.(payload.groupId);
|
|
3911
3911
|
}}
|
|
3912
3912
|
onScopeRejectSuggestionGroup={(payload) => {
|
|
3913
|
-
|
|
3913
|
+
commands.onRejectSuggestionGroup?.(payload.groupId);
|
|
3914
3914
|
}}
|
|
3915
3915
|
mediaPreviews={mediaPreviews}
|
|
3916
3916
|
onActivateFloatingImage={(payload) => {
|
|
@@ -3959,10 +3959,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3959
3959
|
}}
|
|
3960
3960
|
/>
|
|
3961
3961
|
<TwRuntimeReplDialog runtime={activeRuntime} editorRef={editorRefForRepl} />
|
|
3962
|
-
<TwDebugPresentation
|
|
3963
|
-
mode={debugMode}
|
|
3964
|
-
sessionId={documentId}
|
|
3965
|
-
/>
|
|
3966
3962
|
</>
|
|
3967
3963
|
</OverlayAnchorBridgeProvider>
|
|
3968
3964
|
</UiShellChannelsProvider>
|
|
@@ -104,6 +104,8 @@ export interface EditorCommandBag {
|
|
|
104
104
|
onRejectRevision(revisionId: string): void;
|
|
105
105
|
onAcceptAllChanges(): void;
|
|
106
106
|
onRejectAllChanges(): void;
|
|
107
|
+
onAcceptSuggestionGroup?(groupId: string): void;
|
|
108
|
+
onRejectSuggestionGroup?(groupId: string): void;
|
|
107
109
|
onCloseStory?(): void;
|
|
108
110
|
/**
|
|
109
111
|
* @deprecated P8.11 — see the matching prop on `TwReviewWorkspaceProps`.
|
|
@@ -79,7 +79,7 @@ let nextControllerId = 0;
|
|
|
79
79
|
export interface ShellUiControllerDeps {
|
|
80
80
|
/**
|
|
81
81
|
* Current host-provided posture slice — reviewMode / markupDisplay /
|
|
82
|
-
*
|
|
82
|
+
* chromePreset. MUST read through a live ref so the factory
|
|
83
83
|
* closure returns current render state, not stale construction-time state.
|
|
84
84
|
*/
|
|
85
85
|
readonly getHostPosture?: () => ChromeHostPosture | undefined;
|
|
@@ -95,7 +95,7 @@ export interface ShellUiControllerDeps {
|
|
|
95
95
|
readonly getOverlayAnchor?: (query: OverlayAnchorQuery) => GeometryRect | null;
|
|
96
96
|
/**
|
|
97
97
|
* Posture-change stream. Fires when any of the inputs to `ChromePosture`
|
|
98
|
-
* change — reviewMode / markupDisplay /
|
|
98
|
+
* change — reviewMode / markupDisplay / chromePreset (host),
|
|
99
99
|
* effectiveMode / blockedReasons / documentMode / readOnly (runtime).
|
|
100
100
|
* Returns an unsubscribe function.
|
|
101
101
|
*
|
|
@@ -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). */
|
|
@@ -50,14 +48,12 @@ export interface TwChromeOverlayProps {
|
|
|
50
48
|
/** Active scope id (for emphasis + rail tab sync). */
|
|
51
49
|
activeScopeId?: string | null;
|
|
52
50
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* Deprecated no-op. Scope rails are no longer visible in the product
|
|
52
|
+
* overlay; scoped text itself is the visible affordance.
|
|
55
53
|
*/
|
|
56
54
|
onScopeStripeClick?: (segment: ScopeRailSegment) => void;
|
|
57
55
|
/**
|
|
58
|
-
*
|
|
59
|
-
* subscribed before the stripe affordance existed. Called alongside
|
|
60
|
-
* `onScopeStripeClick` on a stripe click.
|
|
56
|
+
* Deprecated no-op kept for prop compatibility with pre-inline-scope hosts.
|
|
61
57
|
*/
|
|
62
58
|
onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
|
|
63
59
|
/**
|
|
@@ -183,6 +179,12 @@ export interface TwChromeOverlayProps {
|
|
|
183
179
|
* See `useVisiblePageIndexRange` in `src/ui-tailwind/page-stack/use-visible-block-range.ts`.
|
|
184
180
|
*/
|
|
185
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;
|
|
186
188
|
/** Preview catalog threaded into the page-stack chrome so header /
|
|
187
189
|
* footer / footnote / endnote regions render real <img>s. */
|
|
188
190
|
mediaPreviews?: Record<string, import("../editor-surface/pm-state-from-snapshot").MediaPreviewDescriptor>;
|
|
@@ -239,18 +241,10 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
239
241
|
pmSurfaceElement,
|
|
240
242
|
pmView,
|
|
241
243
|
visiblePageIndexRange,
|
|
244
|
+
plainPageBands,
|
|
242
245
|
mediaPreviews,
|
|
243
246
|
activeBandRibbonProps,
|
|
244
247
|
}) => {
|
|
245
|
-
const ui = useUiApi();
|
|
246
|
-
const scopeRailSegments = React.useMemo(
|
|
247
|
-
() =>
|
|
248
|
-
ui?.scope.rail().segments ??
|
|
249
|
-
workflowFacet?.getAllRailSegments() ??
|
|
250
|
-
[],
|
|
251
|
-
[ui, workflowFacet, renderFrameRevision],
|
|
252
|
-
);
|
|
253
|
-
|
|
254
248
|
return (
|
|
255
249
|
<div
|
|
256
250
|
className="wre-chrome-overlay pointer-events-none absolute inset-0 z-30"
|
|
@@ -268,19 +262,11 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
268
262
|
pmSurfaceElement={pmSurfaceElement}
|
|
269
263
|
pmView={pmView}
|
|
270
264
|
visiblePageIndexRange={visiblePageIndexRange ?? null}
|
|
265
|
+
plainPageBands={plainPageBands ?? false}
|
|
271
266
|
mediaPreviews={mediaPreviews}
|
|
272
267
|
activeBandRibbonProps={activeBandRibbonProps ?? null}
|
|
273
268
|
/>
|
|
274
269
|
) : null}
|
|
275
|
-
<TwScopeRailLayer
|
|
276
|
-
geometryFacet={geometryFacet}
|
|
277
|
-
workflowFacet={workflowFacet}
|
|
278
|
-
scopeRailSegments={scopeRailSegments}
|
|
279
|
-
space={space}
|
|
280
|
-
activeScopeId={activeScopeId}
|
|
281
|
-
onStripeClick={onScopeStripeClick}
|
|
282
|
-
onSegmentClick={onScopeSegmentClick}
|
|
283
|
-
/>
|
|
284
270
|
<TwScopeCardLayer
|
|
285
271
|
facet={facet}
|
|
286
272
|
workflowFacet={workflowFacet}
|
|
@@ -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
|
|
7
|
-
*
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TwScopeCard — inline floating card shown above a scoped region when
|
|
3
|
-
* the user activates
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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. */
|
|
@@ -53,215 +48,22 @@ export interface TwScopeRailLayerProps {
|
|
|
53
48
|
/** Scope id that should render with the `active` emphasis. */
|
|
54
49
|
activeScopeId?: string | null;
|
|
55
50
|
/**
|
|
56
|
-
*
|
|
57
|
-
* P0 wires this directly; P1 replaces with card-layer-aware routing.
|
|
51
|
+
* Deprecated no-op. Scoped text selection/card flows replace rail clicks.
|
|
58
52
|
*/
|
|
59
53
|
onStripeClick?: (segment: ScopeRailSegment) => void;
|
|
60
54
|
/**
|
|
61
|
-
*
|
|
62
|
-
* `onStripeClick` so host apps that subscribed to the pre-stripe API
|
|
63
|
-
* continue to receive clicks.
|
|
55
|
+
* Deprecated no-op kept for existing consumers.
|
|
64
56
|
*/
|
|
65
57
|
onSegmentClick?: (segment: ScopeRailSegment) => void;
|
|
66
58
|
/** Test id applied to the layer root. */
|
|
67
59
|
"data-testid"?: string;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Posture → visual grammar
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
interface PostureStyle {
|
|
75
|
-
labelText: string;
|
|
76
|
-
icon: string;
|
|
77
|
-
railToken: string;
|
|
78
|
-
tintToken: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
|
|
82
|
-
edit: { labelText: "EDIT", icon: "pencil", railToken: "accent", tintToken: "accent" },
|
|
83
|
-
suggest: { labelText: "SUGGEST", icon: "sparkles", railToken: "warning", tintToken: "warning" },
|
|
84
|
-
comment: { labelText: "COMMENT", icon: "message", railToken: "insert", tintToken: "insert" },
|
|
85
|
-
view: { labelText: "IN SCOPE", icon: "eye", railToken: "secondary", tintToken: "secondary" },
|
|
86
|
-
candidate: { labelText: "PROPOSED", icon: "flag", railToken: "warning", tintToken: "warning" },
|
|
87
|
-
"preserve-only": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
|
|
88
|
-
"blocked-import": { labelText: "BLOCKED", icon: "lock", railToken: "danger", tintToken: "danger" },
|
|
89
|
-
};
|
|
90
|
-
|
|
91
62
|
// ---------------------------------------------------------------------------
|
|
92
63
|
// Component
|
|
93
64
|
// ---------------------------------------------------------------------------
|
|
94
65
|
|
|
95
|
-
const
|
|
96
|
-
const STRIPE_WIDTH_PX = 4;
|
|
97
|
-
const LABEL_WIDTH_PX = 28;
|
|
98
|
-
const STACK_OFFSET_PX = 6;
|
|
99
|
-
|
|
100
|
-
export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
101
|
-
geometryFacet,
|
|
102
|
-
workflowFacet,
|
|
103
|
-
scopeRailSegments,
|
|
104
|
-
space,
|
|
105
|
-
railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
|
|
106
|
-
activeScopeId,
|
|
107
|
-
onStripeClick,
|
|
108
|
-
onSegmentClick,
|
|
109
|
-
"data-testid": testId,
|
|
110
|
-
}) => {
|
|
111
|
-
const ui = useUiApi();
|
|
112
|
-
const frame = geometryFacet.getRenderFrame() ?? null;
|
|
113
|
-
const segments =
|
|
114
|
-
scopeRailSegments ??
|
|
115
|
-
ui?.scope.rail().segments ??
|
|
116
|
-
workflowFacet?.getAllRailSegments() ??
|
|
117
|
-
[];
|
|
118
|
-
|
|
119
|
-
if (!frame || segments.length === 0) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// P2: which scopes currently have a `source: "ai"` candidate
|
|
124
|
-
// overlapping — drives the agent-pending shimmer class on their
|
|
125
|
-
// tints. Mounted surfaces read card projections through ui.scope.card;
|
|
126
|
-
// no-provider paths fall back to the workflow facet.
|
|
127
|
-
const cardModels = ui ? [] : workflowFacet?.getAllScopeCardModels() ?? [];
|
|
128
|
-
const agentPendingByScope = new Map<string, boolean>();
|
|
129
|
-
for (const model of cardModels) {
|
|
130
|
-
if (model.agentPending) {
|
|
131
|
-
agentPendingByScope.set(model.scopeId, true);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (ui) {
|
|
135
|
-
for (const segment of segments) {
|
|
136
|
-
const model = ui.scope.card(segment.scopeId);
|
|
137
|
-
if (model?.agentPending) {
|
|
138
|
-
agentPendingByScope.set(segment.scopeId, true);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// P3c: stack offsets for overlapping scopes. Two scopes whose
|
|
144
|
-
// offset ranges intersect on the same page render as stacked
|
|
145
|
-
// stripes in the gutter; the inner stripe shifts STACK_OFFSET_PX
|
|
146
|
-
// further right per overlap count so all are clickable.
|
|
147
|
-
const stackIndexByScope = computeStackIndices(segments);
|
|
148
|
-
|
|
149
|
-
const projectorSpace: OverlayCoordinateSpace = space ?? { originLeftPx: 0, originTopPx: 0 };
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<div
|
|
153
|
-
className="wre-scope-rail-layer pointer-events-none absolute inset-0 z-20"
|
|
154
|
-
data-testid={testId ?? "scope-rail-layer"}
|
|
155
|
-
aria-hidden="false"
|
|
156
|
-
role="group"
|
|
157
|
-
aria-label="Workflow scope rail"
|
|
158
|
-
>
|
|
159
|
-
{segments.map((segment) => {
|
|
160
|
-
const style = POSTURE_STYLES[segment.posture];
|
|
161
|
-
const lineRects = collectLineRectsForSegment(frame, segment);
|
|
162
|
-
if (lineRects.length === 0) return null;
|
|
163
|
-
|
|
164
|
-
const isActive =
|
|
165
|
-
activeScopeId === segment.scopeId || segment.isActiveWorkItem;
|
|
166
|
-
|
|
167
|
-
// Stripe + label span the vertical range of the scope's lines;
|
|
168
|
-
// they live in the gutter lane to the left of the first line.
|
|
169
|
-
const firstLine = lineRects[0];
|
|
170
|
-
const lastLine = lineRects[lineRects.length - 1];
|
|
171
|
-
const stripeTopPx = firstLine.topPx;
|
|
172
|
-
const stripeHeightPx =
|
|
173
|
-
lastLine.topPx + lastLine.heightPx - firstLine.topPx;
|
|
174
|
-
const stackIndex = stackIndexByScope.get(segment.scopeId) ?? 0;
|
|
175
|
-
const stackOffset = stackIndex * STACK_OFFSET_PX;
|
|
176
|
-
const stripeRect: RenderFrameRect = {
|
|
177
|
-
leftPx:
|
|
178
|
-
firstLine.leftPx
|
|
179
|
-
- railLaneWidthPx
|
|
180
|
-
+ (railLaneWidthPx - STRIPE_WIDTH_PX) / 2
|
|
181
|
-
+ stackOffset,
|
|
182
|
-
topPx: stripeTopPx,
|
|
183
|
-
widthPx: STRIPE_WIDTH_PX,
|
|
184
|
-
heightPx: Math.max(stripeHeightPx, 14),
|
|
185
|
-
};
|
|
186
|
-
const labelRect: RenderFrameRect = {
|
|
187
|
-
leftPx: firstLine.leftPx - railLaneWidthPx + stackOffset,
|
|
188
|
-
topPx: stripeTopPx,
|
|
189
|
-
widthPx: LABEL_WIDTH_PX,
|
|
190
|
-
heightPx: 20,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const handleActivate = () => {
|
|
194
|
-
onStripeClick?.(segment);
|
|
195
|
-
onSegmentClick?.(segment);
|
|
196
|
-
};
|
|
197
|
-
const handleStripeKey = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
198
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
199
|
-
event.preventDefault();
|
|
200
|
-
handleActivate();
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<React.Fragment key={`${segment.scopeId}:${segment.pageIndex}:${segment.fromOffset}`}>
|
|
206
|
-
{/* Per-line tint behind the scoped text runs. */}
|
|
207
|
-
{lineRects.map((lineRect, index) => {
|
|
208
|
-
const agentPending = agentPendingByScope.get(segment.scopeId) === true;
|
|
209
|
-
const tintClassList = [
|
|
210
|
-
"wre-scope-rail-tint",
|
|
211
|
-
`wre-scope-rail-tint-${style.tintToken}`,
|
|
212
|
-
];
|
|
213
|
-
if (isActive) tintClassList.push("wre-scope-rail-tint-active");
|
|
214
|
-
if (agentPending) {
|
|
215
|
-
tintClassList.push("wre-scope-rail-tint-agent-pending");
|
|
216
|
-
}
|
|
217
|
-
return (
|
|
218
|
-
<div
|
|
219
|
-
key={`tint:${index}`}
|
|
220
|
-
className={tintClassList.join(" ")}
|
|
221
|
-
data-scope-id={segment.scopeId}
|
|
222
|
-
data-posture={segment.posture}
|
|
223
|
-
data-line-index={index}
|
|
224
|
-
data-agent-pending={agentPending ? "true" : undefined}
|
|
225
|
-
style={projectRectToOverlay(lineRect, projectorSpace)}
|
|
226
|
-
/>
|
|
227
|
-
);
|
|
228
|
-
})}
|
|
229
|
-
{/* Rail stripe in the gutter. */}
|
|
230
|
-
<button
|
|
231
|
-
type="button"
|
|
232
|
-
className={`wre-scope-rail-stripe wre-scope-rail-label-${style.railToken} ${
|
|
233
|
-
isActive ? "wre-scope-rail-stripe-active" : ""
|
|
234
|
-
}`}
|
|
235
|
-
data-scope-id={segment.scopeId}
|
|
236
|
-
data-posture={segment.posture}
|
|
237
|
-
data-stack-index={stackIndex}
|
|
238
|
-
data-testid="scope-rail-stripe"
|
|
239
|
-
aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
|
|
240
|
-
aria-expanded={isActive ? "true" : "false"}
|
|
241
|
-
onClick={handleActivate}
|
|
242
|
-
onKeyDown={handleStripeKey}
|
|
243
|
-
style={projectRectToOverlay(stripeRect, projectorSpace)}
|
|
244
|
-
/>
|
|
245
|
-
{/* Edit handle — revealed on stripe hover via CSS. */}
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
tabIndex={-1}
|
|
249
|
-
className={`wre-scope-rail-label wre-scope-rail-label-${style.railToken}`}
|
|
250
|
-
data-scope-id={segment.scopeId}
|
|
251
|
-
data-posture={segment.posture}
|
|
252
|
-
aria-label={`Edit scope${segment.label ? `: ${segment.label}` : ""}`}
|
|
253
|
-
onClick={handleActivate}
|
|
254
|
-
style={projectRectToOverlay(labelRect, projectorSpace)}
|
|
255
|
-
>
|
|
256
|
-
<span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
|
|
257
|
-
<span className="wre-scope-rail-label-text">{style.labelText}</span>
|
|
258
|
-
</button>
|
|
259
|
-
</React.Fragment>
|
|
260
|
-
);
|
|
261
|
-
})}
|
|
262
|
-
</div>
|
|
263
|
-
);
|
|
264
|
-
};
|
|
66
|
+
export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = (_props) => null;
|
|
265
67
|
|
|
266
68
|
// ---------------------------------------------------------------------------
|
|
267
69
|
// Internals
|
|
@@ -1,57 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Debug UX (internal only)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
##
|
|
12
|
+
## Files
|
|
14
13
|
|
|
15
14
|
| File | Purpose |
|
|
16
15
|
|---|---|
|
|
17
|
-
| `tw-debug-top-bar.tsx` |
|
|
18
|
-
| `tw-debug-overlay.tsx` |
|
|
19
|
-
| `tw-debug-repl.tsx` |
|
|
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
|
-
*
|
|
5
|
-
* `
|
|
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`
|
|
36
|
-
*
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
31
|
-
*
|
|
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
|
|
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
|
}
|