@beyondwork/docx-react-component 1.0.86 → 1.0.87
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/package.json +1 -1
- package/src/api/public-types.ts +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +32 -3
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -0
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "./pm-command-bridge";
|
|
52
52
|
import { buildDecorations } from "./pm-decorations";
|
|
53
53
|
import { buildPageBreakDecorations } from "./pm-page-break-decorations";
|
|
54
|
+
import { findBlockIndexRangeForPage } from "./page-slice-util.ts";
|
|
54
55
|
import { DecorationSet } from "prosemirror-view";
|
|
55
56
|
import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
|
|
56
57
|
import { buildPagePreviewMaps } from "../../api/public-types";
|
|
@@ -63,7 +64,10 @@ import {
|
|
|
63
64
|
} from "./perf-probe";
|
|
64
65
|
import { buildPositionMap, type PositionMap } from "./pm-position-map";
|
|
65
66
|
import { createLocalEditSessionState } from "./local-edit-session-state";
|
|
66
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
createFastTextEditLane,
|
|
69
|
+
getTextCommandRefreshClass,
|
|
70
|
+
} from "./fast-text-edit-lane";
|
|
67
71
|
import { createPredictedTxGate } from "./predicted-tx-gate";
|
|
68
72
|
import { replaceStatePreservingPosition } from "./preserve-position";
|
|
69
73
|
import {
|
|
@@ -148,9 +152,12 @@ function buildPageBreakDecorationsFromProps(
|
|
|
148
152
|
: undefined;
|
|
149
153
|
|
|
150
154
|
// L7 Phase 2 Task 2.2.4a — compute per-page block-index ranges from the
|
|
151
|
-
// render frame's page offsets + the surface blocks list.
|
|
152
|
-
// `from`/`to` offset;
|
|
153
|
-
//
|
|
155
|
+
// render frame's page offsets + the surface blocks list. Each block has a
|
|
156
|
+
// `from`/`to` offset; a block belongs to every page whose offset window it
|
|
157
|
+
// overlaps. That matters for large tables/objects that can straddle a page
|
|
158
|
+
// boundary: matching only `block.from` would omit the active block from the
|
|
159
|
+
// page marker, cull it on the next viewport refresh, and remap the caret to
|
|
160
|
+
// the wrong PM position during snapshot replacement.
|
|
154
161
|
// This map is passed into `buildPageBreakDecorations` so the chrome widgets
|
|
155
162
|
// carry `data-page-first-block-index` / `data-page-last-block-index`
|
|
156
163
|
// attributes needed by `useVisibleBlockRange`.
|
|
@@ -160,24 +167,9 @@ function buildPageBreakDecorationsFromProps(
|
|
|
160
167
|
for (let pi = 0; pi < frame.pages.length; pi++) {
|
|
161
168
|
const page = frame.pages[pi]!;
|
|
162
169
|
if (page.page.isBlankFiller) continue;
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
? frame.pages[pi + 1]!.page.startOffset
|
|
167
|
-
: Infinity;
|
|
168
|
-
let first = -1;
|
|
169
|
-
let last = -1;
|
|
170
|
-
for (let bi = 0; bi < surfaceBlocks.length; bi++) {
|
|
171
|
-
const block = surfaceBlocks[bi]!;
|
|
172
|
-
const blockFrom = block.from; // from is required on all SurfaceBlockSnapshot variants
|
|
173
|
-
// Block belongs to this page if its start falls within the page's offset window.
|
|
174
|
-
if (blockFrom >= pageStart && blockFrom < pageEnd) {
|
|
175
|
-
if (first === -1) first = bi;
|
|
176
|
-
last = bi;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (first !== -1) {
|
|
180
|
-
blockIndexRangeByPageIndex.set(page.page.pageIndex, { first, last });
|
|
170
|
+
const range = findBlockIndexRangeForPage(surfaceBlocks, page.page);
|
|
171
|
+
if (range) {
|
|
172
|
+
blockIndexRangeByPageIndex.set(page.page.pageIndex, range);
|
|
181
173
|
}
|
|
182
174
|
}
|
|
183
175
|
}
|
|
@@ -290,6 +282,7 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
290
282
|
}) => void;
|
|
291
283
|
onCommentActivated?: (commentId: string) => void;
|
|
292
284
|
onRevisionActivated?: (revisionId: string) => void;
|
|
285
|
+
onRevisionHovered?: (revisionId: string | null) => void;
|
|
293
286
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
294
287
|
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
295
288
|
workflowScopes?: readonly WorkflowScope[];
|
|
@@ -441,7 +434,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
441
434
|
const suppressSelectionEchoRef = useRef(false);
|
|
442
435
|
const sessionRef = useRef<import("./local-edit-session-state").LocalEditSessionState | null>(null);
|
|
443
436
|
const laneRef = useRef<import("./fast-text-edit-lane").FastTextEditLane | null>(null);
|
|
444
|
-
const
|
|
437
|
+
const equivalentAckLedgerRef = useRef<Map<string, string>>(new Map());
|
|
445
438
|
const selectionToolbarFrameRef = useRef<number | null>(null);
|
|
446
439
|
const lastSelectionToolbarMeasurementRef = useRef<{
|
|
447
440
|
key: string | null;
|
|
@@ -653,10 +646,11 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
653
646
|
createContextualInteractionPlugin({
|
|
654
647
|
onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
|
|
655
648
|
onRevisionActivated: (revisionId) => props.onRevisionActivated?.(revisionId),
|
|
649
|
+
onRevisionHovered: (revisionId) => props.onRevisionHovered?.(revisionId),
|
|
656
650
|
}),
|
|
657
651
|
createSearchPlugin(),
|
|
658
652
|
];
|
|
659
|
-
}, [props.awareness, props.onCommentActivated, props.onRevisionActivated]);
|
|
653
|
+
}, [props.awareness, props.onCommentActivated, props.onRevisionActivated, props.onRevisionHovered]);
|
|
660
654
|
|
|
661
655
|
const applyDecorationProps = useCallback(
|
|
662
656
|
(view: EditorView, positionMap: PositionMap): void => {
|
|
@@ -771,6 +765,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
771
765
|
useEffect(() => {
|
|
772
766
|
if (!props.dispatchRuntimeCommand || !sessionRef.current) {
|
|
773
767
|
laneRef.current = null;
|
|
768
|
+
equivalentAckLedgerRef.current.clear();
|
|
774
769
|
return;
|
|
775
770
|
}
|
|
776
771
|
// Wave 1 Slice E1/E2 — lane observability.
|
|
@@ -815,28 +810,27 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
815
810
|
toRuntime,
|
|
816
811
|
);
|
|
817
812
|
},
|
|
818
|
-
onEquivalentAck: () => {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
equivalentAckKeyRef.current = documentBuildKeyRef.current;
|
|
813
|
+
onEquivalentAck: (ack) => {
|
|
814
|
+
if (
|
|
815
|
+
ack.opId &&
|
|
816
|
+
ack.newRevisionToken &&
|
|
817
|
+
getTextCommandRefreshClass(ack) === "local-text-equivalent"
|
|
818
|
+
) {
|
|
819
|
+
equivalentAckLedgerRef.current.set(ack.newRevisionToken, ack.opId);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
equivalentAckLedgerRef.current.clear();
|
|
829
823
|
},
|
|
830
824
|
onAdjustedAck: () => {
|
|
831
825
|
// Adjusted path: allow the rebuild effect to run (it will call
|
|
832
826
|
// view.updateState with the canonical snapshot).
|
|
833
|
-
|
|
827
|
+
equivalentAckLedgerRef.current.clear();
|
|
834
828
|
},
|
|
835
829
|
onRejectedAck: () => {
|
|
836
|
-
|
|
830
|
+
equivalentAckLedgerRef.current.clear();
|
|
837
831
|
},
|
|
838
832
|
onStructuralDivergence: () => {
|
|
839
|
-
|
|
833
|
+
equivalentAckLedgerRef.current.clear();
|
|
840
834
|
},
|
|
841
835
|
});
|
|
842
836
|
}, [props.dispatchRuntimeCommand, scopeTagRegistry]);
|
|
@@ -852,13 +846,16 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
852
846
|
// ack, the PM doc already matches the canonical snapshot. Update tracking
|
|
853
847
|
// refs and decorations without rebuilding the PM state.
|
|
854
848
|
//
|
|
855
|
-
// INVARIANT:
|
|
856
|
-
//
|
|
857
|
-
//
|
|
858
|
-
|
|
849
|
+
// INVARIANT: equivalent acks are tracked by revision token and op id, not
|
|
850
|
+
// by the previous build key. This keeps the short-circuit valid if the ack
|
|
851
|
+
// and render snapshot stop arriving in the same synchronous React pass.
|
|
852
|
+
const equivalentAckOpId =
|
|
853
|
+
sessionRef.current && !sessionRef.current.hasPending()
|
|
854
|
+
? equivalentAckLedgerRef.current.get(snapshot.revisionToken)
|
|
855
|
+
: undefined;
|
|
859
856
|
if (
|
|
860
857
|
viewRef.current &&
|
|
861
|
-
|
|
858
|
+
equivalentAckOpId !== undefined &&
|
|
862
859
|
sessionRef.current &&
|
|
863
860
|
!sessionRef.current.hasPending() &&
|
|
864
861
|
sessionRef.current.getBaseRevisionToken() === snapshot.revisionToken
|
|
@@ -868,7 +865,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
868
865
|
positionMapRef.current = buildPositionMap(surface);
|
|
869
866
|
documentBuildKeyRef.current = documentBuildKey;
|
|
870
867
|
applyDecorationProps(viewRef.current, positionMapRef.current);
|
|
871
|
-
|
|
868
|
+
equivalentAckLedgerRef.current.delete(snapshot.revisionToken);
|
|
872
869
|
if (pendingTypingProbeRef.current) {
|
|
873
870
|
finishPerfProbe(pendingTypingProbeRef.current);
|
|
874
871
|
pendingTypingProbeRef.current = null;
|
|
@@ -172,7 +172,7 @@ function resolveBlockRangeFromOffsetSpan(input: {
|
|
|
172
172
|
const block = blocks[index];
|
|
173
173
|
if (!block) continue;
|
|
174
174
|
if (block.from >= endOffset) break;
|
|
175
|
-
if (block.from
|
|
175
|
+
if (block.from < endOffset && block.to > startOffset) {
|
|
176
176
|
if (first < 0) first = index;
|
|
177
177
|
last = index;
|
|
178
178
|
}
|
|
@@ -4,6 +4,7 @@ import { Check, MessageSquare, X } from "lucide-react";
|
|
|
4
4
|
import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
|
|
5
5
|
import { selectVisibleRevisions } from "../../ui/shared/revision-filters";
|
|
6
6
|
import type { MarkupDisplay } from "../../ui/headless/comment-decoration-model";
|
|
7
|
+
import { getAuthorColor } from "../../ui/headless/revision-decoration-model";
|
|
7
8
|
|
|
8
9
|
export interface TwRevisionSidebarProps {
|
|
9
10
|
trackedChanges: TrackedChangesSnapshot;
|
|
@@ -37,6 +38,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
37
38
|
|
|
38
39
|
const [typeFilter, setTypeFilter] = React.useState<TypeFilter>("all");
|
|
39
40
|
const [authorFilter, setAuthorFilter] = React.useState<string | null>(null);
|
|
41
|
+
const activeCardRef = React.useRef<HTMLDivElement | null>(null);
|
|
40
42
|
|
|
41
43
|
// Derive distinct authors from all visible revisions
|
|
42
44
|
const authors = React.useMemo(() => {
|
|
@@ -79,6 +81,11 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
79
81
|
}
|
|
80
82
|
}, [filteredRevisions, typeFilter, authorFilter, props.onRejectAllChanges, props.onRejectRevision]);
|
|
81
83
|
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!activeRevisionId) return;
|
|
86
|
+
activeCardRef.current?.scrollIntoView({ block: "nearest" });
|
|
87
|
+
}, [activeRevisionId, filteredRevisions]);
|
|
88
|
+
|
|
82
89
|
return (
|
|
83
90
|
<div className="flex flex-col outline-none">
|
|
84
91
|
{/* Stats header */}
|
|
@@ -119,17 +126,28 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
119
126
|
<div className="space-y-2">
|
|
120
127
|
{filteredRevisions.map((rev) => {
|
|
121
128
|
const isActive = activeRevisionId === rev.revisionId;
|
|
129
|
+
const authorColor = getAuthorColor(rev.authorId);
|
|
122
130
|
|
|
123
131
|
return (
|
|
124
132
|
<div
|
|
125
133
|
key={rev.revisionId}
|
|
126
|
-
|
|
134
|
+
ref={(node) => {
|
|
135
|
+
if (isActive) {
|
|
136
|
+
activeCardRef.current = node;
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
className={`w-full text-left flex rounded-md bg-surface/90 transition-colors ring-1 ring-border ${isActive ? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]" : "hover:bg-surface"}`}
|
|
140
|
+
style={
|
|
141
|
+
authorColor && isActive
|
|
142
|
+
? { boxShadow: `0 0 0 1px ${authorColor}, var(--shadow-soft)` }
|
|
143
|
+
: undefined
|
|
144
|
+
}
|
|
127
145
|
>
|
|
128
146
|
<div className={`w-0.5 shrink-0 rounded-l-md ${
|
|
129
147
|
rev.kind === "insertion" ? "bg-insert"
|
|
130
148
|
: rev.kind === "deletion" ? "bg-danger"
|
|
131
149
|
: "bg-tertiary"
|
|
132
|
-
}`} />
|
|
150
|
+
}`} style={authorColor ? { backgroundColor: authorColor } : undefined} />
|
|
133
151
|
<div className="flex-1 min-w-0">
|
|
134
152
|
<button
|
|
135
153
|
type="button"
|
|
@@ -140,7 +158,18 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
140
158
|
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
141
159
|
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
142
160
|
</div>
|
|
143
|
-
<p className="mb-1 text-[10px] text-tertiary">
|
|
161
|
+
<p className="mb-1 flex items-center gap-1.5 text-[10px] text-tertiary">
|
|
162
|
+
{authorColor ? (
|
|
163
|
+
<span
|
|
164
|
+
aria-hidden="true"
|
|
165
|
+
className="h-2 w-2 rounded-full"
|
|
166
|
+
style={{ backgroundColor: authorColor }}
|
|
167
|
+
/>
|
|
168
|
+
) : null}
|
|
169
|
+
<span className="truncate">{rev.authorId}</span>
|
|
170
|
+
<span aria-hidden="true">·</span>
|
|
171
|
+
<span>{rev.createdAt}</span>
|
|
172
|
+
</p>
|
|
144
173
|
{rev.excerpt ? (
|
|
145
174
|
<p className={`text-[11px] ${
|
|
146
175
|
rev.kind === "insertion" ? "text-insert"
|
|
@@ -21,7 +21,7 @@ export interface ReviewRailState {
|
|
|
21
21
|
* Review-rail open/close state + the responsive transition effect.
|
|
22
22
|
*
|
|
23
23
|
* When the responsive signature flips (narrow↔wide, or
|
|
24
|
-
* `reviewRailAvailable` changes), the rail resets to its default
|
|
24
|
+
* `reviewRailAvailable` changes), the rail resets to its default closed
|
|
25
25
|
* state per `getInitialReviewRailOpen`. A ref guards the effect so it
|
|
26
26
|
* only fires on actual transitions, not every viewport resize.
|
|
27
27
|
*
|
|
@@ -181,6 +181,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
181
181
|
// in the render tree below.
|
|
182
182
|
const { bodySlotRef, pmSurfaceElement } = usePmSurfaceCapture();
|
|
183
183
|
const { scrollRootRef, pageStackScrollRoot } = useScrollRootCapture();
|
|
184
|
+
const lastHoveredRevisionIdRef = useRef<string | null>(null);
|
|
184
185
|
const caps = props.capabilities;
|
|
185
186
|
const isPageWorkspace = props.workspaceMode === "page";
|
|
186
187
|
const markupDisplay = props.markupDisplay;
|
|
@@ -248,6 +249,22 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
248
249
|
reviewRailAvailable,
|
|
249
250
|
viewportWidth,
|
|
250
251
|
});
|
|
252
|
+
const handleDocumentMouseOver = useCallback(
|
|
253
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
254
|
+
const element = event.target as HTMLElement | null;
|
|
255
|
+
const revisionId =
|
|
256
|
+
element?.closest?.("[data-revision-id]")?.getAttribute("data-revision-id") ?? null;
|
|
257
|
+
if (!revisionId || (revisionId === lastHoveredRevisionIdRef.current && reviewRailOpen)) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
lastHoveredRevisionIdRef.current = revisionId;
|
|
261
|
+
if (reviewRailAvailable) {
|
|
262
|
+
setReviewRailOpen(true);
|
|
263
|
+
props.onActiveRailTabChange?.("changes");
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
[props.onActiveRailTabChange, reviewRailAvailable, reviewRailOpen, setReviewRailOpen],
|
|
267
|
+
);
|
|
251
268
|
// Incremented on zoom_changed / render_frame_ready so the placement
|
|
252
269
|
// useMemo below re-executes when the render kernel emits new rects.
|
|
253
270
|
const renderFrameRevision = useLayoutFacetRenderSignal(props.layoutFacet);
|
|
@@ -928,6 +945,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
928
945
|
<div className="flex flex-1 flex-col min-w-0">
|
|
929
946
|
<div
|
|
930
947
|
ref={scrollRootRef}
|
|
948
|
+
onMouseOver={handleDocumentMouseOver}
|
|
931
949
|
className="flex-1 overflow-y-auto bg-surface"
|
|
932
950
|
data-wre-scroll-root="true"
|
|
933
951
|
>
|