@beyondwork/docx-react-component 1.0.28 → 1.0.30
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 +26 -37
- package/src/api/public-types.ts +531 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/index.ts +201 -79
- package/src/core/commands/table-structure-commands.ts +138 -5
- package/src/core/state/text-transaction.ts +370 -3
- package/src/index.ts +41 -0
- package/src/io/docx-session.ts +318 -25
- package/src/io/export/serialize-footnotes.ts +41 -46
- package/src/io/export/serialize-headers-footers.ts +36 -40
- package/src/io/export/serialize-main-document.ts +55 -89
- package/src/io/export/serialize-numbering.ts +104 -4
- package/src/io/export/serialize-runtime-revisions.ts +196 -2
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
- package/src/io/export/table-properties-xml.ts +318 -0
- package/src/io/normalize/normalize-text.ts +34 -3
- package/src/io/ooxml/parse-comments.ts +6 -0
- package/src/io/ooxml/parse-footnotes.ts +69 -13
- package/src/io/ooxml/parse-headers-footers.ts +54 -11
- package/src/io/ooxml/parse-main-document.ts +112 -42
- package/src/io/ooxml/parse-numbering.ts +341 -26
- package/src/io/ooxml/parse-revisions.ts +118 -4
- package/src/io/ooxml/parse-styles.ts +176 -0
- package/src/io/ooxml/parse-tables.ts +34 -25
- package/src/io/ooxml/revision-boundaries.ts +127 -3
- package/src/io/ooxml/workflow-payload.ts +544 -0
- package/src/model/canonical-document.ts +91 -1
- package/src/model/snapshot.ts +112 -1
- package/src/preservation/store.ts +73 -3
- package/src/review/store/comment-store.ts +19 -1
- package/src/review/store/revision-actions.ts +29 -0
- package/src/review/store/revision-store.ts +12 -1
- package/src/review/store/revision-types.ts +11 -0
- package/src/runtime/context-analytics.ts +824 -0
- package/src/runtime/document-locations.ts +521 -0
- package/src/runtime/document-navigation.ts +14 -1
- package/src/runtime/document-outline.ts +440 -0
- package/src/runtime/document-runtime.ts +941 -45
- package/src/runtime/event-refresh-hints.ts +137 -0
- package/src/runtime/numbering-prefix.ts +67 -39
- package/src/runtime/page-layout-estimation.ts +100 -7
- package/src/runtime/resolved-numbering-geometry.ts +293 -0
- package/src/runtime/session-capabilities.ts +2 -2
- package/src/runtime/suggestions-snapshot.ts +137 -0
- package/src/runtime/surface-projection.ts +223 -27
- package/src/runtime/table-style-resolver.ts +409 -0
- package/src/runtime/view-state.ts +17 -1
- package/src/runtime/workflow-markup.ts +54 -14
- package/src/ui/WordReviewEditor.tsx +1269 -87
- package/src/ui/editor-command-bag.ts +7 -0
- package/src/ui/editor-runtime-boundary.ts +111 -10
- package/src/ui/editor-shell-view.tsx +17 -15
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-tool-context.ts +19 -0
- package/src/ui/headless/selection-tool-resolver.ts +752 -0
- package/src/ui/headless/selection-tool-types.ts +129 -0
- package/src/ui/headless/selection-toolbar-model.ts +10 -33
- package/src/ui/runtime-shortcut-dispatch.ts +365 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
- package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
- package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
- package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
- package/src/ui-tailwind/theme/editor-theme.css +58 -40
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
- package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
- package/src/validation/compatibility-engine.ts +246 -2
- package/src/validation/docx-comment-proof.ts +24 -11
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, type CSSProperties, type FocusEventHandler, type Ref } from "react";
|
|
2
|
+
|
|
3
|
+
import { GripHorizontal } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import type { RuntimeContextAnalyticsSnapshot } from "../../api/public-types";
|
|
6
|
+
import type { ActiveSelectionToolModel } from "../../ui/headless/selection-tool-types";
|
|
7
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
8
|
+
import { TwContextAnalyticsSummary } from "./tw-context-analytics-summary";
|
|
9
|
+
import { TwSelectionToolBlocked } from "./tw-selection-tool-blocked";
|
|
10
|
+
import { TwSelectionToolComment } from "./tw-selection-tool-comment";
|
|
11
|
+
import { TwSelectionToolFormatting } from "./tw-selection-tool-formatting";
|
|
12
|
+
import { TwSelectionToolStructure } from "./tw-selection-tool-structure";
|
|
13
|
+
import { TwSelectionToolSuggestion } from "./tw-selection-tool-suggestion";
|
|
14
|
+
import { TwSelectionToolWorkflow } from "./tw-selection-tool-workflow";
|
|
15
|
+
|
|
16
|
+
export interface SelectionToolPlacement {
|
|
17
|
+
placement: "right" | "left" | "above" | "below";
|
|
18
|
+
style: CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TwSelectionToolHostProps {
|
|
22
|
+
tool: ActiveSelectionToolModel | null;
|
|
23
|
+
contextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
24
|
+
placement: SelectionToolPlacement | null;
|
|
25
|
+
rootRef?: Ref<HTMLDivElement>;
|
|
26
|
+
onFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
27
|
+
onBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
28
|
+
onToggleBold?: () => void;
|
|
29
|
+
onToggleItalic?: () => void;
|
|
30
|
+
onToggleUnderline?: () => void;
|
|
31
|
+
onSetTextColor?: (color: string) => void;
|
|
32
|
+
onSetHighlightColor?: (color: string | null) => void;
|
|
33
|
+
onAddComment?: () => void;
|
|
34
|
+
onAcceptSuggestion?: () => void;
|
|
35
|
+
onRejectSuggestion?: () => void;
|
|
36
|
+
onEditSuggestion?: () => void;
|
|
37
|
+
onSetTableStyle?: (styleId: string) => void;
|
|
38
|
+
onAddRowBefore?: () => void;
|
|
39
|
+
onAddRowAfter?: () => void;
|
|
40
|
+
onAddColumnBefore?: () => void;
|
|
41
|
+
onAddColumnAfter?: () => void;
|
|
42
|
+
onDeleteRow?: () => void;
|
|
43
|
+
onDeleteColumn?: () => void;
|
|
44
|
+
onDeleteTable?: () => void;
|
|
45
|
+
onMergeCells?: () => void;
|
|
46
|
+
onSplitCell?: () => void;
|
|
47
|
+
onSetCellBackground?: (color: string) => void;
|
|
48
|
+
onSetImageLayout?: (
|
|
49
|
+
mediaId: string,
|
|
50
|
+
dimensions: { widthEmu: number; heightEmu: number },
|
|
51
|
+
) => void;
|
|
52
|
+
onSetImageFrame?: (
|
|
53
|
+
mediaId: string,
|
|
54
|
+
offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
|
|
55
|
+
) => void;
|
|
56
|
+
onRestartNumbering?: () => void;
|
|
57
|
+
onContinueNumbering?: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function TwSelectionToolHost(props: TwSelectionToolHostProps) {
|
|
61
|
+
if (!props.tool) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [isDetached, setIsDetached] = useState(false);
|
|
66
|
+
const [detachedOffset, setDetachedOffset] = useState({ x: 0, y: 0 });
|
|
67
|
+
const dragStateRef = useRef<{
|
|
68
|
+
startX: number;
|
|
69
|
+
startY: number;
|
|
70
|
+
originX: number;
|
|
71
|
+
originY: number;
|
|
72
|
+
} | null>(null);
|
|
73
|
+
const supportsTopMenuControls = isTopMenuKind(props.tool.kind);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
setIsDetached(false);
|
|
77
|
+
setDetachedOffset({ x: 0, y: 0 });
|
|
78
|
+
dragStateRef.current = null;
|
|
79
|
+
}, [props.tool.kind]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!supportsTopMenuControls || typeof window === "undefined") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
87
|
+
if (!dragStateRef.current) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
setDetachedOffset({
|
|
91
|
+
x: dragStateRef.current.originX + (event.clientX - dragStateRef.current.startX),
|
|
92
|
+
y: dragStateRef.current.originY + (event.clientY - dragStateRef.current.startY),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleMouseUp = () => {
|
|
97
|
+
dragStateRef.current = null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
101
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
102
|
+
return () => {
|
|
103
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
104
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
105
|
+
};
|
|
106
|
+
}, [supportsTopMenuControls]);
|
|
107
|
+
|
|
108
|
+
const overlayTestId = getOverlayTestId(props.tool.kind, Boolean(props.placement));
|
|
109
|
+
const toolContent = renderTool(props, props.tool);
|
|
110
|
+
const content = toolContent ? (
|
|
111
|
+
<div
|
|
112
|
+
ref={props.rootRef}
|
|
113
|
+
onFocusCapture={props.onFocusCapture}
|
|
114
|
+
onBlurCapture={props.onBlurCapture}
|
|
115
|
+
className="flex flex-col gap-2"
|
|
116
|
+
>
|
|
117
|
+
{props.contextAnalytics ? (
|
|
118
|
+
<TwContextAnalyticsSummary
|
|
119
|
+
snapshot={props.contextAnalytics}
|
|
120
|
+
compact
|
|
121
|
+
testId="selection-context-analytics"
|
|
122
|
+
/>
|
|
123
|
+
) : null}
|
|
124
|
+
{toolContent}
|
|
125
|
+
</div>
|
|
126
|
+
) : null;
|
|
127
|
+
const wrappedContent = content && supportsTopMenuControls ? (
|
|
128
|
+
<div className="flex flex-col gap-1.5">
|
|
129
|
+
<div className="inline-flex items-center gap-2 self-center rounded-xl border border-border/70 bg-canvas/94 px-2 py-1.5 shadow-[0_8px_20px_-18px_var(--color-shadow-strong)]">
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
aria-label="Drag top menu"
|
|
133
|
+
data-testid="selection-tool-drag-handle"
|
|
134
|
+
className="inline-flex h-7 items-center justify-center rounded-lg border border-transparent px-2 text-tertiary transition-colors hover:border-border/60 hover:bg-surface hover:text-primary"
|
|
135
|
+
onMouseDown={(event) => {
|
|
136
|
+
preserveEditorSelectionMouseDown(event);
|
|
137
|
+
setIsDetached(true);
|
|
138
|
+
dragStateRef.current = {
|
|
139
|
+
startX: event.clientX,
|
|
140
|
+
startY: event.clientY,
|
|
141
|
+
originX: isDetached ? detachedOffset.x : 0,
|
|
142
|
+
originY: isDetached ? detachedOffset.y : 0,
|
|
143
|
+
};
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<GripHorizontal className="h-3.5 w-3.5" />
|
|
147
|
+
</button>
|
|
148
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
149
|
+
{getTopMenuLabel(props.tool.kind)}
|
|
150
|
+
</span>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
data-testid="selection-tool-attach-toggle"
|
|
154
|
+
className="inline-flex h-7 items-center rounded-lg border border-border/60 px-2.5 text-[11px] font-medium text-secondary transition-colors hover:bg-surface hover:text-primary"
|
|
155
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
156
|
+
onClick={() => {
|
|
157
|
+
setIsDetached((current) => !current);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{isDetached ? "Attach menu" : "Detach menu"}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
{content}
|
|
164
|
+
</div>
|
|
165
|
+
) : content;
|
|
166
|
+
|
|
167
|
+
if (!wrappedContent) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (isDetached) {
|
|
172
|
+
return (
|
|
173
|
+
<div className="pointer-events-none absolute inset-0 z-20" data-testid={overlayTestId}>
|
|
174
|
+
<div
|
|
175
|
+
className="pointer-events-auto absolute"
|
|
176
|
+
data-placement="detached"
|
|
177
|
+
style={{
|
|
178
|
+
left: `calc(50% + ${detachedOffset.x}px)`,
|
|
179
|
+
top: `${12 + detachedOffset.y}px`,
|
|
180
|
+
transform: "translateX(-50%)",
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{wrappedContent}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (props.placement) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="pointer-events-none absolute inset-0 z-20" data-testid={overlayTestId}>
|
|
192
|
+
<div
|
|
193
|
+
className="pointer-events-auto absolute"
|
|
194
|
+
data-placement={props.placement.placement}
|
|
195
|
+
style={props.placement.style}
|
|
196
|
+
>
|
|
197
|
+
{wrappedContent}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className="pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center px-4 pt-3"
|
|
206
|
+
data-testid={overlayTestId}
|
|
207
|
+
>
|
|
208
|
+
<div className="pointer-events-auto" data-placement="fallback">
|
|
209
|
+
{wrappedContent}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderTool(
|
|
216
|
+
props: TwSelectionToolHostProps,
|
|
217
|
+
tool: ActiveSelectionToolModel,
|
|
218
|
+
): React.ReactNode {
|
|
219
|
+
switch (tool.kind) {
|
|
220
|
+
case "formatting-inline":
|
|
221
|
+
return (
|
|
222
|
+
<TwSelectionToolFormatting
|
|
223
|
+
model={tool}
|
|
224
|
+
onToggleBold={props.onToggleBold}
|
|
225
|
+
onToggleItalic={props.onToggleItalic}
|
|
226
|
+
onToggleUnderline={props.onToggleUnderline}
|
|
227
|
+
onSetTextColor={props.onSetTextColor}
|
|
228
|
+
onSetHighlightColor={props.onSetHighlightColor}
|
|
229
|
+
onAddComment={props.onAddComment}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
case "suggestion-review":
|
|
233
|
+
return (
|
|
234
|
+
<TwSelectionToolSuggestion
|
|
235
|
+
model={tool}
|
|
236
|
+
onAccept={props.onAcceptSuggestion}
|
|
237
|
+
onReject={props.onRejectSuggestion}
|
|
238
|
+
onEditSuggestion={props.onEditSuggestion}
|
|
239
|
+
onAddComment={props.onAddComment}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
case "structure-context":
|
|
243
|
+
return (
|
|
244
|
+
<TwSelectionToolStructure
|
|
245
|
+
model={tool}
|
|
246
|
+
onSetTableStyle={props.onSetTableStyle}
|
|
247
|
+
onAddRowBefore={props.onAddRowBefore}
|
|
248
|
+
onAddRowAfter={props.onAddRowAfter}
|
|
249
|
+
onAddColumnBefore={props.onAddColumnBefore}
|
|
250
|
+
onAddColumnAfter={props.onAddColumnAfter}
|
|
251
|
+
onDeleteRow={props.onDeleteRow}
|
|
252
|
+
onDeleteColumn={props.onDeleteColumn}
|
|
253
|
+
onDeleteTable={props.onDeleteTable}
|
|
254
|
+
onMergeCells={props.onMergeCells}
|
|
255
|
+
onSplitCell={props.onSplitCell}
|
|
256
|
+
onSetCellBackground={props.onSetCellBackground}
|
|
257
|
+
onSetImageLayout={props.onSetImageLayout}
|
|
258
|
+
onSetImageFrame={props.onSetImageFrame}
|
|
259
|
+
onRestartNumbering={props.onRestartNumbering}
|
|
260
|
+
onContinueNumbering={props.onContinueNumbering}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
case "comment-thread":
|
|
264
|
+
return <TwSelectionToolComment model={tool} onAddComment={props.onAddComment} />;
|
|
265
|
+
case "workflow-task":
|
|
266
|
+
return <TwSelectionToolWorkflow model={tool} />;
|
|
267
|
+
case "blocked-explainer":
|
|
268
|
+
return <TwSelectionToolBlocked model={tool} />;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function getOverlayTestId(kind: ActiveSelectionToolModel["kind"], hasPlacement: boolean): string {
|
|
273
|
+
switch (kind) {
|
|
274
|
+
case "suggestion-review":
|
|
275
|
+
return hasPlacement ? "suggestion-card-overlay" : "suggestion-card-fallback";
|
|
276
|
+
case "formatting-inline":
|
|
277
|
+
return hasPlacement ? "selection-toolbar-overlay" : "selection-toolbar-fallback";
|
|
278
|
+
default:
|
|
279
|
+
return hasPlacement ? "selection-tool-overlay" : "selection-tool-fallback";
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isTopMenuKind(kind: ActiveSelectionToolModel["kind"]): boolean {
|
|
284
|
+
return kind === "formatting-inline" || kind === "suggestion-review" || kind === "workflow-task";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getTopMenuLabel(kind: ActiveSelectionToolModel["kind"]): string {
|
|
288
|
+
switch (kind) {
|
|
289
|
+
case "formatting-inline":
|
|
290
|
+
return "Formatting";
|
|
291
|
+
case "suggestion-review":
|
|
292
|
+
return "Suggestion";
|
|
293
|
+
case "workflow-task":
|
|
294
|
+
return "Workflow";
|
|
295
|
+
default:
|
|
296
|
+
return "Menu";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { StructureContextSelectionToolModel } from "../../ui/headless/selection-tool-types";
|
|
4
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
5
|
+
import { TwImageContextToolbar } from "./tw-image-context-toolbar";
|
|
6
|
+
import { TwObjectContextToolbar } from "./tw-object-context-toolbar";
|
|
7
|
+
import { TwTableContextToolbar } from "./tw-table-context-toolbar";
|
|
8
|
+
|
|
9
|
+
export interface TwSelectionToolStructureProps {
|
|
10
|
+
model: StructureContextSelectionToolModel;
|
|
11
|
+
onSetTableStyle?: (styleId: string) => void;
|
|
12
|
+
onAddRowBefore?: () => void;
|
|
13
|
+
onAddRowAfter?: () => void;
|
|
14
|
+
onAddColumnBefore?: () => void;
|
|
15
|
+
onAddColumnAfter?: () => void;
|
|
16
|
+
onDeleteRow?: () => void;
|
|
17
|
+
onDeleteColumn?: () => void;
|
|
18
|
+
onDeleteTable?: () => void;
|
|
19
|
+
onMergeCells?: () => void;
|
|
20
|
+
onSplitCell?: () => void;
|
|
21
|
+
onSetCellBackground?: (color: string) => void;
|
|
22
|
+
onSetImageLayout?: (
|
|
23
|
+
mediaId: string,
|
|
24
|
+
dimensions: { widthEmu: number; heightEmu: number },
|
|
25
|
+
) => void;
|
|
26
|
+
onSetImageFrame?: (
|
|
27
|
+
mediaId: string,
|
|
28
|
+
offsets: { horizontalOffsetEmu?: number; verticalOffsetEmu?: number },
|
|
29
|
+
) => void;
|
|
30
|
+
onRestartNumbering?: () => void;
|
|
31
|
+
onContinueNumbering?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function TwSelectionToolStructure(props: TwSelectionToolStructureProps) {
|
|
35
|
+
switch (props.model.structureKind) {
|
|
36
|
+
case "image":
|
|
37
|
+
return props.model.activeImage ? (
|
|
38
|
+
<TwImageContextToolbar
|
|
39
|
+
activeImage={props.model.activeImage}
|
|
40
|
+
disabled={!props.model.canMutate}
|
|
41
|
+
onSetImageLayout={props.onSetImageLayout}
|
|
42
|
+
onSetImageFrame={props.onSetImageFrame}
|
|
43
|
+
/>
|
|
44
|
+
) : null;
|
|
45
|
+
case "object":
|
|
46
|
+
// Shapes and text boxes are shipped as informational-only structure tools
|
|
47
|
+
// until there is a real runtime-backed object mutation path.
|
|
48
|
+
return props.model.activeObject ? (
|
|
49
|
+
<TwObjectContextToolbar activeObject={props.model.activeObject} />
|
|
50
|
+
) : null;
|
|
51
|
+
case "table":
|
|
52
|
+
return (
|
|
53
|
+
<TwTableContextToolbar
|
|
54
|
+
disabled={!props.model.canMutate}
|
|
55
|
+
tableContext={props.model.activeTable ?? null}
|
|
56
|
+
tableStyles={props.model.tableStyles ?? []}
|
|
57
|
+
onSetTableStyle={props.onSetTableStyle}
|
|
58
|
+
onAddRowBefore={props.onAddRowBefore}
|
|
59
|
+
onAddRowAfter={props.onAddRowAfter}
|
|
60
|
+
onAddColumnBefore={props.onAddColumnBefore}
|
|
61
|
+
onAddColumnAfter={props.onAddColumnAfter}
|
|
62
|
+
onDeleteRow={props.onDeleteRow}
|
|
63
|
+
onDeleteColumn={props.onDeleteColumn}
|
|
64
|
+
onDeleteTable={props.onDeleteTable}
|
|
65
|
+
onMergeCells={props.onMergeCells}
|
|
66
|
+
onSplitCell={props.onSplitCell}
|
|
67
|
+
onSetCellBackground={props.onSetCellBackground}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
case "list":
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
data-testid="list-context-toolbar"
|
|
74
|
+
className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
|
|
75
|
+
>
|
|
76
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
77
|
+
List
|
|
78
|
+
</span>
|
|
79
|
+
<ToolbarButton
|
|
80
|
+
ariaLabel="Continue numbering"
|
|
81
|
+
disabled={!props.model.canMutate || !props.onContinueNumbering}
|
|
82
|
+
onClick={props.onContinueNumbering}
|
|
83
|
+
>
|
|
84
|
+
Continue
|
|
85
|
+
</ToolbarButton>
|
|
86
|
+
<ToolbarButton
|
|
87
|
+
ariaLabel="Restart numbering"
|
|
88
|
+
disabled={!props.model.canMutate || !props.onRestartNumbering}
|
|
89
|
+
onClick={props.onRestartNumbering}
|
|
90
|
+
>
|
|
91
|
+
Restart
|
|
92
|
+
</ToolbarButton>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ToolbarButton(props: {
|
|
99
|
+
ariaLabel: string;
|
|
100
|
+
disabled: boolean;
|
|
101
|
+
onClick?: () => void;
|
|
102
|
+
children: React.ReactNode;
|
|
103
|
+
}) {
|
|
104
|
+
return (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-label={props.ariaLabel}
|
|
108
|
+
disabled={props.disabled}
|
|
109
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
110
|
+
onClick={props.onClick}
|
|
111
|
+
className="inline-flex h-8 items-center 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"
|
|
112
|
+
>
|
|
113
|
+
{props.children}
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { FocusEventHandler } from "react";
|
|
3
|
+
|
|
4
|
+
import type { SuggestionReviewSelectionToolModel } from "../../ui/headless/selection-tool-types";
|
|
5
|
+
import { TwSuggestionCard } from "./tw-suggestion-card";
|
|
6
|
+
|
|
7
|
+
export interface TwSelectionToolSuggestionProps {
|
|
8
|
+
model: SuggestionReviewSelectionToolModel;
|
|
9
|
+
onFocusCapture?: FocusEventHandler<HTMLDivElement>;
|
|
10
|
+
onBlurCapture?: FocusEventHandler<HTMLDivElement>;
|
|
11
|
+
onAccept?: () => void;
|
|
12
|
+
onReject?: () => void;
|
|
13
|
+
onEditSuggestion?: () => void;
|
|
14
|
+
onAddComment?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function TwSelectionToolSuggestion(props: TwSelectionToolSuggestionProps) {
|
|
18
|
+
return (
|
|
19
|
+
<TwSuggestionCard
|
|
20
|
+
model={props.model}
|
|
21
|
+
onFocusCapture={props.onFocusCapture}
|
|
22
|
+
onBlurCapture={props.onBlurCapture}
|
|
23
|
+
onAccept={props.onAccept}
|
|
24
|
+
onReject={props.onReject}
|
|
25
|
+
onEditSuggestion={props.onEditSuggestion}
|
|
26
|
+
onAddComment={props.onAddComment}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { WorkflowTaskSelectionToolModel } from "../../ui/headless/selection-tool-types";
|
|
4
|
+
|
|
5
|
+
export interface TwSelectionToolWorkflowProps {
|
|
6
|
+
model: WorkflowTaskSelectionToolModel;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TwSelectionToolWorkflow(props: TwSelectionToolWorkflowProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-testid="workflow-task-selection-tool"
|
|
13
|
+
className="max-w-[min(24rem,calc(100vw-2rem))] rounded-xl border border-border bg-canvas px-3 py-2 shadow-lg"
|
|
14
|
+
>
|
|
15
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
16
|
+
Workflow task
|
|
17
|
+
</div>
|
|
18
|
+
<div className="mt-1 text-sm text-primary">{props.model.workflowTitle ?? "Scoped task"}</div>
|
|
19
|
+
{props.model.workflowDetail ? (
|
|
20
|
+
<div className="mt-1 text-xs text-secondary">{props.model.workflowDetail}</div>
|
|
21
|
+
) : null}
|
|
22
|
+
{props.model.disabledReason ? (
|
|
23
|
+
<div className="mt-2 text-xs text-tertiary">{props.model.disabledReason}</div>
|
|
24
|
+
) : null}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -87,7 +87,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
87
87
|
disabled={addCommentDisabled}
|
|
88
88
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
89
89
|
onClick={props.onAddComment}
|
|
90
|
-
className={`inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors text-accent hover:bg-
|
|
90
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors text-accent hover:bg-surface disabled:cursor-not-allowed disabled:opacity-30 ${focusRingClass}`}
|
|
91
91
|
>
|
|
92
92
|
<MessageSquare className="h-3.5 w-3.5" />
|
|
93
93
|
</button>
|
|
@@ -117,7 +117,7 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
117
117
|
<span
|
|
118
118
|
className={`min-w-0 max-w-[11rem] truncate rounded-full px-2 py-0.5 text-[10px] font-medium tracking-[0.08em] ${
|
|
119
119
|
model.badges.some((badge) => badge.tone === "accent")
|
|
120
|
-
? "bg-accent-
|
|
120
|
+
? "bg-canvas text-accent ring-1 ring-accent/25"
|
|
121
121
|
: "bg-surface text-tertiary"
|
|
122
122
|
}`}
|
|
123
123
|
>
|
|
@@ -166,7 +166,7 @@ function ToolbarActionButton(props: ToolbarActionButtonProps) {
|
|
|
166
166
|
onClick={props.onClick}
|
|
167
167
|
className={`inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors disabled:cursor-not-allowed disabled:opacity-30 ${
|
|
168
168
|
props.pressed
|
|
169
|
-
? "bg-accent-
|
|
169
|
+
? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm"
|
|
170
170
|
: "text-secondary hover:bg-surface"
|
|
171
171
|
} ${focusRingClass}`}
|
|
172
172
|
>
|
|
@@ -45,7 +45,7 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
|
45
45
|
</div>
|
|
46
46
|
</div>
|
|
47
47
|
{contextLabel ? (
|
|
48
|
-
<div className="shrink-0 rounded-full bg-
|
|
48
|
+
<div className="shrink-0 rounded-full bg-canvas px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.08em] text-comment ring-1 ring-comment/20">
|
|
49
49
|
{contextLabel}
|
|
50
50
|
</div>
|
|
51
51
|
) : null}
|
|
@@ -118,9 +118,9 @@ function SuggestionActionButton(props: {
|
|
|
118
118
|
onClick?: () => void;
|
|
119
119
|
}) {
|
|
120
120
|
const toneClass = props.tone === "accept"
|
|
121
|
-
? "border-
|
|
121
|
+
? "border-success/35 bg-canvas text-success hover:bg-surface"
|
|
122
122
|
: props.tone === "reject"
|
|
123
|
-
? "border-
|
|
123
|
+
? "border-danger/35 bg-canvas text-danger hover:bg-surface"
|
|
124
124
|
: "border-border text-secondary hover:bg-surface";
|
|
125
125
|
|
|
126
126
|
return (
|