@beyondwork/docx-react-component 1.0.22 → 1.0.24-rc
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/README.md +81 -38
- package/package.json +1 -1
- package/src/api/public-types.ts +67 -1
- package/src/core/commands/index.ts +625 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +667 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +6 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +96 -28
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +6 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
|
@@ -718,7 +718,10 @@ function createLoadingRuntimeBridge(input: {
|
|
|
718
718
|
subscribeToEvents: () => () => undefined,
|
|
719
719
|
emitBlockedCommand: () => undefined,
|
|
720
720
|
getRenderSnapshot: () => input.snapshot,
|
|
721
|
+
getCanonicalDocument: () => input.sessionState.canonicalDocument,
|
|
722
|
+
getSourcePackage: () => input.sessionState.sourcePackage,
|
|
721
723
|
replaceText: () => undefined,
|
|
724
|
+
applyActiveStoryTextCommand: () => undefined,
|
|
722
725
|
dispatch: () => undefined,
|
|
723
726
|
undo: () => undefined,
|
|
724
727
|
redo: () => undefined,
|
|
@@ -770,7 +773,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
770
773
|
setWorkflowOverlay: () => undefined,
|
|
771
774
|
clearWorkflowOverlay: () => undefined,
|
|
772
775
|
getWorkflowScopeSnapshot: () => null,
|
|
773
|
-
getInteractionGuardSnapshot: () => ({ blockedReasons: [] }),
|
|
776
|
+
getInteractionGuardSnapshot: () => ({ effectiveMode: "edit", blockedReasons: [] }),
|
|
774
777
|
getWorkflowMarkupSnapshot: () => ({
|
|
775
778
|
totalCount: 0,
|
|
776
779
|
items: [],
|
|
@@ -781,6 +784,12 @@ function createLoadingRuntimeBridge(input: {
|
|
|
781
784
|
protectedRanges: [],
|
|
782
785
|
opaqueFragments: [],
|
|
783
786
|
}),
|
|
787
|
+
setHostAnnotationOverlay: () => undefined,
|
|
788
|
+
clearHostAnnotationOverlay: () => undefined,
|
|
789
|
+
getHostAnnotationSnapshot: () => ({
|
|
790
|
+
totalCount: 0,
|
|
791
|
+
annotations: [],
|
|
792
|
+
}),
|
|
784
793
|
getWorkflowCandidateRanges: () => [],
|
|
785
794
|
replaceWorkflowMarkupText: () => undefined,
|
|
786
795
|
};
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
InteractionGuardSnapshot,
|
|
8
8
|
RuntimeRenderSnapshot,
|
|
9
9
|
StyleCatalogSnapshot,
|
|
10
|
+
WordReviewEditorChromeVisibility,
|
|
10
11
|
WorkflowScopeSnapshot,
|
|
11
12
|
WorkspaceMode,
|
|
12
13
|
ZoomLevel,
|
|
@@ -16,6 +17,7 @@ import type { MarkupDisplay } from "./headless/comment-decoration-model.ts";
|
|
|
16
17
|
import type {
|
|
17
18
|
SelectionToolbarAnchor,
|
|
18
19
|
SelectionToolbarModel,
|
|
20
|
+
SuggestionCardModel,
|
|
19
21
|
} from "./headless/selection-toolbar-model.ts";
|
|
20
22
|
import type { EditorCommandBag } from "./editor-command-bag.ts";
|
|
21
23
|
import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail.tsx";
|
|
@@ -56,17 +58,23 @@ export interface EditorShellViewProps {
|
|
|
56
58
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
57
59
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
58
60
|
selectionToolbar?: SelectionToolbarModel | null;
|
|
61
|
+
suggestionCard?: SuggestionCardModel | null;
|
|
59
62
|
selectionToolbarAnchor?: SelectionToolbarAnchor | null;
|
|
60
63
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
61
64
|
commands: EditorCommandBag;
|
|
62
65
|
document: ReactNode;
|
|
63
66
|
onAddCommentFromSelection?: () => void;
|
|
67
|
+
onAddCommentFromSuggestion?: () => void;
|
|
68
|
+
onAcceptSuggestion?: () => void;
|
|
69
|
+
onRejectSuggestion?: () => void;
|
|
70
|
+
onEditSuggestion?: () => void;
|
|
64
71
|
onDismissSelectionToolbar?: () => void;
|
|
65
72
|
onSelectionToolbarFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
66
73
|
onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
67
74
|
selectionToolbarRef?: React.Ref<HTMLDivElement>;
|
|
68
75
|
activeImageContext?: ActiveImageContext | null;
|
|
69
76
|
activeObjectContext?: ActiveObjectContext | null;
|
|
77
|
+
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export function EditorShellView(props: EditorShellViewProps) {
|
|
@@ -27,6 +27,7 @@ export interface EditorSurfaceControllerProps {
|
|
|
27
27
|
markupDisplay: MarkupDisplay;
|
|
28
28
|
activeRevisionId?: string;
|
|
29
29
|
showTrackedChanges?: boolean;
|
|
30
|
+
suggestionsEnabled?: boolean;
|
|
30
31
|
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
31
32
|
isPageWorkspace?: boolean;
|
|
32
33
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
@@ -40,11 +41,16 @@ export interface EditorSurfaceControllerProps {
|
|
|
40
41
|
onOutdentTab?: () => void;
|
|
41
42
|
onInsertHardBreak?: () => void;
|
|
42
43
|
onSplitParagraph?: () => void;
|
|
44
|
+
onUndo?: () => void;
|
|
45
|
+
onRedo?: () => void;
|
|
46
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
43
47
|
onCommentActivated?: (commentId: string) => void;
|
|
44
48
|
onRevisionActivated?: (revisionId: string) => void;
|
|
45
49
|
workflowScopes?: readonly WorkflowScope[];
|
|
46
50
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
47
51
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
52
|
+
activeWorkflowWorkItemId?: string | null;
|
|
53
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export const EditorSurfaceController = forwardRef<
|
|
@@ -14,6 +14,18 @@ export interface SelectionToolbarModel {
|
|
|
14
14
|
disabledReason?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface SuggestionCardModel {
|
|
18
|
+
revisionId: string;
|
|
19
|
+
kindLabel: string;
|
|
20
|
+
previewText: string;
|
|
21
|
+
badges: SelectionToolbarBadge[];
|
|
22
|
+
canAccept: boolean;
|
|
23
|
+
canReject: boolean;
|
|
24
|
+
canEditSuggestion: boolean;
|
|
25
|
+
canAddComment: boolean;
|
|
26
|
+
disabledReason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export interface SelectionToolbarAnchor {
|
|
18
30
|
left: number;
|
|
19
31
|
right: number;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FocusEventHandler } from "react";
|
|
3
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
|
+
import { Check, MessageSquare, Pencil, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import type { SuggestionCardModel } from "../../ui/headless/selection-toolbar-model";
|
|
7
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
8
|
+
|
|
9
|
+
export interface TwSuggestionCardProps {
|
|
10
|
+
model: SuggestionCardModel;
|
|
11
|
+
onFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
12
|
+
onBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
13
|
+
onAccept?: () => void;
|
|
14
|
+
onReject?: () => void;
|
|
15
|
+
onEditSuggestion?: () => void;
|
|
16
|
+
onAddComment?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const focusRingClass =
|
|
20
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
21
|
+
|
|
22
|
+
export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
23
|
+
const contextLabel = summarizeSuggestionContext(props.model);
|
|
24
|
+
const commentDisabled = !props.model.canAddComment;
|
|
25
|
+
const tooltipLabel = commentDisabled
|
|
26
|
+
? props.model.disabledReason ?? "Commenting is unavailable for this selection"
|
|
27
|
+
: "Comment on suggestion";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
data-testid="suggestion-card"
|
|
32
|
+
className="inline-flex max-w-[min(28rem,calc(100vw-2rem))] flex-col gap-2 rounded-2xl border border-border/80 bg-canvas px-3 py-2 shadow-xl ring-1 ring-border/80"
|
|
33
|
+
onFocusCapture={props.onFocusCapture}
|
|
34
|
+
onBlurCapture={props.onBlurCapture}
|
|
35
|
+
role="group"
|
|
36
|
+
aria-label="Suggestion actions"
|
|
37
|
+
>
|
|
38
|
+
<div className="flex items-start justify-between gap-3">
|
|
39
|
+
<div className="min-w-0">
|
|
40
|
+
<div className="text-[11px] font-semibold uppercase tracking-[0.12em] text-warning">
|
|
41
|
+
{props.model.kindLabel}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="mt-1 max-w-[16rem] truncate text-sm text-primary">
|
|
44
|
+
{props.model.previewText}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
{contextLabel ? (
|
|
48
|
+
<div className="shrink-0 rounded-full bg-accent-soft px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-accent">
|
|
49
|
+
{contextLabel}
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
55
|
+
<SuggestionActionButton
|
|
56
|
+
icon={<Check className="h-3.5 w-3.5" />}
|
|
57
|
+
label="Accept suggestion"
|
|
58
|
+
disabled={!props.model.canAccept}
|
|
59
|
+
tone="accept"
|
|
60
|
+
onClick={props.onAccept}
|
|
61
|
+
/>
|
|
62
|
+
<SuggestionActionButton
|
|
63
|
+
icon={<X className="h-3.5 w-3.5" />}
|
|
64
|
+
label="Reject suggestion"
|
|
65
|
+
disabled={!props.model.canReject}
|
|
66
|
+
tone="reject"
|
|
67
|
+
onClick={props.onReject}
|
|
68
|
+
/>
|
|
69
|
+
<SuggestionActionButton
|
|
70
|
+
icon={<Pencil className="h-3.5 w-3.5" />}
|
|
71
|
+
label="Edit suggestion"
|
|
72
|
+
disabled={!props.model.canEditSuggestion}
|
|
73
|
+
tone="neutral"
|
|
74
|
+
onClick={props.onEditSuggestion}
|
|
75
|
+
/>
|
|
76
|
+
<Tooltip.Root>
|
|
77
|
+
<Tooltip.Trigger asChild>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
aria-label="Comment on suggestion"
|
|
81
|
+
disabled={commentDisabled}
|
|
82
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
83
|
+
onClick={props.onAddComment}
|
|
84
|
+
className={`inline-flex h-8 items-center gap-1 rounded-lg border border-border px-2.5 text-xs font-medium text-secondary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
85
|
+
>
|
|
86
|
+
<MessageSquare className="h-3.5 w-3.5" />
|
|
87
|
+
Comment
|
|
88
|
+
</button>
|
|
89
|
+
</Tooltip.Trigger>
|
|
90
|
+
<Tooltip.Portal>
|
|
91
|
+
<Tooltip.Content
|
|
92
|
+
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
93
|
+
sideOffset={6}
|
|
94
|
+
>
|
|
95
|
+
{tooltipLabel}
|
|
96
|
+
</Tooltip.Content>
|
|
97
|
+
</Tooltip.Portal>
|
|
98
|
+
</Tooltip.Root>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function summarizeSuggestionContext(model: SuggestionCardModel): string | null {
|
|
105
|
+
const labels = model.badges.map((badge) => badge.label.trim()).filter(Boolean);
|
|
106
|
+
if (labels.length === 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const summary = labels.slice(0, 2).join(" · ");
|
|
110
|
+
return summary.length > 36 ? `${summary.slice(0, 33)}...` : summary;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function SuggestionActionButton(props: {
|
|
114
|
+
icon: React.ReactNode;
|
|
115
|
+
label: string;
|
|
116
|
+
disabled: boolean;
|
|
117
|
+
tone: "accept" | "reject" | "neutral";
|
|
118
|
+
onClick?: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
const toneClass = props.tone === "accept"
|
|
121
|
+
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300"
|
|
122
|
+
: props.tone === "reject"
|
|
123
|
+
? "border-rose-500/30 bg-rose-500/10 text-rose-700 hover:bg-rose-500/15 dark:text-rose-300"
|
|
124
|
+
: "border-border text-secondary hover:bg-surface";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
aria-label={props.label}
|
|
130
|
+
disabled={props.disabled}
|
|
131
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
132
|
+
onClick={props.onClick}
|
|
133
|
+
className={`inline-flex h-8 items-center gap-1 rounded-lg border px-2.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${toneClass} ${focusRingClass}`}
|
|
134
|
+
>
|
|
135
|
+
{props.icon}
|
|
136
|
+
{props.label.replace(" suggestion", "").replace(" on suggestion", "")}
|
|
137
|
+
</button>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -19,6 +19,7 @@ export interface CommandBridgeCallbacks {
|
|
|
19
19
|
onOutdentTab?: () => void;
|
|
20
20
|
onUndo: () => void;
|
|
21
21
|
onRedo: () => void;
|
|
22
|
+
onBlockedInput?: (command: "paste" | "drop", message: string) => void;
|
|
22
23
|
onSelectionChange: (selection: SelectionSnapshot) => void;
|
|
23
24
|
getPositionMap: () => PositionMap | null;
|
|
24
25
|
isSelectionSyncSuppressed?: () => boolean;
|
|
@@ -100,17 +101,22 @@ export function createCommandBridgePlugins(
|
|
|
100
101
|
},
|
|
101
102
|
},
|
|
102
103
|
handleTextInput(_view, _from, _to, text) {
|
|
104
|
+
if (isComposing) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
103
107
|
callbacks.onInsertText(text);
|
|
104
108
|
return true; // Block PM from processing
|
|
105
109
|
},
|
|
106
110
|
|
|
107
111
|
// Block paste (rich paste is not safe, plain paste via text.insert is TODO)
|
|
108
112
|
handlePaste() {
|
|
113
|
+
callbacks.onBlockedInput?.("paste", "Paste is not supported in the mounted editor yet.");
|
|
109
114
|
return true; // Block
|
|
110
115
|
},
|
|
111
116
|
|
|
112
117
|
// Block drop
|
|
113
118
|
handleDrop() {
|
|
119
|
+
callbacks.onBlockedInput?.("drop", "Drag and drop is not supported in the mounted editor.");
|
|
114
120
|
return true; // Block
|
|
115
121
|
},
|
|
116
122
|
},
|
|
@@ -21,18 +21,28 @@ type RailDecorationSpec = {
|
|
|
21
21
|
attrs: Record<string, string>;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
function getWorkflowInlineClass(scope: WorkflowScope): string {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
function getWorkflowInlineClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
|
|
25
|
+
const base =
|
|
26
|
+
scope.mode === "edit"
|
|
27
|
+
? "wre-workflow-inline wre-workflow-inline-edit"
|
|
28
|
+
: scope.mode === "suggest"
|
|
29
|
+
? "wre-workflow-inline wre-workflow-inline-suggest"
|
|
30
|
+
: scope.mode === "comment"
|
|
31
|
+
? "wre-workflow-inline wre-workflow-inline-comment"
|
|
32
|
+
: "wre-workflow-inline wre-workflow-inline-view";
|
|
33
|
+
return isActiveWorkItem ? `${base} wre-workflow-inline-active` : base;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
function getWorkflowRailClass(scope: WorkflowScope): string {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
function getWorkflowRailClass(scope: WorkflowScope, isActiveWorkItem: boolean): string {
|
|
37
|
+
const base =
|
|
38
|
+
scope.mode === "edit"
|
|
39
|
+
? "wre-workflow-rail wre-workflow-rail-edit"
|
|
40
|
+
: scope.mode === "suggest"
|
|
41
|
+
? "wre-workflow-rail wre-workflow-rail-suggest"
|
|
42
|
+
: scope.mode === "comment"
|
|
43
|
+
? "wre-workflow-rail wre-workflow-rail-comment"
|
|
44
|
+
: "wre-workflow-rail wre-workflow-rail-view";
|
|
45
|
+
return isActiveWorkItem ? `${base} wre-workflow-rail-active` : base;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
function getWorkflowCandidateInlineClass(): string {
|
|
@@ -142,8 +152,14 @@ function pushRailDecorations(
|
|
|
142
152
|
from: number,
|
|
143
153
|
to: number,
|
|
144
154
|
spec: RailDecorationSpec,
|
|
155
|
+
rangeCache: Map<string, Array<{ from: number; to: number }>>,
|
|
145
156
|
): void {
|
|
146
|
-
|
|
157
|
+
const cacheKey = `${from}:${to}`;
|
|
158
|
+
const ranges = rangeCache.get(cacheKey) ?? collectRailRanges(doc, from, to);
|
|
159
|
+
if (!rangeCache.has(cacheKey)) {
|
|
160
|
+
rangeCache.set(cacheKey, ranges);
|
|
161
|
+
}
|
|
162
|
+
for (const range of ranges) {
|
|
147
163
|
decorations.push(
|
|
148
164
|
Decoration.node(range.from, range.to, {
|
|
149
165
|
class: spec.className,
|
|
@@ -172,8 +188,15 @@ export function buildDecorations(
|
|
|
172
188
|
activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
|
|
173
189
|
workflowCandidates?: readonly WorkflowCandidateRange[],
|
|
174
190
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
|
|
191
|
+
activeWorkflowWorkItemId?: string | null,
|
|
192
|
+
activeWorkflowScopeIds?: readonly string[],
|
|
193
|
+
suggestionsEnabled = false,
|
|
175
194
|
): DecorationSet {
|
|
176
195
|
const decorations: Decoration[] = [];
|
|
196
|
+
const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
|
|
197
|
+
const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
|
|
198
|
+
// In suggestions mode, tracked changes are always shown regardless of the toggle.
|
|
199
|
+
const effectiveShowTracked = suggestionsEnabled ? true : showTrackedChanges;
|
|
177
200
|
|
|
178
201
|
// Walk comment threads and create inline decorations
|
|
179
202
|
if (commentModel) {
|
|
@@ -207,7 +230,7 @@ export function buildDecorations(
|
|
|
207
230
|
// Always hide deletions in clean mode (final-text semantics).
|
|
208
231
|
// This is the critical behavior: "hide tracked changes" must show
|
|
209
232
|
// the document as if accepted, not show deleted text as kept text.
|
|
210
|
-
if (markupDisplay === "clean" && rev.kind === "deletion") {
|
|
233
|
+
if (markupDisplay === "clean" && rev.kind === "deletion" && !suggestionsEnabled) {
|
|
211
234
|
const pmFrom = positionMap.runtimeToPm(rev.from);
|
|
212
235
|
const pmTo = positionMap.runtimeToPm(rev.to);
|
|
213
236
|
if (pmFrom < pmTo) {
|
|
@@ -222,7 +245,48 @@ export function buildDecorations(
|
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
// Skip visual styling when tracked changes display is off
|
|
225
|
-
if (!
|
|
248
|
+
if (!effectiveShowTracked) continue;
|
|
249
|
+
|
|
250
|
+
const pmFrom = positionMap.runtimeToPm(rev.from);
|
|
251
|
+
const pmTo = positionMap.runtimeToPm(rev.to);
|
|
252
|
+
if (pmFrom >= pmTo) continue;
|
|
253
|
+
|
|
254
|
+
if (suggestionsEnabled) {
|
|
255
|
+
if (rev.kind === "insertion") {
|
|
256
|
+
decorations.push(
|
|
257
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
258
|
+
class: "text-insert",
|
|
259
|
+
"data-revision-id": rev.revisionId,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
decorations.push(
|
|
263
|
+
Decoration.widget(pmFrom, () => {
|
|
264
|
+
const el = document.createElement("span");
|
|
265
|
+
el.textContent = "[";
|
|
266
|
+
el.className = "text-insert";
|
|
267
|
+
el.setAttribute("contenteditable", "false");
|
|
268
|
+
return el;
|
|
269
|
+
}, { side: -1, key: `${rev.revisionId}-open` }),
|
|
270
|
+
);
|
|
271
|
+
decorations.push(
|
|
272
|
+
Decoration.widget(pmTo, () => {
|
|
273
|
+
const el = document.createElement("span");
|
|
274
|
+
el.textContent = "]";
|
|
275
|
+
el.className = "text-insert";
|
|
276
|
+
el.setAttribute("contenteditable", "false");
|
|
277
|
+
return el;
|
|
278
|
+
}, { side: 1, key: `${rev.revisionId}-close` }),
|
|
279
|
+
);
|
|
280
|
+
} else if (rev.kind === "deletion") {
|
|
281
|
+
decorations.push(
|
|
282
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
283
|
+
class: "text-danger line-through decoration-danger/80 decoration-1",
|
|
284
|
+
"data-revision-id": rev.revisionId,
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
226
290
|
|
|
227
291
|
const cls = getRevisionHighlightClass(
|
|
228
292
|
revisionModel,
|
|
@@ -232,16 +296,12 @@ export function buildDecorations(
|
|
|
232
296
|
);
|
|
233
297
|
if (!cls) continue;
|
|
234
298
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"data-revision-id": rev.revisionId,
|
|
242
|
-
}),
|
|
243
|
-
);
|
|
244
|
-
}
|
|
299
|
+
decorations.push(
|
|
300
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
301
|
+
class: cls,
|
|
302
|
+
"data-revision-id": rev.revisionId,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
245
305
|
}
|
|
246
306
|
}
|
|
247
307
|
|
|
@@ -251,25 +311,33 @@ export function buildDecorations(
|
|
|
251
311
|
if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
|
|
252
312
|
const pmRange = buildAnchorPmRange(scope.anchor, positionMap);
|
|
253
313
|
if (!pmRange) continue;
|
|
314
|
+
const isActiveWorkItem =
|
|
315
|
+
Boolean(activeWorkflowWorkItemId) &&
|
|
316
|
+
(
|
|
317
|
+
scope.workItemId === activeWorkflowWorkItemId ||
|
|
318
|
+
activeScopeIds.has(scope.scopeId)
|
|
319
|
+
);
|
|
254
320
|
|
|
255
321
|
if (pmRange.allowInline && pmRange.from < pmRange.to) {
|
|
256
322
|
decorations.push(
|
|
257
323
|
Decoration.inline(pmRange.from, pmRange.to, {
|
|
258
|
-
class: getWorkflowInlineClass(scope),
|
|
324
|
+
class: getWorkflowInlineClass(scope, isActiveWorkItem),
|
|
259
325
|
"data-workflow-scope-id": scope.scopeId,
|
|
260
326
|
"data-workflow-scope-mode": scope.mode,
|
|
327
|
+
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
261
328
|
}),
|
|
262
329
|
);
|
|
263
330
|
}
|
|
264
331
|
|
|
265
332
|
pushRailDecorations(decorations, doc, pmRange.from, pmRange.to, {
|
|
266
333
|
railKind: "scope",
|
|
267
|
-
className: getWorkflowRailClass(scope),
|
|
334
|
+
className: getWorkflowRailClass(scope, isActiveWorkItem),
|
|
268
335
|
attrs: {
|
|
269
336
|
"data-workflow-scope-id": scope.scopeId,
|
|
270
337
|
"data-workflow-scope-mode": scope.mode,
|
|
338
|
+
"data-workflow-active": isActiveWorkItem ? "true" : "false",
|
|
271
339
|
},
|
|
272
|
-
});
|
|
340
|
+
}, railRangeCache);
|
|
273
341
|
}
|
|
274
342
|
}
|
|
275
343
|
|
|
@@ -295,7 +363,7 @@ export function buildDecorations(
|
|
|
295
363
|
attrs: {
|
|
296
364
|
"data-workflow-candidate-id": candidate.candidateId,
|
|
297
365
|
},
|
|
298
|
-
});
|
|
366
|
+
}, railRangeCache);
|
|
299
367
|
}
|
|
300
368
|
}
|
|
301
369
|
|
|
@@ -327,7 +395,7 @@ export function buildDecorations(
|
|
|
327
395
|
attrs: {
|
|
328
396
|
"data-workflow-blocked-code": reason.code,
|
|
329
397
|
},
|
|
330
|
-
});
|
|
398
|
+
}, railRangeCache);
|
|
331
399
|
}
|
|
332
400
|
}
|
|
333
401
|
|
|
@@ -383,6 +383,8 @@ function buildTable(
|
|
|
383
383
|
}
|
|
384
384
|
rows.push(editorSchema.nodes.table_row.create(
|
|
385
385
|
{
|
|
386
|
+
gridBefore: row.gridBefore ?? 0,
|
|
387
|
+
gridAfter: row.gridAfter ?? 0,
|
|
386
388
|
height: row.height ?? null,
|
|
387
389
|
heightRule: row.heightRule ?? null,
|
|
388
390
|
isHeader: row.isHeader ?? false,
|
|
@@ -41,6 +41,9 @@ export function createSurfaceDecorationKey(input: {
|
|
|
41
41
|
workflowScopeSignature?: string;
|
|
42
42
|
workflowCandidateSignature?: string;
|
|
43
43
|
workflowBlockedSignature?: string;
|
|
44
|
+
activeWorkflowWorkItemId?: string | null;
|
|
45
|
+
activeWorkflowScopeIds?: readonly string[];
|
|
46
|
+
suggestionsEnabled?: boolean;
|
|
44
47
|
}): string {
|
|
45
48
|
return JSON.stringify({
|
|
46
49
|
markupDisplay: input.markupDisplay,
|
|
@@ -51,5 +54,8 @@ export function createSurfaceDecorationKey(input: {
|
|
|
51
54
|
workflowScopeSignature: input.workflowScopeSignature ?? null,
|
|
52
55
|
workflowCandidateSignature: input.workflowCandidateSignature ?? null,
|
|
53
56
|
workflowBlockedSignature: input.workflowBlockedSignature ?? null,
|
|
57
|
+
activeWorkflowWorkItemId: input.activeWorkflowWorkItemId ?? null,
|
|
58
|
+
activeWorkflowScopeIds: input.activeWorkflowScopeIds ?? [],
|
|
59
|
+
suggestionsEnabled: input.suggestionsEnabled ?? false,
|
|
54
60
|
});
|
|
55
61
|
}
|