@beyondwork/docx-react-component 1.0.41 → 1.0.42

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 (88) hide show
  1. package/package.json +13 -1
  2. package/src/api/awareness-identity-types.ts +35 -0
  3. package/src/api/comment-negotiation-types.ts +130 -0
  4. package/src/api/comment-presentation-types.ts +106 -0
  5. package/src/api/external-custody-types.ts +74 -0
  6. package/src/api/participants-types.ts +18 -0
  7. package/src/api/public-types.ts +347 -4
  8. package/src/api/scope-metadata-resolver-types.ts +88 -0
  9. package/src/core/commands/formatting-commands.ts +1 -1
  10. package/src/core/commands/index.ts +568 -1
  11. package/src/index.ts +118 -1
  12. package/src/io/export/escape-xml-attribute.ts +26 -0
  13. package/src/io/export/external-send.ts +188 -0
  14. package/src/io/export/serialize-comments.ts +13 -16
  15. package/src/io/export/serialize-footnotes.ts +17 -24
  16. package/src/io/export/serialize-headers-footers.ts +17 -24
  17. package/src/io/export/serialize-main-document.ts +59 -62
  18. package/src/io/export/serialize-numbering.ts +20 -27
  19. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  20. package/src/io/export/serialize-tables.ts +8 -15
  21. package/src/io/export/table-properties-xml.ts +25 -32
  22. package/src/io/import/external-reimport.ts +40 -0
  23. package/src/io/ooxml/bw-xml.ts +244 -0
  24. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  25. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  26. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  27. package/src/io/ooxml/external-custody-payload.ts +102 -0
  28. package/src/io/ooxml/participants-payload.ts +97 -0
  29. package/src/io/ooxml/payload-signature.ts +112 -0
  30. package/src/io/ooxml/workflow-payload-validator.ts +271 -0
  31. package/src/io/ooxml/workflow-payload.ts +146 -7
  32. package/src/runtime/awareness-identity.ts +173 -0
  33. package/src/runtime/collab/event-types.ts +27 -0
  34. package/src/runtime/collab-session-bridge.ts +157 -0
  35. package/src/runtime/collab-session-facet.ts +193 -0
  36. package/src/runtime/collab-session.ts +273 -0
  37. package/src/runtime/comment-negotiation-sync.ts +91 -0
  38. package/src/runtime/comment-negotiation.ts +158 -0
  39. package/src/runtime/comment-presentation.ts +223 -0
  40. package/src/runtime/document-runtime.ts +280 -93
  41. package/src/runtime/external-send-runtime.ts +117 -0
  42. package/src/runtime/layout/docx-font-loader.ts +11 -30
  43. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  44. package/src/runtime/layout/layout-engine-instance.ts +122 -12
  45. package/src/runtime/layout/page-graph.ts +79 -7
  46. package/src/runtime/layout/paginated-layout-engine.ts +230 -34
  47. package/src/runtime/layout/public-facet.ts +185 -13
  48. package/src/runtime/layout/table-row-split.ts +316 -0
  49. package/src/runtime/markdown-sanitizer.ts +132 -0
  50. package/src/runtime/participants.ts +134 -0
  51. package/src/runtime/resign-payload.ts +120 -0
  52. package/src/runtime/tamper-gate.ts +157 -0
  53. package/src/runtime/workflow-markup.ts +9 -0
  54. package/src/runtime/workflow-rail-segments.ts +244 -5
  55. package/src/ui/WordReviewEditor.tsx +587 -0
  56. package/src/ui/editor-runtime-boundary.ts +1 -0
  57. package/src/ui/editor-shell-view.tsx +11 -0
  58. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  59. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  60. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  61. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  62. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  63. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  64. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  65. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  66. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  67. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  69. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  70. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  71. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  72. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  73. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +58 -0
  74. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  75. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  76. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  77. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  78. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  79. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  80. package/src/ui-tailwind/editor-surface/pm-schema.ts +15 -13
  81. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  82. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -14
  83. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  84. package/src/ui-tailwind/index.ts +32 -0
  85. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  87. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  88. package/src/ui-tailwind/tw-review-workspace.tsx +293 -34
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.41",
4
+ "version": "1.0.42",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
6
  "packageManager": "pnpm@10.30.3",
7
7
  "type": "module",
@@ -102,6 +102,10 @@
102
102
  "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
103
103
  "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
104
104
  "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
105
+ "test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
106
+ "test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
107
+ "visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
108
+ "mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
105
109
  "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
106
110
  "lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
107
111
  "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
@@ -162,22 +166,29 @@
162
166
  "react-dom": "^19.2.0",
