@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.
- package/README.md +17 -0
- package/package.json +5 -4
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +333 -4
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +60 -10
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/search/search-text.ts +15 -2
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +29 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +692 -2
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +116 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +661 -42
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +63 -2
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +430 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +45 -7
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +142 -237
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +115 -12
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- 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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
/*
|
|
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-
|
|
259
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
738
|
-
|
|
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
|
|