@beyondwork/docx-react-component 1.0.74 → 1.0.76

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.
@@ -65,6 +65,7 @@ import { buildPositionMap, type PositionMap } from "./pm-position-map";
65
65
  import { createLocalEditSessionState } from "./local-edit-session-state";
66
66
  import { createFastTextEditLane } from "./fast-text-edit-lane";
67
67
  import { createPredictedTxGate } from "./predicted-tx-gate";
68
+ import { replaceStatePreservingPosition } from "./preserve-position";
68
69
  import {
69
70
  createScopeTagRegistry,
70
71
  type ScopeTagRegistry,
@@ -772,11 +773,35 @@ export const TwProseMirrorSurface = forwardRef<
772
773
  laneRef.current = null;
773
774
  return;
774
775
  }
776
+ // Wave 1 Slice E1/E2 — lane observability.
777
+ //
778
+ // `typing.reconcile` measures the dispatch → ack window per keystroke
779
+ // (predicted path). `typing.divergence` fires on the
780
+ // structural-divergence ack kind (the rollback-all path). Both probe
781
+ // kinds are declared in `PerfProbeKind` and were previously
782
+ // unemitted — wiring them here closes the instrumentation gap so
783
+ // lane quality regressions show up in the standard perf summary.
784
+ const pendingReconcileTokens = new Map<string, string | null>();
775
785
  laneRef.current = createFastTextEditLane({
776
786
  session: sessionRef.current,
777
787
  getView: () => viewRef.current,
778
788
  getPositionMap: () => positionMapRef.current,
779
789
  dispatchRuntimeCommand: props.dispatchRuntimeCommand,
790
+ probe: {
791
+ markPredicted(opId: string) {
792
+ pendingReconcileTokens.set(opId, startPerfProbe("typing.reconcile"));
793
+ },
794
+ markReconciled(opId: string, kind) {
795
+ const token = pendingReconcileTokens.get(opId);
796
+ if (token !== undefined) {
797
+ finishPerfProbe(token);
798
+ pendingReconcileTokens.delete(opId);
799
+ }
800
+ if (kind === "structural-divergence") {
801
+ recordPerfSample("typing.divergence");
802
+ }
803
+ },
804
+ },
780
805
  suppressSelectionSync: (suppressed) => {
781
806
  suppressSelectionEchoRef.current = suppressed;
782
807
  },
@@ -891,11 +916,30 @@ export const TwProseMirrorSurface = forwardRef<
891
916
  viewRef.current = view;
892
917
  recordPerfSample("pm.mount");
893
918
  } else {
894
- suppressSelectionEchoRef.current = true;
895
- viewRef.current.updateState(state);
896
- queueMicrotask(() => {
897
- suppressSelectionEchoRef.current = false;
898
- });
919
+ // Wave 1 Slice C · the single funnel for snapshot replacement.
920
+ //
921
+ // `replaceStatePreservingPosition` encapsulates two invariants:
922
+ // 1. Scroll position preservation — capture the anchor block
923
+ // before `view.updateState`, restore scroll after, so the
924
+ // user's viewport doesn't jump when blocks above change
925
+ // height (invariant 7: geometry-facet warm path, no DOM
926
+ // measurement on the hot path).
927
+ // 2. Echo-suppression ordering — `suppressSelectionEchoRef` is
928
+ // set to `true` BEFORE the state swap and released in a
929
+ // microtask AFTER, so PM's internal selection-change events
930
+ // during the swap are swallowed by the selection-sync
931
+ // plugin.
932
+ //
933
+ // Ordering is regression-guarded by
934
+ // `test/ui-tailwind/editor-surface/preserve-position-ordering.test.ts`.
935
+ replaceStatePreservingPosition(
936
+ {
937
+ view: viewRef.current,
938
+ geometryFacet: props.geometryFacet,
939
+ suppressionRef: suppressSelectionEchoRef,
940
+ },
941
+ state,
942
+ );
899
943
  }
900
944
  documentBuildKeyRef.current = documentBuildKey;
901
945
  applyDecorationProps(viewRef.current, positionMap);
@@ -145,11 +145,15 @@ export function collectFloatingImageOverlayItems(input: {
145
145
  };
146
146
 
147
147
  // coord-01 §9 / §5.1 — CCEP logos live in header stories; collect from
148
- // the main story for the active-story case AND from every secondary
149
- // story so header/footer images reach the overlay regardless of which
150
- // story is active in the editor.
148
+ // the active story's surface.blocks (always) + every secondary story
149
+ // EXCEPT the one whose target matches activeStory (runtime fills
150
+ // surface.blocks with that same story's blocks, so walking secondary
151
+ // stories unconditionally would double-emit header/footer segments
152
+ // the moment the user enters a header for editing — M1).
151
153
  collectFromStory(surface.blocks, activeStory);
154
+ const activeKey = storyTargetKey(activeStory);
152
155
  for (const secondary of surface.secondaryStories ?? []) {
156
+ if (storyTargetKey(secondary.target) === activeKey) continue;
153
157
  collectFromStory(secondary.blocks, secondary.target);
154
158
  }
155
159
 
@@ -52,7 +52,11 @@ export const TwEndnoteArea: React.FC<TwEndnoteAreaProps> = ({
52
52
  marginBottom: "8pt",
53
53
  }}
54
54
  />
55
- <TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
55
+ <TwRegionBlockRenderer
56
+ blocks={blocks}
57
+ mediaPreviews={mediaPreviews}
58
+ fallbackDisplay="hidden"
59
+ />
56
60
  </div>
57
61
  );
58
62
  };
