@beyondwork/docx-react-component 1.0.22 → 1.0.24-rc

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 (33) hide show
  1. package/README.md +81 -38
  2. package/package.json +1 -1
  3. package/src/api/public-types.ts +67 -1
  4. package/src/core/commands/index.ts +625 -5
  5. package/src/index.ts +5 -0
  6. package/src/io/docx-session.ts +181 -2
  7. package/src/io/export/serialize-main-document.ts +21 -1
  8. package/src/io/normalize/normalize-text.ts +4 -0
  9. package/src/io/ooxml/parse-main-document.ts +88 -7
  10. package/src/model/canonical-document.ts +22 -0
  11. package/src/review/store/revision-store.ts +1 -0
  12. package/src/review/store/revision-types.ts +2 -0
  13. package/src/runtime/document-runtime.ts +503 -51
  14. package/src/runtime/session-capabilities.ts +6 -5
  15. package/src/runtime/surface-projection.ts +2 -0
  16. package/src/runtime/table-schema.ts +2 -0
  17. package/src/runtime/workflow-markup.ts +5 -1
  18. package/src/ui/WordReviewEditor.tsx +667 -132
  19. package/src/ui/editor-runtime-boundary.ts +10 -1
  20. package/src/ui/editor-shell-view.tsx +8 -0
  21. package/src/ui/editor-surface-controller.tsx +6 -0
  22. package/src/ui/headless/selection-toolbar-model.ts +12 -0
  23. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
  24. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
  25. package/src/ui-tailwind/editor-surface/pm-decorations.ts +96 -28
  26. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
  27. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +6 -0
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -10
  29. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
  30. package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
  31. package/src/ui-tailwind/theme/editor-theme.css +10 -0
  32. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
  33. package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
