@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.
Files changed (38) hide show
  1. package/README.md +6 -0
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +16 -1
  4. package/src/api/session-state.ts +2 -0
  5. package/src/io/docx-session.ts +16 -3
  6. package/src/io/ooxml/parse-footnotes.ts +23 -33
  7. package/src/io/ooxml/parse-headers-footers.ts +20 -21
  8. package/src/io/ooxml/workflow-payload.ts +311 -8
  9. package/src/model/snapshot.ts +113 -1
  10. package/src/runtime/document-runtime.ts +207 -33
  11. package/src/runtime/surface-projection.ts +156 -7
  12. package/src/ui/WordReviewEditor.tsx +13 -5
  13. package/src/ui/editor-surface-controller.tsx +2 -0
  14. package/src/ui/headless/selection-tool-resolver.ts +4 -1
  15. package/src/ui/headless/selection-tool-types.ts +1 -2
  16. package/src/ui/workflow-surface-blocked-rails.ts +19 -1
  17. package/src/ui-tailwind/chrome/responsive-chrome.ts +46 -0
  18. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +4 -4
  19. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +5 -5
  20. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +3 -3
  21. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +4 -4
  22. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +14 -9
  23. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +4 -5
  24. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +5 -5
  25. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +6 -6
  26. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +9 -9
  27. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +173 -124
  28. package/src/ui-tailwind/editor-surface/pm-decorations.ts +88 -14
  29. package/src/ui-tailwind/editor-surface/pm-schema.ts +29 -0
  30. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -1
  31. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +3 -3
  32. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +20 -0
  33. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
  34. package/src/ui-tailwind/review/tw-review-rail.tsx +9 -1
  35. package/src/ui-tailwind/theme/editor-theme.css +8 -0
  36. package/src/ui-tailwind/toolbar/toolbar-layout.ts +47 -0
  37. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +367 -22
  38. 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
- className="flex w-[336px] shrink-0 flex-col border-l border-border/60 bg-[var(--color-sidebar-tint)]"
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 className="flex h-11 shrink-0 items-center gap-1 rounded-xl border border-border/70 bg-canvas/92 px-2.5 shadow-[0_8px_20px_-18px_var(--color-shadow-strong)] backdrop-blur-sm">
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="flex min-w-0 flex-1 items-center gap-0.5">
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
- <TwToolbarIconButton
287
- icon={Outdent}
288
- label="Outdent"
289
- disabled={!canEdit}
290
- onClick={props.onOutdent}
291
- />
292
- <TwToolbarIconButton
293
- icon={Indent}
294
- label="Indent"
295
- disabled={!canEdit}
296
- onClick={props.onIndent}
297
- />
298
- {showListActions && props.activeListContext ? (
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="flex items-center gap-0.5">
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;