@beyondwork/docx-react-component 1.0.41 → 1.0.43

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 (118) hide show
  1. package/package.json +38 -37
  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/editor-state-types.ts +110 -0
  6. package/src/api/external-custody-types.ts +74 -0
  7. package/src/api/participants-types.ts +18 -0
  8. package/src/api/public-types.ts +541 -5
  9. package/src/api/scope-metadata-resolver-types.ts +88 -0
  10. package/src/core/commands/formatting-commands.ts +1 -1
  11. package/src/core/commands/index.ts +601 -9
  12. package/src/core/search/search-text.ts +15 -2
  13. package/src/index.ts +131 -1
  14. package/src/io/docx-session.ts +672 -2
  15. package/src/io/export/escape-xml-attribute.ts +26 -0
  16. package/src/io/export/external-send.ts +188 -0
  17. package/src/io/export/serialize-comments.ts +13 -16
  18. package/src/io/export/serialize-footnotes.ts +17 -24
  19. package/src/io/export/serialize-headers-footers.ts +17 -24
  20. package/src/io/export/serialize-main-document.ts +59 -62
  21. package/src/io/export/serialize-numbering.ts +20 -27
  22. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  23. package/src/io/export/serialize-tables.ts +8 -15
  24. package/src/io/export/table-properties-xml.ts +25 -32
  25. package/src/io/import/external-reimport.ts +40 -0
  26. package/src/io/load-scheduler.ts +230 -0
  27. package/src/io/normalize/normalize-text.ts +83 -0
  28. package/src/io/ooxml/bw-xml.ts +244 -0
  29. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  30. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  31. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  32. package/src/io/ooxml/external-custody-payload.ts +102 -0
  33. package/src/io/ooxml/participants-payload.ts +97 -0
  34. package/src/io/ooxml/payload-signature.ts +112 -0
  35. package/src/io/ooxml/workflow-payload-validator.ts +367 -0
  36. package/src/io/ooxml/workflow-payload.ts +317 -7
  37. package/src/runtime/awareness-identity.ts +173 -0
  38. package/src/runtime/collab/event-types.ts +27 -0
  39. package/src/runtime/collab-session-bridge.ts +157 -0
  40. package/src/runtime/collab-session-facet.ts +193 -0
  41. package/src/runtime/collab-session.ts +273 -0
  42. package/src/runtime/comment-negotiation-sync.ts +91 -0
  43. package/src/runtime/comment-negotiation.ts +158 -0
  44. package/src/runtime/comment-presentation.ts +223 -0
  45. package/src/runtime/document-runtime.ts +639 -124
  46. package/src/runtime/editor-state-channel.ts +544 -0
  47. package/src/runtime/editor-state-integration.ts +217 -0
  48. package/src/runtime/external-send-runtime.ts +117 -0
  49. package/src/runtime/layout/docx-font-loader.ts +11 -30
  50. package/src/runtime/layout/index.ts +2 -0
  51. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  52. package/src/runtime/layout/layout-engine-instance.ts +139 -14
  53. package/src/runtime/layout/page-graph.ts +79 -7
  54. package/src/runtime/layout/paginated-layout-engine.ts +441 -48
  55. package/src/runtime/layout/public-facet.ts +585 -14
  56. package/src/runtime/layout/table-row-split.ts +316 -0
  57. package/src/runtime/markdown-sanitizer.ts +132 -0
  58. package/src/runtime/participants.ts +134 -0
  59. package/src/runtime/perf-counters.ts +28 -0
  60. package/src/runtime/render/render-frame-types.ts +17 -0
  61. package/src/runtime/render/render-kernel.ts +172 -29
  62. package/src/runtime/resign-payload.ts +120 -0
  63. package/src/runtime/surface-projection.ts +10 -5
  64. package/src/runtime/tamper-gate.ts +157 -0
  65. package/src/runtime/workflow-markup.ts +80 -16
  66. package/src/runtime/workflow-rail-segments.ts +244 -5
  67. package/src/ui/WordReviewEditor.tsx +654 -45
  68. package/src/ui/editor-command-bag.ts +14 -0
  69. package/src/ui/editor-runtime-boundary.ts +111 -11
  70. package/src/ui/editor-shell-view.tsx +21 -0
  71. package/src/ui/editor-surface-controller.tsx +5 -0
  72. package/src/ui/headless/selection-helpers.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  74. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  75. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  76. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  77. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  78. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  79. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  80. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  81. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  82. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  83. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  84. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  85. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  86. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  87. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  88. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  89. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  90. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
  91. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  92. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  93. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  94. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  95. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  96. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  97. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  98. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  99. package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
  100. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
  101. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  102. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  103. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
  104. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
  105. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  106. package/src/ui-tailwind/index.ts +37 -1
  107. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  108. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  109. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  110. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  111. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  112. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  113. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  114. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  115. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  116. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  117. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  118. package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