@@ -75,6 +75,7 @@ export interface TwProseMirrorSurfaceProps {
75
75
  markupDisplay: MarkupDisplay;
76
76
  activeRevisionId?: string;
77
77
  showTrackedChanges?: boolean;
78
+ suggestionsEnabled?: boolean;
78
79
  /** When true, the surface renders inside the page workspace (vs canvas). */
79
80
  isPageWorkspace?: boolean;
80
81
  onFocus: FocusEventHandler<HTMLDivElement>;
@@ -87,6 +88,9 @@ export interface TwProseMirrorSurfaceProps {
87
88
  onOutdentTab?: () => void;
88
89
  onInsertHardBreak?: () => void;
89
90
  onSplitParagraph?: () => void;
91
+ onUndo?: () => void;
92
+ onRedo?: () => void;
93
+ onBlockedInput?: (command: "paste" | "drop", message: string) => void;
90
94
  onCommentActivated?: (commentId: string) => void;
91
95
  onRevisionActivated?: (revisionId: string) => void;
92
96
  onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
@@ -94,6 +98,8 @@ export interface TwProseMirrorSurfaceProps {
94
98
  workflowScopes?: readonly WorkflowScope[];
95
99
  workflowCandidates?: readonly WorkflowCandidateRange[];
96
100
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
101
+ activeWorkflowWorkItemId?: string | null;
102
+ activeWorkflowScopeIds?: readonly string[];
97
103
  }
98
104
 
99
105
  export interface TwProseMirrorSurfaceRef {
@@ -161,8 +167,11 @@ export const TwProseMirrorSurface = forwardRef<
161
167
  onInsertHardBreak: () => props.onInsertHardBreak?.(),
162
168
  onInsertTab: () => props.onInsertTab?.(),
163
169
  onOutdentTab: () => props.onOutdentTab?.(),
164
- onUndo: () => {}, // Handled by toolbar, not PM
165
- onRedo: () => {}, // Handled by toolbar, not PM
170
+ onUndo: () => props.onUndo?.(),
171
+ onRedo: () => props.onRedo?.(),
172
+ onBlockedInput: (command, message) => {
173
+ props.onBlockedInput?.(command, message);
174
+ },
166
175
  onSelectionChange: (sel) => {
167
176
  pendingSelectionProbeRef.current = startPerfProbe("selection");
168
177
  props.onSelectionChange?.(
@@ -196,6 +205,7 @@ export const TwProseMirrorSurface = forwardRef<
196
205
  }),
197
206
  [mediaPreviewKey, snapshot.activeStory, surface],
198
207
  );
208
+ const suggestionsEnabled = props.suggestionsEnabled === true;
199
209
  const decorationBuildKey = useMemo(
200
210
  () =>
201
211
  createSurfaceDecorationKey({
@@ -204,9 +214,12 @@ export const TwProseMirrorSurface = forwardRef<
204
214
  canEdit,
205
215
  activeCommentId: snapshot.comments.activeCommentId,
206
216
  activeRevisionId: props.activeRevisionId,
207
- workflowScopeSignature: JSON.stringify(props.workflowScopes ?? []),
208
- workflowCandidateSignature: JSON.stringify(props.workflowCandidates ?? []),
209
- workflowBlockedSignature: JSON.stringify(props.workflowBlockedReasons ?? []),
217
+ workflowScopeSignature: createWorkflowScopeSignature(props.workflowScopes),
218
+ workflowCandidateSignature: createWorkflowCandidateSignature(props.workflowCandidates),
219
+ workflowBlockedSignature: createWorkflowBlockedSignature(props.workflowBlockedReasons),
220
+ activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
221
+ activeWorkflowScopeIds: props.activeWorkflowScopeIds ?? [],
222
+ suggestionsEnabled,
210
223
  }),
211
224
  [
212
225
  canEdit,
@@ -214,9 +227,12 @@ export const TwProseMirrorSurface = forwardRef<
214
227
  props.activeRevisionId,
215
228
  props.workflowCandidates,
216
229
  props.workflowBlockedReasons,
230
+ props.activeWorkflowWorkItemId,
231
+ props.activeWorkflowScopeIds,
217
232
  props.workflowScopes,
218
233
  showTrackedChanges,
219
234
  snapshot.comments.activeCommentId,
235
+ suggestionsEnabled,
220
236
  ],
221
237
  );
222
238
 
@@ -233,6 +249,7 @@ export const TwProseMirrorSurface = forwardRef<
233
249
  onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
234
250
  onUndo: () => callbacksRef.current?.onUndo(),
235
251
  onRedo: () => callbacksRef.current?.onRedo(),
252
+ onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
236
253
  onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
237
254
  getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
238
255
  isSelectionSyncSuppressed: () =>
@@ -259,6 +276,9 @@ export const TwProseMirrorSurface = forwardRef<
259
276
  snapshot.activeStory,
260
277
  props.workflowCandidates,
261
278
  props.workflowBlockedReasons,
279
+ props.activeWorkflowWorkItemId,
280
+ props.activeWorkflowScopeIds,
281
+ suggestionsEnabled,
262
282
  );
263
283
  view.setProps({
264
284
  editable: () => canEdit,
@@ -273,13 +293,13 @@ export const TwProseMirrorSurface = forwardRef<
273
293
  commentModel,
274
294
  decorationBuildKey,
275
295
  markupDisplay,
276
- revisionModel,
277
- showTrackedChanges,
296
+ props.activeWorkflowScopeIds,
297
+ props.activeWorkflowWorkItemId,
278
298
  props.workflowBlockedReasons,
279
299
  props.workflowCandidates,
280
300
  props.workflowScopes,
281
- props.workflowCandidates,
282
- props.workflowBlockedReasons,
301
+ revisionModel,
302
+ showTrackedChanges,
283
303
  ],
284
304
  );
285
305
 
@@ -309,6 +329,8 @@ export const TwProseMirrorSurface = forwardRef<
309
329
  snapshot.activeStory,
310
330
  props.workflowCandidates,
311
331
  props.workflowBlockedReasons,
332
+ props.activeWorkflowWorkItemId,
333
+ props.activeWorkflowScopeIds,
312
334
  );
313
335
  recordPerfSample("pm.rebuild");
314
336
  incrementInvalidationCounter("pm.laneA.rebuilds");
@@ -371,6 +393,20 @@ export const TwProseMirrorSurface = forwardRef<
371
393
  applyDecorationProps(view, positionMap);
