@beyondwork/docx-react-component 1.0.84 → 1.0.85

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.
Files changed (46) hide show
  1. package/package.json +1 -1
  2. package/src/api/internal/build-ref-projections.ts +3 -0
  3. package/src/api/public-types.ts +38 -0
  4. package/src/api/v3/_runtime-handle.ts +11 -0
  5. package/src/api/v3/runtime/content.ts +148 -1
  6. package/src/api/v3/runtime/formatting.ts +41 -0
  7. package/src/api/v3/runtime/review.ts +98 -0
  8. package/src/core/commands/index.ts +81 -25
  9. package/src/core/state/editor-state.ts +15 -0
  10. package/src/io/ooxml/header-footer-reference.ts +38 -0
  11. package/src/io/ooxml/parse-headers-footers.ts +11 -23
  12. package/src/io/ooxml/parse-main-document.ts +7 -10
  13. package/src/model/canonical-document.ts +9 -0
  14. package/src/model/review/comment-types.ts +2 -0
  15. package/src/runtime/document-runtime.ts +677 -54
  16. package/src/runtime/formatting/field/resolver.ts +73 -8
  17. package/src/runtime/layout/layout-engine-version.ts +31 -12
  18. package/src/runtime/layout/paginated-layout-engine.ts +18 -11
  19. package/src/runtime/layout/public-facet.ts +119 -16
  20. package/src/runtime/layout/resolve-page-fields.ts +68 -6
  21. package/src/runtime/layout/resolve-page-previews.ts +1 -1
  22. package/src/runtime/suggestions-snapshot.ts +24 -0
  23. package/src/runtime/surface-projection.ts +59 -2
  24. package/src/shell/ref-commands.ts +3 -354
  25. package/src/shell/session-bootstrap.ts +8 -0
  26. package/src/ui/WordReviewEditor.tsx +95 -9
  27. package/src/ui/editor-command-bag.ts +3 -1
  28. package/src/ui/headless/revision-decoration-model.ts +13 -0
  29. package/src/ui/headless/selection-tool-types.ts +2 -0
  30. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
  31. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
  32. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
  33. package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
  34. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
  35. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
  36. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
  37. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
  38. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
  39. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
  40. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
  41. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
  42. package/src/ui-tailwind/review-workspace/types.ts +3 -2
  43. package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
  44. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
  45. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
  46. package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
