@beyondwork/docx-react-component 1.0.30 → 1.0.32
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 +6 -0
- package/package.json +1 -1
- package/src/api/public-types.ts +16 -1
- package/src/api/session-state.ts +2 -0
- package/src/io/docx-session.ts +16 -3
- package/src/io/ooxml/parse-footnotes.ts +23 -33
- package/src/io/ooxml/parse-headers-footers.ts +20 -21
- package/src/io/ooxml/workflow-payload.ts +311 -8
- package/src/model/snapshot.ts +113 -1
- package/src/runtime/document-runtime.ts +207 -33
- package/src/runtime/surface-projection.ts +156 -7
- package/src/ui/WordReviewEditor.tsx +13 -5
- package/src/ui/editor-surface-controller.tsx +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +4 -1
- package/src/ui/headless/selection-tool-types.ts +1 -2
- package/src/ui/workflow-surface-blocked-rails.ts +19 -1
- package/src/ui-tailwind/chrome/responsive-chrome.ts +46 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +4 -4
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +5 -5
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +3 -3
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +4 -4
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +14 -9
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +4 -5
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +5 -5
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +6 -6
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +9 -9
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +173 -124
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +88 -14
- package/src/ui-tailwind/editor-surface/pm-schema.ts +29 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -1
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +3 -3
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +20 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +9 -1
- package/src/ui-tailwind/theme/editor-theme.css +8 -0
- package/src/ui-tailwind/toolbar/toolbar-layout.ts +47 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +367 -22
- package/src/ui-tailwind/tw-review-workspace.tsx +131 -4
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
SelectionSnapshot,
|
|
19
19
|
WorkflowBlockedCommandReason,
|
|
20
20
|
WorkflowCandidateRange,
|
|
21
|
+
WorkflowLockedZone,
|
|
21
22
|
WorkflowMetadataMarkup,
|
|
22
23
|
WorkflowScope,
|
|
23
24
|
} from "../../api/public-types";
|
|
@@ -101,6 +102,7 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
101
102
|
workflowScopes?: readonly WorkflowScope[];
|
|
102
103
|
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
103
104
|
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
105
|
+
workflowLockedZones?: readonly WorkflowLockedZone[];
|
|
104
106
|
activeWorkflowWorkItemId?: string | null;
|
|
105
107
|
activeWorkflowScopeIds?: readonly string[];
|
|
106
108
|
workflowMetadata?: readonly WorkflowMetadataMarkup[];
|
|
@@ -221,6 +223,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
221
223
|
workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
|
|
222
224
|
workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
|
|
223
225
|
workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
|
|
226
|
+
workflowLockedZoneSignature: createWorkflowLockedZoneSignature(props.workflowLockedZones),
|
|
224
227
|
workflowMetadataSignature: createWorkflowMetadataSignature(props.workflowMetadata),
|
|
225
228
|
activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
|
|
226
229
|
activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
|
|
@@ -231,6 +234,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
231
234
|
props.activeRevisionId,
|
|
232
235
|
props.workflowCandidates,
|
|
233
236
|
props.workflowBlockedReasons,
|
|
237
|
+
props.workflowLockedZones,
|
|
234
238
|
props.workflowMetadata,
|
|
235
239
|
props.activeWorkflowWorkItemId,
|
|
236
240
|
props.activeWorkflowScopeIds,
|
|
@@ -280,6 +284,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
280
284
|
snapshot.activeStory,
|
|
281
285
|
props.workflowCandidates,
|
|
282
286
|
props.workflowBlockedReasons,
|
|
287
|
+
props.workflowLockedZones,
|
|
283
288
|
props.activeWorkflowWorkItemId,
|
|
284
289
|
props.activeWorkflowScopeIds,
|
|
285
290
|
props.workflowMetadata,
|
|
@@ -300,6 +305,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
300
305
|
props.activeWorkflowScopeIds,
|
|
301
306
|
props.activeWorkflowWorkItemId,
|
|
302
307
|
props.workflowBlockedReasons,
|
|
308
|
+
props.workflowLockedZones,
|
|
303
309
|
props.workflowMetadata,
|
|
304
310
|
props.workflowCandidates,
|
|
305
311
|
props.workflowScopes,
|
|
@@ -335,6 +341,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
335
341
|
snapshot.activeStory,
|
|
336
342
|
props.workflowCandidates,
|
|
337
343
|
props.workflowBlockedReasons,
|
|
344
|
+
props.workflowLockedZones,
|
|
338
345
|
props.activeWorkflowWorkItemId,
|
|
339
346
|
props.activeWorkflowScopeIds,
|
|
340
347
|
props.workflowMetadata,
|
|
@@ -898,6 +905,24 @@ function createWorkflowBlockedSignature(
|
|
|
898
905
|
).join("|");
|
|
899
906
|
}
|
|
900
907
|
|
|
908
|
+
function createWorkflowLockedZoneSignature(
|
|
909
|
+
lockedZones: readonly WorkflowLockedZone[] | undefined,
|
|
910
|
+
): string {
|
|
911
|
+
if (!lockedZones || lockedZones.length === 0) {
|
|
912
|
+
return "";
|
|
913
|
+
}
|
|
914
|
+
return lockedZones.map((zone) =>
|
|
915
|
+
[
|
|
916
|
+
zone.fragmentId,
|
|
917
|
+
zone.code,
|
|
918
|
+
zone.label,
|
|
919
|
+
zone.detail,
|
|
920
|
+
serializeAnchorSignature(zone.anchor),
|
|
921
|
+
serializeStoryTargetSignature(zone.storyTarget),
|
|
922
|
+
].join(":")
|
|
923
|
+
).join("|");
|
|
924
|
+
}
|
|
925
|
+
|
|
901
926
|
function createWorkflowMetadataSignature(
|
|
902
927
|
metadata: readonly WorkflowMetadataMarkup[] | undefined,
|
|
903
928
|
): string {
|
|
@@ -922,6 +947,7 @@ function serializeAnchorSignature(
|
|
|
922
947
|
| WorkflowScope["anchor"]
|
|
923
948
|
| WorkflowCandidateRange["anchor"]
|
|
924
949
|
| WorkflowBlockedCommandReason["anchor"]
|
|
950
|
+
| WorkflowLockedZone["anchor"]
|
|
925
951
|
| WorkflowMetadataMarkup["anchor"]
|
|
926
952
|
| undefined,
|
|
927
953
|
): string {
|
|
@@ -20,6 +20,7 @@ export type ReviewRailTab = "comments" | "changes";
|
|
|
20
20
|
|
|
21
21
|
export interface TwReviewRailProps {
|
|
22
22
|
activeTab: ReviewRailTab;
|
|
23
|
+
variant?: "docked" | "drawer";
|
|
23
24
|
currentUserId?: string;
|
|
24
25
|
comments: CommentSidebarSnapshot;
|
|
25
26
|
trackedChanges: TrackedChangesSnapshot;
|
|
@@ -46,10 +47,17 @@ const focusRingClass =
|
|
|
46
47
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
47
48
|
|
|
48
49
|
export function TwReviewRail(props: TwReviewRailProps) {
|
|
50
|
+
const variant = props.variant ?? "docked";
|
|
49
51
|
return (
|
|
50
52
|
<aside
|
|
51
53
|
aria-label="Review rail"
|
|
52
|
-
|
|
54
|
+
data-wre-drawer={variant === "drawer" ? "true" : "false"}
|
|
55
|
+
className={[
|
|
56
|
+
"flex flex-col border-l border-border/60 bg-[var(--color-sidebar-tint)]",
|
|
57
|
+
variant === "drawer"
|
|
58
|
+
? "h-full w-[min(336px,calc(100vw-1rem))] max-w-full shrink-0 shadow-[0_18px_40px_-22px_var(--color-shadow-strong)]"
|
|
59
|
+
: "w-[336px] shrink-0",
|
|
60
|
+
].join(" ")}
|
|
53
61
|
>
|
|
54
62
|
<Tabs.Root
|
|
55
63
|
value={props.activeTab}
|
|
@@ -326,6 +326,10 @@
|
|
|
326
326
|
text-underline-offset: 0.18em;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
.prosemirror-surface .ProseMirror .wre-workflow-inline-locked-zone {
|
|
330
|
+
border-radius: 0.35rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
329
333
|
.prosemirror-surface .ProseMirror .wre-workflow-rail {
|
|
330
334
|
position: relative;
|
|
331
335
|
padding-left: 0.875rem;
|
|
@@ -421,6 +425,10 @@
|
|
|
421
425
|
);
|
|
422
426
|
}
|
|
423
427
|
|
|
428
|
+
.prosemirror-surface .ProseMirror .wre-workflow-rail-locked-zone {
|
|
429
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--wre-workflow-rail-color, var(--color-danger)) 10%, transparent);
|
|
430
|
+
}
|
|
431
|
+
|
|
424
432
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone {
|
|
425
433
|
background: transparent;
|
|
426
434
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { WordReviewEditorChromePreset } from "../../api/public-types";
|
|
2
|
+
|
|
3
|
+
export interface ToolbarLayoutModelInput {
|
|
4
|
+
compactMode: boolean;
|
|
5
|
+
preset: WordReviewEditorChromePreset;
|
|
6
|
+
hasActiveListContext: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ToolbarLayoutModel {
|
|
10
|
+
showStyleSelectorsInRow: boolean;
|
|
11
|
+
showListActionsInRow: boolean;
|
|
12
|
+
showSpacingActionsInRow: boolean;
|
|
13
|
+
showInsertActionsInRow: boolean;
|
|
14
|
+
showUpdateActionsInRow: boolean;
|
|
15
|
+
showCompactOverflow: boolean;
|
|
16
|
+
showListContinuationInRow: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveToolbarLayoutModel(
|
|
20
|
+
input: ToolbarLayoutModelInput,
|
|
21
|
+
): ToolbarLayoutModel {
|
|
22
|
+
if (!input.compactMode) {
|
|
23
|
+
const showListActions = input.preset === "simple" || input.preset === "advanced";
|
|
24
|
+
return {
|
|
25
|
+
showStyleSelectorsInRow: input.preset === "advanced",
|
|
26
|
+
showListActionsInRow: showListActions,
|
|
27
|
+
showSpacingActionsInRow: true,
|
|
28
|
+
showInsertActionsInRow: input.preset === "simple" || input.preset === "advanced",
|
|
29
|
+
showUpdateActionsInRow: input.preset === "advanced",
|
|
30
|
+
showCompactOverflow: false,
|
|
31
|
+
showListContinuationInRow: showListActions && input.hasActiveListContext,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const compactOverflowHasSecondaryActions =
|
|
36
|
+
input.preset === "simple" || input.preset === "advanced";
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
showStyleSelectorsInRow: false,
|
|
40
|
+
showListActionsInRow: false,
|
|
41
|
+
showSpacingActionsInRow: false,
|
|
42
|
+
showInsertActionsInRow: false,
|
|
43
|
+
showUpdateActionsInRow: false,
|
|
44
|
+
showCompactOverflow: compactOverflowHasSecondaryActions,
|
|
45
|
+
showListContinuationInRow: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
Monitor,
|
|
29
29
|
MoreHorizontal,
|
|
30
30
|
Outdent,
|
|
31
|
+
PanelRight,
|
|
31
32
|
Plus,
|
|
32
33
|
Redo2,
|
|
34
|
+
RotateCcw,
|
|
33
35
|
Rows3,
|
|
34
36
|
Strikethrough,
|
|
35
37
|
Subscript,
|
|
@@ -56,6 +58,7 @@ import type {
|
|
|
56
58
|
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
57
59
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
58
60
|
import { TwHealthPanel } from "../review/tw-health-panel";
|
|
61
|
+
import { resolveToolbarLayoutModel } from "./toolbar-layout";
|
|
59
62
|
import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
|
|
60
63
|
|
|
61
64
|
export interface TwToolbarProps {
|
|
@@ -66,6 +69,7 @@ export interface TwToolbarProps {
|
|
|
66
69
|
showDiagnosticsChrome?: boolean;
|
|
67
70
|
interactionPolicy?: ToolbarInteractionPolicy;
|
|
68
71
|
preset?: WordReviewEditorChromePreset;
|
|
72
|
+
compactMode?: boolean;
|
|
69
73
|
workspaceMode: WorkspaceMode;
|
|
70
74
|
zoomLevel?: ZoomLevel;
|
|
71
75
|
formattingState?: FormattingStateSnapshot;
|
|
@@ -102,6 +106,9 @@ export interface TwToolbarProps {
|
|
|
102
106
|
onInsertImage?: (options: InsertImageOptions) => void;
|
|
103
107
|
onExport: () => void;
|
|
104
108
|
onWorkspaceModeChange: (value: WorkspaceMode) => void;
|
|
109
|
+
showSidebarToggle?: boolean;
|
|
110
|
+
isSidebarOpen?: boolean;
|
|
111
|
+
onToggleSidebar?: () => void;
|
|
105
112
|
onZoomChange?: (level: ZoomLevel) => void;
|
|
106
113
|
onShowTrackedChangesChange: (show: boolean) => void;
|
|
107
114
|
onRestartNumbering?: () => void;
|
|
@@ -138,6 +145,7 @@ const HIGHLIGHT_COLORS = [
|
|
|
138
145
|
export function TwToolbar(props: TwToolbarProps) {
|
|
139
146
|
const caps = props.capabilities;
|
|
140
147
|
const preset = props.preset ?? "advanced";
|
|
148
|
+
const isCompact = props.compactMode ?? false;
|
|
141
149
|
const workspaceMode = props.workspaceMode;
|
|
142
150
|
const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
|
|
143
151
|
const zoomLevel = props.zoomLevel ?? 100;
|
|
@@ -153,6 +161,18 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
153
161
|
const showHealth = showDiagnosticsChrome && Boolean(props.compatibility && props.warnings);
|
|
154
162
|
const showListActions = preset === "simple" || preset === "advanced";
|
|
155
163
|
const showUpdateActions = preset === "advanced";
|
|
164
|
+
const showSidebarToggle = props.showSidebarToggle ?? false;
|
|
165
|
+
const layoutModel = resolveToolbarLayoutModel({
|
|
166
|
+
compactMode: isCompact,
|
|
167
|
+
preset,
|
|
168
|
+
hasActiveListContext: Boolean(props.activeListContext),
|
|
169
|
+
});
|
|
170
|
+
const showPostFormattingDivider =
|
|
171
|
+
layoutModel.showListActionsInRow ||
|
|
172
|
+
layoutModel.showSpacingActionsInRow ||
|
|
173
|
+
(showInsertMenu && layoutModel.showInsertActionsInRow) ||
|
|
174
|
+
(showUpdateActions && layoutModel.showUpdateActionsInRow) ||
|
|
175
|
+
layoutModel.showCompactOverflow;
|
|
156
176
|
const zoomLabel =
|
|
157
177
|
typeof zoomLevel === "number"
|
|
158
178
|
? `${zoomLevel}%`
|
|
@@ -161,9 +181,16 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
161
181
|
: "Fit page";
|
|
162
182
|
|
|
163
183
|
return (
|
|
164
|
-
<header
|
|
184
|
+
<header
|
|
185
|
+
className={[
|
|
186
|
+
"shrink-0 rounded-xl border border-border/70 bg-canvas/92 px-2.5 shadow-[0_8px_20px_-18px_var(--color-shadow-strong)] backdrop-blur-sm",
|
|
187
|
+
isCompact
|
|
188
|
+
? "flex min-h-11 flex-wrap items-center gap-1.5 py-2"
|
|
189
|
+
: "flex h-11 items-center gap-1",
|
|
190
|
+
].join(" ")}
|
|
191
|
+
>
|
|
165
192
|
{/* Left cluster: undo/redo + formatting */}
|
|
166
|
-
<div className=
|
|
193
|
+
<div className={`flex min-w-0 flex-1 items-center gap-0.5 ${isCompact ? "flex-wrap" : ""}`}>
|
|
167
194
|
<TwToolbarIconButton
|
|
168
195
|
icon={Undo2}
|
|
169
196
|
label="Undo"
|
|
@@ -178,7 +205,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
178
205
|
/>
|
|
179
206
|
<div className="mx-1 h-4 w-px bg-border" />
|
|
180
207
|
|
|
181
|
-
{showStyleSelectors ? (
|
|
208
|
+
{showStyleSelectors && layoutModel.showStyleSelectorsInRow ? (
|
|
182
209
|
<>
|
|
183
210
|
<ToolbarParagraphStyleSelect
|
|
184
211
|
disabled={!canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
@@ -262,9 +289,9 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
262
289
|
</>
|
|
263
290
|
) : null}
|
|
264
291
|
|
|
265
|
-
<div className="mx-1 h-4 w-px bg-border" />
|
|
292
|
+
{showPostFormattingDivider ? <div className="mx-1 h-4 w-px bg-border" /> : null}
|
|
266
293
|
|
|
267
|
-
{showListActions ? (
|
|
294
|
+
{showListActions && layoutModel.showListActionsInRow ? (
|
|
268
295
|
<>
|
|
269
296
|
<TwToolbarIconButton
|
|
270
297
|
icon={List}
|
|
@@ -282,20 +309,23 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
282
309
|
/>
|
|
283
310
|
</>
|
|
284
311
|
) : null}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
{layoutModel.showSpacingActionsInRow ? (
|
|
313
|
+
<>
|
|
314
|
+
<TwToolbarIconButton
|
|
315
|
+
icon={Outdent}
|
|
316
|
+
label="Outdent"
|
|
317
|
+
disabled={!canEdit}
|
|
318
|
+
onClick={props.onOutdent}
|
|
319
|
+
/>
|
|
320
|
+
<TwToolbarIconButton
|
|
321
|
+
icon={Indent}
|
|
322
|
+
label="Indent"
|
|
323
|
+
disabled={!canEdit}
|
|
324
|
+
onClick={props.onIndent}
|
|
325
|
+
/>
|
|
326
|
+
</>
|
|
327
|
+
) : null}
|
|
328
|
+
{layoutModel.showListContinuationInRow ? (
|
|
299
329
|
<>
|
|
300
330
|
<button
|
|
301
331
|
type="button"
|
|
@@ -319,7 +349,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
319
349
|
</button>
|
|
320
350
|
</>
|
|
321
351
|
) : null}
|
|
322
|
-
{showInsertMenu ? (
|
|
352
|
+
{showInsertMenu && layoutModel.showInsertActionsInRow ? (
|
|
323
353
|
<ToolbarInsertMenu
|
|
324
354
|
disabled={!canInsertStructural}
|
|
325
355
|
onInsertImage={props.onInsertImage}
|
|
@@ -328,7 +358,7 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
328
358
|
onInsertTable={props.onInsertTable}
|
|
329
359
|
/>
|
|
330
360
|
) : null}
|
|
331
|
-
{showUpdateActions ? (
|
|
361
|
+
{showUpdateActions && layoutModel.showUpdateActionsInRow ? (
|
|
332
362
|
<>
|
|
333
363
|
<button
|
|
334
364
|
type="button"
|
|
@@ -352,6 +382,34 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
352
382
|
</button>
|
|
353
383
|
</>
|
|
354
384
|
) : null}
|
|
385
|
+
{layoutModel.showCompactOverflow ? (
|
|
386
|
+
<ToolbarCompactOverflow
|
|
387
|
+
activeListContext={props.activeListContext}
|
|
388
|
+
canEdit={canEdit}
|
|
389
|
+
canInsertStructural={canInsertStructural}
|
|
390
|
+
formattingState={props.formattingState}
|
|
391
|
+
paragraphStyles={paragraphStyles}
|
|
392
|
+
showInsertMenu={showInsertMenu}
|
|
393
|
+
showListActions={showListActions}
|
|
394
|
+
showStyleSelectors={showStyleSelectors}
|
|
395
|
+
showUpdateActions={showUpdateActions}
|
|
396
|
+
onSetParagraphStyle={props.onSetParagraphStyle}
|
|
397
|
+
onSetFontFamily={props.onSetFontFamily}
|
|
398
|
+
onSetFontSize={props.onSetFontSize}
|
|
399
|
+
onToggleBulletedList={props.onToggleBulletedList}
|
|
400
|
+
onToggleNumberedList={props.onToggleNumberedList}
|
|
401
|
+
onOutdent={props.onOutdent}
|
|
402
|
+
onIndent={props.onIndent}
|
|
403
|
+
onRestartNumbering={props.onRestartNumbering}
|
|
404
|
+
onContinueNumbering={props.onContinueNumbering}
|
|
405
|
+
onInsertPageBreak={props.onInsertPageBreak}
|
|
406
|
+
onInsertTable={props.onInsertTable}
|
|
407
|
+
onInsertSectionBreak={props.onInsertSectionBreak}
|
|
408
|
+
onInsertImage={props.onInsertImage}
|
|
409
|
+
onUpdateFields={props.onUpdateFields}
|
|
410
|
+
onUpdateTableOfContents={props.onUpdateTableOfContents}
|
|
411
|
+
/>
|
|
412
|
+
) : null}
|
|
355
413
|
|
|
356
414
|
{/* Story focus breadcrumb — visible when editing a secondary story */}
|
|
357
415
|
{props.activeStory && props.activeStory.kind !== "main" ? (
|
|
@@ -372,7 +430,19 @@ export function TwToolbar(props: TwToolbarProps) {
|
|
|
372
430
|
</div>
|
|
373
431
|
|
|
374
432
|
{/* Right cluster: comment, track changes, markup, view, export */}
|
|
375
|
-
<div className=
|
|
433
|
+
<div className={`flex items-center gap-0.5 ${isCompact ? "ml-auto flex-wrap justify-end" : ""}`}>
|
|
434
|
+
{showSidebarToggle ? (
|
|
435
|
+
<>
|
|
436
|
+
<TwToolbarIconButton
|
|
437
|
+
icon={PanelRight}
|
|
438
|
+
label="Toggle sidebar"
|
|
439
|
+
active={props.isSidebarOpen ?? false}
|
|
440
|
+
onClick={props.onToggleSidebar}
|
|
441
|
+
/>
|
|
442
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
443
|
+
</>
|
|
444
|
+
) : null}
|
|
445
|
+
|
|
376
446
|
<TwToolbarIconButton
|
|
377
447
|
icon={MessageSquare}
|
|
378
448
|
label="Add comment"
|
|
@@ -766,6 +836,281 @@ function ToolbarFontSizeSelect(props: {
|
|
|
766
836
|
);
|
|
767
837
|
}
|
|
768
838
|
|
|
839
|
+
function ToolbarCompactOverflow(props: {
|
|
840
|
+
activeListContext?: ActiveListContext | null;
|
|
841
|
+
canEdit: boolean;
|
|
842
|
+
canInsertStructural: boolean;
|
|
843
|
+
formattingState?: FormattingStateSnapshot;
|
|
844
|
+
paragraphStyles: StyleCatalogSnapshot["paragraphs"];
|
|
845
|
+
showInsertMenu: boolean;
|
|
846
|
+
showListActions: boolean;
|
|
847
|
+
showStyleSelectors: boolean;
|
|
848
|
+
showUpdateActions: boolean;
|
|
849
|
+
onSetParagraphStyle?: (styleId: string) => void;
|
|
850
|
+
onSetFontFamily?: (fontFamily: string) => void;
|
|
851
|
+
onSetFontSize?: (fontSize: number) => void;
|
|
852
|
+
onToggleBulletedList?: () => void;
|
|
853
|
+
onToggleNumberedList?: () => void;
|
|
854
|
+
onOutdent?: () => void;
|
|
855
|
+
onIndent?: () => void;
|
|
856
|
+
onRestartNumbering?: () => void;
|
|
857
|
+
onContinueNumbering?: () => void;
|
|
858
|
+
onInsertPageBreak?: () => void;
|
|
859
|
+
onInsertTable?: () => void;
|
|
860
|
+
onInsertSectionBreak?: (type: SectionBreakType) => void;
|
|
861
|
+
onInsertImage?: (options: InsertImageOptions) => void;
|
|
862
|
+
onUpdateFields?: () => void;
|
|
863
|
+
onUpdateTableOfContents?: () => void;
|
|
864
|
+
}) {
|
|
865
|
+
const [open, setOpen] = React.useState(false);
|
|
866
|
+
|
|
867
|
+
async function handleImageChange(event: React.ChangeEvent<HTMLInputElement>): Promise<void> {
|
|
868
|
+
const file = event.target.files?.[0];
|
|
869
|
+
if (!file || !props.canInsertStructural || !props.onInsertImage) {
|
|
870
|
+
event.target.value = "";
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
875
|
+
props.onInsertImage({
|
|
876
|
+
data,
|
|
877
|
+
mimeType: file.type || "image/png",
|
|
878
|
+
altText: file.name,
|
|
879
|
+
});
|
|
880
|
+
setOpen(false);
|
|
881
|
+
event.target.value = "";
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<div className="relative">
|
|
886
|
+
<Tooltip.Root>
|
|
887
|
+
<Tooltip.Trigger asChild>
|
|
888
|
+
<button
|
|
889
|
+
type="button"
|
|
890
|
+
aria-label="More document tools"
|
|
891
|
+
aria-expanded={open}
|
|
892
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
893
|
+
onClick={() => setOpen((value) => !value)}
|
|
894
|
+
className={`inline-flex h-7 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none ${focusRingClass}`}
|
|
895
|
+
>
|
|
896
|
+
More
|
|
897
|
+
<ChevronDown className="h-3.5 w-3.5 text-tertiary" />
|
|
898
|
+
</button>
|
|
899
|
+
</Tooltip.Trigger>
|
|
900
|
+
<Tooltip.Portal>
|
|
901
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
902
|
+
More document tools
|
|
903
|
+
</Tooltip.Content>
|
|
904
|
+
</Tooltip.Portal>
|
|
905
|
+
</Tooltip.Root>
|
|
906
|
+
{open ? (
|
|
907
|
+
<div className="absolute left-0 top-9 z-50 w-[min(20rem,calc(100vw-2rem))] rounded-lg bg-canvas p-2 shadow-lg ring-1 ring-border">
|
|
908
|
+
<div className="space-y-3">
|
|
909
|
+
{props.showStyleSelectors ? (
|
|
910
|
+
<div className="space-y-2">
|
|
911
|
+
<div className="px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
912
|
+
Text
|
|
913
|
+
</div>
|
|
914
|
+
<div className="grid gap-2">
|
|
915
|
+
<ToolbarParagraphStyleSelect
|
|
916
|
+
disabled={!props.canEdit || props.paragraphStyles.length === 0 || !props.onSetParagraphStyle}
|
|
917
|
+
styles={props.paragraphStyles}
|
|
918
|
+
value={props.formattingState?.paragraphStyleId}
|
|
919
|
+
onValueChange={(styleId) => {
|
|
920
|
+
props.onSetParagraphStyle?.(styleId);
|
|
921
|
+
setOpen(false);
|
|
922
|
+
}}
|
|
923
|
+
/>
|
|
924
|
+
<div className="grid grid-cols-2 gap-2">
|
|
925
|
+
<ToolbarFontFamilySelect
|
|
926
|
+
disabled={!props.canEdit || !props.onSetFontFamily}
|
|
927
|
+
value={props.formattingState?.fontFamily}
|
|
928
|
+
onValueChange={(fontFamily) => {
|
|
929
|
+
props.onSetFontFamily?.(fontFamily);
|
|
930
|
+
setOpen(false);
|
|
931
|
+
}}
|
|
932
|
+
/>
|
|
933
|
+
<ToolbarFontSizeSelect
|
|
934
|
+
disabled={!props.canEdit || !props.onSetFontSize}
|
|
935
|
+
value={props.formattingState?.fontSize}
|
|
936
|
+
onValueChange={(fontSize) => {
|
|
937
|
+
props.onSetFontSize?.(fontSize);
|
|
938
|
+
setOpen(false);
|
|
939
|
+
}}
|
|
940
|
+
/>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
) : null}
|
|
945
|
+
|
|
946
|
+
{props.showListActions ? (
|
|
947
|
+
<div className="space-y-1">
|
|
948
|
+
<div className="px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
949
|
+
Structure
|
|
950
|
+
</div>
|
|
951
|
+
<ToolbarMenuButton
|
|
952
|
+
ariaLabel="Bulleted list"
|
|
953
|
+
disabled={!props.canEdit || !props.onToggleBulletedList}
|
|
954
|
+
icon={<List className="h-3.5 w-3.5" />}
|
|
955
|
+
label="Bulleted list"
|
|
956
|
+
onClick={() => {
|
|
957
|
+
props.onToggleBulletedList?.();
|
|
958
|
+
setOpen(false);
|
|
959
|
+
}}
|
|
960
|
+
/>
|
|
961
|
+
<ToolbarMenuButton
|
|
962
|
+
ariaLabel="Numbered list"
|
|
963
|
+
disabled={!props.canEdit || !props.onToggleNumberedList}
|
|
964
|
+
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
965
|
+
label="Numbered list"
|
|
966
|
+
onClick={() => {
|
|
967
|
+
props.onToggleNumberedList?.();
|
|
968
|
+
setOpen(false);
|
|
969
|
+
}}
|
|
970
|
+
/>
|
|
971
|
+
</div>
|
|
972
|
+
) : null}
|
|
973
|
+
|
|
974
|
+
<div className="space-y-1">
|
|
975
|
+
<div className="px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
976
|
+
Paragraph
|
|
977
|
+
</div>
|
|
978
|
+
<ToolbarMenuButton
|
|
979
|
+
ariaLabel="Outdent"
|
|
980
|
+
disabled={!props.canEdit || !props.onOutdent}
|
|
981
|
+
icon={<Outdent className="h-3.5 w-3.5" />}
|
|
982
|
+
label="Outdent"
|
|
983
|
+
onClick={() => {
|
|
984
|
+
props.onOutdent?.();
|
|
985
|
+
setOpen(false);
|
|
986
|
+
}}
|
|
987
|
+
/>
|
|
988
|
+
<ToolbarMenuButton
|
|
989
|
+
ariaLabel="Indent"
|
|
990
|
+
disabled={!props.canEdit || !props.onIndent}
|
|
991
|
+
icon={<Indent className="h-3.5 w-3.5" />}
|
|
992
|
+
label="Indent"
|
|
993
|
+
onClick={() => {
|
|
994
|
+
props.onIndent?.();
|
|
995
|
+
setOpen(false);
|
|
996
|
+
}}
|
|
997
|
+
/>
|
|
998
|
+
{props.activeListContext ? (
|
|
999
|
+
<>
|
|
1000
|
+
<ToolbarMenuButton
|
|
1001
|
+
ariaLabel="Restart numbering"
|
|
1002
|
+
disabled={!props.canEdit || !props.onRestartNumbering}
|
|
1003
|
+
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1004
|
+
label="Restart numbering"
|
|
1005
|
+
onClick={() => {
|
|
1006
|
+
props.onRestartNumbering?.();
|
|
1007
|
+
setOpen(false);
|
|
1008
|
+
}}
|
|
1009
|
+
/>
|
|
1010
|
+
<ToolbarMenuButton
|
|
1011
|
+
ariaLabel="Continue numbering"
|
|
1012
|
+
disabled={!props.canEdit || !props.onContinueNumbering}
|
|
1013
|
+
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1014
|
+
label="Continue numbering"
|
|
1015
|
+
onClick={() => {
|
|
1016
|
+
props.onContinueNumbering?.();
|
|
1017
|
+
setOpen(false);
|
|
1018
|
+
}}
|
|
1019
|
+
/>
|
|
1020
|
+
</>
|
|
1021
|
+
) : null}
|
|
1022
|
+
</div>
|
|
1023
|
+
|
|
1024
|
+
{props.showInsertMenu ? (
|
|
1025
|
+
<div className="space-y-1">
|
|
1026
|
+
<div className="px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
1027
|
+
Insert
|
|
1028
|
+
</div>
|
|
1029
|
+
<ToolbarMenuButton
|
|
1030
|
+
ariaLabel="Insert page break"
|
|
1031
|
+
disabled={!props.canInsertStructural || !props.onInsertPageBreak}
|
|
1032
|
+
icon={<Minus className="h-3.5 w-3.5" />}
|
|
1033
|
+
label="Page break"
|
|
1034
|
+
onClick={() => {
|
|
1035
|
+
props.onInsertPageBreak?.();
|
|
1036
|
+
setOpen(false);
|
|
1037
|
+
}}
|
|
1038
|
+
/>
|
|
1039
|
+
<ToolbarMenuButton
|
|
1040
|
+
ariaLabel="Insert table"
|
|
1041
|
+
disabled={!props.canInsertStructural || !props.onInsertTable}
|
|
1042
|
+
icon={<Rows3 className="h-3.5 w-3.5" />}
|
|
1043
|
+
label="Table"
|
|
1044
|
+
onClick={() => {
|
|
1045
|
+
props.onInsertTable?.();
|
|
1046
|
+
setOpen(false);
|
|
1047
|
+
}}
|
|
1048
|
+
/>
|
|
1049
|
+
<label
|
|
1050
|
+
className={`flex h-8 cursor-pointer items-center gap-2 rounded-md px-2 text-left text-xs font-medium text-primary transition-colors hover:bg-surface ${
|
|
1051
|
+
!props.canInsertStructural || !props.onInsertImage ? "pointer-events-none opacity-40" : ""
|
|
1052
|
+
}`}
|
|
1053
|
+
>
|
|
1054
|
+
<ImagePlus className="h-3.5 w-3.5 text-secondary" />
|
|
1055
|
+
<span>Image</span>
|
|
1056
|
+
<input
|
|
1057
|
+
accept="image/png,image/jpeg,image/gif"
|
|
1058
|
+
aria-label="Insert image"
|
|
1059
|
+
className="sr-only"
|
|
1060
|
+
disabled={!props.canInsertStructural || !props.onInsertImage}
|
|
1061
|
+
type="file"
|
|
1062
|
+
onChange={(event) => {
|
|
1063
|
+
void handleImageChange(event);
|
|
1064
|
+
}}
|
|
1065
|
+
/>
|
|
1066
|
+
</label>
|
|
1067
|
+
<ToolbarMenuButton
|
|
1068
|
+
ariaLabel="Insert next-page section break"
|
|
1069
|
+
disabled={!props.canInsertStructural || !props.onInsertSectionBreak}
|
|
1070
|
+
icon={<FileText className="h-3.5 w-3.5" />}
|
|
1071
|
+
label="Next-page section break"
|
|
1072
|
+
onClick={() => {
|
|
1073
|
+
props.onInsertSectionBreak?.("nextPage");
|
|
1074
|
+
setOpen(false);
|
|
1075
|
+
}}
|
|
1076
|
+
/>
|
|
1077
|
+
</div>
|
|
1078
|
+
) : null}
|
|
1079
|
+
|
|
1080
|
+
{props.showUpdateActions ? (
|
|
1081
|
+
<div className="space-y-1">
|
|
1082
|
+
<div className="px-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
1083
|
+
Refresh
|
|
1084
|
+
</div>
|
|
1085
|
+
<ToolbarMenuButton
|
|
1086
|
+
ariaLabel="Refresh fields"
|
|
1087
|
+
disabled={!props.canEdit || !props.onUpdateFields}
|
|
1088
|
+
icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
1089
|
+
label="Fields"
|
|
1090
|
+
onClick={() => {
|
|
1091
|
+
props.onUpdateFields?.();
|
|
1092
|
+
setOpen(false);
|
|
1093
|
+
}}
|
|
1094
|
+
/>
|
|
1095
|
+
<ToolbarMenuButton
|
|
1096
|
+
ariaLabel="Refresh table of contents"
|
|
1097
|
+
disabled={!props.canEdit || !props.onUpdateTableOfContents}
|
|
1098
|
+
icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
1099
|
+
label="Table of contents"
|
|
1100
|
+
onClick={() => {
|
|
1101
|
+
props.onUpdateTableOfContents?.();
|
|
1102
|
+
setOpen(false);
|
|
1103
|
+
}}
|
|
1104
|
+
/>
|
|
1105
|
+
</div>
|
|
1106
|
+
) : null}
|
|
1107
|
+
</div>
|
|
1108
|
+
</div>
|
|
1109
|
+
) : null}
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
769
1114
|
function ToolbarFormattingOverflow(props: {
|
|
770
1115
|
disabled: boolean;
|
|
771
1116
|
formattingState?: FormattingStateSnapshot;
|