372
394
  }, [applyDecorationProps, decorationBuildKey, surface]);
373
395
 
396
+ useEffect(() => {
397
+ if (!activeSearchRef.current || !surface) {
398
+ return;
399
+ }
400
+ applySearch(activeSearchRef.current.query, activeSearchRef.current.options);
401
+ }, [
402
+ markupDisplay,
403
+ props.canonicalDocument,
404
+ props.documentNavigation,
405
+ snapshot.activeStory,
406
+ snapshot.trackedChanges,
407
+ surface,
408
+ ]);
409
+
374
410
  useEffect(() => {
375
411
  const view = viewRef.current;
376
412
  const positionMap = positionMapRef.current;
@@ -442,7 +478,15 @@ export const TwProseMirrorSurface = forwardRef<
442
478
  return getTableSelectionDescriptor(view.state);
443
479
  },
444
480
  }),
445
- [snapshot.selection, snapshot.surface],
481
+ [
482
+ markupDisplay,
483
+ props.canonicalDocument,
484
+ props.documentNavigation,
485
+ snapshot.activeStory,
486
+ snapshot.selection,
487
+ snapshot.surface,
488
+ snapshot.trackedChanges,
489
+ ],
446
490
  );
447
491
 
448
492
  function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
@@ -788,6 +832,84 @@ export const TwProseMirrorSurface = forwardRef<
788
832
  }
789
833
  });
790
834
 
