@beyondwork/docx-react-component 1.0.85 → 1.0.87
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 +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +112 -33
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-shell-view.tsx +1 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/editor-action-registry.ts +7 -26
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +6 -2
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +8 -3
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +75 -31
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +159 -8
- package/src/ui-tailwind/review-workspace/types.ts +4 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +8 -10
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -17
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -2
|
@@ -1062,7 +1062,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1062
1062
|
} = props;
|
|
1063
1063
|
|
|
1064
1064
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
1065
|
-
const [showTrackedChanges, setShowTrackedChanges] = useState(() => suggestionsEnabled);
|
|
1066
1065
|
const [localMarkupDisplay, setLocalMarkupDisplay] =
|
|
1067
1066
|
useState<WorkflowMarkupMode | null>(() => suggestionsEnabled ? "all" : null);
|
|
1068
1067
|
const [activeRevisionId, setActiveRevisionId] = useState<string | undefined>();
|
|
@@ -1368,6 +1367,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1368
1367
|
const isPageWorkspace = viewState.workspaceMode === "page";
|
|
1369
1368
|
const liveMarkupDisplay = localMarkupDisplay ??
|
|
1370
1369
|
__resolveLiveMarkupDisplay(markupDisplay, isPageWorkspace);
|
|
1370
|
+
const trackedChangesAuthoringEnabled = viewState.documentMode === "suggesting";
|
|
1371
|
+
const showTrackedChanges = toWorkflowMarkupMode(liveMarkupDisplay) !== "clean";
|
|
1371
1372
|
const documentNavigation = useRuntimeValue(
|
|
1372
1373
|
runtime
|
|
1373
1374
|
? {
|
|
@@ -1510,16 +1511,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1510
1511
|
const setReviewMarkupMode = useCallback((mode: MarkupDisplay) => {
|
|
1511
1512
|
const workflowMode = toWorkflowMarkupMode(mode);
|
|
1512
1513
|
setLocalMarkupDisplay(workflowMode);
|
|
1513
|
-
setShowTrackedChanges(workflowMode !== "clean");
|
|
1514
1514
|
api.ui?.viewport.setLocalMarkupMode(workflowMode);
|
|
1515
1515
|
}, [api]);
|
|
1516
1516
|
|
|
1517
1517
|
const setTrackedChangesAuthoring = useCallback((enabled: boolean) => {
|
|
1518
|
-
const workflowMode: WorkflowMarkupMode = enabled ? "all" : "clean";
|
|
1519
|
-
setShowTrackedChanges(enabled);
|
|
1520
|
-
setLocalMarkupDisplay(workflowMode);
|
|
1521
1518
|
api.runtime.document.setMode(enabled ? "suggesting" : "editing");
|
|
1522
|
-
|
|
1519
|
+
if (enabled) {
|
|
1520
|
+
setLocalMarkupDisplay("all");
|
|
1521
|
+
api.ui?.viewport.setLocalMarkupMode("all");
|
|
1522
|
+
}
|
|
1523
1523
|
}, [api]);
|
|
1524
1524
|
|
|
1525
1525
|
useEffect(() => {
|
|
@@ -1527,7 +1527,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1527
1527
|
setTrackedChangesAuthoring(true);
|
|
1528
1528
|
return;
|
|
1529
1529
|
}
|
|
1530
|
-
setShowTrackedChanges(false);
|
|
1531
1530
|
api.runtime.document.setMode("editing");
|
|
1532
1531
|
}, [api, setTrackedChangesAuthoring, suggestionsEnabled]);
|
|
1533
1532
|
|
|
@@ -2467,13 +2466,15 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2467
2466
|
|
|
2468
2467
|
function addReviewComment(): string | null {
|
|
2469
2468
|
try {
|
|
2469
|
+
const currentSnapshot = activeRuntime.getRenderSnapshot();
|
|
2470
2470
|
const { commentId } = activeRuntime.addComment({
|
|
2471
|
-
anchor: resolveCommentCommandAnchor(
|
|
2471
|
+
anchor: resolveCommentCommandAnchor(currentSnapshot),
|
|
2472
2472
|
body: "",
|
|
2473
2473
|
authorId: currentUser.userId,
|
|
2474
2474
|
snapToSafeBoundary: true,
|
|
2475
2475
|
});
|
|
2476
2476
|
activeRuntime.openComment(commentId);
|
|
2477
|
+
setActiveReviewQueueItemId(`comment:${commentId}`);
|
|
2477
2478
|
setActiveRailTab("comments");
|
|
2478
2479
|
return commentId;
|
|
2479
2480
|
} catch {
|
|
@@ -2814,11 +2815,49 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2814
2815
|
}, [
|
|
2815
2816
|
activeRuntime,
|
|
2816
2817
|
activeSelectionTool,
|
|
2818
|
+
addReviewComment,
|
|
2817
2819
|
currentUser.userId,
|
|
2818
2820
|
dismissSelectionToolbar,
|
|
2819
2821
|
focusDocumentSurface,
|
|
2820
2822
|
]);
|
|
2821
2823
|
|
|
2824
|
+
const acceptActiveSuggestion = useCallback(() => {
|
|
2825
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
for (const changeId of activeSelectionTool.changeIds) {
|
|
2829
|
+
activeRuntime.acceptChange(changeId);
|
|
2830
|
+
}
|
|
2831
|
+
dismissSelectionToolbar("chrome-action");
|
|
2832
|
+
}, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
|
|
2833
|
+
|
|
2834
|
+
const rejectActiveSuggestion = useCallback(() => {
|
|
2835
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
for (const changeId of activeSelectionTool.changeIds) {
|
|
2839
|
+
activeRuntime.rejectChange(changeId);
|
|
2840
|
+
}
|
|
2841
|
+
dismissSelectionToolbar("chrome-action");
|
|
2842
|
+
}, [activeRuntime, activeSelectionTool, dismissSelectionToolbar]);
|
|
2843
|
+
|
|
2844
|
+
const editActiveSuggestion = useCallback(() => {
|
|
2845
|
+
if (activeSelectionTool?.kind !== "suggestion-review") {
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
|
|
2849
|
+
const activeSuggestion = suggestionsSnapshot.suggestions.find(
|
|
2850
|
+
(suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
|
|
2851
|
+
);
|
|
2852
|
+
if (activeSuggestion) {
|
|
2853
|
+
applyRuntimeSelection(
|
|
2854
|
+
activeRuntime,
|
|
2855
|
+
createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
setSelectionToolbarFocusWithin(true);
|
|
2859
|
+
}, [activeRuntime, activeSelectionTool, suggestionsSnapshot.suggestions]);
|
|
2860
|
+
|
|
2822
2861
|
const handleSelectionToolbarAnchorChange = useCallback(
|
|
2823
2862
|
(nextAnchor: SelectionToolbarAnchor | null) => {
|
|
2824
2863
|
setSelectionToolbarAnchor((current) =>
|
|
@@ -3209,6 +3248,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3209
3248
|
focusAnchor(revision.anchor, revision.storyTarget);
|
|
3210
3249
|
setActiveRailTab("changes");
|
|
3211
3250
|
},
|
|
3251
|
+
onReplyToRevision: (revision: typeof snapshot.trackedChanges.revisions[number]) => {
|
|
3252
|
+
const linkedThread = activeRuntime.ensureCommentThreadForChange(
|
|
3253
|
+
revision.revisionId,
|
|
3254
|
+
currentUser.userId,
|
|
3255
|
+
);
|
|
3256
|
+
if (!linkedThread?.commentId) {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
setActiveRevisionId(revision.revisionId);
|
|
3260
|
+
focusAnchor(revision.anchor, revision.storyTarget);
|
|
3261
|
+
activeRuntime.openComment(linkedThread.commentId);
|
|
3262
|
+
setActiveRailTab("comments");
|
|
3263
|
+
},
|
|
3212
3264
|
onAcceptRevision: (revisionId: string) => {
|
|
3213
3265
|
activeRuntime.acceptChange(revisionId);
|
|
3214
3266
|
setActiveRailTab("changes");
|
|
@@ -3237,6 +3289,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3237
3289
|
onActiveRailTabChange: setActiveRailTab,
|
|
3238
3290
|
onShowTrackedChangesChange: setTrackedChangesAuthoring,
|
|
3239
3291
|
onReviewMarkupModeChange: setReviewMarkupMode,
|
|
3292
|
+
onChromePinChange: (surface, pin) => activeRuntime.setChromePin(surface, pin),
|
|
3240
3293
|
onToggleBold: () =>
|
|
3241
3294
|
applyRuntimeFormattingOperation(activeRuntime, { type: "toggle", mark: "bold" }),
|
|
3242
3295
|
onToggleItalic: () =>
|
|
@@ -3449,6 +3502,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3449
3502
|
onInsertPageBreak: commands.onInsertPageBreak,
|
|
3450
3503
|
onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
|
|
3451
3504
|
onInsertTable: commands.onInsertTable,
|
|
3505
|
+
onInsertImage: commands.onInsertImage
|
|
3506
|
+
? () => requestImageInsertFromPicker(commands.onInsertImage)
|
|
3507
|
+
: undefined,
|
|
3452
3508
|
onAddComment: commands.onAddComment,
|
|
3453
3509
|
onFindRequested: onFindRequested
|
|
3454
3510
|
? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
|
|
@@ -3546,6 +3602,14 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3546
3602
|
setActiveRevisionId(revisionId);
|
|
3547
3603
|
setActiveRailTab("changes");
|
|
3548
3604
|
}}
|
|
3605
|
+
onRevisionHovered={(revisionId) => {
|
|
3606
|
+
if (!revisionId) {
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
setActiveRevisionId(revisionId);
|
|
3610
|
+
setActiveRailTab("changes");
|
|
3611
|
+
onReviewSidebarTrackedChanges?.();
|
|
3612
|
+
}}
|
|
3549
3613
|
/>
|
|
3550
3614
|
);
|
|
3551
3615
|
|
|
@@ -3592,6 +3656,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3592
3656
|
activeRailTab={activeRailTab}
|
|
3593
3657
|
activeCommentId={snapshot.comments.activeCommentId}
|
|
3594
3658
|
activeRevisionId={activeRevisionId}
|
|
3659
|
+
trackedChangesAuthoringEnabled={trackedChangesAuthoringEnabled}
|
|
3595
3660
|
showTrackedChanges={showTrackedChanges}
|
|
3596
3661
|
workflowScopeSnapshot={workflowScopeSnapshot}
|
|
3597
3662
|
layoutFacet={activeRuntime.layout}
|
|
@@ -3626,35 +3691,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3626
3691
|
onAddCommentFromSelection={addSelectionToolbarComment}
|
|
3627
3692
|
onAddCommentFromSuggestion={addSelectionToolbarComment}
|
|
3628
3693
|
onAcceptSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3629
|
-
?
|
|
3630
|
-
for (const changeId of activeSelectionTool.changeIds) {
|
|
3631
|
-
activeRuntime.acceptChange(changeId);
|
|
3632
|
-
}
|
|
3633
|
-
dismissSelectionToolbar("chrome-action");
|
|
3634
|
-
}
|
|
3694
|
+
? acceptActiveSuggestion
|
|
3635
3695
|
: undefined}
|
|
3636
3696
|
onRejectSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3637
|
-
?
|
|
3638
|
-
for (const changeId of activeSelectionTool.changeIds) {
|
|
3639
|
-
activeRuntime.rejectChange(changeId);
|
|
3640
|
-
}
|
|
3641
|
-
dismissSelectionToolbar("chrome-action");
|
|
3642
|
-
}
|
|
3697
|
+
? rejectActiveSuggestion
|
|
3643
3698
|
: undefined}
|
|
3644
3699
|
onEditSuggestion={activeSelectionTool?.kind === "suggestion-review"
|
|
3645
|
-
?
|
|
3646
|
-
setSuppressedSuggestionRevisionId(activeSelectionTool.suggestionId);
|
|
3647
|
-
const activeSuggestion = suggestionsSnapshot.suggestions.find(
|
|
3648
|
-
(suggestion) => suggestion.suggestionId === activeSelectionTool.suggestionId,
|
|
3649
|
-
);
|
|
3650
|
-
if (activeSuggestion) {
|
|
3651
|
-
applyRuntimeSelection(
|
|
3652
|
-
activeRuntime,
|
|
3653
|
-
createSelectionFromAnchor(activeSuggestion.anchor, activeSuggestion.storyTarget),
|
|
3654
|
-
);
|
|
3655
|
-
}
|
|
3656
|
-
setSelectionToolbarFocusWithin(true);
|
|
3657
|
-
}
|
|
3700
|
+
? editActiveSuggestion
|
|
3658
3701
|
: undefined}
|
|
3659
3702
|
onDismissSelectionToolbar={() => dismissSelectionToolbar("chrome-action")}
|
|
3660
3703
|
onSelectionToolbarFocusCapture={handleSelectionToolbarFocusCapture}
|
|
@@ -4296,6 +4339,42 @@ function resolveCommentCommandAnchor(
|
|
|
4296
4339
|
: selection.activeRange;
|
|
4297
4340
|
}
|
|
4298
4341
|
|
|
4342
|
+
function requestImageInsertFromPicker(
|
|
4343
|
+
onInsertImage: ((options: InsertImageOptions) => void) | undefined,
|
|
4344
|
+
): void {
|
|
4345
|
+
const document = globalThis.document;
|
|
4346
|
+
if (!onInsertImage || typeof document?.createElement !== "function" || !document.body) {
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
const input = document.createElement("input");
|
|
4351
|
+
input.type = "file";
|
|
4352
|
+
input.accept = "image/png,image/jpeg,image/gif";
|
|
4353
|
+
input.style.position = "fixed";
|
|
4354
|
+
input.style.left = "-9999px";
|
|
4355
|
+
input.style.top = "-9999px";
|
|
4356
|
+
input.addEventListener("change", () => {
|
|
4357
|
+
const file = input.files?.[0];
|
|
4358
|
+
if (!file) {
|
|
4359
|
+
input.remove();
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
void file.arrayBuffer()
|
|
4363
|
+
.then((buffer) => {
|
|
4364
|
+
onInsertImage({
|
|
4365
|
+
data: new Uint8Array(buffer),
|
|
4366
|
+
mimeType: file.type || "image/png",
|
|
4367
|
+
altText: file.name,
|
|
4368
|
+
});
|
|
4369
|
+
})
|
|
4370
|
+
.finally(() => {
|
|
4371
|
+
input.remove();
|
|
4372
|
+
});
|
|
4373
|
+
}, { once: true });
|
|
4374
|
+
document.body.appendChild(input);
|
|
4375
|
+
input.click();
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4299
4378
|
function resolveCollapsedCommentRange(
|
|
4300
4379
|
surface: RuntimeRenderSnapshot["surface"],
|
|
4301
4380
|
selection: RuntimeRenderSnapshot["selection"],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useRef } from "react";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
ChromePinSurface,
|
|
4
5
|
CommentSidebarThreadSnapshot,
|
|
5
6
|
EditorStoryTarget,
|
|
6
7
|
FormattingAlignment,
|
|
@@ -9,6 +10,7 @@ import type {
|
|
|
9
10
|
SectionBreakType,
|
|
10
11
|
SectionLayoutPatch,
|
|
11
12
|
SectionPageNumberingPatch,
|
|
13
|
+
PinState,
|
|
12
14
|
TrackedChangeEntrySnapshot,
|
|
13
15
|
ZoomLevel,
|
|
14
16
|
WorkspaceMode,
|
|
@@ -24,6 +26,7 @@ export interface EditorCommandBag {
|
|
|
24
26
|
onActiveRailTabChange(value: ReviewRailTab): void;
|
|
25
27
|
onShowTrackedChangesChange(show: boolean): void;
|
|
26
28
|
onReviewMarkupModeChange?(mode: MarkupDisplay): void;
|
|
29
|
+
onChromePinChange?(surface: ChromePinSurface, pin: PinState | null): void;
|
|
27
30
|
onUndo(): void;
|
|
28
31
|
onRedo(): void;
|
|
29
32
|
onSetParagraphStyle?(styleId: string): void;
|
|
@@ -85,6 +88,7 @@ export interface EditorCommandBag {
|
|
|
85
88
|
onAddReply?(commentId: string, body: string): void;
|
|
86
89
|
onEditBody?(commentId: string, body: string): void;
|
|
87
90
|
onOpenRevision(revision: TrackedChangeEntrySnapshot): void;
|
|
91
|
+
onReplyToRevision?(revision: TrackedChangeEntrySnapshot): void;
|
|
88
92
|
onAcceptRevision(revisionId: string): void;
|
|
89
93
|
onRejectRevision(revisionId: string): void;
|
|
90
94
|
onAcceptAllChanges(): void;
|
|
@@ -55,6 +55,7 @@ export interface EditorShellViewProps {
|
|
|
55
55
|
activeRailTab: ReviewRailTab;
|
|
56
56
|
activeCommentId?: string;
|
|
57
57
|
activeRevisionId?: string;
|
|
58
|
+
trackedChangesAuthoringEnabled?: boolean;
|
|
58
59
|
showTrackedChanges: boolean;
|
|
59
60
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
60
61
|
/**
|
|
@@ -102,6 +102,7 @@ export interface EditorSurfaceControllerProps {
|
|
|
102
102
|
}) => void;
|
|
103
103
|
onCommentActivated?: (commentId: string) => void;
|
|
104
104
|
onRevisionActivated?: (revisionId: string) => void;
|
|
105
|
+
onRevisionHovered?: (revisionId: string | null) => void;
|
|
105
106
|
workflowScopes?: readonly WorkflowScope[];
|
|
106
107
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
107
108
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
@@ -204,18 +204,16 @@ export function buildClassFromRevisionDisplay(
|
|
|
204
204
|
|
|
205
205
|
// Insertion underline (simple / all markup, insertion kind).
|
|
206
206
|
if (display.insertionUnderline) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// visual diff.
|
|
211
|
-
parts.push(
|
|
212
|
-
"underline decoration-insert/60 decoration-1 underline-offset-2 text-primary",
|
|
213
|
-
);
|
|
207
|
+
parts.push(display.markupMode === "all"
|
|
208
|
+
? "rounded-[2px] bg-insert-soft/35 px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2"
|
|
209
|
+
: "rounded-[2px] px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2");
|
|
214
210
|
}
|
|
215
211
|
|
|
216
212
|
// Strikethrough (deletion kind, simple or all markup).
|
|
217
213
|
if (display.strikethrough) {
|
|
218
|
-
parts.push(
|
|
214
|
+
parts.push(display.markupMode === "all"
|
|
215
|
+
? "rounded-[2px] bg-delete-soft/35 px-[1px] text-danger line-through decoration-danger decoration-2"
|
|
216
|
+
: "text-danger line-through decoration-danger decoration-2");
|
|
219
217
|
}
|
|
220
218
|
|
|
221
219
|
// De-emphasize (e.g. inactive revision in all-markup; reviewer
|
|
@@ -234,7 +232,7 @@ export function buildClassFromRevisionDisplay(
|
|
|
234
232
|
(display.kind === "formatting" || display.kind === "property-change")
|
|
235
233
|
) {
|
|
236
234
|
parts.push(
|
|
237
|
-
"underline decoration-accent/
|
|
235
|
+
"rounded-[2px] bg-accent-soft/70 px-[1px] underline decoration-accent/80 decoration-dotted decoration-2 underline-offset-2",
|
|
238
236
|
);
|
|
239
237
|
}
|
|
240
238
|
|
|
@@ -277,18 +275,18 @@ export function getRevisionHighlightClass(
|
|
|
277
275
|
return "";
|
|
278
276
|
case "simple-markup":
|
|
279
277
|
if (state.hasInsertions) {
|
|
280
|
-
return `underline decoration-insert
|
|
278
|
+
return `rounded-[2px] px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2${activeRing}`;
|
|
281
279
|
}
|
|
282
280
|
if (state.hasDeletions) {
|
|
283
|
-
return `text-
|
|
281
|
+
return `text-danger line-through decoration-danger decoration-2${activeRing}`;
|
|
284
282
|
}
|
|
285
283
|
return activeRing;
|
|
286
284
|
case "all-markup":
|
|
287
285
|
if (state.hasInsertions) {
|
|
288
|
-
return `
|
|
286
|
+
return `rounded-[2px] bg-insert-soft/35 px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2${activeRing}`;
|
|
289
287
|
}
|
|
290
288
|
if (state.hasDeletions) {
|
|
291
|
-
return `text-danger line-through decoration-danger
|
|
289
|
+
return `rounded-[2px] bg-delete-soft/35 px-[1px] text-danger line-through decoration-danger decoration-2${activeRing}`;
|
|
292
290
|
}
|
|
293
291
|
return activeRing;
|
|
294
292
|
}
|
|
@@ -3,16 +3,17 @@
|
|
|
3
3
|
* contextual actions exist, keyed by `TargetKind`.
|
|
4
4
|
*
|
|
5
5
|
* Consumed by three access routes so DESIGN-EDITOR.md §6.4 holds by
|
|
6
|
-
* construction
|
|
7
|
-
* cannot duplicate command trees"):
|
|
6
|
+
* construction for general editor actions:
|
|
8
7
|
*
|
|
9
8
|
* 1. Right-click context menu (via `build-context-menu-entries.ts`)
|
|
10
9
|
* 2. Inline "More…" affordance on reduced floating toolbars (Phase D)
|
|
11
10
|
* 3. Command palette groups (Phase C.δ)
|
|
12
11
|
*
|
|
13
|
-
* All three routes produce the same actions from this registry
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* All three routes produce the same general actions from this registry.
|
|
13
|
+
* Suggestion-specific review actions are intentionally absent for now:
|
|
14
|
+
* they require an exact revision/suggestion id payload, so they live on
|
|
15
|
+
* the floating suggestion card and Changes rail until the context-menu
|
|
16
|
+
* target resolver can supply that payload.
|
|
16
17
|
*
|
|
17
18
|
* Perf discipline: pure data + pure filter helpers. No DOM reads, no
|
|
18
19
|
* runtime calls, no observers. Actions are dispatched through host-
|
|
@@ -106,10 +107,6 @@ export interface EditorActionHostCallbacks {
|
|
|
106
107
|
readonly onPrintRequested?: () => void;
|
|
107
108
|
readonly onGoToRequested?: () => void;
|
|
108
109
|
|
|
109
|
-
// Tracked-change operations (suggestion target)
|
|
110
|
-
readonly onAcceptSuggestion?: () => void;
|
|
111
|
-
readonly onRejectSuggestion?: () => void;
|
|
112
|
-
|
|
113
110
|
// Comment operations (comment-anchor target)
|
|
114
111
|
readonly onResolveComment?: () => void;
|
|
115
112
|
readonly onReplyToComment?: () => void;
|
|
@@ -576,7 +573,7 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
576
573
|
mkImportant({
|
|
577
574
|
id: "insert-image",
|
|
578
575
|
label: "Insert image…",
|
|
579
|
-
description: "
|
|
576
|
+
description: "Choose an image file and insert it at the caret.",
|
|
580
577
|
group: "misc",
|
|
581
578
|
targetKinds: [],
|
|
582
579
|
callback: "onInsertImage",
|
|
@@ -636,22 +633,6 @@ export const EDITOR_ACTION_REGISTRY: readonly EditorAction[] = [
|
|
|
636
633
|
callback: "onPrintRequested",
|
|
637
634
|
}),
|
|
638
635
|
|
|
639
|
-
// -------- Suggestion / tracked change --------
|
|
640
|
-
mk({
|
|
641
|
-
id: "accept-suggestion",
|
|
642
|
-
label: "Accept suggestion",
|
|
643
|
-
group: "suggestion",
|
|
644
|
-
targetKinds: ["suggestion"],
|
|
645
|
-
callback: "onAcceptSuggestion",
|
|
646
|
-
}),
|
|
647
|
-
mk({
|
|
648
|
-
id: "reject-suggestion",
|
|
649
|
-
label: "Reject suggestion",
|
|
650
|
-
group: "suggestion",
|
|
651
|
-
targetKinds: ["suggestion"],
|
|
652
|
-
callback: "onRejectSuggestion",
|
|
653
|
-
}),
|
|
654
|
-
|
|
655
636
|
// -------- Comment --------
|
|
656
637
|
mk({
|
|
657
638
|
id: "resolve-comment",
|
|
@@ -17,11 +17,11 @@ export function isNarrowChromeViewport(viewportWidth?: number): boolean {
|
|
|
17
17
|
return typeof viewportWidth === "number" && viewportWidth <= NARROW_CHROME_MAX_WIDTH;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function getInitialReviewRailOpen(
|
|
20
|
+
export function getInitialReviewRailOpen(_input: {
|
|
21
21
|
viewportWidth?: number;
|
|
22
22
|
reviewRailAvailable: boolean;
|
|
23
23
|
}): boolean {
|
|
24
|
-
return
|
|
24
|
+
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function resolveResponsiveChromeState(
|
|
@@ -123,6 +123,7 @@ export function TwAlertBanner(
|
|
|
123
123
|
// 3. Workflow blocked — host policy refuses a command, per reasons.
|
|
124
124
|
if (workflowBlockedReasons.length > 0) {
|
|
125
125
|
const firstReason = workflowBlockedReasons[0]!;
|
|
126
|
+
const hint = getWorkflowBlockedHint(firstReason);
|
|
126
127
|
return renderBanner({
|
|
127
128
|
severity: "warning",
|
|
128
129
|
icon: (
|
|
@@ -131,6 +132,7 @@ export function TwAlertBanner(
|
|
|
131
132
|
message: (
|
|
132
133
|
<>
|
|
133
134
|
{firstReason.message}
|
|
135
|
+
{hint ? <span className="opacity-80"> {hint}</span> : null}
|
|
134
136
|
{workflowBlockedReasons.length > 1
|
|
135
137
|
? ` (+${workflowBlockedReasons.length - 1} more)`
|
|
136
138
|
: ""}
|
|
@@ -161,3 +163,28 @@ export function TwAlertBanner(
|
|
|
161
163
|
|
|
162
164
|
return null;
|
|
163
165
|
}
|
|
166
|
+
|
|
167
|
+
function getWorkflowBlockedHint(
|
|
168
|
+
reason: WorkflowBlockedCommandReason,
|
|
169
|
+
): string | null {
|
|
170
|
+
switch (reason.code) {
|
|
171
|
+
case "suggesting_unsupported":
|
|
172
|
+
return "Switch to Edit for this command, or insert plain text.";
|
|
173
|
+
case "workflow_comment_only":
|
|
174
|
+
return "Add a comment, or use an editing scope.";
|
|
175
|
+
case "outside_workflow_scope":
|
|
176
|
+
return "Move into an editable workflow scope.";
|
|
177
|
+
case "workflow_view_only":
|
|
178
|
+
case "document_viewing_mode":
|
|
179
|
+
case "document_read_only":
|
|
180
|
+
return "Open an editable copy or request edit access.";
|
|
181
|
+
case "workflow_preserve_only":
|
|
182
|
+
case "workflow_blocked_import":
|
|
183
|
+
case "protected_range":
|
|
184
|
+
case "unsupported_surface":
|
|
185
|
+
return "Use detail for the safe path.";
|
|
186
|
+
case "workflow_round_locked":
|
|
187
|
+
return "Wait for the round to unlock or request approval.";
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
@@ -43,6 +43,10 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
43
43
|
const { surface, pin, onChange, label } = props;
|
|
44
44
|
const isDetached = pin?.detached ?? false;
|
|
45
45
|
const offset = pin?.offset ?? { x: 0, y: 0 };
|
|
46
|
+
const dragHandleTestId =
|
|
47
|
+
surface === "selectionTier" ? "selection-tool-drag-handle" : `${surface}-detach-drag-handle`;
|
|
48
|
+
const toggleTestId =
|
|
49
|
+
surface === "selectionTier" ? "selection-tool-attach-toggle" : `${surface}-detach-toggle`;
|
|
46
50
|
const dragState = useRef<
|
|
47
51
|
| {
|
|
48
52
|
startX: number;
|
|
@@ -117,7 +121,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
117
121
|
<button
|
|
118
122
|
type="button"
|
|
119
123
|
aria-label={isDetached ? "Drag floating menu" : "Drag to float menu"}
|
|
120
|
-
data-testid={
|
|
124
|
+
data-testid={dragHandleTestId}
|
|
121
125
|
className="inline-flex h-6 items-center justify-center rounded-md border border-transparent px-1.5 text-tertiary transition-colors hover:border-border/60 hover:bg-surface hover:text-primary"
|
|
122
126
|
onMouseDown={beginDrag}
|
|
123
127
|
>
|
|
@@ -133,7 +137,7 @@ export function TwDetachHandle(props: TwDetachHandleProps): React.JSX.Element {
|
|
|
133
137
|
type="button"
|
|
134
138
|
aria-label={isDetached ? "Dock menu" : "Float menu"}
|
|
135
139
|
aria-pressed={isDetached}
|
|
136
|
-
data-testid={
|
|
140
|
+
data-testid={toggleTestId}
|
|
137
141
|
className="inline-flex h-6 items-center rounded-md border border-border/60 px-2 text-[10px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
|
|
138
142
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
139
143
|
onClick={toggle}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Transaction } from "prosemirror-state";
|
|
2
2
|
import type { EditorView } from "prosemirror-view";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
TextCommandAck,
|
|
6
|
+
TextCommandRefreshClass,
|
|
7
|
+
} from "../../api/public-types.ts";
|
|
5
8
|
import type {
|
|
6
9
|
LocalEditSessionState,
|
|
7
10
|
PendingOp,
|
|
@@ -78,7 +81,11 @@ export interface FastTextEditLaneOptions {
|
|
|
78
81
|
/** Optional probe hooks for perf instrumentation. */
|
|
79
82
|
probe?: {
|
|
80
83
|
markPredicted(opId: string): void;
|
|
81
|
-
markReconciled(
|
|
84
|
+
markReconciled(
|
|
85
|
+
opId: string,
|
|
86
|
+
kind: TextCommandAck["kind"],
|
|
87
|
+
refreshClass: TextCommandRefreshClass,
|
|
88
|
+
): void;
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -96,6 +103,22 @@ function allocOpId(): string {
|
|
|
96
103
|
return `op-${Date.now().toString(36)}-${nextOpIdCounter}`;
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
export function getTextCommandRefreshClass(
|
|
107
|
+
ack: TextCommandAck,
|
|
108
|
+
): TextCommandRefreshClass {
|
|
109
|
+
if (ack.refreshClass) return ack.refreshClass;
|
|
110
|
+
switch (ack.kind) {
|
|
111
|
+
case "equivalent":
|
|
112
|
+
return "local-text-equivalent";
|
|
113
|
+
case "adjusted":
|
|
114
|
+
return "surface-only";
|
|
115
|
+
case "rejected":
|
|
116
|
+
return "blocked";
|
|
117
|
+
case "structural-divergence":
|
|
118
|
+
return "full-projection";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
export function createFastTextEditLane(
|
|
100
123
|
options: FastTextEditLaneOptions,
|
|
101
124
|
): FastTextEditLane {
|
|
@@ -136,9 +159,11 @@ export function createFastTextEditLane(
|
|
|
136
159
|
|
|
137
160
|
if (options.shouldBailBeforePredict?.(intent, fromRuntime, toRuntime)) {
|
|
138
161
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
162
|
+
const refreshClass = getTextCommandRefreshClass(ack);
|
|
139
163
|
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.bailBeforePredict);
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
incrementRefreshClassCounter(refreshClass);
|
|
165
|
+
markLaneDebugReconciled(debugEntry, ack.kind, refreshClass, true);
|
|
166
|
+
options.probe?.markReconciled(opId, ack.kind, refreshClass);
|
|
142
167
|
switch (ack.kind) {
|
|
143
168
|
case "equivalent":
|
|
144
169
|
options.session.advanceToRevision({
|
|
@@ -184,8 +209,10 @@ export function createFastTextEditLane(
|
|
|
184
209
|
op.predictedSelectionHead = view.state.selection.head;
|
|
185
210
|
|
|
186
211
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
const refreshClass = getTextCommandRefreshClass(ack);
|
|
213
|
+
markLaneDebugReconciled(debugEntry, ack.kind, refreshClass, false);
|
|
214
|
+
incrementRefreshClassCounter(refreshClass);
|
|
215
|
+
options.probe?.markReconciled(opId, ack.kind, refreshClass);
|
|
189
216
|
|
|
190
217
|
switch (ack.kind) {
|
|
191
218
|
case "equivalent":
|
|
@@ -284,6 +311,8 @@ interface LaneDebugEntry {
|
|
|
284
311
|
toRuntime: number;
|
|
285
312
|
/** Dispatch → reconcile observation. Filled by `markLaneDebugReconciled`. */
|
|
286
313
|
ackKind?: TextCommandAck["kind"];
|
|
314
|
+
/** Narrow refresh tier derived from the runtime ack. */
|
|
315
|
+
refreshClass?: TextCommandRefreshClass;
|
|
287
316
|
/** Wall-clock ms between `pushLaneDebug` and `markLaneDebugReconciled`. */
|
|
288
317
|
reconcileMs?: number;
|
|
289
318
|
/** Whether the lane short-circuited to dispatch-only (no predicted TX). */
|
|
@@ -336,10 +365,12 @@ function pushLaneDebug(
|
|
|
336
365
|
function markLaneDebugReconciled(
|
|
337
366
|
entry: LaneDebugEntry | null,
|
|
338
367
|
ackKind: TextCommandAck["kind"],
|
|
368
|
+
refreshClass: TextCommandRefreshClass,
|
|
339
369
|
bailed: boolean,
|
|
340
370
|
): void {
|
|
341
371
|
if (!entry) return;
|
|
342
372
|
entry.ackKind = ackKind;
|
|
373
|
+
entry.refreshClass = refreshClass;
|
|
343
374
|
entry.bailed = bailed;
|
|
344
375
|
const now =
|
|
345
376
|
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
@@ -348,6 +379,26 @@ function markLaneDebugReconciled(
|
|
|
348
379
|
entry.reconcileMs = now - entry.startedAtMs;
|
|
349
380
|
}
|
|
350
381
|
|
|
382
|
+
function incrementRefreshClassCounter(refreshClass: TextCommandRefreshClass): void {
|
|
383
|
+
switch (refreshClass) {
|
|
384
|
+
case "selection-only":
|
|
385
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshSelectionOnly);
|
|
386
|
+
return;
|
|
387
|
+
case "local-text-equivalent":
|
|
388
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshLocalTextEquivalent);
|
|
389
|
+
return;
|
|
390
|
+
case "surface-only":
|
|
391
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshSurfaceOnly);
|
|
392
|
+
return;
|
|
393
|
+
case "full-projection":
|
|
394
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshFullProjection);
|
|
395
|
+
return;
|
|
396
|
+
case "blocked":
|
|
397
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshBlocked);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
351
402
|
function buildTxCompat(
|
|
352
403
|
view: EditorView,
|
|
353
404
|
_intent: PredictedIntent,
|
|
@@ -13,3 +13,20 @@ export function sliceBlocksForPage(
|
|
|
13
13
|
(b) => b.from < page.endOffset && b.to > page.startOffset,
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export function findBlockIndexRangeForPage(
|
|
18
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
19
|
+
page: Pick<DocumentPageSnapshot, "startOffset" | "endOffset">,
|
|
20
|
+
): { first: number; last: number } | null {
|
|
21
|
+
if (page.endOffset <= page.startOffset) return null;
|
|
22
|
+
let first = -1;
|
|
23
|
+
let last = -1;
|
|
24
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
25
|
+
const block = blocks[index]!;
|
|
26
|
+
if (block.from < page.endOffset && block.to > page.startOffset) {
|
|
27
|
+
if (first === -1) first = index;
|
|
28
|
+
last = index;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return first === -1 ? null : { first, last };
|
|
32
|
+
}
|
|
@@ -41,6 +41,11 @@ export const PREDICTED_LANE_COUNTERS = {
|
|
|
41
41
|
rollback: "predictions.rollback",
|
|
42
42
|
structuralDivergence: "predictions.structuralDivergence",
|
|
43
43
|
bailBeforePredict: "predictions.bailBeforePredict",
|
|
44
|
+
refreshSelectionOnly: "predictions.refresh.selectionOnly",
|
|
45
|
+
refreshLocalTextEquivalent: "predictions.refresh.localTextEquivalent",
|
|
46
|
+
refreshSurfaceOnly: "predictions.refresh.surfaceOnly",
|
|
47
|
+
refreshFullProjection: "predictions.refresh.fullProjection",
|
|
48
|
+
refreshBlocked: "predictions.refresh.blocked",
|
|
44
49
|
} as const;
|
|
45
50
|
|
|
46
51
|
export interface PerfProbeSample {
|