@beyondwork/docx-react-component 1.0.59 → 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 (46) hide show
  1. package/package.json +33 -44
  2. package/src/api/public-types.ts +43 -0
  3. package/src/core/state/editor-state.ts +2 -0
  4. package/src/io/docx-session.ts +167 -8
  5. package/src/io/export/serialize-footnotes.ts +36 -5
  6. package/src/io/export/serialize-headers-footers.ts +7 -0
  7. package/src/io/export/serialize-main-document.ts +25 -18
  8. package/src/io/export/serialize-paragraph-formatting.ts +6 -0
  9. package/src/io/export/serialize-settings.ts +130 -3
  10. package/src/io/normalize/normalize-text.ts +8 -4
  11. package/src/io/ooxml/parse-footnotes.ts +11 -0
  12. package/src/io/ooxml/parse-headers-footers.ts +117 -42
  13. package/src/io/ooxml/parse-main-document.ts +20 -8
  14. package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
  15. package/src/io/ooxml/parse-settings.ts +91 -1
  16. package/src/io/ooxml/workflow-payload.ts +6 -1
  17. package/src/model/canonical-document.ts +36 -2
  18. package/src/model/snapshot.ts +2 -0
  19. package/src/runtime/diagnostics/build-diagnostic.ts +2 -0
  20. package/src/runtime/diagnostics/code-metadata-table.ts +9 -0
  21. package/src/runtime/document-runtime.ts +770 -21
  22. package/src/runtime/footnote-resolver.ts +32 -8
  23. package/src/runtime/layout/layout-engine-version.ts +7 -1
  24. package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
  25. package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
  26. package/src/runtime/layout/paginated-layout-engine.ts +41 -8
  27. package/src/runtime/layout/resolved-formatting-document.ts +11 -9
  28. package/src/runtime/layout/resolved-formatting-state.ts +4 -0
  29. package/src/runtime/numbering-prefix.ts +26 -2
  30. package/src/runtime/query-scopes.ts +103 -2
  31. package/src/runtime/surface-projection.ts +75 -14
  32. package/src/runtime/table-schema.ts +26 -0
  33. package/src/ui/WordReviewEditor.tsx +25 -0
  34. package/src/ui/editor-runtime-boundary.ts +1 -0
  35. package/src/ui/editor-shell-view.tsx +8 -0
  36. package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
  37. package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
  38. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
  39. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  40. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
  41. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
  42. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
  43. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
  44. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
  45. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
  46. package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
