@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.
@@ -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; // consumed via paragraph padding-left geometry
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
- setPageRects([]);
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
- setRects([]);
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) &&