@beyondwork/docx-react-component 1.0.81 → 1.0.82
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 +2 -1
- package/src/api/v3/_runtime-handle.ts +4 -0
- package/src/api/v3/runtime/document.ts +61 -3
- package/src/api/v3/runtime/review.ts +55 -2
- package/src/io/normalize/normalize-text.ts +4 -1
- package/src/io/ooxml/parse-drawing.ts +4 -0
- package/src/model/canonical-document.ts +2 -0
- package/src/ui/WordReviewEditor.tsx +71 -1
- package/src/ui-tailwind/chrome/editor-action-registry.ts +220 -0
- package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +59 -35
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +256 -37
- package/src/ui-tailwind/editor-surface/pm-schema.ts +54 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +31 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +24 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +35 -6
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +333 -43
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +273 -24
- package/src/ui-tailwind/theme/editor-theme.css +3 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +3 -0
|
@@ -205,6 +205,25 @@ export function buildParagraphStyle(
|
|
|
205
205
|
if (indentHanging) style.textIndent = `-${indentHanging / 20}pt`;
|
|
206
206
|
else if (indentFirstLine) style.textIndent = `${indentFirstLine / 20}pt`;
|
|
207
207
|
|
|
208
|
+
// Numbering marker-lane outdent (coord-04 §1.21 — "legal numbering
|
|
209
|
+
// touches the left margin / clips into page chrome"). Word/LO model:
|
|
210
|
+
// a numbered paragraph occupies `[markerLane.start, textColumn.right]`;
|
|
211
|
+
// the marker lane lives at `[textStart - hanging, textStart]`. When
|
|
212
|
+
// `hanging > indentLeft` the marker starts at negative x relative to
|
|
213
|
+
// the paragraph's content-frame origin — legitimate Word (e.g.
|
|
214
|
+
// w:ind w:left="360" w:hanging="720"). The marker span's
|
|
215
|
+
// `margin-left: -markerWidth` (emitted by `buildMarkerStyle`)
|
|
216
|
+
// depends on the paragraph's content box reaching that far left;
|
|
217
|
+
// without this outdent the marker slides off the page frame and
|
|
218
|
+
// clips into surrounding chrome. `margin-left` expands the
|
|
219
|
+
// paragraph's content box leftward while keeping body-text
|
|
220
|
+
// alignment (via `padding-left`) untouched. Mirrors the PM branch
|
|
221
|
+
// in `pm-schema.ts::paragraph.toDOM`.
|
|
222
|
+
const markerLaneStart = block.resolvedNumbering?.geometry.markerLane?.start;
|
|
223
|
+
if (typeof markerLaneStart === "number" && markerLaneStart < 0) {
|
|
224
|
+
style.marginLeft = `${markerLaneStart / 20}pt`;
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
// Shading
|
|
209
228
|
const shadingFill = block.shading?.fill;
|
|
210
229
|
const shadingColor = safeHexColor(shadingFill);
|
|
@@ -341,6 +360,17 @@ export function buildMarkerStyle(
|
|
|
341
360
|
const hasResolvedMarkerWidth = typeof markerWidth === "number" && markerWidth > 0;
|
|
342
361
|
if (hasResolvedMarkerWidth) {
|
|
343
362
|
// P13.a: emit marker geometry in pt so it self-scales under CSS `zoom`.
|
|
363
|
+
//
|
|
364
|
+
// Marker-lane contract (coord-04 §1.21 — Word/LO model): marker occupies
|
|
365
|
+
// `[markerStart, markerStart + markerWidth]` where
|
|
366
|
+
// `markerStart = textStart - hanging`. The negative `margin-left` pulls
|
|
367
|
+
// the marker leftward by its own width into the paragraph's padding
|
|
368
|
+
// zone. When `markerStart < 0` the paragraph carries a compensating
|
|
369
|
+
// negative `margin-left` via `buildParagraphStyle`, so the content box
|
|
370
|
+
// reaches far enough left to hold the marker without clipping into
|
|
371
|
+
// page chrome. `markerStart` is therefore consumed via the paragraph
|
|
372
|
+
// geometry — the marker span itself stays at `-markerWidth` relative
|
|
373
|
+
// to the paragraph's (now-outdented) content-box left.
|
|
344
374
|
const markerWidthPt = Math.max(1, markerWidth! / 20);
|
|
345
375
|
style.width = `${markerWidthPt}pt`;
|
|
346
376
|
style.minWidth = `${markerWidthPt}pt`;
|
|
@@ -348,7 +378,7 @@ export function buildMarkerStyle(
|
|
|
348
378
|
style.marginLeft = `-${markerWidthPt}pt`;
|
|
349
379
|
style.marginRight = 0;
|
|
350
380
|
style.overflow = "visible";
|
|
351
|
-
void markerStart;
|
|
381
|
+
void markerStart;
|
|
352
382
|
} else {
|
|
353
383
|
const fallbackMinWidth = Math.min(Math.max(prefix.length + 1, 4), 14);
|
|
354
384
|
const fallbackMarginRight =
|
|
@@ -126,6 +126,9 @@ function ParagraphBlock({
|
|
|
126
126
|
"data-heading-level"?: string;
|
|
127
127
|
"data-numbered"?: string;
|
|
128
128
|
"data-contextual-spacing"?: string;
|
|
129
|
+
"data-numbering-marker-start-twips"?: string;
|
|
130
|
+
"data-numbering-marker-width-twips"?: string;
|
|
131
|
+
"data-numbering-body-start-twips"?: string;
|
|
129
132
|
} = {
|
|
130
133
|
className: classes.join(" "),
|
|
131
134
|
style: Object.keys(pStyle).length > 0 ? pStyle : undefined,
|
|
@@ -140,6 +143,18 @@ function ParagraphBlock({
|
|
|
140
143
|
if (block.contextualSpacing) {
|
|
141
144
|
attrs["data-contextual-spacing"] = "true";
|
|
142
145
|
}
|
|
146
|
+
// Numbering geometry debug visibility (coord-04 §1.21 Deliverable D):
|
|
147
|
+
// mirror the PM path so parity investigators can read the resolved
|
|
148
|
+
// marker lane directly off the DOM on both page 1 (PM) and pages 2+
|
|
149
|
+
// (static) renders. Values are twips.
|
|
150
|
+
const debugMarkerLane = block.resolvedNumbering?.geometry?.markerLane;
|
|
151
|
+
if (debugMarkerLane) {
|
|
152
|
+
attrs["data-numbering-marker-start-twips"] = String(debugMarkerLane.start);
|
|
153
|
+
if (debugMarkerLane.width > 0) {
|
|
154
|
+
attrs["data-numbering-marker-width-twips"] = String(debugMarkerLane.width);
|
|
155
|
+
}
|
|
156
|
+
attrs["data-numbering-body-start-twips"] = String(debugMarkerLane.textStart);
|
|
157
|
+
}
|
|
143
158
|
if (block.bidi) {
|
|
144
159
|
attrs.dir = "rtl";
|
|
145
160
|
}
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection.ts";
|
|
12
12
|
import {
|
|
13
13
|
measureWidgetsViaBoundingRect,
|
|
14
|
+
resolvePageOverlayRectsFromGeometry,
|
|
14
15
|
resolvePageOverlayRects,
|
|
15
16
|
type PageOverlayRect,
|
|
16
17
|
type VisiblePageIndexRange,
|
|
@@ -60,6 +61,19 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
60
61
|
const [pageRects, setPageRects] = React.useState<readonly PageOverlayRect[]>([]);
|
|
61
62
|
|
|
62
63
|
const refreshPageRectsNow = React.useCallback(() => {
|
|
64
|
+
const pageCount = facet.getPageCount();
|
|
65
|
+
if (geometryFacet) {
|
|
66
|
+
const geometryRects = resolvePageOverlayRectsFromGeometry(
|
|
67
|
+
geometryFacet,
|
|
68
|
+
pageCount,
|
|
69
|
+
visiblePageIndexRange,
|
|
70
|
+
);
|
|
71
|
+
if (geometryRects !== null) {
|
|
72
|
+
setPageRects(geometryRects);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
if (!scrollRoot) {
|
|
64
78
|
setPageRects([]);
|
|
65
79
|
return;
|
|
@@ -69,7 +83,6 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
69
83
|
setPageRects([]);
|
|
70
84
|
return;
|
|
71
85
|
}
|
|
72
|
-
const pageCount = facet.getPageCount();
|
|
73
86
|
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
|
|
74
87
|
pageCount,
|
|
75
88
|
visiblePageIndexRange,
|
|
@@ -84,11 +97,11 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
84
97
|
visiblePageIndexRange,
|
|
85
98
|
}),
|
|
86
99
|
);
|
|
87
|
-
}, [facet, scrollRoot, visiblePageIndexRange]);
|
|
100
|
+
}, [facet, geometryFacet, scrollRoot, visiblePageIndexRange]);
|
|
88
101
|
|
|
89
102
|
const refreshPageRects = React.useCallback(() => {
|
|
90
103
|
if (!scrollRoot) {
|
|
91
|
-
|
|
104
|
+
refreshPageRectsNow();
|
|
92
105
|
return;
|
|
93
106
|
}
|
|
94
107
|
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
@@ -125,6 +138,9 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
125
138
|
}, [refreshPageRects, renderFrameRevision, scrollRoot]);
|
|
126
139
|
|
|
127
140
|
React.useEffect(() => {
|
|
141
|
+
if (geometryFacet) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
128
144
|
if (!scrollRoot) {
|
|
129
145
|
return;
|
|
130
146
|
}
|
|
@@ -137,9 +153,12 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
137
153
|
const observer = new runtime.ResizeObserver(() => refreshPageRects());
|
|
138
154
|
observer.observe(scrollRoot);
|
|
139
155
|
return () => observer.disconnect();
|
|
140
|
-
}, [refreshPageRects, scrollRoot]);
|
|
156
|
+
}, [geometryFacet, refreshPageRects, scrollRoot]);
|
|
141
157
|
|
|
142
158
|
React.useEffect(() => {
|
|
159
|
+
if (geometryFacet) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
143
162
|
if (!scrollRoot) {
|
|
144
163
|
return;
|
|
145
164
|
}
|
|
@@ -163,7 +182,7 @@ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
|
163
182
|
});
|
|
164
183
|
observer.observe(scrollRoot, { childList: true, subtree: false });
|
|
165
184
|
return () => observer.disconnect();
|
|
166
|
-
}, [refreshPageRects, scrollRoot]);
|
|
185
|
+
}, [geometryFacet, refreshPageRects, scrollRoot]);
|
|
167
186
|
|
|
168
187
|
const items = React.useMemo(() => {
|
|
169
188
|
const pxPerTwip = geometryFacet?.getRenderZoom()?.pxPerTwip;
|
|
@@ -70,11 +70,13 @@
|
|
|
70
70
|
import React from "react";
|
|
71
71
|
import type {
|
|
72
72
|
EditorStoryTarget,
|
|
73
|
+
GeometryFacet,
|
|
73
74
|
WordReviewEditorLayoutFacet,
|
|
74
75
|
} from "../../api/public-types.ts";
|
|
75
76
|
import {
|
|
76
77
|
measureWidgetsViaOffsetChain,
|
|
77
78
|
measureWidgetsViaBoundingRect,
|
|
79
|
+
resolvePageOverlayRectsFromGeometry,
|
|
78
80
|
resolvePageOverlayRects,
|
|
79
81
|
type PageOverlayRect,
|
|
80
82
|
} from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
@@ -97,6 +99,8 @@ export interface PmPortalView {
|
|
|
97
99
|
export interface TwPageStackChromeLayerProps {
|
|
98
100
|
/** Layout facet — source of per-page regions, stories, and blocks. */
|
|
99
101
|
facet: WordReviewEditorLayoutFacet;
|
|
102
|
+
/** Geometry facet — warm-path source of per-page frame rects. */
|
|
103
|
+
geometryFacet?: GeometryFacet;
|
|
100
104
|
/** Scroll root whose `[data-page-frame-*]` markers drive per-page rects. */
|
|
101
105
|
scrollRoot: HTMLElement | null;
|
|
102
106
|
/**
|
|
@@ -161,6 +165,7 @@ export interface TwPageStackChromeLayerProps {
|
|
|
161
165
|
|
|
162
166
|
const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
163
167
|
facet,
|
|
168
|
+
geometryFacet,
|
|
164
169
|
scrollRoot,
|
|
165
170
|
renderFrameRevision,
|
|
166
171
|
activeStory,
|
|
@@ -172,7 +177,16 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
172
177
|
mediaPreviews,
|
|
173
178
|
activeBandRibbonProps,
|
|
174
179
|
}) => {
|
|
175
|
-
const [rects, setRects] = React.useState<readonly PageOverlayRect[]>(
|
|
180
|
+
const [rects, setRects] = React.useState<readonly PageOverlayRect[]>(() => {
|
|
181
|
+
if (!geometryFacet) return [];
|
|
182
|
+
const pageCount = facet.getPageCount();
|
|
183
|
+
const warm = resolvePageOverlayRectsFromGeometry(
|
|
184
|
+
geometryFacet,
|
|
185
|
+
pageCount,
|
|
186
|
+
visiblePageIndexRange,
|
|
187
|
+
);
|
|
188
|
+
return warm ?? [];
|
|
189
|
+
});
|
|
176
190
|
const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
|
|
177
191
|
const rafHandleRef = React.useRef<number | null>(null);
|
|
178
192
|
|
|
@@ -184,12 +198,24 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
184
198
|
// --------------------------------------------------------------------
|
|
185
199
|
|
|
186
200
|
const refreshRectsNow = React.useCallback(() => {
|
|
201
|
+
const pageCount = facet.getPageCount();
|
|
202
|
+
if (geometryFacet) {
|
|
203
|
+
const geometryRects = resolvePageOverlayRectsFromGeometry(
|
|
204
|
+
geometryFacet,
|
|
205
|
+
pageCount,
|
|
206
|
+
visiblePageIndexRange,
|
|
207
|
+
);
|
|
208
|
+
if (geometryRects !== null) {
|
|
209
|
+
setRects(geometryRects);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
187
214
|
if (!scrollRoot) {
|
|
188
215
|
setRects([]);
|
|
189
216
|
return;
|
|
190
217
|
}
|
|
191
218
|
const origin = overlayRootRef.current;
|
|
192
|
-
const pageCount = facet.getPageCount();
|
|
193
219
|
if (origin) {
|
|
194
220
|
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
|
|
195
221
|
pageCount,
|
|
@@ -229,11 +255,11 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
229
255
|
}),
|
|
230
256
|
);
|
|
231
257
|
}
|
|
232
|
-
}, [facet, scrollRoot, visiblePageIndexRange]);
|
|
258
|
+
}, [facet, geometryFacet, scrollRoot, visiblePageIndexRange]);
|
|
233
259
|
|
|
234
260
|
const refreshRects = React.useCallback(() => {
|
|
235
261
|
if (!scrollRoot) {
|
|
236
|
-
|
|
262
|
+
refreshRectsNow();
|
|
237
263
|
return;
|
|
238
264
|
}
|
|
239
265
|
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
@@ -269,6 +295,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
269
295
|
|
|
270
296
|
// Observe scroll-root size changes.
|
|
271
297
|
React.useEffect(() => {
|
|
298
|
+
if (geometryFacet) return;
|
|
272
299
|
if (!scrollRoot) return;
|
|
273
300
|
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
274
301
|
| (Window & { ResizeObserver?: typeof ResizeObserver })
|
|
@@ -277,7 +304,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
277
304
|
const observer = new runtime.ResizeObserver(() => refreshRects());
|
|
278
305
|
observer.observe(scrollRoot);
|
|
279
306
|
return () => observer.disconnect();
|
|
280
|
-
}, [scrollRoot, refreshRects]);
|
|
307
|
+
}, [geometryFacet, scrollRoot, refreshRects]);
|
|
281
308
|
|
|
282
309
|
// Observe DOM mutations on the scroll root so PM re-renders
|
|
283
310
|
// (page-break widgets added / removed) re-trigger measurement.
|
|
@@ -289,6 +316,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
289
316
|
// live inside the overlay root itself, which prevents the
|
|
290
317
|
// `setRects` → reconciliation → mutation callback loop.
|
|
291
318
|
React.useEffect(() => {
|
|
319
|
+
if (geometryFacet) return;
|
|
292
320
|
if (!scrollRoot) return;
|
|
293
321
|
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
294
322
|
| (Window & { MutationObserver?: typeof MutationObserver })
|
|
@@ -306,7 +334,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
306
334
|
});
|
|
307
335
|
observer.observe(scrollRoot, { childList: true, subtree: false });
|
|
308
336
|
return () => observer.disconnect();
|
|
309
|
-
}, [scrollRoot, refreshRects]);
|
|
337
|
+
}, [geometryFacet, scrollRoot, refreshRects]);
|
|
310
338
|
|
|
311
339
|
// --------------------------------------------------------------------
|
|
312
340
|
// P8.10 — PM portal reparent.
|
|
@@ -461,6 +489,7 @@ function propsAreEqual(
|
|
|
461
489
|
): boolean {
|
|
462
490
|
return (
|
|
463
491
|
prev.facet === next.facet &&
|
|
492
|
+
prev.geometryFacet === next.geometryFacet &&
|
|
464
493
|
prev.scrollRoot === next.scrollRoot &&
|
|
465
494
|
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
466
495
|
storyTargetEqual(prev.activeStory, next.activeStory) &&
|