@@ -32,6 +32,7 @@ import type {
32
32
  BorderSpec,
33
33
  TableBorders,
34
34
  TableCellBorders,
35
+ TableCellMargins,
35
36
  TableNode,
36
37
  TextMark,
37
38
  DrawingFrameNode,
@@ -491,6 +492,10 @@ function createTableBlock(
491
492
  .map((region) => `band-${region}`)
492
493
  .join(" ")
493
494
  : null;
495
+ const cellPadding = resolveEffectiveCellMargins(
496
+ cell.margins,
497
+ resolvedTable.table?.cellMargins,
498
+ );
494
499
  cells.push({
495
500
  gridSpan: cell.gridSpan ?? 1,
496
501
  verticalMerge: cell.verticalMerge ?? null,
@@ -502,6 +507,10 @@ function createTableBlock(
502
507
  ...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
503
508
  ...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
504
509
  ...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
510
+ ...(cellPadding.top !== undefined ? { paddingTop: cellPadding.top } : {}),
511
+ ...(cellPadding.right !== undefined ? { paddingRight: cellPadding.right } : {}),
512
+ ...(cellPadding.bottom !== undefined ? { paddingBottom: cellPadding.bottom } : {}),
513
+ ...(cellPadding.left !== undefined ? { paddingLeft: cellPadding.left } : {}),
505
514
  // R3.a Phase 2: per-cell text-flow direction copied from canonical
506
515
  // TableCellNode. Node-view renders `tbRl` / `btLr` as CSS writing-mode.
507
516
  ...(cell.textDirection ? { textDirection: cell.textDirection } : {}),
@@ -626,6 +635,18 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
626
635
  return rowSpans;
627
636
  }
628
637
 
638
+ function resolveEffectiveCellMargins(
639
+ direct: TableCellMargins | undefined,
640
+ tableDefault: TableCellMargins | undefined,
641
+ ): TableCellMargins {
642
+ return {
643
+ ...(direct?.top !== undefined ? { top: direct.top } : tableDefault?.top !== undefined ? { top: tableDefault.top } : {}),
644
+ ...(direct?.right !== undefined ? { right: direct.right } : tableDefault?.right !== undefined ? { right: tableDefault.right } : {}),
645
+ ...(direct?.bottom !== undefined ? { bottom: direct.bottom } : tableDefault?.bottom !== undefined ? { bottom: tableDefault.bottom } : {}),
646
+ ...(direct?.left !== undefined ? { left: direct.left } : tableDefault?.left !== undefined ? { left: tableDefault.left } : {}),
647
+ };
648
+ }
649
+
629
650
  /**
630
651
  * SOW gap G3 — resolve the final paintable fill for a cell `w:shd`. Precedence
631
652
  * matches Word: a concrete `w:fill` (non-"auto") wins; otherwise the theme
@@ -660,6 +681,42 @@ function resolveCellShadingFill(
660
681
  return resolved.startsWith("#") ? resolved.slice(1) : resolved;
661
682
  }
662
683
 
684
+ function resolveSurfaceParagraphShading(
685
+ shading: ParagraphNode["shading"],
686
+ themeResolver: ThemeColorResolver | undefined,
687
+ ): NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["shading"]> {
688
+ const resolvedFill = resolveCellShadingFill(shading, themeResolver);
689
+ return {
690
+ ...(shading?.val ? { val: shading.val } : {}),
691
+ ...(shading?.color ? { color: shading.color } : {}),
692
+ ...(resolvedFill !== undefined
693
+ ? { fill: resolvedFill }
694
+ : shading?.fill !== undefined
695
+ ? { fill: shading.fill }
696
+ : {}),
697
+ };
698
+ }
699
+
700
+ function resolveSurfaceParagraphFormatting(
701
+ formatting: CanonicalParagraphFormatting,
702
+ themeResolver: ThemeColorResolver | undefined,
703
+ ): CanonicalParagraphFormatting {
704
+ if (!formatting.shading) {
705
+ return formatting;
706
+ }
707
+ const resolvedFill = resolveCellShadingFill(formatting.shading, themeResolver);
708
+ if (resolvedFill === undefined && formatting.shading.fill === undefined) {
709
+ return formatting;
710
+ }
711
+ return {
712
+ ...formatting,
713
+ shading: {
714
+ ...formatting.shading,
715
+ ...(resolvedFill !== undefined ? { fill: resolvedFill } : {}),
716
+ },
717
+ };
718
+ }
719
+
663
720
  /**
664
721
  * SOW gap G1 — derive relative column widths (percent 0–100, sum 100)
665
722
  * from the canonical `gridColumns` (twips). Only populated when the table
@@ -842,6 +899,9 @@ function createParagraphBlock(
842
899
  nextCursor: number;
843
900
  lockedFragmentIds: string[];
844
901
  } {
902
+ const themeResolver = document.subParts?.canonicalTheme
903
+ ? new ThemeColorResolver(document.subParts.canonicalTheme)
904
+ : undefined;
845
905
  // L7 Phase 2.9 — viewport bail. When the paragraph is outside the
846
906
  // viewport, the returned block is discarded (the outer caller in
847
907
  // `createEditorSurfaceSnapshot` replaces it with a placeholder-culled
@@ -871,6 +931,9 @@ function createParagraphBlock(
871
931
  { styleId: paragraph.styleId, direct: directParagraphFormatting },
872
932
  stylesCatalog,
873
933
  );
934
+ const surfaceResolvedParagraphFormatting = resolvedParagraphFormatting
935
+ ? resolveSurfaceParagraphFormatting(resolvedParagraphFormatting, themeResolver)
936
+ : undefined;
874
937
 
875
938
  // Task 11: compute cascaded marker run formatting (expensive).
876
939
  const markerRunProperties =
@@ -905,8 +968,8 @@ function createParagraphBlock(
905
968
  },
906
969
  }
907
970
  : {}),
908
- ...(resolvedParagraphFormatting && Object.keys(resolvedParagraphFormatting).length > 0
909
- ? { resolvedParagraphFormatting }
971
+ ...(surfaceResolvedParagraphFormatting && Object.keys(surfaceResolvedParagraphFormatting).length > 0
972
+ ? { resolvedParagraphFormatting: surfaceResolvedParagraphFormatting }
910
973
  : {}),
911
974
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
912
975
  ...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
@@ -915,28 +978,26 @@ function createParagraphBlock(
915
978
  : {}),
916
979
  ...(paragraph.indentation ? { indentation: paragraph.indentation } : {}),
917
980
  ...(paragraph.borders ? { borders: paragraph.borders } : {}),
918
- ...(paragraph.shading ? { shading: paragraph.shading } : {}),
981
+ ...(paragraph.shading
982
+ ? { shading: resolveSurfaceParagraphShading(paragraph.shading, themeResolver) }
983
+ : {}),
919
984
  ...(paragraph.tabStops && paragraph.tabStops.length > 0
920
985
  ? { tabStops: paragraph.tabStops.map((tabStop) => toSurfaceTabStop(tabStop)) }
921
986
  : {}),
922
- ...(paragraph.keepNext ? { keepNext: true } : {}),
923
- ...(paragraph.keepLines ? { keepLines: true } : {}),
924
- ...(paragraph.pageBreakBefore ? { pageBreakBefore: true } : {}),
987
+ ...(paragraph.keepNext !== undefined ? { keepNext: paragraph.keepNext } : {}),
988
+ ...(paragraph.keepLines !== undefined ? { keepLines: paragraph.keepLines } : {}),
989
+ ...(paragraph.pageBreakBefore !== undefined ? { pageBreakBefore: paragraph.pageBreakBefore } : {}),
925
990
  ...(paragraph.widowControl !== undefined ? { widowControl: paragraph.widowControl } : {}),
926
991
  ...(paragraph.outlineLevel !== undefined ? { outlineLevel: paragraph.outlineLevel } : {}),
927
- ...(paragraph.bidi ? { bidi: true } : {}),
928
- ...(paragraph.suppressLineNumbers ? { suppressLineNumbers: true } : {}),
992
+ ...(paragraph.bidi !== undefined ? { bidi: paragraph.bidi } : {}),
993
+ ...(paragraph.suppressLineNumbers !== undefined
994
+ ? { suppressLineNumbers: paragraph.suppressLineNumbers }
995
+ : {}),
929
996
  segments: [],
930
997
  };
931
998
  const lockedFragmentIds: string[] = [];
932
999
  let cursor = start;
933
1000
  const children = Array.isArray(paragraph.children) ? paragraph.children : [];
934
- // Build once per paragraph block — ThemeColorResolver is a thin wrapper
935
- // around CanonicalTheme that applies clrMap remapping. Constructed here so
936
- // it is not recreated on every text segment (inner hot loop).
937
- const themeResolver = document.subParts?.canonicalTheme
938
- ? new ThemeColorResolver(document.subParts.canonicalTheme)
939
- : undefined;
940
1001
 
941
1002
  for (const child of children) {
942
1003
  const result = appendInlineSegments(
@@ -42,6 +42,10 @@ type TableCellAttrs = {
42
42
  borderRight?: string | null;
43
43
  borderBottom?: string | null;
44
44
  borderLeft?: string | null;
45
+ paddingTop?: number | null;
46
+ paddingRight?: number | null;
47
+ paddingBottom?: number | null;
48
+ paddingLeft?: number | null;
45
49
  /** R3.a Phase 2 — per-cell text-flow direction. */
46
50
  textDirection?: "lrTb" | "tbRl" | "btLr" | null;
47
51
  bandClasses?: string | null;
@@ -83,6 +87,12 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
83
87
  const backgroundColor =
84
88
  dom.getAttribute("data-cell-background") ?? dom.style.backgroundColor ?? null;
85
89
  const verticalAlign = dom.getAttribute("data-vertical-align") as "top" | "center" | "bottom" | null;
90
+ const readPadding = (name: string): number | null => {
91
+ const raw = dom.getAttribute(name);
92
+ if (!raw) return null;
93
+ const parsed = Number.parseInt(raw, 10);
94
+ return Number.isFinite(parsed) ? parsed : null;
95
+ };
86
96
 
87
97
  return {
88
98
  colspan,
@@ -99,6 +109,10 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
99
109
  borderRight: dom.getAttribute("data-border-right"),
100
110
  borderBottom: dom.getAttribute("data-border-bottom"),
101
111
  borderLeft: dom.getAttribute("data-border-left"),
112
+ paddingTop: readPadding("data-cell-padding-top"),
113
+ paddingRight: readPadding("data-cell-padding-right"),
114
+ paddingBottom: readPadding("data-cell-padding-bottom"),
115
+ paddingLeft: readPadding("data-cell-padding-left"),
102
116
  bandClasses: dom.getAttribute("data-band-classes"),
103
117
  };
104
118
  }
@@ -133,6 +147,10 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
133
147
  if (nodeAttrs.borderRight) attrs["data-border-right"] = nodeAttrs.borderRight;
134
148
  if (nodeAttrs.borderBottom) attrs["data-border-bottom"] = nodeAttrs.borderBottom;
135
149
  if (nodeAttrs.borderLeft) attrs["data-border-left"] = nodeAttrs.borderLeft;
150
+ if (typeof nodeAttrs.paddingTop === "number") attrs["data-cell-padding-top"] = String(nodeAttrs.paddingTop);
151
+ if (typeof nodeAttrs.paddingRight === "number") attrs["data-cell-padding-right"] = String(nodeAttrs.paddingRight);
152
+ if (typeof nodeAttrs.paddingBottom === "number") attrs["data-cell-padding-bottom"] = String(nodeAttrs.paddingBottom);
153
+ if (typeof nodeAttrs.paddingLeft === "number") attrs["data-cell-padding-left"] = String(nodeAttrs.paddingLeft);
136
154
  if (nodeAttrs.bandClasses) {
137
155
  attrs["data-band-classes"] = nodeAttrs.bandClasses;
138
156
  // Concatenate band classes onto the base `class` so Tailwind's @apply resolves at parse time.
@@ -151,6 +169,10 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
151
169
  if (bBottom) styles.push(`border-bottom: ${bBottom}`);
152
170
  const bLeft = safeCssBorder(nodeAttrs.borderLeft);
153
171
  if (bLeft) styles.push(`border-left: ${bLeft}`);
172
+ if (typeof nodeAttrs.paddingTop === "number") styles.push(`padding-top: ${nodeAttrs.paddingTop / 20}pt`);
173
+ if (typeof nodeAttrs.paddingRight === "number") styles.push(`padding-right: ${nodeAttrs.paddingRight / 20}pt`);
174
+ if (typeof nodeAttrs.paddingBottom === "number") styles.push(`padding-bottom: ${nodeAttrs.paddingBottom / 20}pt`);
175
+ if (typeof nodeAttrs.paddingLeft === "number") styles.push(`padding-left: ${nodeAttrs.paddingLeft / 20}pt`);
154
176
  if (styles.length > 0) attrs.style = styles.join("; ");
155
177
 
156
178
  return attrs;
@@ -190,6 +212,10 @@ const tableCellSpecAttrs = {
190
212
  borderRight: { default: null },
191
213
  borderBottom: { default: null },
192
214
  borderLeft: { default: null },
215
+ paddingTop: { default: null },
216
+ paddingRight: { default: null },
217
+ paddingBottom: { default: null },
218
+ paddingLeft: { default: null },
193
219
  /**
194
220
  * R3.a Phase 2 — per-cell text-flow direction copied from
195
221
  * `TableCellNode.textDirection` ("lrTb" | "tbRl" | "btLr"). Node-view maps
@@ -218,6 +218,7 @@ import {
218
218
  resolveChromePreset,
219
219
  resolveChromeVisibilityForPreset,
220
220
  } from "../ui-tailwind/chrome/chrome-preset-model.ts";
221
+ import { TwRuntimeReplDialog } from "../ui-tailwind/chrome/tw-runtime-repl-dialog.tsx";
221
222
  import { createRuntimeCollabSync } from "../runtime/collab/runtime-collab-sync.ts";
222
223
  import {
223
224
  clearLocalCursorState,
@@ -689,6 +690,7 @@ export function __createWordReviewEditorRefBridge(
689
690
  color,
690
691
  });
691
692
  },
693
+ clearHighlight: (options) => runtime.clearHighlight(options),
692
694
  setAlignment: (alignment) => {
693
695
  applyRuntimeFormattingOperation(runtime, {
694
696
  type: "set-alignment",
@@ -1237,6 +1239,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1237
1239
  const surfaceRef = useRef<TwProseMirrorSurfaceRef | null>(null);
1238
1240
  const selectionToolbarElementRef = useRef<HTMLDivElement | null>(null);
1239
1241
  const shellRef = useRef<HTMLDivElement | null>(null);
1242
+ const editorRefForRepl = useRef<WordReviewEditorRef | null>(null);
1240
1243
  const lastSelectionToolbarKeyRef = useRef<string | null>(null);
1241
1244
  const lastAnnouncedErrorIdRef = useRef<string | null>(null);
1242
1245
  const scopeMetadataResolverRef = useRef<ScopeMetadataResolver | null>(null);
@@ -1817,6 +1820,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1817
1820
  color,
1818
1821
  });
1819
1822
  },
1823
+ clearHighlight: (options) => activeRuntime.clearHighlight(options),
1820
1824
  setAlignment: (alignment) => {
1821
1825
  applyRuntimeFormattingOperation(activeRuntime, {
1822
1826
  type: "set-alignment",
@@ -2304,6 +2308,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
2304
2308
  ...projections,
2305
2309
  }) as WordReviewEditorRef;
2306
2310
  refHolder.current = refValue;
2311
+ editorRefForRepl.current = refValue;
2307
2312
  return refValue;
2308
2313
  },
2309
2314
  [
@@ -3368,6 +3373,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3368
3373
  );
3369
3374
 
3370
3375
  return (
3376
+ <>
3371
3377
  <EditorShellView
3372
3378
  shellRef={shellRef}
3373
3379
  documentId={documentId}
@@ -3486,6 +3492,23 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3486
3492
  onScopeRejectSuggestionGroup={(payload) => {
3487
3493
  applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
3488
3494
  }}
3495
+ mediaPreviews={mediaPreviews}
3496
+ onActivateFloatingImage={(payload) => {
3497
+ activeRuntime.focus();
3498
+ applyRuntimeSelection(activeRuntime, {
3499
+ anchor: payload.from,
3500
+ head: payload.from,
3501
+ isCollapsed: true,
3502
+ activeRange: {
3503
+ kind: "node",
3504
+ at: payload.from,
3505
+ assoc: 1,
3506
+ },
3507
+ ...(payload.storyTarget.kind === "main"
3508
+ ? {}
3509
+ : { storyTarget: payload.storyTarget }),
3510
+ });
3511
+ }}
3489
3512
  onDeselectObject={() => activeRuntime.deselectObject()}
3490
3513
  onScopeAskAgent={(payload) => {
3491
3514
  // Resolve the scope's anchor + story from the facet's card
@@ -3522,6 +3545,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3522
3545
  onEventRef.current?.(eventPayload);
3523
3546
  }}
3524
3547
  />
3548
+ <TwRuntimeReplDialog runtime={activeRuntime} editorRef={editorRefForRepl} />
3549
+ </>
3525
3550
  );
3526
3551
  },
3527
3552
  );
@@ -1071,6 +1071,7 @@ function createLoadingRuntimeBridge(input: {
1071
1071
  rejectChange: () => undefined,
1072
1072
  acceptAllChanges: () => undefined,
1073
1073
  rejectAllChanges: () => undefined,
1074
+ clearHighlight: () => undefined,
1074
1075
  openStory: () => false,
1075
1076
  closeStory: () => undefined,
1076
1077
  getActiveStory: () => input.snapshot.activeStory,
@@ -27,6 +27,7 @@ import type { EditorCommandBag } from "./editor-command-bag.ts";
27
27
  import type { ReviewRailTab } from "../ui-tailwind/review/tw-review-rail.tsx";
28
28
  import { TwReviewWorkspace } from "../ui-tailwind/tw-review-workspace.tsx";
29
29
  import type { EditorViewStateSnapshot } from "../api/public-types.ts";
30
+ import type { MediaPreviewDescriptor } from "../ui-tailwind/editor-surface/pm-state-from-snapshot.ts";
30
31
 
31
32
  export interface EditorShellViewProps {
32
33
  shellRef: React.RefObject<HTMLDivElement | null>;
@@ -129,6 +130,13 @@ export interface EditorShellViewProps {
129
130
  onScopeAskAgent?: (payload: { scopeId: string }) => void;
130
131
  /** N6 — deselects the currently grabbed object; wired to runtime.deselectObject(). */
131
132
  onDeselectObject?: () => void;
133
+ mediaPreviews?: Record<string, MediaPreviewDescriptor>;
134
+ onActivateFloatingImage?: (payload: {
135
+ mediaId: string;
136
+ from: number;
137
+ to: number;
138
+ storyTarget: import("../api/public-types.ts").EditorStoryTarget;
139
+ }) => void;
132
140
  }
133
141
 
134
142
  export function EditorShellView(props: EditorShellViewProps) {