@@ -111,7 +111,7 @@ export interface TwPageStackChromeLayerProps {
111
111
  renderFrameRevision: number;
112
112
  /** Current active story target — used to promote the matching band to slot mode. */
113
113
  activeStory: EditorStoryTarget;
114
- /** Fires when a band is clicked. Task 10 routes PM into the matching band. */
114
+ /** Fires when a band is double-clicked. Routes PM into the matching band. */
115
115
  onOpenStory?: (target: EditorStoryTarget) => void;
116
116
  /**
117
117
  * PM surface DOM element (typically `view.dom`, i.e. the outer
@@ -189,6 +189,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
189
189
  });
190
190
  const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
191
191
  const rafHandleRef = React.useRef<number | null>(null);
192
+ const [activeStoryPageIndex, setActiveStoryPageIndex] = React.useState<number | null>(null);
192
193
 
193
194
  // --------------------------------------------------------------------
194
195
  // rAF-debounced refresh. Mirrors the pattern in
@@ -293,6 +294,20 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
293
294
  };
294
295
  }, [refreshRects, renderFrameRevision, scrollRoot]);
295
296
 
297
+ React.useEffect(() => {
298
+ if (activeStory.kind !== "header" && activeStory.kind !== "footer") {
299
+ setActiveStoryPageIndex(null);
300
+ }
301
+ }, [activeStory]);
302
+
303
+ const handleOpenStoryForPage = React.useCallback(
304
+ (target: EditorStoryTarget, pageIndex: number) => {
305
+ setActiveStoryPageIndex(pageIndex);
306
+ onOpenStory?.(target);
307
+ },
308
+ [onOpenStory],
309
+ );
310
+
296
311
  // Observe scroll-root size changes.
297
312
  React.useEffect(() => {
298
313
  if (geometryFacet) return;
@@ -357,14 +372,17 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
357
372
  React.useLayoutEffect(() => {
358
373
  if (!pmSurfaceElement) return;
359
374
  const overlay = overlayRootRef.current;
360
- // Find a portal slot the ChromeLayer currently exposes. Every
361
- // visible band whose story equals `activeStory` renders a slot
362
- // so if a header variant repeats across pages, every one of those
363
- // pages emits a `[data-pm-portal-slot]` div. PM is a single DOM
364
- // element, so we pick the first slot in document order via
365
- // `querySelector` (singular) and park the element there. The
366
- // remaining slot divs stay empty until the next reparent.
375
+ // Find the active portal slot. Double-click activation records the
376
+ // page index, so repeated shared header/footer stories still route PM
377
+ // into the actual page band the user opened.
378
+ const pageScopedSelector =
379
+ activeStoryPageIndex == null
380
+ ? null
381
+ : `[data-pm-portal-slot][data-page-index="${activeStoryPageIndex}"]`;
367
382
  const activeSlot =
383
+ (pageScopedSelector
384
+ ? overlay?.querySelector<HTMLElement>(pageScopedSelector)
385
+ : null) ??
368
386
  overlay?.querySelector<HTMLElement>("[data-pm-portal-slot]") ?? null;
369
387
  // Body slot lives outside the chrome overlay root. Walk up from
370
388
  // the PM element's owner document so jsdom tests and the real
@@ -396,7 +414,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
396
414
  // updateState pass.
397
415
  }
398
416
  }
399
- }, [pmSurfaceElement, pmView, activeStory, rects, scrollRoot]);
417
+ }, [pmSurfaceElement, pmView, activeStory, activeStoryPageIndex, rects, scrollRoot]);
400
418
 
401
419
  // --------------------------------------------------------------------
402
420
  // Render
@@ -440,7 +458,8 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
440
458
  page={page}
441
459
  facet={facet}
442
460
  activeStory={activeStory}
443
- onOpenStory={onOpenStory}
461
+ activeStoryPageIndex={activeStoryPageIndex}
462
+ onOpenStory={handleOpenStoryForPage}
444
463
  visiblePageIndexRange={visiblePageIndexRange}
445
464
  renderFrameRevision={renderFrameRevision}
446
465
  mediaPreviews={mediaPreviews}
@@ -174,7 +174,7 @@ function renderSegment(
174
174
  data-node-type="field_ref"
175
175
  style={{ opacity: 0.6, fontSize: "0.85em" }}
176
176
  >
177
- [field]
177
+ {seg.displayText ?? seg.label}
178
178
  </span>
179
179
  );
180
180
  case "note_ref":
@@ -192,6 +192,7 @@ export interface TwReviewWorkspaceProps {
192
192
  onZoomChange?: (level: ZoomLevel) => void;
193
193
  onActiveRailTabChange?: (value: ReviewRailTab) => void;
194
194
  onShowTrackedChangesChange?: (show: boolean) => void;
195
+ onReviewMarkupModeChange?: (mode: MarkupDisplay) => void;
195
196
  onUndo?: () => void;
196
197
  onRedo?: () => void;
197
198
  onSetParagraphStyle?: (styleId: string) => void;
@@ -293,7 +294,7 @@ export interface TwReviewWorkspaceProps {
293
294
  /**
294
295
  * @deprecated P8.11 — the workspace no longer renders a workspace-level
295
296
  * header band with an "Edit header" button; per-page header bands route
296
- * clicks via `onOpenStory` / `runtime.openStory` directly. The prop
297
+ * double-clicks via `onOpenStory` / `runtime.openStory` directly. The prop
297
298
  * remains optional for one release so existing hosts continue to
298
299
  * compile; supplying it emits a `console.warn` on mount.
299
300
  */
@@ -313,7 +314,7 @@ export interface TwReviewWorkspaceProps {
313
314
  onOpenFooterStoryForPage?: (pageIndex: number) => void;
314
315
  /**
315
316
  * P8.11 — fired when a per-page chrome band (header / footer) is
316
- * clicked to promote it into the active editing surface. Wire to
317
+ * double-clicked to promote it into the active editing surface. Wire to
317
318
  * `runtime.openStory(target)`; the chrome layer's portal mechanism
318
319
  * then reparents the PM surface into the matching band's active slot.
319
320
  */
@@ -212,6 +212,16 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
212
212
  const [pageMarkers, setPageMarkers] = useState<readonly HTMLElement[]>([]);
213
213
 
214
214
  useEffect(() => {
215
+ if (geometryFacet && layoutFacet) {
216
+ // Warm path: render-kernel geometry gives us page frames and the layout
217
+ // facet gives us page offsets, so DOM page-break markers are only legacy
218
+ // fallback. Skipping the marker scan avoids querySelectorAll +
219
+ // IntersectionObserver churn when PM swaps virtualized blocks at page
220
+ // dividers during scroll.
221
+ setPageMarkers((prev) => (prev.length === 0 ? prev : []));
222
+ return undefined;
223
+ }
224
+
215
225
  const root = pageStackScrollRoot;
216
226
  if (!root) {
217
227
  setPageMarkers((prev) => (prev.length === 0 ? prev : []));
@@ -283,7 +293,7 @@ export function usePageMarkers(options: UsePageMarkersOptions): PageMarkersResul
283
293
  // every requestViewportRefresh(), which would add two extra render passes per
284
294
  // scroll event. The page-0 fallback uses -1 when page1First is unknown,
285
295
  // which is correct (no page-0 blocks → synthetic marker contributes nothing).
286
- }, [pageStackScrollRoot, snapshot.revisionToken]);
296
+ }, [geometryFacet, layoutFacet, pageStackScrollRoot, snapshot.revisionToken]);
287
297
 
288
298
  const selectionBlockIndex = useMemo(() => {
289
299
  const sel = snapshot.selection;
@@ -233,6 +233,7 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
233
233
  <Toggle.Root
234
234
  pressed={props.showTrackedChanges ?? false}
235
235
  onPressedChange={(v) => props.onShowTrackedChangesChange?.(v)}
236
+ aria-label={(props.showTrackedChanges ?? false) ? "Turn off tracked changes" : "Turn on tracked changes"}
236
237
  disabled={props.capabilities ? !props.capabilities.trackChangesSupported : false}
237
238
  onMouseDown={preserveEditorSelectionMouseDown}
238
239
  className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
@@ -247,7 +248,7 @@ function RoleActionButton(arg: RoleActionButtonProps): React.JSX.Element | null
247
248
  </Tooltip.Trigger>
248
249
  <Tooltip.Portal>
249
250
  <Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
250
- {(props.showTrackedChanges ?? false) ? "Hide tracked changes" : "Show tracked changes"}
251
+ {(props.showTrackedChanges ?? false) ? "Turn off tracked changes" : "Turn on tracked changes"}
251
252
  </Tooltip.Content>
252
253
  </Tooltip.Portal>
253
254
  </Tooltip.Root>
@@ -669,6 +669,7 @@ export function TwToolbar(props: TwToolbarProps) {
669
669
  <Toggle.Root
670
670
  pressed={props.showTrackedChanges}
671
671
  onPressedChange={props.onShowTrackedChangesChange}
672
+ aria-label={props.showTrackedChanges ? "Turn off tracked changes" : "Turn on tracked changes"}
672
673
  disabled={caps ? !caps.trackChangesSupported : false}
673
674
  onMouseDown={preserveEditorSelectionMouseDown}
674
675
  className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-canvas data-[state=on]:text-accent data-[state=on]:ring-1 data-[state=on]:ring-accent/30 data-[state=on]:shadow-sm outline-none disabled:opacity-40 ${focusRingClass}`}
@@ -681,7 +682,7 @@ export function TwToolbar(props: TwToolbarProps) {
681
682
  className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
682
683
  sideOffset={6}
683
684
  >
684
- {props.showTrackedChanges ? "Hide tracked changes" : "Show tracked changes"}
685
+ {props.showTrackedChanges ? "Turn off tracked changes" : "Turn on tracked changes"}
685
686
  </Tooltip.Content>
686
687
  </Tooltip.Portal>
687
688
  </Tooltip.Root>
@@ -85,7 +85,7 @@ export type {
85
85
  TwReviewWorkspaceProps,
86
86
  } from "./review-workspace/types.ts";
87
87
 
88
- import type { EditorRole } from "../api/public-types.ts";
88
+ import type { EditorRole, EditorStoryTarget } from "../api/public-types.ts";
89
89
 
90
90
  // Default shell-header modes for the workspace's default composition.
91
91
  // Designsystem §6.1 prescribes a 4-mode switcher; all four are reachable in
@@ -185,6 +185,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
185
185
  const isPageWorkspace = props.workspaceMode === "page";
186
186
  const markupDisplay = props.markupDisplay;
187
187
  const [navOpen, setNavOpen] = useState(false);
188
+ const handleOpenPageModeStory = useCallback(
189
+ (target: EditorStoryTarget) => {
190
+ if (
191
+ !isPageWorkspace &&
192
+ (target.kind === "header" || target.kind === "footer")
193
+ ) {
194
+ return;
195
+ }
196
+ props.onOpenStory?.(target);
197
+ },
198
+ [isPageWorkspace, props.onOpenStory],
199
+ );
188
200
 
189
201
  // Scope card state — tracks which scope's card is currently open so
190
202
  // the ChromeOverlay's card layer renders the right one. Open/close
@@ -608,6 +620,10 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
608
620
  dismissSelectionToolbar();
609
621
  props.onShowTrackedChangesChange(show);
610
622
  }}
623
+ onReviewMarkupMode={(mode) => {
624
+ dismissSelectionToolbar();
625
+ props.onReviewMarkupModeChange?.(mode);
626
+ }}
611
627
  onReviewSidebarTrackedChanges={
612
628
  props.onReviewSidebarTrackedChanges
613
629
  ? runWithSelectionToolbarDismiss(props.onReviewSidebarTrackedChanges)
@@ -1159,7 +1175,7 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1159
1175
  }
1160
1176
  renderFrameRevision={renderFrameRevision}
1161
1177
  activeStory={snapshot.activeStory}
1162
- onOpenStory={props.onOpenStory}
1178
+ onOpenStory={handleOpenPageModeStory}
1163
1179
  pmSurfaceElement={pmSurfaceElement}
1164
1180
  visiblePageIndexRange={visiblePageIndexRange}
1165
1181
  mediaPreviews={props.mediaPreviews}