@beyondwork/docx-react-component 1.0.93 → 1.0.95
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/v3/ai/replacement.ts +3 -3
- package/src/api/v3/runtime/formatting.ts +3 -3
- package/src/core/commands/formatting-commands.ts +146 -3
- package/src/core/state/text-transaction.ts +6 -3
- package/src/runtime/document-runtime.ts +3 -0
- package/src/runtime/scopes/semantic-scope-types.ts +9 -0
- package/src/ui/WordReviewEditor.tsx +9 -14
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +109 -42
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +30 -5
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +46 -15
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -7
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +34 -12
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +78 -16
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +389 -109
- package/src/ui-tailwind/tw-review-workspace.tsx +20 -8
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
projectRectToOverlay,
|
|
18
18
|
type OverlayCoordinateSpace,
|
|
19
19
|
} from "./chrome-overlay-projector";
|
|
20
|
+
import { useUiApi } from "../ui-api-context";
|
|
20
21
|
import type { RenderFrame, RenderFrameRect } from "../../api/public-types.ts";
|
|
21
22
|
import type { ScopeRailSegment, ScopeRailPosture } from "../../api/public-types.ts";
|
|
22
23
|
import type { WorkflowFacet } from "../../api/public-types.ts";
|
|
@@ -30,22 +31,29 @@ export interface TwScopeRailLayerProps {
|
|
|
30
31
|
/**
|
|
31
32
|
* Geometry facet — used for `getRenderFrame()`. Migrated from
|
|
32
33
|
* `facet: WordReviewEditorLayoutFacet` in refactor/05 cross-lane-coord
|
|
33
|
-
* §8.4 pass.
|
|
34
|
-
* `workflowFacet` per Layer-06 Slice 4's seam inversion.
|
|
34
|
+
* §8.4 pass. Mounted rail/card data flows through `api.ui.scope.*`.
|
|
35
35
|
*/
|
|
36
36
|
geometryFacet: GeometryFacet;
|
|
37
37
|
/**
|
|
38
|
-
* Workflow facet —
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
41
41
|
*/
|
|
42
42
|
workflowFacet: WorkflowFacet | null;
|
|
43
|
+
/**
|
|
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.
|
|
47
|
+
*/
|
|
48
|
+
scopeRailSegments?: readonly ScopeRailSegment[];
|
|
43
49
|
/** Overlay's coordinate space. Defaults to the overlay's own origin. */
|
|
44
50
|
space?: OverlayCoordinateSpace;
|
|
45
51
|
/** Horizontal pad (px) the rail gutter occupies to the left of body. */
|
|
46
52
|
railLaneWidthPx?: number;
|
|
47
53
|
/** Scope id that should render with the `active` emphasis. */
|
|
48
54
|
activeScopeId?: string | null;
|
|
55
|
+
/** Posture filters shared with the Workflow rail. Omitted means all scopes render. */
|
|
56
|
+
visibleScopePostures?: ReadonlySet<ScopeRailPosture>;
|
|
49
57
|
/**
|
|
50
58
|
* Fires when the user clicks the rail stripe — opens the scope card.
|
|
51
59
|
* P0 wires this directly; P1 replaces with card-layer-aware routing.
|
|
@@ -87,22 +95,32 @@ const POSTURE_STYLES: Record<ScopeRailPosture, PostureStyle> = {
|
|
|
87
95
|
// ---------------------------------------------------------------------------
|
|
88
96
|
|
|
89
97
|
const DEFAULT_RAIL_LANE_PX = 44;
|
|
90
|
-
const STRIPE_WIDTH_PX =
|
|
91
|
-
const LABEL_WIDTH_PX =
|
|
98
|
+
const STRIPE_WIDTH_PX = 6;
|
|
99
|
+
const LABEL_WIDTH_PX = 58;
|
|
92
100
|
const STACK_OFFSET_PX = 6;
|
|
93
101
|
|
|
94
102
|
export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
95
103
|
geometryFacet,
|
|
96
104
|
workflowFacet,
|
|
105
|
+
scopeRailSegments,
|
|
97
106
|
space,
|
|
98
107
|
railLaneWidthPx = DEFAULT_RAIL_LANE_PX,
|
|
99
108
|
activeScopeId,
|
|
109
|
+
visibleScopePostures,
|
|
100
110
|
onStripeClick,
|
|
101
111
|
onSegmentClick,
|
|
102
112
|
"data-testid": testId,
|
|
103
113
|
}) => {
|
|
114
|
+
const ui = useUiApi();
|
|
104
115
|
const frame = geometryFacet.getRenderFrame() ?? null;
|
|
105
|
-
const
|
|
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
|
+
);
|
|
106
124
|
|
|
107
125
|
if (!frame || segments.length === 0) {
|
|
108
126
|
return null;
|
|
@@ -110,15 +128,23 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
110
128
|
|
|
111
129
|
// P2: which scopes currently have a `source: "ai"` candidate
|
|
112
130
|
// overlapping — drives the agent-pending shimmer class on their
|
|
113
|
-
// tints.
|
|
114
|
-
//
|
|
115
|
-
const cardModels = workflowFacet?.getAllScopeCardModels() ?? [];
|
|
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() ?? [];
|
|
116
134
|
const agentPendingByScope = new Map<string, boolean>();
|
|
117
135
|
for (const model of cardModels) {
|
|
118
136
|
if (model.agentPending) {
|
|
119
137
|
agentPendingByScope.set(model.scopeId, true);
|
|
120
138
|
}
|
|
121
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
|
+
}
|
|
122
148
|
|
|
123
149
|
// P3c: stack offsets for overlapping scopes. Two scopes whose
|
|
124
150
|
// offset ranges intersect on the same page render as stacked
|
|
@@ -223,16 +249,21 @@ export const TwScopeRailLayer: React.FC<TwScopeRailLayerProps> = ({
|
|
|
223
249
|
style={projectRectToOverlay(stripeRect, projectorSpace)}
|
|
224
250
|
/>
|
|
225
251
|
{/* Label pill — revealed on stripe hover via CSS. */}
|
|
226
|
-
<
|
|
227
|
-
|
|
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
|
+
}`}
|
|
228
258
|
data-scope-id={segment.scopeId}
|
|
229
259
|
data-posture={segment.posture}
|
|
230
|
-
aria-
|
|
260
|
+
aria-label={`${style.labelText}${segment.label ? `: ${segment.label}` : ""}`}
|
|
261
|
+
onClick={handleActivate}
|
|
231
262
|
style={projectRectToOverlay(labelRect, projectorSpace)}
|
|
232
263
|
>
|
|
233
264
|
<span aria-hidden="true" className={`wre-scope-rail-icon wre-scope-rail-icon-${style.icon}`} />
|
|
234
265
|
<span className="wre-scope-rail-label-text">{style.labelText}</span>
|
|
235
|
-
</
|
|
266
|
+
</button>
|
|
236
267
|
</React.Fragment>
|
|
237
268
|
);
|
|
238
269
|
})}
|
|
@@ -402,13 +402,7 @@ function buildParagraph(
|
|
|
402
402
|
indentRight: paragraphLayout.indentation?.right ?? null,
|
|
403
403
|
indentFirstLine: paragraphLayout.indentation?.firstLine ?? null,
|
|
404
404
|
indentHanging: paragraphLayout.indentation?.hanging ?? null,
|
|
405
|
-
numberingMarkerWidth:
|
|
406
|
-
paragraphLayout.markerLane?.width ??
|
|
407
|
-
paragraphLayout.indentation?.hanging ??
|
|
408
|
-
(paragraphLayout.indentation?.firstLine !== undefined &&
|
|
409
|
-
paragraphLayout.indentation.firstLine < 0
|
|
410
|
-
? Math.abs(paragraphLayout.indentation.firstLine)
|
|
411
|
-
: null),
|
|
405
|
+
numberingMarkerWidth: paragraphLayout.markerLane?.width ?? null,
|
|
412
406
|
numberingMarkerStart: paragraphLayout.markerLane?.start ?? null,
|
|
413
407
|
numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
|
|
414
408
|
numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
cycleScopeIndex,
|
|
10
10
|
shouldHandleScopeNavKey,
|
|
11
11
|
} from "../chrome-overlay/scope-keyboard-cycle";
|
|
12
|
+
import { useUiApi } from "../ui-api-context.tsx";
|
|
12
13
|
|
|
13
14
|
export interface UseScopeCardStateOptions {
|
|
14
15
|
layoutFacet?: import("../../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
|
|
15
16
|
/**
|
|
16
|
-
* Layer-06 workflow facet —
|
|
17
|
-
*
|
|
18
|
-
* rail-seam inversion removed those methods from `layoutFacet`.
|
|
17
|
+
* Layer-06 workflow facet — no-provider fallback for scope card models.
|
|
18
|
+
* Mounted paths prefer `api.ui.scope.rail/card`.
|
|
19
19
|
*/
|
|
20
20
|
workflowFacet?: import("../../runtime/workflow/rail/types.ts").WorkflowFacet;
|
|
21
21
|
onScopeModeChangeRequested?: (payload: {
|
|
@@ -79,6 +79,31 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
79
79
|
void layoutFacet;
|
|
80
80
|
|
|
81
81
|
const [activeScopeId, setActiveScopeId] = useState<string | null>(null);
|
|
82
|
+
const ui = useUiApi();
|
|
83
|
+
|
|
84
|
+
const readScopeIds = useCallback((): string[] => {
|
|
85
|
+
if (ui) {
|
|
86
|
+
const ids: string[] = [];
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
for (const segment of ui.scope.rail().segments) {
|
|
89
|
+
if (seen.has(segment.scopeId)) continue;
|
|
90
|
+
seen.add(segment.scopeId);
|
|
91
|
+
ids.push(segment.scopeId);
|
|
92
|
+
}
|
|
93
|
+
return ids;
|
|
94
|
+
}
|
|
95
|
+
return workflowFacet?.getAllScopeCardModels().map((model) => model.scopeId) ?? [];
|
|
96
|
+
}, [ui, workflowFacet]);
|
|
97
|
+
|
|
98
|
+
const readScopeCard = useCallback(
|
|
99
|
+
(scopeId: string) => {
|
|
100
|
+
if (ui) return ui.scope.card(scopeId);
|
|
101
|
+
return workflowFacet
|
|
102
|
+
?.getAllScopeCardModels()
|
|
103
|
+
.find((m) => m.scopeId === scopeId) ?? null;
|
|
104
|
+
},
|
|
105
|
+
[ui, workflowFacet],
|
|
106
|
+
);
|
|
82
107
|
|
|
83
108
|
const handleScopeStripeClick = useCallback(
|
|
84
109
|
(segment: { scopeId: string }) => {
|
|
@@ -94,14 +119,13 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
94
119
|
}, []);
|
|
95
120
|
|
|
96
121
|
useEffect(() => {
|
|
97
|
-
if (!workflowFacet) {
|
|
122
|
+
if (!ui && !workflowFacet) {
|
|
98
123
|
return undefined;
|
|
99
124
|
}
|
|
100
125
|
const onKey = (event: KeyboardEvent) => {
|
|
101
126
|
if (!shouldHandleScopeNavKey(event)) return;
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
const ids = models.map((model) => model.scopeId);
|
|
127
|
+
const ids = readScopeIds();
|
|
128
|
+
if (ids.length === 0) return;
|
|
105
129
|
const key = event.key.toLowerCase();
|
|
106
130
|
if (key === "enter") {
|
|
107
131
|
if (!activeScopeId) {
|
|
@@ -117,7 +141,7 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
117
141
|
};
|
|
118
142
|
window.addEventListener("keydown", onKey);
|
|
119
143
|
return () => window.removeEventListener("keydown", onKey);
|
|
120
|
-
}, [workflowFacet, activeScopeId]);
|
|
144
|
+
}, [ui, workflowFacet, activeScopeId, readScopeIds]);
|
|
121
145
|
|
|
122
146
|
const handleScopeCardModeChange = useCallback(
|
|
123
147
|
(scopeId: string, mode: WorkflowScopeMode) => {
|
|
@@ -149,12 +173,10 @@ export function useScopeCardState(options: UseScopeCardStateOptions): ScopeCardS
|
|
|
149
173
|
|
|
150
174
|
const handleScopeCardAskAgent = useCallback(
|
|
151
175
|
(scopeId: string) => {
|
|
152
|
-
const cardModel =
|
|
153
|
-
?.getAllScopeCardModels()
|
|
154
|
-
.find((m) => m.scopeId === scopeId);
|
|
176
|
+
const cardModel = readScopeCard(scopeId);
|
|
155
177
|
onScopeAskAgent?.({ scopeId, anchor: cardModel?.anchor });
|
|
156
178
|
},
|
|
157
|
-
[onScopeAskAgent,
|
|
179
|
+
[onScopeAskAgent, readScopeCard],
|
|
158
180
|
);
|
|
159
181
|
|
|
160
182
|
return {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* traversal + claim/skip/complete.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import React, { useState } from "react";
|
|
17
|
-
import
|
|
16
|
+
import React, { useLayoutEffect, useRef, useState, type CSSProperties } from "react";
|
|
17
|
+
import { createPortal } from "react-dom";
|
|
18
18
|
import * as Toggle from "@radix-ui/react-toggle";
|
|
19
19
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
20
20
|
import {
|
|
@@ -433,38 +433,100 @@ function RoleActionOverflow({
|
|
|
433
433
|
props,
|
|
434
434
|
}: RoleActionOverflowProps): React.JSX.Element {
|
|
435
435
|
const [open, setOpen] = useState(false);
|
|
436
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
436
437
|
|
|
437
438
|
return (
|
|
438
|
-
|
|
439
|
-
<Popover.Trigger asChild>
|
|
439
|
+
<>
|
|
440
440
|
<button
|
|
441
|
+
ref={triggerRef}
|
|
441
442
|
type="button"
|
|
442
443
|
aria-label="More role actions"
|
|
443
444
|
aria-expanded={open}
|
|
445
|
+
aria-haspopup="menu"
|
|
444
446
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
447
|
+
onClick={(event) => {
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
setOpen((value) => !value);
|
|
450
|
+
}}
|
|
445
451
|
title="More role actions"
|
|
446
|
-
className=
|
|
452
|
+
className={`inline-flex h-6 w-6 items-center justify-center rounded-md border border-border bg-canvas text-primary transition-colors hover:bg-surface outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas ${
|
|
453
|
+
open ? "text-accent ring-1 ring-accent/30 shadow-sm" : ""
|
|
454
|
+
}`}
|
|
447
455
|
data-testid="role-action-overflow-trigger"
|
|
448
456
|
>
|
|
449
457
|
<MoreHorizontal className="h-3.5 w-3.5 text-tertiary" aria-hidden="true" />
|
|
450
458
|
</button>
|
|
451
|
-
|
|
452
|
-
<Popover.Portal>
|
|
453
|
-
<Popover.Content
|
|
454
|
-
className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
455
|
-
sideOffset={8}
|
|
456
|
-
align="start"
|
|
457
|
-
data-testid="role-action-overflow-content"
|
|
458
|
-
>
|
|
459
|
+
<RoleActionPortalMenu anchorRef={triggerRef} open={open}>
|
|
459
460
|
{ids.map((id) => (
|
|
460
461
|
<OverflowAction key={id} id={id} props={props} onClose={() => setOpen(false)} />
|
|
461
462
|
))}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
</RoleActionPortalMenu>
|
|
464
|
+
</>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function RoleActionPortalMenu(props: {
|
|
469
|
+
anchorRef: React.RefObject<HTMLButtonElement | null>;
|
|
470
|
+
children: React.ReactNode;
|
|
471
|
+
open: boolean;
|
|
472
|
+
}): React.ReactPortal | null {
|
|
473
|
+
const style = useRoleActionPortalPosition(props.anchorRef, props.open);
|
|
474
|
+
const body = props.anchorRef.current?.ownerDocument?.body;
|
|
475
|
+
if (!props.open || !body) return null;
|
|
476
|
+
return createPortal(
|
|
477
|
+
<div
|
|
478
|
+
className="z-50 w-[220px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
|
|
479
|
+
data-testid="role-action-overflow-content"
|
|
480
|
+
style={style}
|
|
481
|
+
>
|
|
482
|
+
{props.children}
|
|
483
|
+
</div>,
|
|
484
|
+
body,
|
|
465
485
|
);
|
|
466
486
|
}
|
|
467
487
|
|
|
488
|
+
function useRoleActionPortalPosition(
|
|
489
|
+
anchorRef: React.RefObject<HTMLButtonElement | null>,
|
|
490
|
+
open: boolean,
|
|
491
|
+
): CSSProperties {
|
|
492
|
+
const [style, setStyle] = useState<CSSProperties>({
|
|
493
|
+
left: 8,
|
|
494
|
+
position: "fixed",
|
|
495
|
+
top: 8,
|
|
496
|
+
zIndex: 50,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
useLayoutEffect(() => {
|
|
500
|
+
if (!open) return;
|
|
501
|
+
const anchor = anchorRef.current;
|
|
502
|
+
const ownerWindow = anchor?.ownerDocument?.defaultView;
|
|
503
|
+
if (!anchor || !ownerWindow) return;
|
|
504
|
+
const update = () => {
|
|
505
|
+
const rect = anchor.getBoundingClientRect();
|
|
506
|
+
const width = 220;
|
|
507
|
+
const left = Math.min(
|
|
508
|
+
Math.max(8, rect.left),
|
|
509
|
+
Math.max(8, (ownerWindow.innerWidth || width + 16) - width - 8),
|
|
510
|
+
);
|
|
511
|
+
setStyle({
|
|
512
|
+
left,
|
|
513
|
+
position: "fixed",
|
|
514
|
+
top: Math.max(8, rect.bottom + 8),
|
|
515
|
+
zIndex: 50,
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
update();
|
|
519
|
+
ownerWindow.addEventListener("resize", update);
|
|
520
|
+
ownerWindow.addEventListener("scroll", update, true);
|
|
521
|
+
return () => {
|
|
522
|
+
ownerWindow.removeEventListener("resize", update);
|
|
523
|
+
ownerWindow.removeEventListener("scroll", update, true);
|
|
524
|
+
};
|
|
525
|
+
}, [anchorRef, open]);
|
|
526
|
+
|
|
527
|
+
return style;
|
|
528
|
+
}
|
|
529
|
+
|
|
468
530
|
function OverflowAction(arg: {
|
|
469
531
|
id: ToolbarChromeItemId;
|
|
470
532
|
props: TwRoleActionRegionProps;
|
|
@@ -27,6 +27,8 @@ export interface TwToolbarIconButtonProps {
|
|
|
27
27
|
* vs. Windows however they like.
|
|
28
28
|
*/
|
|
29
29
|
shortcut?: string;
|
|
30
|
+
/** Explanation surfaced when the button is disabled by selection/mode state. */
|
|
31
|
+
disabledReason?: string;
|
|
30
32
|
onClick?: () => void;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -42,7 +44,9 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
|
|
|
42
44
|
aria-label={props.label}
|
|
43
45
|
aria-pressed={props.active ?? undefined}
|
|
44
46
|
data-active={props.active ? "true" : undefined}
|
|
47
|
+
data-disabled-reason={props.disabled && props.disabledReason ? props.disabledReason : undefined}
|
|
45
48
|
disabled={props.disabled}
|
|
49
|
+
title={props.disabled && props.disabledReason ? `Not available: ${props.disabledReason}` : undefined}
|
|
46
50
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
47
51
|
onClick={props.onClick}
|
|
48
52
|
className={[
|