@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
@@ -1,254 +0,0 @@
1
- import * as Y from "yjs";
2
-
3
- import type { DocumentRuntime, DocumentRuntimeEvent, Unsubscribe } from "./document-runtime.ts";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Serialised shapes stored inside the Y.Maps
7
- // ---------------------------------------------------------------------------
8
-
9
- interface YCommentThread {
10
- commentId: string;
11
- status: "open" | "resolved" | "detached";
12
- anchor: { kind: string; [key: string]: unknown };
13
- createdAt: string;
14
- createdBy: string;
15
- authorId: string;
16
- body: string;
17
- entries: Array<{
18
- entryId: string;
19
- authorId: string;
20
- body: string;
21
- createdAt: string;
22
- }>;
23
- resolvedAt?: string;
24
- resolvedBy?: string;
25
- warningIds: string[];
26
- sourceClientId: number;
27
- }
28
-
29
- interface YRevisionAction {
30
- changeId: string;
31
- action: "accept" | "reject";
32
- sourceClientId: number;
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Public API
37
- // ---------------------------------------------------------------------------
38
-
39
- export interface CollabReviewSyncHandle {
40
- destroy(): void;
41
- }
42
-
43
- export function createCollabReviewSync(
44
- ydoc: Y.Doc,
45
- runtime: DocumentRuntime,
46
- ): CollabReviewSyncHandle {
47
- const yComments = ydoc.getMap<YCommentThread>("comments");
48
- const yRevisionActions = ydoc.getMap<YRevisionAction>("revisionActions");
49
- const clientId = ydoc.clientID;
50
-
51
- let suppressLocalEvents = false;
52
-
53
- // --- Local → Yjs ---------------------------------------------------------
54
-
55
- const unsubEvents: Unsubscribe = runtime.subscribeToEvents((event) => {
56
- if (suppressLocalEvents) return;
57
-
58
- switch (event.type) {
59
- case "comment_added":
60
- pushCommentToYjs(event.commentId);
61
- break;
62
- case "comment_resolved":
63
- syncCommentFieldToYjs(event.commentId);
64
- break;
65
- case "change_accepted":
66
- yRevisionActions.set(revisionActionKey(event.changeId, "accept"), {
67
- changeId: event.changeId,
68
- action: "accept",
69
- sourceClientId: clientId,
70
- });
71
- break;
72
- case "change_rejected":
73
- yRevisionActions.set(revisionActionKey(event.changeId, "reject"), {
74
- changeId: event.changeId,
75
- action: "reject",
76
- sourceClientId: clientId,
77
- });
78
- break;
79
- }
80
- });
81
-
82
- function pushCommentToYjs(commentId: string): void {
83
- const thread = runtime
84
- .getRenderSnapshot()
85
- .comments.threads.find((t) => t.commentId === commentId);
86
- if (!thread) return;
87
-
88
- yComments.set(commentId, {
89
- commentId: thread.commentId,
90
- status: thread.status,
91
- anchor: thread.anchor,
92
- createdAt: thread.createdAt,
93
- createdBy: thread.createdBy,
94
- authorId: thread.createdBy,
95
- body: thread.entries[0]?.body ?? "",
96
- entries: thread.entries.map((e) => ({
97
- entryId: e.entryId,
98
- authorId: e.authorId,
99
- body: e.body,
100
- createdAt: e.createdAt,
101
- })),
102
- resolvedAt: thread.resolvedAt,
103
- resolvedBy: thread.resolvedBy,
104
- warningIds: [],
105
- sourceClientId: clientId,
106
- });
107
- }
108
-
109
- function syncCommentFieldToYjs(commentId: string): void {
110
- const existing = yComments.get(commentId);
111
- const thread = runtime
112
- .getRenderSnapshot()
113
- .comments.threads.find((t) => t.commentId === commentId);
114
- if (!existing || !thread) return;
115
-
116
- yComments.set(commentId, {
117
- ...existing,
118
- status: thread.status,
119
- entries: thread.entries.map((e) => ({
120
- entryId: e.entryId,
121
- authorId: e.authorId,
122
- body: e.body,
123
- createdAt: e.createdAt,
124
- })),
125
- resolvedAt: thread.resolvedAt,
126
- resolvedBy: thread.resolvedBy,
127
- sourceClientId: clientId,
128
- });
129
- }
130
-
131
- // --- Yjs → Local ---------------------------------------------------------
132
-
133
- function onCommentMapChange(event: Y.YMapEvent<YCommentThread>): void {
134
- suppressLocalEvents = true;
135
- try {
136
- for (const [commentId, change] of event.changes.keys) {
137
- const entry = yComments.get(commentId);
138
- if (!entry || entry.sourceClientId === clientId) continue;
139
-
140
- const existing = runtime
141
- .getRenderSnapshot()
142
- .comments.threads.find((t) => t.commentId === commentId);
143
-
144
- if (change.action === "add" && !existing) {
145
- runtime.dispatch({
146
- type: "comment.add",
147
- comment: {
148
- commentId: entry.commentId,
149
- status: entry.status,
150
- anchor: entry.anchor as never,
151
- createdAt: entry.createdAt,
152
- createdBy: entry.createdBy,
153
- authorId: entry.authorId,
154
- body: entry.body,
155
- entries: entry.entries,
156
- warningIds: entry.warningIds,
157
- isResolved: entry.status === "resolved",
158
- metadata: { source: "runtime" },
159
- },
160
- });
161
- } else if (change.action === "update" && existing) {
162
- if (entry.status === "resolved" && existing.status !== "resolved") {
163
- runtime.dispatch({
164
- type: "comment.resolve",
165
- commentId,
166
- resolvedBy: entry.resolvedBy,
167
- });
168
- } else if (entry.status === "open" && existing.status === "resolved") {
169
- runtime.dispatch({ type: "comment.reopen", commentId });
170
- }
171
-
172
- const localEntryCount = existing.entries.length;
173
- if (entry.entries.length > localEntryCount) {
174
- for (const newEntry of entry.entries.slice(localEntryCount)) {
175
- runtime.dispatch({
176
- type: "comment.add-reply",
177
- commentId,
178
- body: newEntry.body,
179
- authorId: newEntry.authorId,
180
- });
181
- }
182
- }
183
- }
184
- }
185
- } finally {
186
- suppressLocalEvents = false;
187
- }
188
- }
189
-
190
- function onRevisionActionMapChange(event: Y.YMapEvent<YRevisionAction>): void {
191
- suppressLocalEvents = true;
192
- try {
193
- for (const [, change] of event.changes.keys) {
194
- if (change.action !== "add") continue;
195
- const entries = [...yRevisionActions.entries()];
196
- const latest = entries[entries.length - 1];
197
- if (!latest) continue;
198
- const entry = latest[1];
199
- if (entry.sourceClientId === clientId) continue;
200
-
201
- runtime.dispatch({
202
- type: entry.action === "accept" ? "change.accept" : "change.reject",
203
- changeId: entry.changeId,
204
- });
205
- }
206
- } finally {
207
- suppressLocalEvents = false;
208
- }
209
- }
210
-
211
- yComments.observe(onCommentMapChange);
212
- yRevisionActions.observe(onRevisionActionMapChange);
213
-
214
- // --- Initial sync: push existing comments to Yjs if first client ----------
215
-
216
- const snapshot = runtime.getRenderSnapshot();
217
- if (yComments.size === 0 && snapshot.comments.threads.length > 0) {
218
- ydoc.transact(() => {
219
- for (const thread of snapshot.comments.threads) {
220
- yComments.set(thread.commentId, {
221
- commentId: thread.commentId,
222
- status: thread.status,
223
- anchor: thread.anchor,
224
- createdAt: thread.createdAt,
225
- createdBy: thread.createdBy,
226
- authorId: thread.createdBy,
227
- body: thread.entries[0]?.body ?? "",
228
- entries: thread.entries.map((e) => ({
229
- entryId: e.entryId,
230
- authorId: e.authorId,
231
- body: e.body,
232
- createdAt: e.createdAt,
233
- })),
234
- resolvedAt: thread.resolvedAt,
235
- resolvedBy: thread.resolvedBy,
236
- warningIds: [],
237
- sourceClientId: clientId,
238
- });
239
- }
240
- });
241
- }
242
-
243
- return {
244
- destroy() {
245
- unsubEvents();
246
- yComments.unobserve(onCommentMapChange);
247
- yRevisionActions.unobserve(onRevisionActionMapChange);
248
- },
249
- };
250
- }
251
-
252
- function revisionActionKey(changeId: string, action: string): string {
253
- return `${changeId}:${action}`;
254
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * TwWorkspaceViewSwitcher — floating dock that sits at the bottom of the
3
- * chrome overlay. Mirrors the "WORKFLOW VIEW" bottom bar in image copy.png.
4
- *
5
- * Per runtime-rendering-and-chrome-phase.md §6.3, the dock is a
6
- * `DraggableFloat`-compatible surface that lives inside the `ChromeOverlay`
7
- * plane. Today it ships the preset selector + the canonical workflow
8
- * actions (review / comment / approve) so consumers can wire them without
9
- * waiting for the full DraggableFloat primitive.
10
- */
11
-
12
- import * as React from "react";
13
-
14
- export type WorkspaceView = "draft" | "layout" | "review" | "workflow";
15
-
16
- export interface WorkspaceViewAction {
17
- id: string;
18
- label: string;
19
- icon: string;
20
- onClick?: () => void;
21
- disabled?: boolean;
22
- }
23
-
24
- export interface TwWorkspaceViewSwitcherProps {
25
- activeView: WorkspaceView;
26
- onViewChange?: (view: WorkspaceView) => void;
27
- actions?: readonly WorkspaceViewAction[];
28
- "data-testid"?: string;
29
- }
30
-
31
- const DEFAULT_VIEW_ORDER: readonly WorkspaceView[] = [
32
- "draft",
33
- "layout",
34
- "review",
35
- "workflow",
36
- ];
37
-
38
- const VIEW_LABELS: Record<WorkspaceView, string> = {
39
- draft: "DRAFT",
40
- layout: "LAYOUT",
41
- review: "REVIEW",
42
- workflow: "WORKFLOW VIEW",
43
- };
44
-
45
- export const TwWorkspaceViewSwitcher: React.FC<TwWorkspaceViewSwitcherProps> = ({
46
- activeView,
47
- onViewChange,
48
- actions,
49
- "data-testid": testId,
50
- }) => {
51
- return (
52
- <div
53
- className="wre-workspace-dock pointer-events-auto absolute left-1/2 bottom-6 z-10 -translate-x-1/2 flex items-center gap-2 rounded-full border border-border/70 bg-canvas/95 px-4 py-2 shadow-lg backdrop-blur"
54
- data-testid={testId ?? "workspace-view-switcher"}
55
- role="toolbar"
56
- aria-label="Workspace view"
57
- >
58
- {DEFAULT_VIEW_ORDER.map((view) => (
59
- <button
60
- key={view}
61
- type="button"
62
- className={`wre-workspace-dock-view-btn inline-flex items-center gap-1 rounded-full px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] ${
63
- view === activeView
64
- ? "wre-workspace-dock-view-btn-active bg-primary text-white"
65
- : "text-secondary hover:text-primary"
66
- }`}
67
- aria-pressed={view === activeView}
68
- onClick={onViewChange ? () => onViewChange(view) : undefined}
69
- disabled={!onViewChange}
70
- >
71
- <span aria-hidden="true" className="wre-workspace-dock-icon h-3 w-3" />
72
- {VIEW_LABELS[view]}
73
- </button>
74
- ))}
75
- {actions && actions.length > 0 ? (
76
- <div className="wre-workspace-dock-sep mx-1 h-5 w-px bg-border" />
77
- ) : null}
78
- {actions?.map((action) => (
79
- <button
80
- key={action.id}
81
- type="button"
82
- className="wre-workspace-dock-action inline-flex h-7 w-7 items-center justify-center rounded-full text-secondary hover:bg-surface hover:text-primary disabled:opacity-40"
83
- aria-label={action.label}
84
- disabled={action.disabled}
85
- onClick={action.onClick}
86
- data-action-id={action.id}
87
- >
88
- <span aria-hidden="true" className={`wre-workspace-dock-icon wre-workspace-dock-icon-${action.icon}`} />
89
- </button>
90
- ))}
91
- </div>
92
- );
93
- };
94
-
95
- export default TwWorkspaceViewSwitcher;
@@ -1,40 +0,0 @@
1
- import { type Plugin } from "prosemirror-state";
2
- import { keymap } from "prosemirror-keymap";
3
- import { columnResizing, tableEditing } from "prosemirror-tables";
4
- import { yCursorPlugin, ySyncPlugin, yUndoPlugin, undo, redo } from "y-prosemirror";
5
- import type { Awareness } from "y-protocols/awareness";
6
- import type { Doc as YDoc } from "yjs";
7
-
8
- import {
9
- createSelectionSyncPlugin,
10
- type SelectionSyncCallbacks,
11
- } from "./pm-command-bridge";
12
-
13
- export interface CollabPluginOptions {
14
- ydoc: YDoc;
15
- awareness?: Awareness;
16
- selectionCallbacks: SelectionSyncCallbacks;
17
- }
18
-
19
- export function createCollabPlugins(options: CollabPluginOptions): Plugin[] {
20
- const yXmlFragment = options.ydoc.getXmlFragment("prosemirror");
21
-
22
- const plugins: Plugin[] = [
23
- ySyncPlugin(yXmlFragment),
24
- yUndoPlugin(),
25
- keymap({
26
- "Mod-z": undo,
27
- "Mod-y": redo,
28
- "Shift-Mod-z": redo,
29
- }),
30
- createSelectionSyncPlugin(options.selectionCallbacks),
31
- tableEditing(),
32
- columnResizing(),
33
- ];
34
-
35
- if (options.awareness) {
36
- plugins.splice(1, 0, yCursorPlugin(options.awareness));
37
- }
38
-
39
- return plugins;
40
- }