@beyondwork/docx-react-component 1.0.60 → 1.0.61

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 (40) hide show
  1. package/package.json +33 -44
  2. package/src/api/public-types.ts +41 -0
  3. package/src/io/docx-session.ts +167 -8
  4. package/src/io/export/serialize-footnotes.ts +36 -5
  5. package/src/io/export/serialize-headers-footers.ts +7 -0
  6. package/src/io/export/serialize-main-document.ts +25 -18
  7. package/src/io/export/serialize-paragraph-formatting.ts +6 -0
  8. package/src/io/export/serialize-settings.ts +130 -3
  9. package/src/io/normalize/normalize-text.ts +8 -4
  10. package/src/io/ooxml/parse-footnotes.ts +11 -0
  11. package/src/io/ooxml/parse-headers-footers.ts +117 -42
  12. package/src/io/ooxml/parse-main-document.ts +20 -8
  13. package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
  14. package/src/io/ooxml/parse-settings.ts +91 -1
  15. package/src/model/canonical-document.ts +36 -2
  16. package/src/runtime/document-runtime.ts +424 -0
  17. package/src/runtime/footnote-resolver.ts +32 -8
  18. package/src/runtime/layout/layout-engine-version.ts +7 -1
  19. package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
  20. package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
  21. package/src/runtime/layout/paginated-layout-engine.ts +41 -8
  22. package/src/runtime/layout/resolved-formatting-document.ts +11 -9
  23. package/src/runtime/layout/resolved-formatting-state.ts +4 -0
  24. package/src/runtime/numbering-prefix.ts +26 -2
  25. package/src/runtime/surface-projection.ts +75 -14
  26. package/src/runtime/table-schema.ts +26 -0
  27. package/src/ui/WordReviewEditor.tsx +25 -0
  28. package/src/ui/editor-runtime-boundary.ts +1 -0
  29. package/src/ui/editor-shell-view.tsx +8 -0
  30. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
  31. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
  32. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
  33. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  34. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
  35. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
  36. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
  37. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
  38. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
  39. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
  40. package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
