@beyondwork/docx-react-component 1.0.43 → 1.0.46
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 +35 -1
- package/package.json +44 -32
- package/src/api/public-types.ts +156 -3
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +27 -2
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +16 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +21 -1
- 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/normalize/normalize-text.ts +33 -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/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/document-runtime.ts +351 -25
- 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-surface/capabilities.ts +411 -0
- package/src/runtime/event-refresh-hints.ts +1 -0
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +46 -0
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/public-facet.ts +30 -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/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +35 -2
- package/src/ui/WordReviewEditor.tsx +75 -192
- package/src/ui/editor-runtime-boundary.ts +5 -1
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +23 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +38 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +131 -29
|
@@ -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
|
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
incrementInvalidationCounter,
|
|
53
53
|
recordPerfSample,
|
|
54
54
|
} from "./editor-surface/perf-probe.ts";
|
|
55
|
+
import { useVisibleBlockRange } from "./page-stack/use-visible-block-range.ts";
|
|
55
56
|
import { sliceBlocksForPage } from "./editor-surface/page-slice-util.ts";
|
|
56
57
|
import { TwPageBlockView } from "./editor-surface/tw-page-block-view.tsx";
|
|
57
58
|
import { computeLineMarkersIfEnabled } from "./page-chrome-model.ts";
|
|
@@ -895,6 +896,101 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
895
896
|
);
|
|
896
897
|
}, [reviewRailAvailable, viewportWidth]);
|
|
897
898
|
|
|
899
|
+
// L7 Phase 2 Task 2.2.4a — viewport-scroll wiring.
|
|
900
|
+
// Collect DOM elements with `[data-page-frame]` from the PM surface so the
|
|
901
|
+
// IntersectionObserver inside `useVisibleBlockRange` can determine which
|
|
902
|
+
// pages are currently visible. The MutationObserver refreshes the set when
|
|
903
|
+
// the PM surface re-renders (e.g. a document load changes the page count).
|
|
904
|
+
const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
|
|
905
|
+
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
const root = pageStackScrollRoot;
|
|
908
|
+
if (!root) {
|
|
909
|
+
setPageMarkers([]);
|
|
910
|
+
return undefined;
|
|
911
|
+
}
|
|
912
|
+
const refresh = () => {
|
|
913
|
+
const found = Array.from(root.querySelectorAll<HTMLElement>("[data-page-frame]"));
|
|
914
|
+
// The boundary widgets between pages N and N+1 carry `data-page-frame`
|
|
915
|
+
// for the *next* page (pages 1 … N). Page 0 has no widget before it,
|
|
916
|
+
// so we synthesize an in-memory element that carries page-0's attributes.
|
|
917
|
+
// This element is NOT in the DOM — the IntersectionObserver won't fire
|
|
918
|
+
// on it — but the hook's useMemo uses it to look up block indices when
|
|
919
|
+
// the overscan expansion includes page 0 (which happens whenever any of
|
|
920
|
+
// pages 1-2 are visible with overscan ≥ 1).
|
|
921
|
+
const hasPage0 = found.some(
|
|
922
|
+
(el) => el.getAttribute("data-page-frame") === "0",
|
|
923
|
+
);
|
|
924
|
+
if (!hasPage0 && found.length > 0) {
|
|
925
|
+
// Derive page-0 block range from the snapshot surface: page 0 starts
|
|
926
|
+
// at block 0 and ends just before the first block that belongs to page 1.
|
|
927
|
+
// The boundary widget's `data-page-first-block-index` for page 1 tells
|
|
928
|
+
// us where page 0 ends.
|
|
929
|
+
const page1Marker = found.find(
|
|
930
|
+
(el) => el.getAttribute("data-page-frame") === "1",
|
|
931
|
+
);
|
|
932
|
+
const page1First = page1Marker
|
|
933
|
+
? Number(page1Marker.getAttribute("data-page-first-block-index") ?? "")
|
|
934
|
+
: NaN;
|
|
935
|
+
const page0Last = Number.isFinite(page1First) && page1First > 0
|
|
936
|
+
? page1First - 1
|
|
937
|
+
: -1; // page 0 empty or unknown; synthetic marker contributes nothing.
|
|
938
|
+
const ownerDoc = found[0]!.ownerDocument;
|
|
939
|
+
const synth = ownerDoc.createElement("span");
|
|
940
|
+
synth.setAttribute("data-page-frame", "0");
|
|
941
|
+
synth.setAttribute("data-page-first-block-index", "0");
|
|
942
|
+
synth.setAttribute("data-page-last-block-index", String(Math.max(0, page0Last)));
|
|
943
|
+
setPageMarkers([synth, ...found]);
|
|
944
|
+
} else {
|
|
945
|
+
setPageMarkers(found);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
refresh();
|
|
949
|
+
// Observe PM surface mutations for page-count changes (new doc load, etc).
|
|
950
|
+
const view = root.ownerDocument?.defaultView;
|
|
951
|
+
if (!view?.MutationObserver) return undefined;
|
|
952
|
+
const mo = new view.MutationObserver(refresh);
|
|
953
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
954
|
+
return () => mo.disconnect();
|
|
955
|
+
// Re-run when the scroll root changes OR when a new snapshot lands
|
|
956
|
+
// (which may have a different page count and new widgets in the PM DOM).
|
|
957
|
+
// NOTE: snapshot.surface is intentionally excluded — its reference changes on
|
|
958
|
+
// every requestViewportRefresh(), which would add two extra render passes per
|
|
959
|
+
// scroll event. The page-0 fallback uses -1 when page1First is unknown,
|
|
960
|
+
// which is correct (no page-0 blocks → synthetic marker contributes nothing).
|
|
961
|
+
}, [pageStackScrollRoot, snapshot.revisionToken]);
|
|
962
|
+
|
|
963
|
+
// Derive the surface block index for the current selection head so the
|
|
964
|
+
// hook can extend the visible range to always include the selection.
|
|
965
|
+
const selectionBlockIndex = useMemo(() => {
|
|
966
|
+
const sel = snapshot.selection;
|
|
967
|
+
const blocks = snapshot.surface?.blocks;
|
|
968
|
+
if (!sel || !blocks) return null;
|
|
969
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
970
|
+
const block = blocks[i]!; // from/to are required on all SurfaceBlockSnapshot variants
|
|
971
|
+
const blockFrom = block.from;
|
|
972
|
+
const blockTo = block.to;
|
|
973
|
+
if (sel.head >= blockFrom && sel.head <= blockTo) return i;
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}, [snapshot.selection, snapshot.surface]);
|
|
977
|
+
|
|
978
|
+
const visibleBlockRange = useVisibleBlockRange({
|
|
979
|
+
pageMarkers,
|
|
980
|
+
overscanPages: 2,
|
|
981
|
+
selectionBlockIndex,
|
|
982
|
+
totalBlockCount: snapshot.surface?.blocks.length ?? 0,
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
// Push the visible range into the layout facet (which delegates to the
|
|
986
|
+
// runtime's viewport-culling machinery). Depend on `[start, end]` values
|
|
987
|
+
// (not the range object) so identity-preserving updates are a no-op.
|
|
988
|
+
useEffect(() => {
|
|
989
|
+
if (!props.layoutFacet) return;
|
|
990
|
+
props.layoutFacet.setVisibleBlockRange(visibleBlockRange);
|
|
991
|
+
props.layoutFacet.requestViewportRefresh();
|
|
992
|
+
}, [props.layoutFacet, visibleBlockRange.start, visibleBlockRange.end]);
|
|
993
|
+
|
|
898
994
|
const dismissSelectionToolbar = useCallback(() => {
|
|
899
995
|
props.onDismissSelectionToolbar?.();
|
|
900
996
|
}, [props.onDismissSelectionToolbar]);
|
|
@@ -1222,32 +1318,13 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1222
1318
|
ref={selectionToolbarRootRef}
|
|
1223
1319
|
className={`mx-auto min-h-full w-full ${
|
|
1224
1320
|
isPageWorkspace
|
|
1225
|
-
? "wre-page-chrome
|
|
1321
|
+
? "wre-page-chrome relative overflow-hidden"
|
|
1226
1322
|
: "wre-canvas-surface relative my-8 overflow-hidden"
|
|
1227
1323
|
}`}
|
|
1228
1324
|
data-zoom-bucket={pageZoomBucket}
|
|
1229
1325
|
data-zoom-scale={isPageWorkspace ? zoomScale : undefined}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
? {
|
|
1233
|
-
// P2.a — real-dim page frame: width/height from
|
|
1234
|
-
// `pageLayout.pageWidth/pageHeight × FRAME_PX_PER_TWIP_AT_96DPI`
|
|
1235
|
-
// so every paper size renders at its
|
|
1236
|
-
// Word-matching CSS px. `max-w-[840px]` retired.
|
|
1237
|
-
...(pageShellMetrics.frameWidthPx
|
|
1238
|
-
? { width: `${pageShellMetrics.frameWidthPx}px` }
|
|
1239
|
-
: {}),
|
|
1240
|
-
...(pageShellMetrics.frameHeightPx
|
|
1241
|
-
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1242
|
-
: {}),
|
|
1243
|
-
// P2.b — browser-native CSS `zoom` rescales layout
|
|
1244
|
-
// so `getBoundingClientRect()` and hit-test offsets
|
|
1245
|
-
// stay truthful at any zoom — no inverse-projection
|
|
1246
|
-
// math needed downstream.
|
|
1247
|
-
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1248
|
-
}
|
|
1249
|
-
: undefined
|
|
1250
|
-
}
|
|
1326
|
+
data-workspace-canvas={isPageWorkspace ? "true" : undefined}
|
|
1327
|
+
data-workspace-mode={isPageWorkspace ? "page" : "canvas"}
|
|
1251
1328
|
>
|
|
1252
1329
|
{isPageWorkspace && chromeVisibility.pageChrome && snapshot.pageLayout ? (
|
|
1253
1330
|
<div className="border-b border-border/70 bg-surface/65 px-5 py-3" data-testid="page-context-summary">
|
|
@@ -1448,8 +1525,38 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1448
1525
|
/>
|
|
1449
1526
|
) : null}
|
|
1450
1527
|
<div
|
|
1451
|
-
className={
|
|
1528
|
+
className={
|
|
1529
|
+
isPageWorkspace
|
|
1530
|
+
? "wre-page-surface relative mx-auto my-8"
|
|
1531
|
+
: "relative"
|
|
1532
|
+
}
|
|
1452
1533
|
data-line-numbering={pageChromeModel.lineNumberingEnabled ? "enabled" : "disabled"}
|
|
1534
|
+
data-paper-frame={isPageWorkspace ? "true" : undefined}
|
|
1535
|
+
data-debug-page-layout={
|
|
1536
|
+
isPageWorkspace && snapshot.pageLayout
|
|
1537
|
+
? `${snapshot.pageLayout.pageWidth}:${snapshot.pageLayout.pageHeight}:${snapshot.pageLayout.orientation}:${snapshot.pageLayout.sectionIndex}`
|
|
1538
|
+
: undefined
|
|
1539
|
+
}
|
|
1540
|
+
style={
|
|
1541
|
+
isPageWorkspace
|
|
1542
|
+
? {
|
|
1543
|
+
// Phase A (L8 page-native layout): the paper frame
|
|
1544
|
+
// owns paper width/height + browser-native CSS
|
|
1545
|
+
// `zoom` so layout measurement stays truthful
|
|
1546
|
+
// inside the card. `pageShellMetrics.pageFrameStyle`
|
|
1547
|
+
// carries the paper chrome (bg + shadow + rounded +
|
|
1548
|
+
// border) — painted exactly once.
|
|
1549
|
+
...(pageShellMetrics.frameWidthPx
|
|
1550
|
+
? { width: `${pageShellMetrics.frameWidthPx}px` }
|
|
1551
|
+
: {}),
|
|
1552
|
+
...(pageShellMetrics.frameHeightPx
|
|
1553
|
+
? { minHeight: `${pageShellMetrics.frameHeightPx}px` }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(zoomScale !== 1 ? { zoom: zoomScale } : {}),
|
|
1556
|
+
...pageShellMetrics.pageFrameStyle,
|
|
1557
|
+
}
|
|
1558
|
+
: undefined
|
|
1559
|
+
}
|
|
1453
1560
|
>
|
|
1454
1561
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
|
|
1455
1562
|
<div
|
|
@@ -1477,12 +1584,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1477
1584
|
className={isPageWorkspace ? "relative" : undefined}
|
|
1478
1585
|
data-document-grid={pageChromeModel.documentGridType}
|
|
1479
1586
|
data-page-border-display={pageChromeModel.pageBorderDisplay}
|
|
1480
|
-
style={
|
|
1481
|
-
? {
|
|
1482
|
-
...pageChromeModel.documentGridStyle,
|
|
1483
|
-
...pageShellMetrics.pageFrameStyle,
|
|
1484
|
-
}
|
|
1485
|
-
: pageChromeModel.documentGridStyle}
|
|
1587
|
+
style={pageChromeModel.documentGridStyle}
|
|
1486
1588
|
>
|
|
1487
1589
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.showPageBorder && !hidePageBorderForActiveEditing ? (
|
|
1488
1590
|
<div
|