@beyondwork/docx-react-component 1.0.30 → 1.0.31

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.
@@ -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;