@beyondwork/docx-react-component 1.0.38 → 1.0.40
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.
- package/package.json +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- 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 =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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.
|
|
474
|
+
}, [props.awareness, props.onCommentActivated, props.onRevisionActivated]);
|
|
357
475
|
|
|
358
476
|
const applyDecorationProps = useCallback(
|
|
359
477
|
(view: EditorView, positionMap: PositionMap): void => {
|
|
360
|
-
const
|
|
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
|
-
}, [
|
|
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 ?? "";
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -47,17 +47,18 @@ export { TwStatusBar } from "./status/tw-status-bar";
|
|
|
47
47
|
// Chrome
|
|
48
48
|
export { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
49
49
|
export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
50
|
+
export {
|
|
51
|
+
TwModeDock,
|
|
52
|
+
type TwModeDockAction,
|
|
53
|
+
type TwModeDockProps,
|
|
54
|
+
} from "./chrome/tw-mode-dock";
|
|
50
55
|
|
|
51
|
-
// Chrome overlay plane (R3a — scope rail
|
|
56
|
+
// Chrome overlay plane (R3a — scope rail layer)
|
|
52
57
|
export {
|
|
53
58
|
TwChromeOverlay,
|
|
54
59
|
type TwChromeOverlayProps,
|
|
55
60
|
TwScopeRailLayer,
|
|
56
61
|
type TwScopeRailLayerProps,
|
|
57
|
-
TwWorkspaceViewSwitcher,
|
|
58
|
-
type TwWorkspaceViewSwitcherProps,
|
|
59
|
-
type WorkspaceView,
|
|
60
|
-
type WorkspaceViewAction,
|
|
61
62
|
} from "./chrome-overlay";
|
|
62
63
|
|
|
63
64
|
// Session capabilities
|