@beyondwork/docx-react-component 1.0.37 → 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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. 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,70 +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
- });
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
+ });
344
455
 
345
456
  return [
346
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
+ : []),
347
468
  createContextualInteractionPlugin({
348
469
  onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
349
470
  onRevisionActivated: (revisionId) => props.onRevisionActivated?.(revisionId),
350
471
  }),
351
472
  createSearchPlugin(),
352
473
  ];
353
- }, [props.onCommentActivated, props.onRevisionActivated, props.ydoc, props.awareness]);
474
+ }, [props.awareness, props.onCommentActivated, props.onRevisionActivated]);
354
475
 
355
476
  const applyDecorationProps = useCallback(
356
477
  (view: EditorView, positionMap: PositionMap): void => {
357
- const decorations = buildDecorations(
478
+ const baseDecorations = buildDecorations(
358
479
  view.state.doc,
359
480
  positionMap,
360
481
  commentModel,
@@ -371,6 +492,24 @@ export const TwProseMirrorSurface = forwardRef<
371
492
  props.activeWorkflowScopeIds,
372
493
  props.workflowMetadata,
373
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;
374
513
  view.setProps({
375
514
  editable: () => canEdit,
376
515
  decorations: () => decorations,
@@ -411,6 +550,33 @@ export const TwProseMirrorSurface = forwardRef<
411
550
  // eslint-disable-next-line react-hooks/exhaustive-deps
412
551
  }, []);
413
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
+
414
580
  // Build the FastTextEditLane whenever `dispatchRuntimeCommand` changes.
415
581
  // The lane is consulted via `laneRef.current` inside PM plugin callbacks,
416
582
  // so the plugins memo does not need to depend on this effect.
@@ -466,11 +632,6 @@ export const TwProseMirrorSurface = forwardRef<
466
632
  useEffect(() => {
467
633
  if (!mountRef.current || !surface) return;
468
634
 
469
- // Collab mode: y-prosemirror owns the doc after initial mount
470
- if (isCollabMode && viewRef.current) {
471
- return;
472
- }
473
-
474
635
  if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
475
636
  return;
476
637
  }
@@ -540,15 +701,6 @@ export const TwProseMirrorSurface = forwardRef<
540
701
  });
541
702
  viewRef.current = view;
542
703
  recordPerfSample("pm.mount");
543
-
544
- if (isCollabMode && props.ydoc) {
545
- const yXmlFragment = props.ydoc.getXmlFragment("prosemirror");
546
- if (yXmlFragment.length === 0) {
547
- props.ydoc.transact(() => {
548
- prosemirrorToYXmlFragment(view.state.doc, yXmlFragment);
549
- });
550
- }
551
- }
552
704
  } else {
553
705
  suppressSelectionEchoRef.current = true;
554
706
  viewRef.current.updateState(state);
@@ -572,12 +724,10 @@ export const TwProseMirrorSurface = forwardRef<
572
724
  }, [
573
725
  applyDecorationProps,
574
726
  documentBuildKey,
575
- isCollabMode,
576
727
  surface,
577
728
  snapshot.selection,
578
729
  plugins,
579
730
  props.mediaPreviews,
580
- props.ydoc,
581
731
  ]);
582
732
 
583
733
  // Update decorations and editability without rebuilding the PM document.
@@ -610,7 +760,6 @@ export const TwProseMirrorSurface = forwardRef<
610
760
  ]);
611
761
 
612
762
  useEffect(() => {
613
- if (isCollabMode) return;
614
763
  const view = viewRef.current;
615
764
  const positionMap = positionMapRef.current;
616
765
  if (!view || !surface || !positionMap) {
@@ -646,7 +795,7 @@ export const TwProseMirrorSurface = forwardRef<
646
795
  queueMicrotask(() => {
647
796
  suppressSelectionEchoRef.current = false;
648
797
  });
649
- }, [isCollabMode, snapshot.selection, surface]);
798
+ }, [snapshot.selection, surface]);
650
799
 
651
800
  useEffect(() => {
652
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 ?? "";
@@ -14,10 +14,31 @@ export { TwReviewRail, type TwReviewRailProps, type ReviewRailTab } from "./revi
14
14
  export { TwCommentSidebar } from "./review/tw-comment-sidebar";
15
15
  export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
16
16
  export { TwHealthPanel } from "./review/tw-health-panel";
17
+ export { TwWorkflowTab, type TwWorkflowTabProps } from "./review/tw-workflow-tab";
18
+ export {
19
+ TwRailCard,
20
+ type TwRailCardProps,
21
+ type RailCardTone,
22
+ type RailCardAvatar,
23
+ type RailCardCounter,
24
+ type RailCardProgress,
25
+ } from "./review/tw-rail-card";
26
+ export {
27
+ TwReviewRailFooter,
28
+ type TwReviewRailFooterProps,
29
+ } from "./review/tw-review-rail-footer";
17
30
 
18
31
  // Toolbar
19
32
  export { TwToolbar, type TwToolbarProps } from "./toolbar/tw-toolbar";
20
33
  export { TwToolbarIconButton } from "./toolbar/tw-toolbar-icon-button";
34
+ export {
35
+ TwShellHeader,
36
+ type TwShellHeaderProps,
37
+ type ShellHeaderMode,
38
+ type ShellHeaderModeOption,
39
+ type ShellHeaderPrimaryAction,
40
+ type ShellHeaderIconAction,
41
+ } from "./toolbar/tw-shell-header";
21
42
  export type { WorkspaceMode, ZoomLevel } from "../api/public-types";
22
43
 
23
44
  // Status
@@ -27,6 +48,14 @@ export { TwStatusBar } from "./status/tw-status-bar";
27
48
  export { TwAlertBanner } from "./chrome/tw-alert-banner";
28
49
  export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
29
50
 
51
+ // Chrome overlay plane (R3a — scope rail layer)
52
+ export {
53
+ TwChromeOverlay,
54
+ type TwChromeOverlayProps,
55
+ TwScopeRailLayer,
56
+ type TwScopeRailLayerProps,
57
+ } from "./chrome-overlay";
58
+
30
59
  // Session capabilities
31
60
  export {
32
61
  deriveCapabilities,
@@ -93,10 +93,10 @@ function CommentThreadCard(props: {
93
93
  role="button"
94
94
  tabIndex={0}
95
95
  className={[
96
- "cursor-pointer rounded-md bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
96
+ "cursor-pointer rounded-lg bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
97
97
  focusRingClass,
98
98
  isActive
99
- ? "bg-accent-soft/40 ring-accent/25"
99
+ ? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
100
100
  : "hover:bg-surface",
101
101
  thread.status === "detached" ? "opacity-70" : "",
102
102
  ].join(" ")}