@@ -66,7 +66,11 @@ export const TwFootnoteArea: React.FC<TwFootnoteAreaProps> = React.memo(({
66
66
  marginBottom: "4pt",
67
67
  }}
68
68
  />
69
- <TwRegionBlockRenderer blocks={blocks} mediaPreviews={mediaPreviews} />
69
+ <TwRegionBlockRenderer
70
+ blocks={blocks}
71
+ mediaPreviews={mediaPreviews}
72
+ fallbackDisplay="hidden"
73
+ />
70
74
  </div>
71
75
  );
72
76
  });
@@ -14,6 +14,7 @@ import {
14
14
  headingClassList,
15
15
  resolveHeadingLevel,
16
16
  } from "../editor-surface/tw-page-block-view.helpers.ts";
17
+ import { shouldRenderAbsoluteFloatingImageInPageOverlay } from "./floating-image-overlay-model.ts";
17
18
 
18
19
  const EMU_PER_PX = 9525;
19
20
 
@@ -94,11 +95,12 @@ function renderSegment(
94
95
  case "hard_break":
95
96
  return <br key={seg.segmentId} />;
96
97
  case "image": {
97
- // §5.1 gap 3 — floating-anchor images are owned by the absolute
98
- // floating-image overlay (`TwFloatingImageLayer`). Emitting them
99
- // inline here would double-paint the CCEP header logo on every
100
- // page. Skip entirely so only the overlay renders them.
101
- if (seg.anchor?.display === "floating") {
98
+ // §5.1 gap 3 — floating-anchor images the overlay can render
99
+ // (`TwFloatingImageLayer`) are owned by the overlay. Skip inline
100
+ // emission ONLY for anchors the overlay predicate accepts
101
+ // otherwise wrap-mode=square / column-relative / tight-wrapped
102
+ // floats get dropped from inline AND from the overlay (M2).
103
+ if (shouldRenderAbsoluteFloatingImageInPageOverlay(seg.anchor)) {
102
104
  return null;
103
105
  }
104
106
  // Mirror body-renderer behavior (`pm-state-from-snapshot.ts` :500+):
@@ -2,7 +2,6 @@ import { useCallback, useEffect } from "react";
2
2
  import type { Dispatch, SetStateAction } from "react";
3
3
 
4
4
  import { createCanvasBackend } from "../../api/public-types.ts";
5
- import type { RuntimeRenderSnapshot } from "../../api/public-types.ts";
6
5
  import {
7
6
  incrementInvalidationCounter,
8
7
  recordPerfSample,
@@ -17,8 +16,6 @@ export interface UseWorkspaceSideEffectsOptions {
17
16
  activeParagraphLayout: ActiveParagraphLayout | null;
18
17
  pageChromeModel: PageChromeModel;
19
18
  pageShellMetrics: PageShellMetrics;
20
- isPageWorkspace: boolean;
21
- activeStoryKind: RuntimeRenderSnapshot["activeStory"]["kind"];
22
19
  showDrawerReviewRail: boolean;
23
20
  setReviewRailOpen: Dispatch<SetStateAction<boolean>>;
24
21
  onOpenHeaderStory?: () => void;
@@ -58,8 +55,6 @@ export function useWorkspaceSideEffects(
58
55
  activeParagraphLayout,
59
56
  pageChromeModel,
60
57
  pageShellMetrics,
61
- isPageWorkspace,
62
- activeStoryKind,
63
58
  showDrawerReviewRail,
64
59
  setReviewRailOpen,
65
60
  onOpenHeaderStory,
@@ -67,14 +62,6 @@ export function useWorkspaceSideEffects(
67
62
  onDismissSelectionToolbar,
68
63
  } = options;
69
64
 
70
- // Slice A (designsystem §6.20 reshape, 2026-04-24): isPageWorkspace +
71
- // activeStoryKind referenced here so the prop sweep stays a no-op
72
- // type-checker pass even though the auto-open-layout-tools effect
73
- // they fed retired with the strip. Slice B mounts an active-band
74
- // ribbon that observes activeStoryKind directly.
75
- void isPageWorkspace;
76
- void activeStoryKind;
77
-
78
65
  useEffect(() => {
79
66
  recordPerfSample("workspace.chrome");
80
67
  incrementInvalidationCounter("workspace.chrome.recomputes");
@@ -10,10 +10,6 @@ import React, {
10
10
  import * as Tooltip from "@radix-ui/react-tooltip";
11
11
  import { ChevronRight } from "lucide-react";
12
12
 
13
- import {
14
- useVisibleBlockRange,
15
- useVisiblePageIndexRange,
16
- } from "./page-stack/use-visible-block-range.ts";
17
13
  import { sliceBlocksForPage } from "./editor-surface/page-slice-util.ts";
18
14
  import {
19
15
  findScrollAnchor,
@@ -330,8 +326,6 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
330
326
  activeParagraphLayout,
331
327
  pageChromeModel,
332
328
  pageShellMetrics,
333
- isPageWorkspace,
334
- activeStoryKind: snapshot.activeStory.kind,
335
329
  showDrawerReviewRail: responsiveChrome.showDrawerReviewRail,
336
330
  setReviewRailOpen,
337
331
  onOpenHeaderStory: props.onOpenHeaderStory,