@@ -0,0 +1,158 @@
1
+ import type {
2
+ CommentNegotiationAction,
3
+ CommentNegotiationActionType,
4
+ CommentNegotiationEntry,
5
+ CommentNegotiationState,
6
+ NegotiationBlockReason,
7
+ NegotiationHistoryRow,
8
+ NegotiationRole,
9
+ } from "../api/comment-negotiation-types.ts";
10
+
11
+ export interface ReduceContext {
12
+ role: NegotiationRole;
13
+ now: string;
14
+ }
15
+
16
+ export type ReduceResult =
17
+ | { ok: true; entry: CommentNegotiationEntry }
18
+ | { ok: false; reason: NegotiationBlockReason };
19
+
20
+ export function createInitialEntry(commentId: string): CommentNegotiationEntry {
21
+ return {
22
+ commentId,
23
+ state: "proposed",
24
+ requiredApprovers: [],
25
+ votes: [],
26
+ counterProposals: [],
27
+ history: [],
28
+ };
29
+ }
30
+
31
+ const ALLOWED_BY_STATE: Record<
32
+ CommentNegotiationState,
33
+ ReadonlyArray<CommentNegotiationActionType>
34
+ > = {
35
+ proposed: ["propose-change", "counter-propose", "reject"],
36
+ negotiating: ["counter-propose", "vote", "accept", "reject"],
37
+ accepted: ["lock"],
38
+ rejected: ["lock"],
39
+ resolved: ["reopen"],
40
+ };
41
+
42
+ export function reduceNegotiation(
43
+ entry: CommentNegotiationEntry,
44
+ action: CommentNegotiationAction,
45
+ ctx: ReduceContext,
46
+ ): ReduceResult {
47
+ if (ctx.role === "observer") {
48
+ return { ok: false, reason: "collab_observer_readonly" };
49
+ }
50
+ if (
51
+ ctx.role === "reviewer" &&
52
+ (action.type === "accept" || action.type === "lock")
53
+ ) {
54
+ return { ok: false, reason: "collab_role_restricted" };
55
+ }
56
+ if (!ALLOWED_BY_STATE[entry.state].includes(action.type)) {
57
+ return { ok: false, reason: "negotiation_invalid_transition" };
58
+ }
59
+ if (action.type === "accept" && !quorumMet(entry)) {
60
+ return { ok: false, reason: "negotiation_quorum_not_met" };
61
+ }
62
+ return { ok: true, entry: applyAction(entry, action, ctx) };
63
+ }
64
+
65
+ export function quorumMet(entry: CommentNegotiationEntry): boolean {
66
+ if (entry.requiredApprovers.length > 0) {
67
+ return entry.requiredApprovers.every(
68
+ (userId) =>
69
+ entry.votes.find((v) => v.authorId === userId)?.verdict === "approve",
70
+ );
71
+ }
72
+ const approves = entry.votes.filter((v) => v.verdict === "approve").length;
73
+ const rejects = entry.votes.filter((v) => v.verdict === "reject").length;
74
+ return approves >= 1 && rejects === 0;
75
+ }
76
+
77
+ function applyAction(
78
+ entry: CommentNegotiationEntry,
79
+ action: CommentNegotiationAction,
80
+ ctx: ReduceContext,
81
+ ): CommentNegotiationEntry {
82
+ const from = entry.state;
83
+ let next: CommentNegotiationEntry = { ...entry };
84
+
85
+ switch (action.type) {
86
+ case "propose-change":
87
+ next.state = "negotiating";
88
+ break;
89
+
90
+ case "counter-propose": {
91
+ next.state = from === "proposed" ? "negotiating" : from;
92
+ next.counterProposals = [
93
+ ...entry.counterProposals,
94
+ {
95
+ id: action.proposalId,
96
+ authorId: action.authorId,
97
+ createdAt: action.createdAt,
98
+ body: action.body,
99
+ proposedRangeEdit: action.proposedRangeEdit,
100
+ },
101
+ ];
102
+ break;
103
+ }
104
+
105
+ case "vote":
106
+ next.votes = [
107
+ ...entry.votes.filter((v) => v.authorId !== action.authorId),
108
+ {
109
+ authorId: action.authorId,
110
+ verdict: action.verdict,
111
+ castAt: ctx.now,
112
+ },
113
+ ];
114
+ break;
115
+
116
+ case "accept":
117
+ next.state = "accepted";
118
+ next.acceptedProposalId = action.acceptedProposalId;
119
+ break;
120
+
121
+ case "reject":
122
+ next.state = "rejected";
123
+ break;
124
+
125
+ case "lock":
126
+ next.state = "resolved";
127
+ next.lockedAt = ctx.now;
128
+ next.lockedBy = action.actorId;
129
+ break;
130
+
131
+ case "reopen":
132
+ next.state = "negotiating";
133
+ next.lockedAt = undefined;
134
+ next.lockedBy = undefined;
135
+ break;
136
+ }
137
+
138
+ if (next.state !== from) {
139
+ const row: NegotiationHistoryRow = {
140
+ from,
141
+ to: next.state,
142
+ actorId: actorOf(action),
143
+ at: ctx.now,
144
+ action: action.type,
145
+ };
146
+ if (action.type === "reject" && action.reasonCode !== undefined) {
147
+ row.reasonCode = action.reasonCode;
148
+ }
149
+ next.history = [...entry.history, row];
150
+ }
151
+
152
+ return next;
153
+ }
154
+
155
+ function actorOf(action: CommentNegotiationAction): string {
156
+ if ("actorId" in action) return action.actorId;
157
+ return action.authorId;
158
+ }
@@ -0,0 +1,223 @@
1
+ import * as Y from "yjs";
2
+
3
+ import type {
4
+ CommentAudience,
5
+ CommentAttachment,
6
+ CommentBody,
7
+ CommentLabel,
8
+ CommentMention,
9
+ CommentPresentation,
10
+ CommentPresentationAction,
11
+ CommentPresentationReply,
12
+ CommentPresentationSnapshot,
13
+ CommentReaction,
14
+ } from "../api/comment-presentation-types.ts";
15
+ import { sanitizeMarkdown, sha256Hex } from "./markdown-sanitizer.ts";
16
+
17
+ const MAP_KEY = "commentPresentation";
18
+
19
+ export interface PresentationStoreHandle {
20
+ get(commentId: string): CommentPresentation | undefined;
21
+ snapshot(): CommentPresentationSnapshot;
22
+ apply(action: CommentPresentationAction): Promise<CommentPresentation>;
23
+ ingestRemote(entry: CommentPresentation): void;
24
+ subscribe(fn: (changedIds: string[]) => void): () => void;
25
+ destroy(): void;
26
+ }
27
+
28
+ export function createPresentationStore(ydoc: Y.Doc): PresentationStoreHandle {
29
+ const yMap = ydoc.getMap<CommentPresentation>(MAP_KEY);
30
+ const listeners = new Set<(ids: string[]) => void>();
31
+
32
+ const onChange = (event: Y.YMapEvent<CommentPresentation>): void => {
33
+ if (listeners.size === 0) return;
34
+ const ids = Array.from(event.keysChanged);
35
+ for (const fn of listeners) fn(ids);
36
+ };
37
+ yMap.observe(onChange);
38
+
39
+ const seed = (commentId: string): CommentPresentation =>
40
+ yMap.get(commentId) ?? {
41
+ commentId,
42
+ audience: "internal",
43
+ body: emptyBody(),
44
+ replies: [],
45
+ mentions: [],
46
+ attachments: [],
47
+ reactions: [],
48
+ labels: [],
49
+ };
50
+
51
+ return {
52
+ get: (id) => cloneMaybe(yMap.get(id)),
53
+ snapshot: () => ({
54
+ schemaVersion: 1,
55
+ entries: Array.from(yMap.values()).map(clone),
56
+ }),
57
+ ingestRemote: (entry) => {
58
+ yMap.set(entry.commentId, clone(entry));
59
+ },
60
+ subscribe: (fn) => {
61
+ listeners.add(fn);
62
+ return () => {
63
+ listeners.delete(fn);
64
+ };
65
+ },
66
+ destroy: () => {
67
+ yMap.unobserve(onChange);
68
+ listeners.clear();
69
+ },
70
+ async apply(action) {
71
+ const prev = seed(action.commentId);
72
+ const next = await reducePresentation(prev, action);
73
+ yMap.set(action.commentId, next);
74
+ return clone(next);
75
+ },
76
+ };
77
+ }
78
+
79
+ export async function reducePresentation(
80
+ prev: CommentPresentation,
81
+ action: CommentPresentationAction,
82
+ ): Promise<CommentPresentation> {
83
+ switch (action.type) {
84
+ case "set-body": {
85
+ const body = await buildBody(action.text);
86
+ const next: CommentPresentation = { ...prev, body };
87
+ if (action.audience) next.audience = action.audience;
88
+ return next;
89
+ }
90
+ case "set-audience":
91
+ return { ...prev, audience: action.audience };
92
+ case "set-reply-body": {
93
+ const body = await buildBody(action.text);
94
+ return { ...prev, replies: upsertReply(prev.replies, action.entryId, body) };
95
+ }
96
+ case "add-mention":
97
+ return { ...prev, mentions: [...prev.mentions, { ...action.mention }] };
98
+ case "add-attachment":
99
+ return {
100
+ ...prev,
101
+ attachments: [...prev.attachments, { ...action.attachment }],
102
+ };
103
+ case "remove-attachment":
104
+ return {
105
+ ...prev,
106
+ attachments: prev.attachments.filter((a) => a.id !== action.attachmentId),
107
+ };
108
+ case "toggle-reaction": {
109
+ const match = (r: CommentReaction): boolean =>
110
+ r.emoji === action.emoji && r.authorId === action.authorId;
111
+ if (prev.reactions.some(match)) {
112
+ return { ...prev, reactions: prev.reactions.filter((r) => !match(r)) };
113
+ }
114
+ return {
115
+ ...prev,
116
+ reactions: [
117
+ ...prev.reactions,
118
+ {
119
+ emoji: action.emoji,
120
+ authorId: action.authorId,
121
+ reactedAt: action.now,
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ case "set-labels":
127
+ return { ...prev, labels: action.labels.map((l) => ({ ...l })) };
128
+ }
129
+ }
130
+
131
+ async function buildBody(raw: string): Promise<CommentBody> {
132
+ const { text, sanitized } = sanitizeMarkdown(raw);
133
+ const digest = `sha256:${await sha256Hex(text)}`;
134
+ const body: CommentBody = { format: "markdown", text, digest };
135
+ if (sanitized) body.sanitized = true;
136
+ return body;
137
+ }
138
+
139
+ function emptyBody(): CommentBody {
140
+ return {
141
+ format: "markdown",
142
+ text: "",
143
+ digest:
144
+ "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
145
+ };
146
+ }
147
+
148
+ function upsertReply(
149
+ replies: CommentPresentationReply[],
150
+ entryId: string,
151
+ body: CommentBody,
152
+ ): CommentPresentationReply[] {
153
+ const idx = replies.findIndex((r) => r.entryId === entryId);
154
+ if (idx < 0) return [...replies, { entryId, body }];
155
+ const next = replies.slice();
156
+ next[idx] = { entryId, body };
157
+ return next;
158
+ }
159
+
160
+ function clone(entry: CommentPresentation): CommentPresentation {
161
+ return {
162
+ commentId: entry.commentId,
163
+ audience: entry.audience,
164
+ body: cloneBody(entry.body),
165
+ replies: entry.replies.map((r) => ({
166
+ entryId: r.entryId,
167
+ body: cloneBody(r.body),
168
+ })),
169
+ mentions: entry.mentions.map(cloneMention),
170
+ attachments: entry.attachments.map(cloneAttachment),
171
+ reactions: entry.reactions.map((r) => ({ ...r })),
172
+ labels: entry.labels.map(cloneLabel),
173
+ };
174
+ }
175
+
176
+ function cloneMaybe(
177
+ entry: CommentPresentation | undefined,
178
+ ): CommentPresentation | undefined {
179
+ return entry ? clone(entry) : undefined;
180
+ }
181
+
182
+ function cloneBody(body: CommentBody): CommentBody {
183
+ const copy: CommentBody = {
184
+ format: body.format,
185
+ text: body.text,
186
+ digest: body.digest,
187
+ };
188
+ if (body.sanitized) copy.sanitized = true;
189
+ return copy;
190
+ }
191
+
192
+ function cloneMention(mention: CommentMention): CommentMention {
193
+ const copy: CommentMention = {
194
+ userId: mention.userId,
195
+ displayName: mention.displayName,
196
+ offsetInBody: mention.offsetInBody,
197
+ };
198
+ if (mention.entryId !== undefined) copy.entryId = mention.entryId;
199
+ return copy;
200
+ }
201
+
202
+ function cloneAttachment(a: CommentAttachment): CommentAttachment {
203
+ const copy: CommentAttachment = {
204
+ id: a.id,
205
+ kind: a.kind,
206
+ displayName: a.displayName,
207
+ };
208
+ if (a.mimeType !== undefined) copy.mimeType = a.mimeType;
209
+ if (a.relationshipId !== undefined) copy.relationshipId = a.relationshipId;
210
+ if (a.href !== undefined) copy.href = a.href;
211
+ if (a.byteLength !== undefined) copy.byteLength = a.byteLength;
212
+ if (a.width !== undefined) copy.width = a.width;
213
+ if (a.height !== undefined) copy.height = a.height;
214
+ return copy;
215
+ }
216
+
217
+ function cloneLabel(label: CommentLabel): CommentLabel {
218
+ const copy: CommentLabel = { key: label.key, text: label.text };
219
+ if (label.color !== undefined) copy.color = label.color;
220
+ return copy;
221
+ }
222
+
223
+ export type { CommentAudience };