@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.
Files changed (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -26,13 +26,13 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
26
26
  onMouseDown={preserveEditorSelectionMouseDown}
27
27
  onClick={props.onClick}
28
28
  className={[
29
- "inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors outline-none",
29
+ "inline-flex h-7 w-7 items-center justify-center rounded-lg border border-transparent transition-colors outline-none",
30
30
  "disabled:opacity-30 disabled:cursor-not-allowed",
31
31
  props.emphasis
32
- ? "text-accent hover:bg-accent-soft"
32
+ ? "text-accent hover:border-border/60 hover:bg-surface"
33
33
  : props.active
34
- ? "bg-accent-soft text-accent"
35
- : "text-secondary hover:bg-surface hover:text-primary",
34
+ ? "border-border/70 bg-surface text-accent shadow-[0_4px_12px_-10px_var(--color-shadow-strong)]"
35
+ : "text-secondary hover:border-border/60 hover:bg-surface hover:text-primary",
36
36
  focusRingClass,
37
37
  ].join(" ")}
38
38
  >
@@ -6,6 +6,7 @@ import * as Toggle from "@radix-ui/react-toggle";
6
6
  import * as ToggleGroup from "@radix-ui/react-toggle-group";
7
7
  import * as Tooltip from "@radix-ui/react-tooltip";
8
8
  import {
9
+ AlertCircle,
9
10
  AlignCenter,
10
11
  AlignJustify,
11
12
  AlignLeft,
@@ -21,6 +22,7 @@ import {
21
22
  ImagePlus,
22
23
  Indent,
23
24
  Italic,
25
+ List,
24
26
  MessageSquare,
25
27
  Minus,
26
28
  Monitor,
@@ -32,13 +34,12 @@ import {
32
34
  Strikethrough,
33
35
  Subscript,
34
36
  Superscript,
35
- ShieldAlert,
36
- ShieldCheck,
37
37
  Underline,
38
38
  Undo2,
39
39
  } from "lucide-react";
40
40
 
41
41
  import type {
42
+ ActiveListContext,
42
43
  CompatibilityPanelSnapshot,
43
44
  EditorStoryTarget,
44
45
  EditorWarning,
@@ -48,6 +49,7 @@ import type {
48
49
  SectionBreakType,
49
50
  StyleCatalogSnapshot,
50
51
  WorkflowBlockedCommandReason,
52
+ WordReviewEditorChromePreset,
51
53
  WorkspaceMode,
52
54
  ZoomLevel,
53
55
  } from "../../api/public-types";
@@ -57,15 +59,17 @@ import { TwHealthPanel } from "../review/tw-health-panel";
57
59
  import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
58
60
 
59
61
  export interface TwToolbarProps {
60
- sourceLabel?: string;
61
62
  capabilities?: SessionCapabilities;
62
63
  compatibility?: CompatibilityPanelSnapshot;
63
64
  warnings?: EditorWarning[];
64
65
  blockedReasons?: WorkflowBlockedCommandReason[];
66
+ showDiagnosticsChrome?: boolean;
65
67
  interactionPolicy?: ToolbarInteractionPolicy;
68
+ preset?: WordReviewEditorChromePreset;
66
69
  workspaceMode: WorkspaceMode;
67
70
  zoomLevel?: ZoomLevel;
68
71
  formattingState?: FormattingStateSnapshot;
72
+ activeListContext?: ActiveListContext | null;
69
73
  styleCatalog?: StyleCatalogSnapshot;
70
74
  /** Display toggle for tracked change decorations (not a runtime mutation toggle). */
71
75
  showTrackedChanges: boolean;
@@ -87,6 +91,8 @@ export interface TwToolbarProps {
87
91
  onSetTextColor?: (color: string) => void;
88
92
  onSetHighlightColor?: (color: string | null) => void;
89
93
  onSetAlignment?: (alignment: FormattingAlignment) => void;
94
+ onToggleBulletedList?: () => void;
95
+ onToggleNumberedList?: () => void;
90
96
  onOutdent?: () => void;
91
97
  onIndent?: () => void;
92
98
  onAddComment: () => void;
@@ -98,6 +104,10 @@ export interface TwToolbarProps {
98
104
  onWorkspaceModeChange: (value: WorkspaceMode) => void;
99
105
  onZoomChange?: (level: ZoomLevel) => void;
100
106
  onShowTrackedChangesChange: (show: boolean) => void;
107
+ onRestartNumbering?: () => void;
108
+ onContinueNumbering?: () => void;
109
+ onUpdateFields?: () => void;
110
+ onUpdateTableOfContents?: () => void;
101
111
  }
102
112
 
103
113
  export interface ToolbarInteractionPolicy {
@@ -116,24 +126,33 @@ const focusRingClass =
116
126
 
117
127
  const FONT_FAMILIES = ["Arial", "Times New Roman", "Calibri", "Cambria", "Georgia", "Verdana"];
118
128
  const FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36];
119
- const TEXT_COLORS = ["#000000", "#434343", "#1660a8", "#1a7f37", "#cf222e", "#7a4f00"];
129
+ const TEXT_COLORS = ["#1f1f1f", "#5c5852", "#1660a8", "#50684d", "#9b4f49", "#7b5f32"];
120
130
  const HIGHLIGHT_COLORS = [
121
131
  { value: "#ffff00", label: "Yellow" },
122
- { value: "#00ff00", label: "Green" },
123
- { value: "#00ffff", label: "Cyan" },
124
- { value: "#ff69b4", label: "Pink" },
132
+ { value: "#d9e7f7", label: "Blue" },
133
+ { value: "#f1dbd6", label: "Rose" },
134
+ { value: "#e6e0d4", label: "Stone" },
125
135
  { value: null, label: "None" },
126
136
  ] as const;
127
137
 
128
138
  export function TwToolbar(props: TwToolbarProps) {
129
139
  const caps = props.capabilities;
140
+ const preset = props.preset ?? "advanced";
130
141
  const workspaceMode = props.workspaceMode;
131
- const isPageMode = workspaceMode === "page";
132
142
  const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
133
143
  const zoomLevel = props.zoomLevel ?? 100;
134
144
  const canEdit = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
135
145
  const canInsertStructural = props.interactionPolicy?.canInsertStructural ?? canEdit;
136
146
  const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
147
+ const showStyleSelectors = preset === "advanced";
148
+ const showAdvancedFormatting = preset === "advanced";
149
+ const showFormattingColors = preset !== "review";
150
+ const showInsertMenu = preset === "simple" || preset === "advanced";
151
+ const showTrackedChangesToggle = preset !== "simple";
152
+ const showDiagnosticsChrome = props.showDiagnosticsChrome ?? true;
153
+ const showHealth = showDiagnosticsChrome && Boolean(props.compatibility && props.warnings);
154
+ const showListActions = preset === "simple" || preset === "advanced";
155
+ const showUpdateActions = preset === "advanced";
137
156
  const zoomLabel =
138
157
  typeof zoomLevel === "number"
139
158
  ? `${zoomLevel}%`
@@ -142,9 +161,9 @@ export function TwToolbar(props: TwToolbarProps) {
142
161
  : "Fit page";
143
162
 
144
163
  return (
145
- <header className="flex h-10 shrink-0 items-center gap-1 border-b border-border px-2">
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">
146
165
  {/* Left cluster: undo/redo + formatting */}
147
- <div className="flex items-center gap-0.5">
166
+ <div className="flex min-w-0 flex-1 items-center gap-0.5">
148
167
  <TwToolbarIconButton
149
168
  icon={Undo2}
150
169
  label="Undo"
@@ -159,25 +178,29 @@ export function TwToolbar(props: TwToolbarProps) {
159
178
  />
160
179
  <div className="mx-1 h-4 w-px bg-border" />
161
180
 
162
- <ToolbarParagraphStyleSelect
163
- disabled={!canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
164
- styles={paragraphStyles}
165
- value={props.formattingState?.paragraphStyleId}
166
- onValueChange={props.onSetParagraphStyle}
167
- />
181
+ {showStyleSelectors ? (
182
+ <>
183
+ <ToolbarParagraphStyleSelect
184
+ disabled={!canEdit || paragraphStyles.length === 0 || !props.onSetParagraphStyle}
185
+ styles={paragraphStyles}
186
+ value={props.formattingState?.paragraphStyleId}
187
+ onValueChange={props.onSetParagraphStyle}
188
+ />
168
189
 
169
- <ToolbarFontFamilySelect
170
- disabled={!canEdit || !props.onSetFontFamily}
171
- value={props.formattingState?.fontFamily}
172
- onValueChange={props.onSetFontFamily}
173
- />
174
- <ToolbarFontSizeSelect
175
- disabled={!canEdit || !props.onSetFontSize}
176
- value={props.formattingState?.fontSize}
177
- onValueChange={props.onSetFontSize}
178
- />
190
+ <ToolbarFontFamilySelect
191
+ disabled={!canEdit || !props.onSetFontFamily}
192
+ value={props.formattingState?.fontFamily}
193
+ onValueChange={props.onSetFontFamily}
194
+ />
195
+ <ToolbarFontSizeSelect
196
+ disabled={!canEdit || !props.onSetFontSize}
197
+ value={props.formattingState?.fontSize}
198
+ onValueChange={props.onSetFontSize}
199
+ />
179
200
 
180
- <div className="mx-1 h-4 w-px bg-border" />
201
+ <div className="mx-1 h-4 w-px bg-border" />
202
+ </>
203
+ ) : null}
181
204
 
182
205
  <TwToolbarIconButton
183
206
  icon={Bold}
@@ -200,41 +223,66 @@ export function TwToolbar(props: TwToolbarProps) {
200
223
  disabled={!canEdit}
201
224
  onClick={props.onToggleUnderline}
202
225
  />
203
- <ToolbarFormattingOverflow
204
- disabled={!canEdit}
205
- formattingState={props.formattingState}
206
- onToggleStrikethrough={props.onToggleStrikethrough}
207
- onToggleSuperscript={props.onToggleSuperscript}
208
- onToggleSubscript={props.onToggleSubscript}
209
- />
210
- <ToolbarColorPopover
211
- ariaLabel="Text color"
212
- colors={TEXT_COLORS.map((value) => ({ value, label: value }))}
213
- disabled={!canEdit || !props.onSetTextColor}
214
- icon={<Baseline className="h-3.5 w-3.5" />}
215
- onSelect={(value) => {
216
- if (value) {
217
- props.onSetTextColor?.(value);
218
- }
219
- }}
220
- title="Text color"
221
- />
222
- <ToolbarColorPopover
223
- ariaLabel="Highlight color"
224
- colors={HIGHLIGHT_COLORS.map((entry) => ({ value: entry.value, label: entry.label }))}
225
- disabled={!canEdit || !props.onSetHighlightColor}
226
- icon={<Highlighter className="h-3.5 w-3.5" />}
227
- onSelect={(value) => props.onSetHighlightColor?.(value)}
228
- title="Highlight color"
229
- />
230
- <ToolbarAlignmentPopover
231
- activeAlignment={props.formattingState?.alignment}
232
- disabled={!canEdit || !props.onSetAlignment}
233
- onSelect={(alignment) => props.onSetAlignment?.(alignment)}
234
- />
226
+ {showAdvancedFormatting ? (
227
+ <ToolbarFormattingOverflow
228
+ disabled={!canEdit}
229
+ formattingState={props.formattingState}
230
+ onToggleStrikethrough={props.onToggleStrikethrough}
231
+ onToggleSuperscript={props.onToggleSuperscript}
232
+ onToggleSubscript={props.onToggleSubscript}
233
+ />
234
+ ) : null}
235
+ {showFormattingColors ? (
236
+ <>
237
+ <ToolbarColorPopover
238
+ ariaLabel="Text color"
239
+ colors={TEXT_COLORS.map((value) => ({ value, label: value }))}
240
+ disabled={!canEdit || !props.onSetTextColor}
241
+ icon={<Baseline className="h-3.5 w-3.5" />}
242
+ onSelect={(value) => {
243
+ if (value) {
244
+ props.onSetTextColor?.(value);
245
+ }
246
+ }}
247
+ title="Text color"
248
+ />
249
+ <ToolbarColorPopover
250
+ ariaLabel="Highlight color"
251
+ colors={HIGHLIGHT_COLORS.map((entry) => ({ value: entry.value, label: entry.label }))}
252
+ disabled={!canEdit || !props.onSetHighlightColor}
253
+ icon={<Highlighter className="h-3.5 w-3.5" />}
254
+ onSelect={(value) => props.onSetHighlightColor?.(value)}
255
+ title="Highlight color"
256
+ />
257
+ <ToolbarAlignmentPopover
258
+ activeAlignment={props.formattingState?.alignment}
259
+ disabled={!canEdit || !props.onSetAlignment}
260
+ onSelect={(alignment) => props.onSetAlignment?.(alignment)}
261
+ />
262
+ </>
263
+ ) : null}
235
264
 
236
265
  <div className="mx-1 h-4 w-px bg-border" />
237
266
 
267
+ {showListActions ? (
268
+ <>
269
+ <TwToolbarIconButton
270
+ icon={List}
271
+ label="Bulleted list"
272
+ active={Boolean(props.activeListContext && !props.activeListContext.isOrdered)}
273
+ disabled={!canEdit}
274
+ onClick={props.onToggleBulletedList}
275
+ />
276
+ <TwToolbarIconButton
277
+ icon={Rows3}
278
+ label="Numbered list"
279
+ active={Boolean(props.activeListContext?.isOrdered)}
280
+ disabled={!canEdit}
281
+ onClick={props.onToggleNumberedList}
282
+ />
283
+ </>
284
+ ) : null}
285
+
238
286
  <TwToolbarIconButton
239
287
  icon={Outdent}
240
288
  label="Outdent"
@@ -247,13 +295,63 @@ export function TwToolbar(props: TwToolbarProps) {
247
295
  disabled={!canEdit}
248
296
  onClick={props.onIndent}
249
297
  />
250
- <ToolbarInsertMenu
251
- disabled={!canInsertStructural}
252
- onInsertImage={props.onInsertImage}
253
- onInsertPageBreak={props.onInsertPageBreak}
254
- onInsertSectionBreak={props.onInsertSectionBreak}
255
- onInsertTable={props.onInsertTable}
256
- />
298
+ {showListActions && props.activeListContext ? (
299
+ <>
300
+ <button
301
+ type="button"
302
+ aria-label="Restart numbering"
303
+ disabled={!canEdit || !props.onRestartNumbering}
304
+ onMouseDown={preserveEditorSelectionMouseDown}
305
+ onClick={props.onRestartNumbering}
306
+ className={`inline-flex h-7 items-center rounded-md px-2 text-xs font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
307
+ >
308
+ Restart
309
+ </button>
310
+ <button
311
+ type="button"
312
+ aria-label="Continue numbering"
313
+ disabled={!canEdit || !props.onContinueNumbering}
314
+ onMouseDown={preserveEditorSelectionMouseDown}
315
+ onClick={props.onContinueNumbering}
316
+ className={`inline-flex h-7 items-center rounded-md px-2 text-xs font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
317
+ >
318
+ Continue
319
+ </button>
320
+ </>
321
+ ) : null}
322
+ {showInsertMenu ? (
323
+ <ToolbarInsertMenu
324
+ disabled={!canInsertStructural}
325
+ onInsertImage={props.onInsertImage}
326
+ onInsertPageBreak={props.onInsertPageBreak}
327
+ onInsertSectionBreak={props.onInsertSectionBreak}
328
+ onInsertTable={props.onInsertTable}
329
+ />
330
+ ) : null}
331
+ {showUpdateActions ? (
332
+ <>
333
+ <button
334
+ type="button"
335
+ aria-label="Refresh fields"
336
+ disabled={!canEdit || !props.onUpdateFields}
337
+ onMouseDown={preserveEditorSelectionMouseDown}
338
+ onClick={props.onUpdateFields}
339
+ className={`inline-flex h-7 items-center rounded-md px-2 text-xs font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
340
+ >
341
+ Fields
342
+ </button>
343
+ <button
344
+ type="button"
345
+ aria-label="Refresh table of contents"
346
+ disabled={!canEdit || !props.onUpdateTableOfContents}
347
+ onMouseDown={preserveEditorSelectionMouseDown}
348
+ onClick={props.onUpdateTableOfContents}
349
+ className={`inline-flex h-7 items-center rounded-md px-2 text-xs font-medium text-secondary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
350
+ >
351
+ TOC
352
+ </button>
353
+ </>
354
+ ) : null}
257
355
 
258
356
  {/* Story focus breadcrumb — visible when editing a secondary story */}
259
357
  {props.activeStory && props.activeStory.kind !== "main" ? (
@@ -263,7 +361,7 @@ export function TwToolbar(props: TwToolbarProps) {
263
361
  type="button"
264
362
  onClick={props.onCloseStory}
265
363
  onMouseDown={preserveEditorSelectionMouseDown}
266
- className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent hover:bg-accent-soft transition-colors outline-none ${focusRingClass}`}
364
+ className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium text-accent hover:bg-surface transition-colors outline-none ${focusRingClass}`}
267
365
  aria-label={`Editing ${storyLabel(props.activeStory)} — click to return to main body`}
268
366
  >
269
367
  <span className="text-secondary">&larr;</span>
@@ -273,13 +371,6 @@ export function TwToolbar(props: TwToolbarProps) {
273
371
  ) : null}
274
372
  </div>
275
373
 
276
- {/* Center: document title */}
277
- <div className="flex-1 text-center min-w-0">
278
- <span className="text-sm font-medium truncate block">
279
- {props.sourceLabel ?? "Untitled"}
280
- </span>
281
- </div>
282
-
283
374
  {/* Right cluster: comment, track changes, markup, view, export */}
284
375
  <div className="flex items-center gap-0.5">
285
376
  <TwToolbarIconButton
@@ -290,29 +381,33 @@ export function TwToolbar(props: TwToolbarProps) {
290
381
  onClick={props.onAddComment}
291
382
  />
292
383
 
293
- <Tooltip.Root>
294
- <Tooltip.Trigger asChild>
295
- <Toggle.Root
296
- pressed={props.showTrackedChanges}
297
- onPressedChange={props.onShowTrackedChangesChange}
298
- disabled={caps ? !caps.trackChangesSupported : false}
299
- onMouseDown={preserveEditorSelectionMouseDown}
300
- className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none disabled:opacity-40 ${focusRingClass}`}
301
- >
302
- {props.showTrackedChanges ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
303
- </Toggle.Root>
304
- </Tooltip.Trigger>
305
- <Tooltip.Portal>
306
- <Tooltip.Content
307
- className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
308
- sideOffset={6}
309
- >
310
- {props.showTrackedChanges ? "Hide tracked changes" : "Show tracked changes"}
311
- </Tooltip.Content>
312
- </Tooltip.Portal>
313
- </Tooltip.Root>
384
+ {showTrackedChangesToggle ? (
385
+ <>
386
+ <Tooltip.Root>
387
+ <Tooltip.Trigger asChild>
388
+ <Toggle.Root
389
+ pressed={props.showTrackedChanges}
390
+ onPressedChange={props.onShowTrackedChangesChange}
391
+ disabled={caps ? !caps.trackChangesSupported : false}
392
+ onMouseDown={preserveEditorSelectionMouseDown}
393
+ className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
394
+ >
395
+ {props.showTrackedChanges ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
396
+ </Toggle.Root>
397
+ </Tooltip.Trigger>
398
+ <Tooltip.Portal>
399
+ <Tooltip.Content
400
+ className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
401
+ sideOffset={6}
402
+ >
403
+ {props.showTrackedChanges ? "Hide tracked changes" : "Show tracked changes"}
404
+ </Tooltip.Content>
405
+ </Tooltip.Portal>
406
+ </Tooltip.Root>
314
407
 
315
- <div className="mx-1 h-4 w-px bg-border" />
408
+ <div className="mx-1 h-4 w-px bg-border" />
409
+ </>
410
+ ) : null}
316
411
 
317
412
  {/* View mode toggle group: Canvas (clean, flowing) / Page (layout-sensitive) */}
318
413
  <ToggleGroup.Root
@@ -329,7 +424,7 @@ export function TwToolbar(props: TwToolbarProps) {
329
424
  value="canvas"
330
425
  aria-label="Canvas workspace"
331
426
  onMouseDown={preserveEditorSelectionMouseDown}
332
- className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
427
+ className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none ${focusRingClass}`}
333
428
  >
334
429
  <Monitor className="h-3.5 w-3.5" />
335
430
  </ToggleGroup.Item>
@@ -346,7 +441,7 @@ export function TwToolbar(props: TwToolbarProps) {
346
441
  value="page"
347
442
  aria-label="Page workspace"
348
443
  onMouseDown={preserveEditorSelectionMouseDown}
349
- className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
444
+ className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none ${focusRingClass}`}
350
445
  >
351
446
  <FileText className="h-3.5 w-3.5" />
352
447
  </ToggleGroup.Item>
@@ -359,8 +454,8 @@ export function TwToolbar(props: TwToolbarProps) {
359
454
  </Tooltip.Root>
360
455
  </ToggleGroup.Root>
361
456
 
362
- {/* Zoom controls — visible in page mode */}
363
- {isPageMode && props.onZoomChange ? (
457
+ {/* Zoom controls — available in all workspace modes */}
458
+ {props.onZoomChange ? (
364
459
  <>
365
460
  <div className="mx-1 h-4 w-px bg-border" />
366
461
  <div className="flex items-center gap-0.5">
@@ -460,86 +555,60 @@ export function TwToolbar(props: TwToolbarProps) {
460
555
  </>
461
556
  ) : null}
462
557
 
463
- {/* Health indicator */}
464
- {props.compatibility && props.warnings ? (
465
- <Popover.Root>
466
- <Tooltip.Root>
467
- <Tooltip.Trigger asChild>
468
- <Popover.Trigger asChild>
469
- <button
470
- type="button"
471
- onMouseDown={preserveEditorSelectionMouseDown}
472
- className={`relative inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-surface hover:text-primary outline-none ${focusRingClass} ${
473
- (caps?.healthIssueCount ?? 0) > 0 ? "text-secondary" : "text-secondary"
474
- }`}
475
- >
558
+ {showHealth ? (
559
+ <>
560
+ <Popover.Root>
561
+ <Tooltip.Root>
562
+ <Tooltip.Trigger asChild>
563
+ <Popover.Trigger asChild>
564
+ <button
565
+ type="button"
566
+ aria-label="Document health"
567
+ onMouseDown={preserveEditorSelectionMouseDown}
568
+ className={`relative inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface hover:text-primary outline-none ${focusRingClass}`}
569
+ >
570
+ <AlertCircle className="h-4 w-4" />
571
+ {(caps?.healthIssueCount ?? 0) > 0 ? (
572
+ <span className="absolute -top-0.5 -right-0.5 flex h-3 min-w-[12px] items-center justify-center rounded-full bg-tertiary text-[8px] font-medium text-white">
573
+ {caps?.healthIssueCount}
574
+ </span>
575
+ ) : null}
576
+ </button>
577
+ </Popover.Trigger>
578
+ </Tooltip.Trigger>
579
+ <Tooltip.Portal>
580
+ <Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
476
581
  {(caps?.healthIssueCount ?? 0) > 0
477
- ? <ShieldAlert className="h-4 w-4" />
478
- : <ShieldCheck className="h-4 w-4" />
479
- }
480
- {(caps?.healthIssueCount ?? 0) > 0 ? (
481
- <span className="absolute -top-0.5 -right-0.5 flex h-3 min-w-[12px] items-center justify-center rounded-full bg-tertiary text-[8px] font-medium text-white">
482
- {caps?.healthIssueCount}
483
- </span>
484
- ) : null}
485
- </button>
486
- </Popover.Trigger>
487
- </Tooltip.Trigger>
488
- <Tooltip.Portal>
489
- <Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
490
- {(caps?.healthIssueCount ?? 0) > 0
491
- ? `Document health — ${caps?.healthIssueCount} issue${(caps?.healthIssueCount ?? 0) !== 1 ? "s" : ""}`
492
- : "Document health — no issues"
493
- }
494
- </Tooltip.Content>
495
- </Tooltip.Portal>
496
- </Tooltip.Root>
497
- <Popover.Portal>
498
- <Popover.Content
499
- className="w-[360px] max-h-[480px] overflow-y-auto rounded-lg bg-canvas shadow-lg ring-1 ring-border p-3 z-50"
500
- sideOffset={8}
501
- align="end"
502
- >
503
- <TwHealthPanel
504
- blockedReasons={props.blockedReasons}
505
- compatibility={props.compatibility}
506
- warnings={props.warnings}
507
- />
508
- </Popover.Content>
509
- </Popover.Portal>
510
- </Popover.Root>
582
+ ? `Document health — ${caps?.healthIssueCount} issue${(caps?.healthIssueCount ?? 0) !== 1 ? "s" : ""}`
583
+ : "Document health — no issues"}
584
+ </Tooltip.Content>
585
+ </Tooltip.Portal>
586
+ </Tooltip.Root>
587
+ <Popover.Portal>
588
+ <Popover.Content
589
+ className="w-[360px] max-h-[480px] overflow-y-auto rounded-lg bg-canvas shadow-lg ring-1 ring-border p-3 z-50"
590
+ sideOffset={8}
591
+ align="end"
592
+ >
593
+ <TwHealthPanel
594
+ blockedReasons={props.blockedReasons}
595
+ compatibility={props.compatibility!}
596
+ warnings={props.warnings!}
597
+ />
598
+ </Popover.Content>
599
+ </Popover.Portal>
600
+ </Popover.Root>
601
+ <div className="mx-1 h-4 w-px bg-border" />
602
+ </>
511
603
  ) : null}
512
604
 
513
- <div className="mx-1 h-4 w-px bg-border" />
514
-
515
- {/* Export button */}
516
- <Tooltip.Root>
517
- <Tooltip.Trigger asChild>
518
- <button
519
- type="button"
520
- disabled={caps ? !caps.canExport : true}
521
- onMouseDown={preserveEditorSelectionMouseDown}
522
- className={[
523
- "inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-semibold transition-colors outline-none",
524
- focusRingClass,
525
- caps?.exportBlocked
526
- ? "cursor-not-allowed text-danger opacity-50"
527
- : "text-accent hover:bg-accent-soft",
528
- ].join(" ")}
529
- onClick={props.onExport}
530
- >
531
- <Download className="h-3.5 w-3.5" />
532
- Export .docx
533
- </button>
534
- </Tooltip.Trigger>
535
- <Tooltip.Portal>
536
- <Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
537
- {caps?.exportBlocked
538
- ? "Export blocked by unsupported content"
539
- : "Export document"}
540
- </Tooltip.Content>
541
- </Tooltip.Portal>
542
- </Tooltip.Root>
605
+ <TwToolbarIconButton
606
+ icon={Download}
607
+ label={caps?.exportBlocked ? "Export blocked" : "Download document"}
608
+ disabled={caps ? !caps.canExport : true}
609
+ emphasis
610
+ onClick={props.onExport}
611
+ />
543
612
  </div>
544
613
  </header>
545
614
  );
@@ -584,7 +653,7 @@ function ToolbarParagraphStyleSelect(props: {
584
653
  <Select.Viewport className="p-1">
585
654
  {props.styles.map((style) => (
586
655
  <Select.Item
587
- className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-accent-soft data-[state=checked]:text-accent ${focusRingClass}`}
656
+ className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-canvas data-[state=checked]:text-accent data-[state=checked]:ring-1 data-[state=checked]:ring-accent/20 ${focusRingClass}`}
588
657
  key={style.styleId}
589
658
  value={style.styleId}
590
659
  >
@@ -633,7 +702,7 @@ function ToolbarFontFamilySelect(props: {
633
702
  <Select.Viewport className="p-1">
634
703
  {FONT_FAMILIES.map((font) => (
635
704
  <Select.Item
636
- className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-accent-soft data-[state=checked]:text-accent ${focusRingClass}`}
705
+ className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-canvas data-[state=checked]:text-accent data-[state=checked]:ring-1 data-[state=checked]:ring-accent/20 ${focusRingClass}`}
637
706
  key={font}
638
707
  value={font}
639
708
  >
@@ -683,7 +752,7 @@ function ToolbarFontSizeSelect(props: {
683
752
  <Select.Viewport className="p-1">
684
753
  {FONT_SIZES.map((size) => (
685
754
  <Select.Item
686
- className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-accent-soft data-[state=checked]:text-accent ${focusRingClass}`}
755
+ className={`flex cursor-pointer items-center rounded-md px-2.5 py-1.5 text-xs text-primary outline-none data-[highlighted]:bg-surface data-[state=checked]:bg-canvas data-[state=checked]:text-accent data-[state=checked]:ring-1 data-[state=checked]:ring-accent/20 ${focusRingClass}`}
687
756
  key={size}
688
757
  value={String(size)}
689
758
  >
@@ -1018,7 +1087,7 @@ function ToolbarPopoverActionButton(props: {
1018
1087
  onMouseDown={preserveEditorSelectionMouseDown}
1019
1088
  onClick={props.onClick}
1020
1089
  className={`inline-flex h-8 items-center justify-center rounded-md border border-border transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
1021
- props.active ? "bg-accent-soft text-accent" : "bg-canvas text-secondary hover:bg-surface"
1090
+ props.active ? "bg-canvas text-accent ring-1 ring-accent/30 shadow-sm" : "bg-canvas text-secondary hover:bg-surface"
1022
1091
  } ${focusRingClass}`}
1023
1092
  >
1024
1093
  {props.icon}