@@ -0,0 +1,248 @@
1
+ import * as React from "react";
2
+
3
+ import type {
4
+ EditorStoryTarget,
5
+ RuntimeRenderSnapshot,
6
+ } from "../../api/public-types.ts";
7
+ import type { WordReviewEditorLayoutFacet } from "../../runtime/layout/index.ts";
8
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection.ts";
9
+ import {
10
+ measureWidgetsViaBoundingRect,
11
+ resolvePageOverlayRects,
12
+ type PageOverlayRect,
13
+ type VisiblePageIndexRange,
14
+ } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
15
+ import {
16
+ collectFloatingImageOverlayItems,
17
+ type FloatingImagePreviewDescriptor,
18
+ } from "./floating-image-overlay-model.ts";
19
+
20
+ export interface TwFloatingImageLayerProps {
21
+ facet: WordReviewEditorLayoutFacet;
22
+ scrollRoot: HTMLElement | null;
23
+ renderFrameRevision: number;
24
+ visiblePageIndexRange?: VisiblePageIndexRange | null;
25
+ snapshot: RuntimeRenderSnapshot;
26
+ mediaPreviews?: Record<string, FloatingImagePreviewDescriptor>;
27
+ plane: "behind" | "front";
28
+ onActivateFloatingImage?: (payload: {
29
+ mediaId: string;
30
+ from: number;
31
+ to: number;
32
+ storyTarget: EditorStoryTarget;
33
+ }) => void;
34
+ }
35
+
36
+ export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
37
+ facet,
38
+ scrollRoot,
39
+ renderFrameRevision,
40
+ visiblePageIndexRange,
41
+ snapshot,
42
+ mediaPreviews,
43
+ plane,
44
+ onActivateFloatingImage,
45
+ }) => {
46
+ const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
47
+ const rafHandleRef = React.useRef<number | null>(null);
48
+ const [pageRects, setPageRects] = React.useState<readonly PageOverlayRect[]>([]);
49
+
50
+ const refreshPageRectsNow = React.useCallback(() => {
51
+ if (!scrollRoot) {
52
+ setPageRects([]);
53
+ return;
54
+ }
55
+ const origin = overlayRootRef.current;
56
+ if (!origin) {
57
+ setPageRects([]);
58
+ return;
59
+ }
60
+ const pageCount = facet.getPageCount();
61
+ const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
62
+ pageCount,
63
+ visiblePageIndexRange,
64
+ });
65
+ const originRect = origin.getBoundingClientRect();
66
+ setPageRects(
67
+ resolvePageOverlayRects({
68
+ widgets,
69
+ pageCount,
70
+ scrollHeight:
71
+ origin.clientHeight > 0 ? origin.clientHeight : originRect.height,
72
+ visiblePageIndexRange,
73
+ }),
74
+ );
75
+ }, [facet, scrollRoot, visiblePageIndexRange]);
76
+
77
+ const refreshPageRects = React.useCallback(() => {
78
+ if (!scrollRoot) {
79
+ setPageRects([]);
80
+ return;
81
+ }
82
+ const runtime = scrollRoot.ownerDocument?.defaultView as
83
+ | (Window & {
84
+ requestAnimationFrame?: (cb: () => void) => number;
85
+ cancelAnimationFrame?: (handle: number) => void;
86
+ })
87
+ | null;
88
+ const raf = runtime?.requestAnimationFrame;
89
+ if (!raf) {
90
+ refreshPageRectsNow();
91
+ return;
92
+ }
93
+ if (rafHandleRef.current !== null) {
94
+ return;
95
+ }
96
+ rafHandleRef.current = raf(() => {
97
+ rafHandleRef.current = null;
98
+ refreshPageRectsNow();
99
+ });
100
+ }, [refreshPageRectsNow, scrollRoot]);
101
+
102
+ React.useEffect(() => {
103
+ refreshPageRects();
104
+ return () => {
105
+ const runtime = scrollRoot?.ownerDocument?.defaultView as
106
+ | (Window & { cancelAnimationFrame?: (handle: number) => void })
107
+ | null;
108
+ if (rafHandleRef.current !== null && runtime?.cancelAnimationFrame) {
109
+ runtime.cancelAnimationFrame(rafHandleRef.current);
110
+ rafHandleRef.current = null;
111
+ }
112
+ };
113
+ }, [refreshPageRects, renderFrameRevision, scrollRoot]);
114
+
115
+ React.useEffect(() => {
116
+ if (!scrollRoot) {
117
+ return;
118
+ }
119
+ const runtime = scrollRoot.ownerDocument?.defaultView as
120
+ | (Window & { ResizeObserver?: typeof ResizeObserver })
121
+ | null;
122
+ if (!runtime?.ResizeObserver) {
123
+ return;
124
+ }
125
+ const observer = new runtime.ResizeObserver(() => refreshPageRects());
126
+ observer.observe(scrollRoot);
127
+ return () => observer.disconnect();
128
+ }, [refreshPageRects, scrollRoot]);
129
+
130
+ React.useEffect(() => {
131
+ if (!scrollRoot) {
132
+ return;
133
+ }
134
+ const runtime = scrollRoot.ownerDocument?.defaultView as
135
+ | (Window & { MutationObserver?: typeof MutationObserver })
136
+ | null;
137
+ if (!runtime?.MutationObserver) {
138
+ return;
139
+ }
140
+ const observer = new runtime.MutationObserver((records) => {
141
+ const overlay = overlayRootRef.current;
142
+ if (overlay) {
143
+ const allSelf = records.every(
144
+ (record) => record.target instanceof Node && overlay.contains(record.target),
145
+ );
146
+ if (allSelf) {
147
+ return;
148
+ }
149
+ }
150
+ refreshPageRects();
151
+ });
152
+ observer.observe(scrollRoot, { childList: true, subtree: false });
153
+ return () => observer.disconnect();
154
+ }, [refreshPageRects, scrollRoot]);
155
+
156
+ const items = React.useMemo(() => {
157
+ const allItems = collectFloatingImageOverlayItems({
158
+ surface: snapshot.surface,
159
+ activeStory: snapshot.activeStory,
160
+ facet,
161
+ pageRects,
162
+ mediaPreviews,
163
+ });
164
+ return allItems.filter((item) =>
165
+ plane === "behind" ? item.behindDoc : !item.behindDoc,
166
+ );
167
+ }, [facet, mediaPreviews, pageRects, plane, snapshot.activeStory, snapshot.surface]);
168
+
169
+ return (
170
+ <div
171
+ ref={overlayRootRef}
172
+ className={
173
+ plane === "behind"
174
+ ? "pointer-events-none absolute inset-0 z-[5]"
175
+ : "pointer-events-none absolute inset-0 z-20"
176
+ }
177
+ aria-hidden={plane === "behind" ? "true" : undefined}
178
+ data-testid={plane === "behind" ? "floating-image-layer-behind" : "floating-image-layer-front"}
179
+ data-floating-image-count={items.length}
180
+ >
181
+ {items.map((item) => {
182
+ const interactive = plane === "front" && typeof onActivateFloatingImage === "function";
183
+ const commonProps = {
184
+ key: item.key,
185
+ className: interactive
186
+ ? "pointer-events-auto absolute m-0 border-0 bg-transparent p-0"
187
+ : "pointer-events-none absolute m-0 border-0 bg-transparent p-0",
188
+ "data-floating-image-overlay": "",
189
+ "data-floating-image-id": item.mediaId,
190
+ style: {
191
+ top: `${item.topPx}px`,
192
+ left: `${item.leftPx}px`,
193
+ width: `${item.widthPx}px`,
194
+ height: `${item.heightPx}px`,
195
+ },
196
+ } as const;
197
+ const content = item.src ? (
198
+ <img
199
+ src={item.src}
200
+ alt={interactive ? item.altText ?? "" : ""}
201
+ title={item.detail ?? item.altText ?? "Floating image"}
202
+ draggable={false}
203
+ style={{
204
+ display: "block",
205
+ width: "100%",
206
+ height: "100%",
207
+ objectFit: "fill",
208
+ }}
209
+ />
210
+ ) : (
211
+ <span
212
+ className="flex h-full w-full items-center justify-center rounded border border-border/70 bg-surface/90 px-2 text-[10px] font-medium uppercase tracking-[0.12em] text-secondary"
213
+ title={item.detail ?? item.altText ?? "Floating image"}
214
+ >
215
+ Image
216
+ </span>
217
+ );
218
+ if (!interactive) {
219
+ return (
220
+ <div {...commonProps} aria-hidden="true">
221
+ {content}
222
+ </div>
223
+ );
224
+ }
225
+ return (
226
+ <button
227
+ {...commonProps}
228
+ type="button"
229
+ tabIndex={-1}
230
+ aria-label={item.altText ?? "Select floating image"}
231
+ onMouseDown={preserveEditorSelectionMouseDown}
232
+ onClick={() =>
233
+ onActivateFloatingImage?.({
234
+ mediaId: item.mediaId,
235
+ from: item.from,
236
+ to: item.to,
237
+ storyTarget: snapshot.activeStory,
238
+ })}
239
+ >
240
+ {content}
241
+ </button>
242
+ );
243
+ })}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ export default TwFloatingImageLayer;
@@ -260,6 +260,10 @@ function RegionTable({
260
260
  if (cell.borderRight) cellStyle.borderRight = cell.borderRight;
261
261
  if (cell.borderBottom) cellStyle.borderBottom = cell.borderBottom;
262
262
  if (cell.borderLeft) cellStyle.borderLeft = cell.borderLeft;
263
+ if (typeof cell.paddingTop === "number") cellStyle.paddingTop = `${cell.paddingTop / 20}pt`;
264
+ if (typeof cell.paddingRight === "number") cellStyle.paddingRight = `${cell.paddingRight / 20}pt`;
265
+ if (typeof cell.paddingBottom === "number") cellStyle.paddingBottom = `${cell.paddingBottom / 20}pt`;
266
+ if (typeof cell.paddingLeft === "number") cellStyle.paddingLeft = `${cell.paddingLeft / 20}pt`;
263
267
 
264
268
  return (
265
269
  <td
@@ -91,6 +91,8 @@ import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
91
91
  import { TwStatusBar } from "./status/tw-status-bar";
92
92
  import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
93
93
  import { TwChromeOverlay, TwPageStackOverlayLayer } from "./chrome-overlay";
94
+ import { TwFloatingImageLayer } from "./page-stack/tw-floating-image-layer.tsx";
95
+ import type { MediaPreviewDescriptor } from "./editor-surface/pm-state-from-snapshot.ts";
94
96
  import {
95
97
  cycleScopeIndex,
96
98
  shouldHandleScopeNavKey,
@@ -104,6 +106,7 @@ export interface TwReviewWorkspaceProps {
104
106
  markupDisplay: MarkupDisplay;
105
107
  currentUserId?: string;
106
108
  capabilities?: SessionCapabilities;
109
+ mediaPreviews?: Record<string, MediaPreviewDescriptor>;
107
110
  reviewMode?: "editing" | "review";
108
111
  /**
109
112
  * Runtime-owned layout facet. Optional so existing tests + host apps
@@ -192,6 +195,12 @@ export interface TwReviewWorkspaceProps {
192
195
  selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
193
196
  currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
194
197
  commands: EditorCommandBag;
198
+ onActivateFloatingImage?: (payload: {
199
+ mediaId: string;
200
+ from: number;
201
+ to: number;
202
+ storyTarget: EditorStoryTarget;
203
+ }) => void;
195
204
  /** N6 — release the grabbed image/shape. Wired to `runtime.deselectObject()` by the host. */
196
205
  onDeselectObject?: () => void;
197
206
  activeSelectionTool?: ActiveSelectionToolModel | null;
@@ -903,6 +912,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
903
912
  // of N times.
904
913
  useEffect(() => {
905
914
  if (!props.layoutFacet) return;
915
+ let pendingBump = false;
916
+ let cancelled = false;
917
+ const scheduleBump = () => {
918
+ if (pendingBump || cancelled) return;
919
+ pendingBump = true;
920
+ queueMicrotask(() => {
921
+ pendingBump = false;
922
+ if (cancelled) return;
923
+ setRenderFrameRevision((n) => n + 1);
924
+ });
925
+ };
906
926
  const unsub = props.layoutFacet.subscribe((event) => {
907
927
  switch (event.kind) {
908
928
  case "zoom_changed":
@@ -913,13 +933,16 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
913
933
  case "page_field_dirtied":
914
934
  case "measurement_backend_ready":
915
935
  case "layout_committed":
916
- setRenderFrameRevision((n) => n + 1);
936
+ scheduleBump();
917
937
  break;
918
938
  default:
919
939
  break;
920
940
  }
921
941
  });
922
- return unsub;
942
+ return () => {
943
+ cancelled = true;
944
+ unsub();
945
+ };
923
946
  }, [props.layoutFacet]);
924
947
 
925
948
  useEffect(() => {
@@ -1077,7 +1100,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1077
1100
  useEffect(() => {
1078
1101
  if (!props.layoutFacet) return;
1079
1102
  const facet = props.layoutFacet;
1080
- void document.fonts.ready.then(() => {
1103
+ const fontSet = document.fonts;
1104
+ if (!fontSet?.ready) {
1105
+ facet.swapMeasurementProvider(createCanvasBackend());
1106
+ return;
1107
+ }
1108
+ void fontSet.ready.then(() => {
1081
1109
  facet.swapMeasurementProvider(createCanvasBackend());
1082
1110
  });
1083
1111
  }, [props.layoutFacet]);
@@ -1634,6 +1662,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1634
1662
  data-layer="page-card-backgrounds"
1635
1663
  />
1636
1664
  ) : null}
1665
+ {isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
1666
+ <TwFloatingImageLayer
1667
+ facet={props.layoutFacet}
1668
+ scrollRoot={pageStackScrollRoot}
1669
+ renderFrameRevision={renderFrameRevision}
1670
+ visiblePageIndexRange={visiblePageIndexRange}
1671
+ snapshot={snapshot}
1672
+ mediaPreviews={props.mediaPreviews}
1673
+ plane="behind"
1674
+ />
1675
+ ) : null}
1637
1676
  {isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
1638
1677
  <div
1639
1678
  aria-hidden="true"
@@ -1688,6 +1727,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
1688
1727
  >
1689
1728
  {props.document}
1690
1729
  </div>
1730
+ {isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
1731
+ <TwFloatingImageLayer
1732
+ facet={props.layoutFacet}
1733
+ scrollRoot={pageStackScrollRoot}
1734
+ renderFrameRevision={renderFrameRevision}
1735
+ visiblePageIndexRange={visiblePageIndexRange}
1736
+ snapshot={snapshot}
1737
+ mediaPreviews={props.mediaPreviews}
1738
+ plane="front"
1739
+ onActivateFloatingImage={props.onActivateFloatingImage}
1740
+ />
1741
+ ) : null}
1691
1742
  {props.layoutFacet ? (
1692
1743
  <TwChromeOverlay
1693
1744
  facet={props.layoutFacet}