@beyondwork/docx-react-component 1.0.38 → 1.0.39

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 (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -42,9 +42,11 @@ import {
42
42
  createCommandBridgePlugins,
43
43
  type CommandBridgeCallbacks,
44
44
  } from "./pm-command-bridge";
45
- import { createCollabPlugins } from "./pm-collab-plugins";
46
- import { prosemirrorToYXmlFragment } from "y-prosemirror";
47
45
  import { buildDecorations } from "./pm-decorations";
46
+ import { buildPageBreakDecorations } from "./pm-page-break-decorations";
47
+ import { DecorationSet } from "prosemirror-view";
48
+ import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
49
+ import { buildPagePreviewMaps } from "../../runtime/layout/resolve-page-previews";
48
50
  import { createContextualInteractionPlugin } from "./pm-contextual-ui";
49
51
  import {
50
52
  finishPerfProbe,
@@ -65,6 +67,7 @@ import {
65
67
  performSearch,
66
68
  searchPluginKey,
67
69
  } from "./search-plugin";
70
+ import { createRemoteCursorPlugin } from "./remote-cursor-plugin.ts";
68
71
  import {
69
72
  createSurfaceDecorationKey,
70
73
  createSurfaceDocumentBuildKey,
@@ -73,12 +76,90 @@ import { tableNodeViews } from "./tw-table-node-view";
73
76
  import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
74
77
  import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
75
78
 
79
+ /**
80
+ * Build page-break widget decorations from the layout facet's current page
81
+ * graph. Returns `[]` when the facet is unavailable, the active story is
82
+ * not the main body, or there is only one page.
83
+ *
84
+ * The widget DOM renders the full inter-page chrome (footer band of prev
85
+ * page, visible gap, header band of next page) INLINE in the PM flow.
86
+ * Because it lives inside the flow, no absolute-positioned overlay is
87
+ * required — PM paints the chrome at exactly the right Y coordinate
88
+ * between adjacent blocks.
89
+ */
90
+ function buildPageBreakDecorationsFromProps(
91
+ facet: WordReviewEditorLayoutFacet | undefined,
92
+ isMainStory: boolean,
93
+ positionMap: PositionMap,
94
+ posture: "canvas" | "page",
95
+ canonicalDocument: import("../../core/state/editor-state.ts").CanonicalDocumentEnvelope,
96
+ dimensions: {
97
+ headerBandPx?: number;
98
+ footerBandPx?: number;
99
+ interGapPx?: number;
100
+ } = {},
101
+ ): ReturnType<typeof buildPageBreakDecorations> {
102
+ if (!facet || !isMainStory) return [];
103
+ if (typeof facet.getRenderFrame !== "function") return [];
104
+ const frame = facet.getRenderFrame();
105
+ if (!frame || frame.pages.length < 2) return [];
106
+ const fakeGraph = {
107
+ revision: frame.revision,
108
+ contentPageCount: frame.pages.filter((p) => !p.page.isBlankFiller).length,
109
+ pages: frame.pages.map((p) => ({
110
+ pageId: p.page.pageId,
111
+ pageIndex: p.page.pageIndex,
112
+ startOffset: p.page.startOffset,
113
+ isBlankFiller: p.page.isBlankFiller,
114
+ stories: {
115
+ displayPageNumber: p.page.stories.displayPageNumber,
116
+ header: p.page.stories.header,
117
+ footer: p.page.stories.footer,
118
+ },
119
+ })),
120
+ };
121
+
122
+ // Build per-page header/footer preview maps so the bands show live
123
+ // content (with PAGE/NUMPAGES resolved) instead of generic labels.
124
+ // We pass a graph-shape compatible adapter — `buildPagePreviewMaps`
125
+ // reads only `pages[].pageId / stories / isBlankFiller`, which the
126
+ // render-frame projection above already provides.
127
+ const subParts = canonicalDocument.subParts;
128
+ const previews =
129
+ subParts && (subParts.headers?.length || subParts.footers?.length)
130
+ ? buildPagePreviewMaps(fakeGraph as never, {
131
+ headers: subParts.headers,
132
+ footers: subParts.footers,
133
+ })
134
+ : undefined;
135
+
136
+ return buildPageBreakDecorations({
137
+ graph: fakeGraph as never,
138
+ posture,
139
+ headerBandPx: dimensions.headerBandPx,
140
+ footerBandPx: dimensions.footerBandPx,
141
+ interGapPx: dimensions.interGapPx,
142
+ runtimeToPmOffset: (offset) => positionMap.runtimeToPm(offset),
143
+ headerPreviewByPageId: previews?.headerPreviewByPageId,
144
+ footerPreviewByPageId: previews?.footerPreviewByPageId,
145
+ });
146
+ }
147
+
148
+ function extractDecorations(
149
+ set: DecorationSet,
150
+ _doc: unknown,
151
+ ): ReturnType<typeof buildPageBreakDecorations> {
152
+ // PM's DecorationSet doesn't expose its decorations directly; `find` with
153
+ // no range returns every decoration. This is the standard pattern when we
154
+ // need to merge sets.
155
+ return set.find() as unknown as ReturnType<typeof buildPageBreakDecorations>;
156
+ }
157
+
76
158
  /**
77
159
  * Same props interface as the legacy TwEditorSurface — drop-in replacement.
78
160
  */
79
161
  export interface TwProseMirrorSurfaceProps {
80
162
  currentUser: EditorUser;
81
- ydoc?: import("yjs").Doc;
82
163
  awareness?: import("y-protocols/awareness").Awareness;
83
164
  snapshot: RuntimeRenderSnapshot;
84
165
  canonicalDocument: CanonicalDocumentEnvelope;
@@ -100,6 +181,8 @@ export interface TwProseMirrorSurfaceProps {
100
181
  onDeleteForward?: () => void;
101
182
  onInsertTab?: () => void;
102
183
  onOutdentTab?: () => void;
184
+ onListIndent?: () => void;
185
+ onListOutdent?: () => void;
103
186
  onInsertHardBreak?: () => void;
104
187
  onSplitParagraph?: () => void;
105
188
  onUndo?: () => void;
@@ -125,6 +208,31 @@ export interface TwProseMirrorSurfaceProps {
125
208
  dispatchRuntimeCommand?: (
126
209
  command: import("./fast-text-edit-lane").LaneRuntimeCommand,
127
210
  ) => import("../../api/public-types").TextCommandAck;
211
+ /**
212
+ * Optional layout facet. When supplied and the active story is `main`,
213
+ * the surface injects widget decorations at every page boundary. The
214
+ * widget DOM renders full inter-page chrome (page-N footer band +
215
+ * visible gap + page-N+1 header band) INLINE, so chrome stays aligned
216
+ * with PM content without any absolute overlay.
217
+ */
218
+ layoutFacet?: WordReviewEditorLayoutFacet;
219
+ /** Height in px of each page's header band. Default 32. */
220
+ pageChromeHeaderBandPx?: number;
221
+ /** Height in px of each page's footer band. Default 32. */
222
+ pageChromeFooterBandPx?: number;
223
+ /** Visible vertical gap between adjacent pages. Default 24 for page mode. */
224
+ pageChromeInterGapPx?: number;
225
+ /**
226
+ * Revision counter; incremented by the host to trigger a decoration
227
+ * rebuild when the render frame changes (zoom, incremental relayout).
228
+ */
229
+ pageBreakRevision?: number;
230
+ /**
231
+ * Called when the user double-clicks a per-page header band.
232
+ * Receives the pageIndex of the band the user clicked.
233
+ */
234
+ onOpenHeaderStoryForPage?: (pageIndex: number) => void;
235
+ onOpenFooterStoryForPage?: (pageIndex: number) => void;
128
236
  }
129
237
 
130
238
  export interface TwProseMirrorSurfaceRef {
@@ -162,6 +270,10 @@ export const TwProseMirrorSurface = forwardRef<
162
270
  );
163
271
 
164
272
  const mountRef = useRef<HTMLDivElement>(null);
273
+ const openHeaderForPageRef = useRef(props.onOpenHeaderStoryForPage);
274
+ const openFooterForPageRef = useRef(props.onOpenFooterStoryForPage);
275
+ openHeaderForPageRef.current = props.onOpenHeaderStoryForPage;
276
+ openFooterForPageRef.current = props.onOpenFooterStoryForPage;
165
277
  const viewRef = useRef<EditorView | null>(null);
166
278
  const positionMapRef = useRef<PositionMap | null>(null);
167
279
  const callbacksRef = useRef<CommandBridgeCallbacks | null>(null);
@@ -202,6 +314,8 @@ export const TwProseMirrorSurface = forwardRef<
202
314
  onInsertHardBreak: () => props.onInsertHardBreak?.(),
203
315
  onInsertTab: () => props.onInsertTab?.(),
204
316
  onOutdentTab: () => props.onOutdentTab?.(),
317
+ onListIndent: () => props.onListIndent?.(),
318
+ onListOutdent: () => props.onListOutdent?.(),
205
319
  onUndo: () => props.onUndo?.(),
206
320
  onRedo: () => props.onRedo?.(),
207
321
  onBlockedInput: (command, message) => {
@@ -276,8 +390,6 @@ export const TwProseMirrorSurface = forwardRef<
276
390
  ],
277
391
  );
278
392
 
279
- const isCollabMode = Boolean(props.ydoc);
280
-
281
393
  // Create PM plugins (stable across renders — callbacks accessed via ref)
282
394
  const plugins = useMemo(() => {
283
395
  const selectionCallbacks = {
@@ -291,73 +403,79 @@ export const TwProseMirrorSurface = forwardRef<
291
403
  isPredicted: (opId) => sessionRef.current?.isPredicted(opId) ?? false,
292
404
  });
293
405
 
294
- const corePlugins = props.ydoc
295
- ? createCollabPlugins({
296
- ydoc: props.ydoc,
297
- awareness: props.awareness,
298
- selectionCallbacks,
299
- })
300
- : createCommandBridgePlugins({
301
- ...selectionCallbacks,
302
- gate,
303
- onInsertText: (text) => {
304
- if (laneRef.current) {
305
- laneRef.current.onInsertText(text);
306
- } else {
307
- callbacksRef.current?.onInsertText(text);
308
- }
309
- },
310
- onDeleteBackward: () => {
311
- if (laneRef.current) {
312
- laneRef.current.onDeleteBackward();
313
- } else {
314
- callbacksRef.current?.onDeleteBackward();
315
- }
316
- },
317
- onDeleteForward: () => {
318
- if (laneRef.current) {
319
- laneRef.current.onDeleteForward();
320
- } else {
321
- callbacksRef.current?.onDeleteForward();
322
- }
323
- },
324
- onSplitParagraph: () => {
325
- if (laneRef.current) {
326
- laneRef.current.onSplitParagraph();
327
- } else {
328
- callbacksRef.current?.onSplitParagraph();
329
- }
330
- },
331
- onInsertHardBreak: () => {
332
- if (laneRef.current) {
333
- laneRef.current.onInsertHardBreak();
334
- } else {
335
- callbacksRef.current?.onInsertHardBreak();
336
- }
337
- },
338
- onInsertTab: () => callbacksRef.current?.onInsertTab(),
339
- onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
340
- onUndo: () => callbacksRef.current?.onUndo(),
341
- onRedo: () => callbacksRef.current?.onRedo(),
342
- onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
343
- onCompositionChange: (composing) => {
344
- sessionRef.current?.setComposing(composing);
345
- },
346
- });
406
+ const corePlugins = createCommandBridgePlugins({
407
+ ...selectionCallbacks,
408
+ gate,
409
+ onInsertText: (text) => {
410
+ if (laneRef.current) {
411
+ laneRef.current.onInsertText(text);
412
+ } else {
413
+ callbacksRef.current?.onInsertText(text);
414
+ }
415
+ },
416
+ onDeleteBackward: () => {
417
+ if (laneRef.current) {
418
+ laneRef.current.onDeleteBackward();
419
+ } else {
420
+ callbacksRef.current?.onDeleteBackward();
421
+ }
422
+ },
423
+ onDeleteForward: () => {
424
+ if (laneRef.current) {
425
+ laneRef.current.onDeleteForward();
426
+ } else {
427
+ callbacksRef.current?.onDeleteForward();
428
+ }
429
+ },
430
+ onSplitParagraph: () => {
431
+ if (laneRef.current) {
432
+ laneRef.current.onSplitParagraph();
433
+ } else {
434
+ callbacksRef.current?.onSplitParagraph();
435
+ }
436
+ },
437
+ onInsertHardBreak: () => {
438
+ if (laneRef.current) {
439
+ laneRef.current.onInsertHardBreak();
440
+ } else {
441
+ callbacksRef.current?.onInsertHardBreak();
442
+ }
443
+ },
444
+ onInsertTab: () => callbacksRef.current?.onInsertTab(),
445
+ onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
446
+ onListIndent: () => callbacksRef.current?.onListIndent?.(),
447
+ onListOutdent: () => callbacksRef.current?.onListOutdent?.(),
448
+ onUndo: () => callbacksRef.current?.onUndo(),
449
+ onRedo: () => callbacksRef.current?.onRedo(),
450
+ onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
451
+ onCompositionChange: (composing) => {
452
+ sessionRef.current?.setComposing(composing);
453
+ },
454
+ });
347
455
 
348
456
  return [
349
457
  ...corePlugins,
458
+ ...(props.awareness
459
+ ? [
460
+ createRemoteCursorPlugin({
461
+ awareness: props.awareness,
462
+ localClientId: props.awareness.clientID,
463
+ getPositionMap: () => positionMapRef.current,
464
+ getActiveStory: () => snapshotRef.current.activeStory,
465
+ }),
466
+ ]
467
+ : []),
350
468
  createContextualInteractionPlugin({
351
469
  onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
352
470
  onRevisionActivated: (revisionId) => props.onRevisionActivated?.(revisionId),
353
471
  }),
354
472
  createSearchPlugin(),
355
473
  ];
356
- }, [props.onCommentActivated, props.onRevisionActivated, props.ydoc, props.awareness]);
474
+ }, [props.awareness, props.onCommentActivated, props.onRevisionActivated]);
357
475
 
358
476
  const applyDecorationProps = useCallback(
359
477
  (view: EditorView, positionMap: PositionMap): void => {
360
- const decorations = buildDecorations(
478
+ const baseDecorations = buildDecorations(
361
479
  view.state.doc,
362
480
  positionMap,
363
481
  commentModel,
@@ -374,6 +492,24 @@ export const TwProseMirrorSurface = forwardRef<
374
492
  props.activeWorkflowScopeIds,
375
493
  props.workflowMetadata,
376
494
  );
495
+ const pageBreakDecos = buildPageBreakDecorationsFromProps(
496
+ props.layoutFacet,
497
+ snapshot.activeStory.kind === "main",
498
+ positionMap,
499
+ props.isPageWorkspace ? "page" : "canvas",
500
+ props.canonicalDocument,
501
+ {
502
+ headerBandPx: props.pageChromeHeaderBandPx,
503
+ footerBandPx: props.pageChromeFooterBandPx,
504
+ interGapPx: props.pageChromeInterGapPx,
505
+ },
506
+ );
507
+ const decorations = pageBreakDecos.length > 0
508
+ ? DecorationSet.create(view.state.doc, [
509
+ ...pageBreakDecos,
510
+ ...extractDecorations(baseDecorations, view.state.doc),
511
+ ])
512
+ : baseDecorations;
377
513
  view.setProps({
378
514
  editable: () => canEdit,
379
515
  decorations: () => decorations,
@@ -414,6 +550,33 @@ export const TwProseMirrorSurface = forwardRef<
414
550
  // eslint-disable-next-line react-hooks/exhaustive-deps
415
551
  }, []);
416
552
 
553
+ // Listen for the custom events dispatched by the page-chrome widget
554
+ // decorations (`wre-open-header-story-for-page` /
555
+ // `wre-open-footer-story-for-page`). The widget DOM lives inside the
556
+ // mount element, so bubbling events reach us here.
557
+ useEffect(() => {
558
+ const mount = mountRef.current;
559
+ if (!mount) return;
560
+ const handleHeader = (event: Event) => {
561
+ const detail = (event as CustomEvent<{ pageIndex: number }>).detail;
562
+ if (typeof detail?.pageIndex === "number") {
563
+ openHeaderForPageRef.current?.(detail.pageIndex);
564
+ }
565
+ };
566
+ const handleFooter = (event: Event) => {
567
+ const detail = (event as CustomEvent<{ pageIndex: number }>).detail;
568
+ if (typeof detail?.pageIndex === "number") {
569
+ openFooterForPageRef.current?.(detail.pageIndex);
570
+ }
571
+ };
572
+ mount.addEventListener("wre-open-header-story-for-page", handleHeader);
573
+ mount.addEventListener("wre-open-footer-story-for-page", handleFooter);
574
+ return () => {
575
+ mount.removeEventListener("wre-open-header-story-for-page", handleHeader);
576
+ mount.removeEventListener("wre-open-footer-story-for-page", handleFooter);
577
+ };
578
+ }, []);
579
+
417
580
  // Build the FastTextEditLane whenever `dispatchRuntimeCommand` changes.
418
581
  // The lane is consulted via `laneRef.current` inside PM plugin callbacks,
419
582
  // so the plugins memo does not need to depend on this effect.
@@ -469,11 +632,6 @@ export const TwProseMirrorSurface = forwardRef<
469
632
  useEffect(() => {
470
633
  if (!mountRef.current || !surface) return;
471
634
 
472
- // Collab mode: y-prosemirror owns the doc after initial mount
473
- if (isCollabMode && viewRef.current) {
474
- return;
475
- }
476
-
477
635
  if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
478
636
  return;
479
637
  }
@@ -543,15 +701,6 @@ export const TwProseMirrorSurface = forwardRef<
543
701
  });
544
702
  viewRef.current = view;
545
703
  recordPerfSample("pm.mount");
546
-
547
- if (isCollabMode && props.ydoc) {
548
- const yXmlFragment = props.ydoc.getXmlFragment("prosemirror");
549
- if (yXmlFragment.length === 0) {
550
- props.ydoc.transact(() => {
551
- prosemirrorToYXmlFragment(view.state.doc, yXmlFragment);
552
- });
553
- }
554
- }
555
704
  } else {
556
705
  suppressSelectionEchoRef.current = true;
557
706
  viewRef.current.updateState(state);
@@ -575,12 +724,10 @@ export const TwProseMirrorSurface = forwardRef<
575
724
  }, [
576
725
  applyDecorationProps,
577
726
  documentBuildKey,
578
- isCollabMode,
579
727
  surface,
580
728
  snapshot.selection,
581
729
  plugins,
582
730
  props.mediaPreviews,
583
- props.ydoc,
584
731
  ]);
585
732
 
586
733
  // Update decorations and editability without rebuilding the PM document.
@@ -613,7 +760,6 @@ export const TwProseMirrorSurface = forwardRef<
613
760
  ]);
