@beyondwork/docx-react-component 1.0.42 → 1.0.45

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 (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -1,12 +1,33 @@
1
- import React, { useCallback, useEffect, useRef, useState } from "react";
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Check, CornerDownRight, RotateCcw } from "lucide-react";
3
3
 
4
4
  import type { CommentSidebarSnapshot, CommentSidebarThreadSnapshot } from "../../api/public-types";
5
+ import type {
6
+ CommentPresentation,
7
+ CommentPresentationReply,
8
+ CommentPresentationSnapshot,
9
+ } from "../../api/comment-presentation-types";
10
+ import { CommentMarkdownRenderer } from "./comment-markdown-renderer";
5
11
 
6
12
  export interface TwCommentSidebarProps {
7
13
  comments: CommentSidebarSnapshot;
8
14
  activeCommentId?: string;
9
15
  currentUserId?: string;
16
+ /**
17
+ * Optional per-comment rich presentation (markdown body + mentions +
18
+ * attachments + replies). When a thread's commentId matches an entry here
19
+ * we render its body through {@link CommentMarkdownRenderer} instead of
20
+ * the flat plaintext fallback. Threads without a presentation entry keep
21
+ * the legacy plaintext render — required for back-compat with docx
22
+ * authored outside BW.
23
+ */
24
+ commentPresentations?: CommentPresentationSnapshot;
25
+ /**
26
+ * Forwarded to the markdown renderer to resolve attachment
27
+ * `relationshipId` refs against `word/_rels/` image blobs. Hosts that do
28
+ * not render inline images can omit this.
29
+ */
30
+ resolveAttachmentHref?: (relationshipId: string) => string | undefined;
10
31
  onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
11
32
  onResolveComment?: (commentId: string) => void;
12
33
  onReopenComment?: (commentId: string) => void;
@@ -18,7 +39,15 @@ const focusRingClass =
18
39
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
19
40
 
20
41
  export function TwCommentSidebar(props: TwCommentSidebarProps) {
21
- const { comments, activeCommentId, currentUserId } = props;
42
+ const { comments, activeCommentId, currentUserId, commentPresentations, resolveAttachmentHref } = props;
43
+
44
+ const presentationByCommentId = useMemo(() => {
45
+ const map = new Map<string, CommentPresentation>();
46
+ for (const entry of commentPresentations?.entries ?? []) {
47
+ map.set(entry.commentId, entry);
48
+ }
49
+ return map;
50
+ }, [commentPresentations]);
22
51
 
23
52
  return (
24
53
  <div className="outline-none">
@@ -41,6 +70,8 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
41
70
  thread={thread}
42
71
  isActive={activeCommentId === thread.commentId}
43
72
  currentUserId={currentUserId}
73
+ presentation={presentationByCommentId.get(thread.commentId)}
74
+ resolveAttachmentHref={resolveAttachmentHref}
44
75
  onOpenComment={props.onOpenComment}
45
76
  onResolveComment={props.onResolveComment}
46
77
  onReopenComment={props.onReopenComment}
@@ -62,13 +93,22 @@ function CommentThreadCard(props: {
62
93
  thread: CommentSidebarThreadSnapshot;
63
94
  isActive: boolean;
64
95
  currentUserId?: string;
96
+ presentation?: CommentPresentation;
97
+ resolveAttachmentHref?: (relationshipId: string) => string | undefined;
65
98
  onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
66
99
  onResolveComment?: (commentId: string) => void;
67
100
  onReopenComment?: (commentId: string) => void;
68
101
  onAddReply?: (commentId: string, body: string) => void;
69
102
  onEditBody?: (commentId: string, body: string) => void;
70
103
  }) {
71
- const { thread, isActive } = props;
104
+ const { thread, isActive, presentation, resolveAttachmentHref } = props;
105
+ const replyPresentationByEntryId = useMemo(() => {
106
+ const map = new Map<string, CommentPresentationReply>();
107
+ for (const reply of presentation?.replies ?? []) {
108
+ map.set(reply.entryId, reply);
109
+ }
110
+ return map;
111
+ }, [presentation]);
72
112
  const leadEntry = thread.entries[0];
73
113
  const isDraftThread = thread.status === "open" && thread.entryCount === 1 && isEmptyCommentBody(leadEntry?.body);
74
114
  const isOwnComment = props.currentUserId != null && leadEntry?.authorId === props.currentUserId;
@@ -138,6 +178,14 @@ function CommentThreadCard(props: {
138
178
  onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
139
179
  label={isDraftThread ? "New comment" : undefined}
140
180
  />
181
+ ) : presentation ? (
182
+ <CommentMarkdownRenderer
183
+ body={presentation.body}
184
+ mentions={presentation.mentions}
185
+ attachments={presentation.attachments}
186
+ resolveAttachmentHref={resolveAttachmentHref}
187
+ className="text-[10px] leading-[1.1rem] text-secondary break-words"
188
+ />
141
189
  ) : leadEntry?.body ? (
142
190
  <p
143
191
  className="text-[10px] leading-[1.1rem] text-secondary whitespace-pre-wrap break-words line-clamp-4"
@@ -158,20 +206,33 @@ function CommentThreadCard(props: {
158
206
  ) : null}
159
207
 
160
208
  {/* Reply entries (compact) */}
161
- {thread.entries.slice(1).map((entry) => (
162
- <div key={entry.entryId} className="mt-2 ml-4 border-l border-border/50 pl-2.5">
163
- <div className="mb-0.5 flex items-center gap-1">
164
- <span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
165
- <span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
209
+ {thread.entries.slice(1).map((entry) => {
210
+ const replyPresentation = replyPresentationByEntryId.get(entry.entryId);
211
+ return (
212
+ <div key={entry.entryId} className="mt-2 ml-4 border-l border-border/50 pl-2.5">
213
+ <div className="mb-0.5 flex items-center gap-1">
214
+ <span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
215
+ <span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
216
+ </div>
217
+ {replyPresentation ? (
218
+ <CommentMarkdownRenderer
219
+ body={replyPresentation.body}
220
+ mentions={presentation?.mentions}
221
+ attachments={presentation?.attachments}
222
+ resolveAttachmentHref={resolveAttachmentHref}
223
+ className="text-[10px] leading-4 text-secondary break-words"
224
+ />
225
+ ) : (
226
+ <p
227
+ className="text-[10px] leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-3"
228
+ data-comment-reply-body="true"
229
+ >
230
+ {entry.body}
231
+ </p>
232
+ )}
166
233
  </div>
167
- <p
168
- className="text-[10px] leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-3"
169
- data-comment-reply-body="true"
170
- >
171
- {entry.body}
172
- </p>
173
- </div>
174
- ))}
234
+ );
235
+ })}
175
236
 
176
237
  {thread.entryCount > thread.entries.length ? (
177
238
  <p className="mt-1 text-[9px] text-tertiary">
@@ -77,6 +77,18 @@
77
77
  --color-page-border: rgba(0, 0, 0, 0.04);
78
78
  --color-page-bg: #ffffff;
79
79
  --color-page-ruler: color-mix(in srgb, var(--color-border) 65%, transparent);
80
+ /*
81
+ * Phase A (L8 page-native layout): the workspace canvas is the gray
82
+ * surface BEHIND the paper frame(s) — LibreOffice Print Layout idiom.
83
+ * The outer `.wre-page-chrome` wrapper paints this; the inner paper
84
+ * frame paints the white `--color-page-bg` on top.
85
+ *
86
+ * L8 polish (2026-04-19): darkened from #e7e5e4 → #d4d1cc so the canvas
87
+ * reads as a distinctly different surface from the white paper (the
88
+ * earlier tone was close enough that the paper edges blurred against
89
+ * the canvas on high-brightness screens).
90
+ */
91
+ --color-workspace-canvas: #d4d1cc;
80
92
 
81
93
  /*
82
94
  * ─── Radius tokens (balanced 10 / 8 / 4 / 2) ───
@@ -188,6 +200,7 @@
188
200
  --color-page-border: rgba(255, 255, 255, 0.06);
189
201
  --color-page-bg: #1B2620;
190
202
  --color-page-ruler: color-mix(in srgb, var(--color-border) 70%, transparent);
203
+ --color-workspace-canvas: #111827;
191
204
 
192
205
  --shadow-soft: 0 6px 18px -10px rgba(0, 0, 0, 0.55);
193
206
  --shadow-float: 0 18px 40px -22px rgba(0, 0, 0, 0.7);
@@ -253,26 +266,39 @@
253
266
  widows: 2;
254
267
  }
255
268
 
256
- /* Page chrome — shadow, border, and background for the page-mode document panel */
269
+ /*
270
+ * Phase A (L8 page-native layout): the outer `.wre-page-chrome` wrapper is
271
+ * now the **workspace canvas** — the gray surface behind the paper frame(s),
272
+ * mirroring LibreOffice Writer's Print Layout. Paper styling (white
273
+ * background, rounded corners, drop shadow, border) lives solely on the
274
+ * inner `[data-paper-frame]` wrapper via `pageShellMetrics.pageFrameStyle`,
275
+ * eliminating the pre-Phase-A double-chrome artefact.
276
+ *
277
+ * Historical note: before 2026-04-19 this selector painted paper chrome
278
+ * (background/border/radius/shadow) AND the outer wrapper was sized to
279
+ * `frameWidthPx`. The inner wrapper painted the SAME paper chrome, producing
280
+ * two nested rounded rectangles visible in every page-mode screenshot.
281
+ */
257
282
  .wre-page-chrome {
258
- background: var(--color-page-bg);
259
- border: 1px solid var(--color-page-border);
260
- border-radius: var(--radius-page);
261
- box-shadow: 0 8px 24px -20px var(--color-page-shadow);
283
+ background: var(--color-workspace-canvas);
284
+ padding: 2rem 0;
262
285
  }
263
286
 
264
- /* Canvas-mode typography — lighter, review-first baseline */
287
+ /*
288
+ * Canvas-mode typography — lighter, review-first baseline.
289
+ *
290
+ * L8 Phase B (2026-04-19): canvas mode is **continuous flow, no paper
291
+ * card**. Paper-card declarations (width cap, min-height, background,
292
+ * border, rounded corners, drop shadow) were removed so the review
293
+ * surface reads as a single scrollable column. The mode-contract gate
294
+ * (page-mode paper card vs canvas-mode continuous flow) is pinned by
295
+ * `test/ui/pm-page-break-decorations-posture.test.ts`.
296
+ */
265
297
  .wre-canvas-surface {
266
298
  font-family: var(--font-legal-serif);
267
299
  font-size: 15px;
268
300
  line-height: 1.6;
269
301
  color: var(--color-primary);
270
- width: min(100%, 920px);
271
- min-height: 100%;
272
- background: var(--color-page-bg);
273
- border: 1px solid var(--color-page-border);
274
- border-radius: var(--radius-page);
275
- box-shadow: 0 6px 18px -14px var(--color-page-shadow);
276
302
  -webkit-font-smoothing: antialiased;
277
303
  -moz-osx-font-smoothing: grayscale;
278
304
  text-rendering: optimizeLegibility;
@@ -734,8 +760,15 @@
734
760
  pointer-events: none;
735
761
  }
736
762
 
737
- /* ─── Page workspace zoom scaling ─── */
738
- .wre-page-chrome[style*="scale"] {
763
+ /*
764
+ * ─── Paper-frame zoom scaling ───
765
+ *
766
+ * Phase A (L8 page-native layout): browser-native CSS `zoom` lives on the
767
+ * inner paper frame, not the workspace canvas, so layout measurement stays
768
+ * truthful inside the card. `will-change: transform` primes the compositor
769
+ * when a zoom is active.
770
+ */
771
+ [data-paper-frame][style*="zoom"] {
739
772
  will-change: transform;
740
773
  }
741
774