163
167
  "tailwindcss": "^4.2.2",
164
168
  "yjs": "^13.6.0",
169
+ "y-prosemirror": "^1.2.0",
165
170
  "y-protocols": "^1.0.0"
166
171
  },
167
172
  "peerDependenciesMeta": {
168
173
  "yjs": {
169
174
  "optional": true
170
175
  },
176
+ "y-prosemirror": {
177
+ "optional": true
178
+ },
171
179
  "y-protocols": {
172
180
  "optional": true
173
181
  }
174
182
  },
175
183
  "devDependencies": {
176
184
  "@chllming/wave-orchestration": "^0.9.15",
185
+ "@playwright/test": "^1.59.1",
177
186
  "@types/react": "19.2.14",
178
187
  "@types/react-dom": "19.2.3",
179
188
  "@typescript/native-preview": "7.0.0-dev.20260409.1",
180
189
  "jsdom": "^29.0.1",
190
+ "pixelmatch": "^7.1.0",
191
+ "pngjs": "^7.0.0",
181
192
  "prosemirror-commands": "^1.7.1",
182
193
  "prosemirror-keymap": "^1.2.3",
183
194
  "prosemirror-model": "^1.25.4",
@@ -189,6 +200,7 @@
189
200
  "react-dom": "19.2.4",
190
201
  "tsup": "^8.3.0",
191
202
  "tsx": "^4.21.0",
203
+ "y-prosemirror": "^1.3.7",
192
204
  "y-protocols": "^1.0.7",
193
205
  "yjs": "^13.6.30"
194
206
  },
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Typed shape carried in the Awareness `identity` field. One per
3
+ * connected client. Written by the local client on attach; read by
4
+ * every client to build `PresenceSnapshot`.
5
+ */
6
+ export interface AwarenessIdentity {
7
+ userId: string;
8
+ displayName: string;
9
+ collabIdentity?: string;
10
+ role: "author" | "reviewer" | "observer";
11
+ authorKind: "human" | "agent" | "system";
12
+ /** Identifier of the story the peer is currently looking at, if any. */
13
+ activeStoryId?: string;
14
+ }
15
+
16
+ export interface AwarenessPeer extends AwarenessIdentity {
17
+ /** `Awareness`-local client id. Advisory — hosts should prefer `userId`. */
18
+ clientId: number;
19
+ }
20
+
21
+ export type TransportStatus = "connected" | "syncing" | "offline";
22
+
23
+ export interface PresenceSnapshot {
24
+ peers: AwarenessPeer[];
25
+ transportStatus: TransportStatus;
26
+ /** Count of mutations queued locally while offline. */
27
+ queuedLocalEvents: number;
28
+ }
29
+
30
+ export interface CollabPosture {
31
+ role: "author" | "reviewer" | "observer";
32
+ transport: "none" | "attached";
33
+ /** Count of other connected peers (self excluded). */
34
+ peers: number;
35
+ }
@@ -0,0 +1,130 @@
1
+ export type CommentNegotiationState =
2
+ | "proposed"
3
+ | "negotiating"
4
+ | "accepted"
5
+ | "rejected"
6
+ | "resolved";
7
+
8
+ export interface NegotiationVote {
9
+ authorId: string;
10
+ verdict: "approve" | "reject" | "abstain";
11
+ castAt: string;
12
+ }
13
+
14
+ export interface NegotiationCounterProposal {
15
+ id: string;
16
+ authorId: string;
17
+ createdAt: string;
18
+ body: string;
19
+ proposedRangeEdit?: {
20
+ kind: "replace" | "insert" | "delete";
21
+ start: number;
22
+ end: number;
23
+ text?: string;
24
+ };
25
+ supersededBy?: string;
26
+ }
27
+
28
+ export type CommentNegotiationActionType =
29
+ | "propose-change"
30
+ | "counter-propose"
31
+ | "vote"
32
+ | "accept"
33
+ | "reject"
34
+ | "lock"
35
+ | "reopen";
36
+
37
+ export interface NegotiationHistoryRow {
38
+ from: CommentNegotiationState;
39
+ to: CommentNegotiationState;
40
+ actorId: string;
41
+ at: string;
42
+ action: CommentNegotiationActionType;
43
+ reasonCode?: string;
44
+ }
45
+
46
+ export interface CommentNegotiationEntry {
47
+ commentId: string;
48
+ state: CommentNegotiationState;
49
+ requiredApprovers: string[];
50
+ votes: NegotiationVote[];
51
+ counterProposals: NegotiationCounterProposal[];
52
+ acceptedProposalId?: string;
53
+ lockedAt?: string;
54
+ lockedBy?: string;
55
+ history: NegotiationHistoryRow[];
56
+ }
57
+
58
+ export interface CommentNegotiationSnapshot {
59
+ schemaVersion: 1;
60
+ entries: CommentNegotiationEntry[];
61
+ }
62
+
63
+ export type CommentNegotiationAction =
64
+ | {
65
+ type: "propose-change";
66
+ commentId: string;
67
+ actorId: string;
68
+ counterProposalId?: string;
69
+ }
70
+ | {
71
+ type: "counter-propose";
72
+ commentId: string;
73
+ authorId: string;
74
+ proposalId: string;
75
+ body: string;
76
+ createdAt: string;
77
+ proposedRangeEdit?: NegotiationCounterProposal["proposedRangeEdit"];
78
+ }
79
+ | {
80
+ type: "vote";
81
+ commentId: string;
82
+ authorId: string;
83
+ verdict: NegotiationVote["verdict"];
84
+ }
85
+ | {
86
+ type: "accept";
87
+ commentId: string;
88
+ actorId: string;
89
+ acceptedProposalId?: string;
90
+ }
91
+ | {
92
+ type: "reject";
93
+ commentId: string;
94
+ actorId: string;
95
+ reasonCode?: string;
96
+ }
97
+ | {
98
+ type: "lock";
99
+ commentId: string;
100
+ actorId: string;
101
+ }
102
+ | {
103
+ type: "reopen";
104
+ commentId: string;
105
+ actorId: string;
106
+ };
107
+
108
+ export type NegotiationRole = "author" | "reviewer" | "observer";
109
+
110
+ export type NegotiationBlockReason =
111
+ | "negotiation_invalid_transition"
112
+ | "negotiation_quorum_not_met"
113
+ | "collab_observer_readonly"
114
+ | "collab_role_restricted";
115
+
116
+ /**
117
+ * Superset of every `ok: false` reason the collab runtime surface can
118
+ * return. Kept in one place so hosts + agents can narrow once and
119
+ * handle every case.
120
+ *
121
+ * - `NegotiationBlockReason` — reducer-level role + state-machine checks.
122
+ * - `"metadata_tampered"` — raised by the tamper gate + external-send
123
+ * pipeline when `metadataIntegrity === "tampered"`.
124
+ * - `"collab_not_attached"` — raised by the session facet / bridge when
125
+ * callers dispatch before a `Y.Doc` is wired.
126
+ */
127
+ export type CollabBlockReason =
128
+ | NegotiationBlockReason
129
+ | "metadata_tampered"
130
+ | "collab_not_attached";
@@ -0,0 +1,106 @@
1
+ export type CommentAudience = "internal" | "external" | "shared";
2
+
3
+ export interface CommentMention {
4
+ userId: string;
5
+ displayName: string;
6
+ offsetInBody: number;
7
+ entryId?: string;
8
+ }
9
+
10
+ export interface CommentAttachment {
11
+ id: string;
12
+ kind: "image" | "file" | "link";
13
+ displayName: string;
14
+ mimeType?: string;
15
+ relationshipId?: string;
16
+ href?: string;
17
+ byteLength?: number;
18
+ width?: number;
19
+ height?: number;
20
+ }
21
+
22
+ export interface CommentReaction {
23
+ emoji: string;
24
+ authorId: string;
25
+ reactedAt: string;
26
+ }
27
+
28
+ export interface CommentLabel {
29
+ key: string;
30
+ text: string;
31
+ color?: string;
32
+ }
33
+
34
+ export interface CommentBody {
35
+ format: "markdown";
36
+ text: string;
37
+ digest: string;
38
+ sanitized?: boolean;
39
+ }
40
+
41
+ export interface CommentPresentationReply {
42
+ entryId: string;
43
+ body: CommentBody;
44
+ }
45
+
46
+ export interface CommentPresentation {
47
+ commentId: string;
48
+ audience: CommentAudience;
49
+ body: CommentBody;
50
+ replies: CommentPresentationReply[];
51
+ mentions: CommentMention[];
52
+ attachments: CommentAttachment[];
53
+ reactions: CommentReaction[];
54
+ labels: CommentLabel[];
55
+ }
56
+
57
+ export interface CommentPresentationSnapshot {
58
+ schemaVersion: 1;
59
+ entries: CommentPresentation[];
60
+ }
61
+
62
+ export type CommentPresentationAction =
63
+ | {
64
+ type: "set-body";
65
+ commentId: string;
66
+ text: string;
67
+ audience?: CommentAudience;
68
+ }
69
+ | {
70
+ type: "set-audience";
71
+ commentId: string;
72
+ audience: CommentAudience;
73
+ }
74
+ | {
75
+ type: "set-reply-body";
76
+ commentId: string;
77
+ entryId: string;
78
+ text: string;
79
+ }
80
+ | {
81
+ type: "add-mention";
82
+ commentId: string;
83
+ mention: CommentMention;
84
+ }
85
+ | {
86
+ type: "add-attachment";
87
+ commentId: string;
88
+ attachment: CommentAttachment;
89
+ }
90
+ | {
91
+ type: "remove-attachment";
92
+ commentId: string;
93
+ attachmentId: string;
94
+ }
95
+ | {
96
+ type: "toggle-reaction";
97
+ commentId: string;
98
+ authorId: string;
99
+ emoji: string;
100
+ now: string;
101
+ }
102
+ | {
103
+ type: "set-labels";
104
+ commentId: string;
105
+ labels: CommentLabel[];
106
+ };
@@ -0,0 +1,74 @@
1
+ import type {
2
+ CommentNegotiationEntry,
3
+ } from "./comment-negotiation-types.ts";
4
+ import type {
5
+ CommentPresentation,
6
+ } from "./comment-presentation-types.ts";
7
+ import type { Participant } from "./participants-types.ts";
8
+
9
+ /**
10
+ * Custody receipt attached to a docx when internal comments have been
11
+ * stripped out and sent to an external recipient (supplier). The receipt
12
+ * rides inside `bw:extensions/bw:externalCustody` and points at an
13
+ * opaque host archive that a later re-import uses to restore the
14
+ * stripped content.
15
+ *
16
+ * See docs/reference/bw-collab-schema-additions.md §bw:externalCustody
17
+ * for the normative schema.
18
+ */
19
+ export interface ExternalCustody {
20
+ schemaVersion: 1;
21
+ custodyId: string;
22
+ originDocumentId: string;
23
+ originPayloadId: string;
24
+ /** sha256:{hex} of canonicalized word/document.xml at send time. */
25
+ originContentHash: string;
26
+ sentAt: string;
27
+ sentBy: string;
28
+ /** Opaque recipient identifier (supplier user or orgId). */
29
+ recipient: string;
30
+ /**
31
+ * Opaque host-owned reference the resolver uses to fetch the
32
+ * stripped-out internal content on re-upload. Not interpreted by
33
+ * this library.
34
+ */
35
+ archiveRef: string;
36
+ strippedCommentIds: string[];
37
+ strippedParticipantIds: string[];
38
+ }
39
+
40
+ /**
41
+ * Payload handed to the host resolver when internal content is archived
42
+ * during `sendToExternal`. The host persists this opaque bundle keyed
43
+ * by `custodyId` for later `restore` calls.
44
+ */
45
+ export interface ExternalCustodyArchivePayload {
46
+ custodyId: string;
47
+ strippedPresentation: CommentPresentation[];
48
+ strippedNegotiation: CommentNegotiationEntry[];
49
+ strippedParticipants: Participant[];
50
+ originContentHash: string;
51
+ }
52
+
53
+ /**
54
+ * Content returned by the host resolver on `restore`. Same shape as
55
+ * what was archived, minus the custody ID.
56
+ */
57
+ export interface ExternalCustodyRestoredContent {
58
+ presentation: CommentPresentation[];
59
+ negotiation: CommentNegotiationEntry[];
60
+ participants: Participant[];
61
+ }
62
+
63
+ /**
64
+ * Host-owned contract. The editor hands archive + restore calls through
65
+ * this so the library never directly persists internal content.
66
+ */
67
+ export interface ExternalCustodyResolver {
68
+ archive(payload: ExternalCustodyArchivePayload): Promise<void>;
69
+ restore(args: {
70
+ custodyId: string;
71
+ originContentHash: string;
72
+ }): Promise<ExternalCustodyRestoredContent | undefined>;
73
+ delete?(custodyId: string): Promise<void>;
74
+ }
@@ -0,0 +1,18 @@
1
+ export type ParticipantRole = "author" | "reviewer" | "observer";
2
+ export type AuthorKind = "human" | "agent" | "system";
3
+
4
+ export interface Participant {
5
+ userId: string;
6
+ email: string;
7
+ displayName: string;
8
+ collabIdentity: string;
9
+ authorKind: AuthorKind;
10
+ role?: ParticipantRole;
11
+ organization?: string;
12
+ avatarHref?: string;
13
+ }
14
+
15
+ export interface ParticipantRoster {
16
+ schemaVersion: 1;
17
+ entries: Participant[];
18
+ }