614
761
 
615
762
  useEffect(() => {
616
- if (isCollabMode) return;
617
763
  const view = viewRef.current;
618
764
  const positionMap = positionMapRef.current;
619
765
  if (!view || !surface || !positionMap) {
@@ -649,7 +795,7 @@ export const TwProseMirrorSurface = forwardRef<
649
795
  queueMicrotask(() => {
650
796
  suppressSelectionEchoRef.current = false;
651
797
  });
652
- }, [isCollabMode, snapshot.selection, surface]);
798
+ }, [snapshot.selection, surface]);
653
799
 
654
800
  useEffect(() => {
655
801
  if (!pendingSelectionProbeRef.current) {
@@ -0,0 +1,61 @@
1
+ /*
2
+ * R2c: table-style conditional-region band classes.
3
+ *
4
+ * These classes are applied to <td> elements by TwTableNodeView.applyCellAttrs
5
+ * whenever the surface snapshot's SurfaceTableCellSnapshot.bandClasses is set.
6
+ * The class set comes from `resolveTableStyleResolution().cells[*].activeConditionalRegions`
7
+ * mapped through the `band-<region>` convention.
8
+ *
9
+ * Band styling uses @apply against theme variables so light↔dark toggle repaints
10
+ * cell colors via the cascade instead of forcing a surface rebuild.
11
+ *
12
+ * Consumer integration: import this stylesheet alongside editor-theme.css in
13
+ * the app's build pipeline, e.g.:
14
+ * @import "@beyondwork/docx-react-component/ui-tailwind/editor-surface/tw-table-bands.css";
15
+ *
16
+ * Direct overrides: when a cell has a direct `shading.fill`, applyCellAttrs
17
+ * writes an inline `background-color` that wins over the band default.
18
+ */
19
+
20
+ .band-firstRow {
21
+ @apply font-semibold bg-surface-raised;
22
+ }
23
+
24
+ .band-lastRow {
25
+ @apply font-semibold bg-surface-raised;
26
+ }
27
+
28
+ .band-firstColumn {
29
+ @apply font-semibold;
30
+ }
31
+
32
+ .band-lastColumn {
33
+ @apply font-semibold;
34
+ }
35
+
36
+ .band-band1Horz {
37
+ @apply bg-surface-secondary;
38
+ }
39
+
40
+ .band-band2Horz {
41
+ @apply bg-surface-tertiary;
42
+ }
43
+
44
+ .band-band1Vert {
45
+ @apply bg-surface-secondary/50;
46
+ }
47
+
48
+ .band-band2Vert {
49
+ @apply bg-surface-tertiary/50;
50
+ }
51
+
52
+ /* Corner cells — reserved for table styles that define them explicitly. */
53
+ .band-nwCell { /* top-left */ }
54
+ .band-neCell { /* top-right */ }
55
+ .band-swCell { /* bottom-left */ }
56
+ .band-seCell { /* bottom-right */ }
57
+
58
+ /* R3e: virtual header repeated on continuation pages. Visual parity with firstRow. */
59
+ .band-virtual-header {
60
+ @apply font-semibold bg-surface-raised;
61
+ }
@@ -11,6 +11,11 @@
11
11
  import type { Node as PMNode } from "prosemirror-model";
12
12
  import type { NodeViewConstructor, ViewMutationRecord } from "prosemirror-view";
13
13
 
14
+ // R2c: band class styles live in ./tw-table-bands.module.css. Consumers import
15
+ // that stylesheet through their build pipeline (same pattern as editor-theme.css).
16
+ // Importing .css directly from a .tsx breaks the node:test runner and the npm
17
+ // library build — keep CSS as a consumer-side concern.
18
+
14
19
  const TABLE_LAYOUT_SYNC_EVENT = "pm-table-layout-sync";
15
20
 
16
21
  interface TableCellLayout {
@@ -318,6 +323,7 @@ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: bool
318
323
  const borderRight = node.attrs.borderRight as string | null | undefined;
319
324
  const borderBottom = node.attrs.borderBottom as string | null | undefined;
320
325
  const borderLeft = node.attrs.borderLeft as string | null | undefined;
326
+ const bandClasses = node.attrs.bandClasses as string | null | undefined;
321
327
 
322
328
  if (backgroundColor) cell.setAttribute("data-cell-background", backgroundColor);
323
329
  else cell.removeAttribute("data-cell-background");
@@ -332,6 +338,19 @@ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: bool
332
338
  if (borderLeft) cell.setAttribute("data-border-left", borderLeft);
333
339
  else cell.removeAttribute("data-border-left");
334
340
 
341
+ // R2b: band classes come from the resolved style cascade. They layer on top
342
+ // of the base Tailwind classes so theme vars (bg-surface-*, text-secondary,
343
+ // etc.) repaint on light↔dark toggle without a surface rebuild. Direct
344
+ // `backgroundColor` overrides still win via the inline-style block below.
345
+ if (bandClasses) {
346
+ cell.setAttribute("data-band-classes", bandClasses);
347
+ for (const token of bandClasses.split(/\s+/).filter(Boolean)) {
348
+ cell.classList.add(token);
349
+ }
350
+ } else {
351
+ cell.removeAttribute("data-band-classes");
352
+ }
353
+
335
354
  cell.style.backgroundColor = backgroundColor ?? "";
336
355
  cell.style.verticalAlign = verticalAlign === "center" ? "middle" : (verticalAlign ?? "");
337
356
  cell.style.borderTop = borderTop ?? "";
@@ -48,16 +48,12 @@ export { TwStatusBar } from "./status/tw-status-bar";
48
48
  export { TwAlertBanner } from "./chrome/tw-alert-banner";
49
49
  export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
50
50
 
51
- // Chrome overlay plane (R3a — scope rail, workspace dock)
51
+ // Chrome overlay plane (R3a — scope rail layer)
52
52
  export {
53
53
  TwChromeOverlay,
54
54
  type TwChromeOverlayProps,
55
55
  TwScopeRailLayer,
56
56
  type TwScopeRailLayerProps,
57
- TwWorkspaceViewSwitcher,
58
- type TwWorkspaceViewSwitcherProps,
59
- type WorkspaceView,
60
- type WorkspaceViewAction,
61
57
  } from "./chrome-overlay";
62
58
 
63
59
  // Session capabilities