835
+ function createWorkflowScopeSignature(scopes: readonly WorkflowScope[] | undefined): string {
836
+ if (!scopes || scopes.length === 0) {
837
+ return "";
838
+ }
839
+ return scopes.map((scope) =>
840
+ [
841
+ scope.scopeId,
842
+ scope.mode,
843
+ scope.workItemId ?? "",
844
+ serializeAnchorSignature(scope.anchor),
845
+ serializeStoryTargetSignature(scope.storyTarget),
846
+ ].join(":")
847
+ ).join("|");
848
+ }
849
+
850
+ function createWorkflowCandidateSignature(
851
+ candidates: readonly WorkflowCandidateRange[] | undefined,
852
+ ): string {
853
+ if (!candidates || candidates.length === 0) {
854
+ return "";
855
+ }
856
+ return candidates.map((candidate) =>
857
+ [
858
+ candidate.candidateId,
859
+ serializeAnchorSignature(candidate.anchor),
860
+ serializeStoryTargetSignature(candidate.storyTarget),
861
+ candidate.source ?? "",
862
+ ].join(":")
863
+ ).join("|");
864
+ }
865
+
866
+ function createWorkflowBlockedSignature(
867
+ blockedReasons: readonly WorkflowBlockedCommandReason[] | undefined,
868
+ ): string {
869
+ if (!blockedReasons || blockedReasons.length === 0) {
870
+ return "";
871
+ }
872
+ return blockedReasons.map((reason) =>
873
+ [
874
+ reason.code,
875
+ reason.scopeId ?? "",
876
+ reason.workItemId ?? "",
877
+ serializeAnchorSignature(reason.anchor),
878
+ serializeStoryTargetSignature(reason.storyTarget),
879
+ ].join(":")
880
+ ).join("|");
881
+ }
882
+
883
+ function serializeAnchorSignature(anchor: WorkflowScope["anchor"] | WorkflowCandidateRange["anchor"] | WorkflowBlockedCommandReason["anchor"] | undefined): string {
884
+ if (!anchor) {
885
+ return "";
886
+ }
887
+ switch (anchor.kind) {
888
+ case "range":
889
+ return `range:${anchor.from}:${anchor.to}:${anchor.assoc.start}:${anchor.assoc.end}`;
890
+ case "node":
891
+ return `node:${anchor.at}:${anchor.assoc}`;
892
+ case "detached":
893
+ return `detached:${anchor.lastKnownRange.from}:${anchor.lastKnownRange.to}:${anchor.reason}`;
894
+ }
895
+ }
896
+
897
+ function serializeStoryTargetSignature(storyTarget: WorkflowScope["storyTarget"] | WorkflowCandidateRange["storyTarget"] | WorkflowBlockedCommandReason["storyTarget"]): string {
898
+ if (!storyTarget) {
899
+ return "";
900
+ }
901
+ switch (storyTarget.kind) {
902
+ case "main":
903
+ return "main";
904
+ case "header":
905
+ case "footer":
906
+ return `${storyTarget.kind}:${storyTarget.relationshipId}:${storyTarget.variant}:${storyTarget.sectionIndex ?? ""}`;
907
+ case "footnote":
908
+ case "endnote":
909
+ return `${storyTarget.kind}:${storyTarget.noteId}`;
910
+ }
911
+ }
912
+
791
913
  function buildSelectionToolbarMeasurementKey(
792
914
  selection: SelectionSnapshot,
793
915
  activeStory: RuntimeRenderSnapshot["activeStory"],
@@ -33,6 +33,58 @@ interface OpenExplicitRowSpan {
33
33
  remainingRows: number;
34
34
  }
35
35
 
36
+ function readRowPadding(node: PMNode, side: "gridBefore" | "gridAfter"): number {
37
+ const value = node.attrs[side];
38
+ return typeof value === "number" && value > 0 ? value : 0;
39
+ }
40
+
41
+ function sumGridColumns(
42
+ gridColumns: readonly number[],
43
+ start: number,
44
+ count: number,
45
+ ): number {
46
+ let total = 0;
47
+ for (let index = start; index < start + count; index += 1) {
48
+ total += gridColumns[index] ?? 0;
49
+ }
50
+ return total;
51
+ }
52
+
53
+ function removePaddingCells(rowElement: HTMLTableRowElement): void {
54
+ for (const cell of Array.from(rowElement.cells)) {
55
+ if (cell.hasAttribute("data-row-padding")) {
56
+ cell.remove();
57
+ }
58
+ }
59
+ }
60
+
61
+ function createPaddingCell(colSpan: number, widthTwips: number): HTMLTableCellElement {
62
+ const cell = document.createElement("td");
63
+ cell.setAttribute("data-row-padding", "true");
64
+ cell.setAttribute("aria-hidden", "true");
65
+ cell.colSpan = Math.max(1, colSpan);
66
+ cell.style.border = "none";
67
+ cell.style.padding = "0";
68
+ cell.style.background = "transparent";
69
+ if (widthTwips > 0) {
70
+ cell.style.width = `${widthTwips / 20}pt`;
71
+ }
72
+ return cell;
73
+ }
74
+
75
+ function nodesAreOnlyRowPadding(nodes: ArrayLike<Node> & { item?(index: number): Node | null }): boolean {
76
+ for (let index = 0; index < nodes.length; index += 1) {
77
+ const node = nodes.item ? nodes.item(index) : nodes[index];
78
+ if (!node) {
79
+ continue;
80
+ }
81
+ if (!(node instanceof HTMLElement) || !node.hasAttribute("data-row-padding")) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+
36
88
  function resolveRenderedColspan(node: PMNode): number {
37
89
  const colspan = node.attrs.colspan as number | undefined;
38
90
  if (typeof colspan === "number" && colspan > 1) {
@@ -172,6 +224,7 @@ function computeTableLayout(tableNode: PMNode): TableCellLayout[][] {
172
224
  function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode: PMNode): void {
173
225
  const rowLayouts = computeTableLayout(tableNode);
174
226
  const rowElements = Array.from(tableBody.rows);
227
+ const gridColumns = Array.isArray(tableNode.attrs.gridColumns) ? tableNode.attrs.gridColumns as number[] : [];
175
228
 
176
229
  for (let rowIndex = 0; rowIndex < rowLayouts.length; rowIndex += 1) {
177
230
  const rowLayout = rowLayouts[rowIndex];
@@ -180,6 +233,7 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
180
233
  continue;
181
234
  }
182
235
 
236
+ removePaddingCells(rowElement);
183
237
  const cellElements = Array.from(rowElement.cells);
184
238
  for (const cellLayout of rowLayout) {
185
239
  const element = cellElements[cellLayout.cellIndex];
@@ -199,6 +253,22 @@ function syncRenderedTableLayout(tableBody: HTMLTableSectionElement, tableNode:
199
253
  element.removeAttribute("data-vertical-merge-hidden");
200
254
  }
201
255
  }
256
+
257
+ const rowNode = tableNode.child(rowIndex);
258
+ const gridBefore = readRowPadding(rowNode, "gridBefore");
259
+ const gridAfter = readRowPadding(rowNode, "gridAfter");
260
+ if (gridBefore > 0) {
261
+ rowElement.insertBefore(
262
+ createPaddingCell(gridBefore, sumGridColumns(gridColumns, 0, gridBefore)),
263
+ rowElement.firstChild,
264
+ );
265
+ }
266
+ if (gridAfter > 0) {
267
+ const start = Math.max(0, gridColumns.length - gridAfter);
268
+ rowElement.appendChild(
269
+ createPaddingCell(gridAfter, sumGridColumns(gridColumns, start, gridAfter)),
270
+ );
271
+ }
202
272
  }
203
273
  }
204
274
 
@@ -253,7 +323,18 @@ export class TableNodeView {
253
323
  }
254
324
 
255
325
  ignoreMutation(record: ViewMutationRecord): boolean {
256
- return record.type === "attributes" && record.target === this.dom;
326
+ if (record.type === "attributes") {
327
+ return record.target === this.dom
328
+ || (record.target instanceof HTMLElement && record.target.hasAttribute("data-row-padding"));
329
+ }
330
+ if (record.type === "childList" && this.dom.contains(record.target)) {
331
+ const addedNodes = record.addedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
332
+ const removedNodes = record.removedNodes as ArrayLike<Node> & { item?(index: number): Node | null };
333
+ const addedOkay = (addedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(addedNodes);
334
+ const removedOkay = (removedNodes?.length ?? 0) === 0 || nodesAreOnlyRowPadding(removedNodes);
335
+ return addedOkay && removedOkay;
336
+ }
337
+ return false;
257
338
  }
258
339
 
259
340
  private scheduleLayoutSync(): void {
@@ -22,7 +22,10 @@ export function TwStatusBar(props: TwStatusBarProps) {
22
22
  : "Ready";
23
23
 
24
24
  return (
25
- <footer className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary">
25
+ <footer
26
+ data-testid="status-bar"
27
+ className="flex h-7 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-tertiary"
28
+ >
26
29
  <span className="flex items-center gap-1.5">
27
30
  <span
28
31
  className={`inline-block h-1.5 w-1.5 rounded-full ${
@@ -323,6 +323,16 @@
323
323
  background: var(--wre-workflow-rail-color, var(--color-border-strong));
324
324
  }
325
325
 
326
+ .prosemirror-surface .ProseMirror .wre-workflow-rail-active::before {
327
+ width: 0.3125rem;
328
+ opacity: 1;
329
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 30%, transparent);
330
+ }
331
+
332
+ .prosemirror-surface .ProseMirror .wre-workflow-inline-active {
333
+ box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
334
+ }
335
+
326
336
  .prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
327
337
  --wre-workflow-rail-color: var(--color-accent);
328
338
  background: color-mix(in srgb, var(--color-accent) 7%, transparent);
@@ -62,6 +62,7 @@ export interface TwToolbarProps {
62
62
  compatibility?: CompatibilityPanelSnapshot;
63
63
  warnings?: EditorWarning[];
64
64
  blockedReasons?: WorkflowBlockedCommandReason[];
65
+ interactionPolicy?: ToolbarInteractionPolicy;
65
66
  workspaceMode: WorkspaceMode;
66
67
  zoomLevel?: ZoomLevel;
67
68
  formattingState?: FormattingStateSnapshot;
@@ -99,6 +100,13 @@ export interface TwToolbarProps {
99
100
  onShowTrackedChangesChange: (show: boolean) => void;
100
101
  }
101
102
 
103
+ export interface ToolbarInteractionPolicy {
104
+ mode: "edit" | "suggest" | "comment" | "view" | "blocked";
105
+ canFormatText: boolean;
106
+ canInsertStructural: boolean;
107
+ canAddComment: boolean;
108
+ }
109
+
102
110
  export function getSupportedZoomPresets(): ReadonlyArray<number> {
103
111
  return [75, 100, 125, 150];
104
112
  }
@@ -123,7 +131,9 @@ export function TwToolbar(props: TwToolbarProps) {
123
131
  const isPageMode = workspaceMode === "page";
124
132
  const paragraphStyles = props.styleCatalog?.paragraphs ?? [];
125
133
  const zoomLevel = props.zoomLevel ?? 100;
126
- const canEdit = caps ? caps.canEdit : false;
134
+ const canEdit = props.interactionPolicy?.canFormatText ?? (caps ? caps.canEdit : false);
135
+ const canInsertStructural = props.interactionPolicy?.canInsertStructural ?? canEdit;
136
+ const canAddComment = props.interactionPolicy?.canAddComment ?? (caps ? caps.canAddComment : false);
127
137
  const zoomLabel =
128
138
  typeof zoomLevel === "number"
129
139
  ? `${zoomLevel}%`
@@ -173,14 +183,14 @@ export function TwToolbar(props: TwToolbarProps) {
173
183
  icon={Bold}
174
184
  label="Bold"
175
185
  active={props.formattingState?.bold ?? false}
176
- disabled={caps ? !caps.canEdit : true}
186
+ disabled={!canEdit}
177
187
  onClick={props.onToggleBold}
178
188
  />
179
189
  <TwToolbarIconButton
180
190
  icon={Italic}
181
191
  label="Italic"
182
192
  active={props.formattingState?.italic ?? false}
183
- disabled={caps ? !caps.canEdit : true}
193
+ disabled={!canEdit}
184
194
  onClick={props.onToggleItalic}
185
195
  />
186
196
  <TwToolbarIconButton
@@ -238,7 +248,7 @@ export function TwToolbar(props: TwToolbarProps) {
238
248
  onClick={props.onIndent}
239
249
  />
240
250
  <ToolbarInsertMenu
241
- disabled={!canEdit}
251
+ disabled={!canInsertStructural}
242
252
  onInsertImage={props.onInsertImage}
243
253
  onInsertPageBreak={props.onInsertPageBreak}
244
254
  onInsertSectionBreak={props.onInsertSectionBreak}
@@ -275,7 +285,7 @@ export function TwToolbar(props: TwToolbarProps) {
275
285
  <TwToolbarIconButton
276
286
  icon={MessageSquare}
277
287
  label="Add comment"
278
- disabled={caps ? !caps.canAddComment : true}
288
+ disabled={!canAddComment}
279
289
  emphasis
280
290
  onClick={props.onAddComment}
281
291
  />
@@ -554,6 +564,8 @@ function ToolbarParagraphStyleSelect(props: {
554
564
  >
555
565
  <Select.Trigger
556
566
  aria-label="Paragraph style"
567
+ aria-disabled={props.disabled || undefined}
568
+ data-disabled={props.disabled ? "" : undefined}
557
569
  onMouseDown={preserveEditorSelectionMouseDown}
558
570
  className={`inline-flex h-7 min-w-[8.5rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2.5 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
559
571
  >
@@ -601,6 +613,8 @@ function ToolbarFontFamilySelect(props: {
601
613
  >
602
614
  <Select.Trigger
603
615
  aria-label="Font family"
616
+ aria-disabled={props.disabled || undefined}
617
+ data-disabled={props.disabled ? "" : undefined}
604
618
  onMouseDown={preserveEditorSelectionMouseDown}
605
619
  className={`inline-flex h-7 min-w-[7rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
606
620
  >
@@ -649,6 +663,8 @@ function ToolbarFontSizeSelect(props: {
649
663
  >
650
664
  <Select.Trigger
651
665
  aria-label="Font size"
666
+ aria-disabled={props.disabled || undefined}
667
+ data-disabled={props.disabled ? "" : undefined}
652
668
  onMouseDown={preserveEditorSelectionMouseDown}
653
669
  className={`inline-flex h-7 min-w-[4rem] items-center justify-between gap-2 rounded-md border border-border bg-canvas px-2 text-xs font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
654
670
  >