@beyondwork/docx-react-component 1.0.57 → 1.0.59

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 (135) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/src/api/awareness-identity-types.ts +4 -2
  4. package/src/api/comment-negotiation-types.ts +4 -1
  5. package/src/api/external-custody-types.ts +16 -0
  6. package/src/api/internal/build-ref-projections.ts +108 -0
  7. package/src/api/package-version.ts +1 -1
  8. package/src/api/participants-types.ts +11 -1
  9. package/src/api/public-types.ts +1149 -8
  10. package/src/api/scope-metadata-resolver-types.ts +6 -0
  11. package/src/compare/diff-engine.ts +3 -0
  12. package/src/core/commands/formatting-commands.ts +1 -0
  13. package/src/core/commands/index.ts +225 -16
  14. package/src/core/commands/legacy-form-field-commands.ts +181 -0
  15. package/src/core/commands/table-structure-commands.ts +149 -31
  16. package/src/core/selection/mapping.ts +20 -0
  17. package/src/core/state/editor-state.ts +2 -1
  18. package/src/index.ts +28 -0
  19. package/src/io/docx-session.ts +22 -3
  20. package/src/io/export/export-session.ts +11 -7
  21. package/src/io/export/ooxml-namespaces.ts +47 -0
  22. package/src/io/export/reattach-preserved-parts.ts +4 -16
  23. package/src/io/export/serialize-comments.ts +3 -131
  24. package/src/io/export/serialize-ffdata.ts +89 -0
  25. package/src/io/export/serialize-headers-footers.ts +5 -0
  26. package/src/io/export/serialize-main-document.ts +224 -34
  27. package/src/io/export/serialize-numbering.ts +22 -2
  28. package/src/io/export/serialize-revisions.ts +99 -0
  29. package/src/io/export/serialize-tables.ts +9 -0
  30. package/src/io/export/split-review-boundaries.ts +1 -0
  31. package/src/io/export/table-properties-xml.ts +14 -0
  32. package/src/io/load-scheduler.ts +70 -28
  33. package/src/io/normalize/normalize-text.ts +13 -0
  34. package/src/io/ooxml/_mini-xml.ts +198 -0
  35. package/src/io/ooxml/canonicalize-payload.ts +1 -4
  36. package/src/io/ooxml/chart/chart-style-table.ts +4 -3
  37. package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
  38. package/src/io/ooxml/chart/parse-series.ts +2 -1
  39. package/src/io/ooxml/chart/resolve-color.ts +2 -2
  40. package/src/io/ooxml/chart/types.ts +6 -434
  41. package/src/io/ooxml/comment-presentation-payload.ts +6 -5
  42. package/src/io/ooxml/highlight-colors.ts +8 -5
  43. package/src/io/ooxml/parse-anchor.ts +68 -53
  44. package/src/io/ooxml/parse-comments.ts +14 -142
  45. package/src/io/ooxml/parse-complex-content.ts +3 -106
  46. package/src/io/ooxml/parse-drawing.ts +100 -195
  47. package/src/io/ooxml/parse-ffdata.ts +93 -0
  48. package/src/io/ooxml/parse-fields.ts +7 -146
  49. package/src/io/ooxml/parse-fill.ts +88 -8
  50. package/src/io/ooxml/parse-font-table.ts +5 -105
  51. package/src/io/ooxml/parse-footnotes.ts +28 -152
  52. package/src/io/ooxml/parse-headers-footers.ts +106 -212
  53. package/src/io/ooxml/parse-inline-media.ts +3 -200
  54. package/src/io/ooxml/parse-main-document.ts +180 -217
  55. package/src/io/ooxml/parse-numbering.ts +154 -335
  56. package/src/io/ooxml/parse-object.ts +147 -0
  57. package/src/io/ooxml/parse-ole-relationship.ts +82 -0
  58. package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
  59. package/src/io/ooxml/parse-picture-sdt.ts +85 -0
  60. package/src/io/ooxml/parse-picture.ts +120 -39
  61. package/src/io/ooxml/parse-revisions.ts +285 -51
  62. package/src/io/ooxml/parse-settings.ts +6 -99
  63. package/src/io/ooxml/parse-shapes.ts +25 -140
  64. package/src/io/ooxml/parse-styles.ts +3 -218
  65. package/src/io/ooxml/parse-tables.ts +76 -256
  66. package/src/io/ooxml/parse-theme.ts +1 -4
  67. package/src/io/ooxml/property-grab-bag.ts +5 -47
  68. package/src/io/ooxml/xml-element-serialize.ts +32 -0
  69. package/src/io/ooxml/xml-parser.ts +183 -0
  70. package/src/legal/bookmarks.ts +1 -1
  71. package/src/legal/cross-references.ts +1 -1
  72. package/src/legal/defined-terms.ts +1 -1
  73. package/src/legal/{_document-root.ts → document-root.ts} +8 -0
  74. package/src/legal/signature-blocks.ts +1 -1
  75. package/src/model/canonical-document.ts +165 -6
  76. package/src/model/chart-types.ts +439 -0
  77. package/src/model/snapshot.ts +3 -1
  78. package/src/review/store/comment-remapping.ts +24 -11
  79. package/src/review/store/revision-actions.ts +482 -2
  80. package/src/review/store/revision-store.ts +15 -0
  81. package/src/review/store/revision-types.ts +76 -0
  82. package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
  83. package/src/runtime/collab/runtime-collab-sync.ts +33 -0
  84. package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
  85. package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
  86. package/src/runtime/document-runtime.ts +544 -35
  87. package/src/runtime/document-search.ts +176 -0
  88. package/src/runtime/edit-ops/index.ts +18 -2
  89. package/src/runtime/footnote-resolver.ts +130 -0
  90. package/src/runtime/layout/layout-engine-instance.ts +31 -4
  91. package/src/runtime/layout/layout-engine-version.ts +37 -1
  92. package/src/runtime/layout/page-graph.ts +14 -1
  93. package/src/runtime/layout/resolved-formatting-state.ts +21 -0
  94. package/src/runtime/numbering-prefix.ts +17 -0
  95. package/src/runtime/query-scopes.ts +183 -0
  96. package/src/runtime/resolved-numbering-geometry.ts +37 -6
  97. package/src/runtime/revision-runtime.ts +27 -1
  98. package/src/runtime/scope-resolver.ts +60 -0
  99. package/src/runtime/selection/post-edit-validator.ts +60 -6
  100. package/src/runtime/structure-ops/index.ts +20 -4
  101. package/src/runtime/surface-projection.ts +293 -18
  102. package/src/runtime/table-schema.ts +6 -0
  103. package/src/runtime/theme-color-resolver.ts +2 -2
  104. package/src/runtime/units.ts +9 -0
  105. package/src/runtime/workflow-rail-segments.ts +4 -0
  106. package/src/ui/WordReviewEditor.tsx +258 -44
  107. package/src/ui/editor-runtime-boundary.ts +13 -0
  108. package/src/ui/editor-shell-view.tsx +4 -1
  109. package/src/ui/headless/chrome-registry.ts +53 -0
  110. package/src/ui/headless/selection-tool-resolver.ts +11 -1
  111. package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
  112. package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
  113. package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
  114. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
  115. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
  116. package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
  117. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
  118. package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
  119. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
  120. package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
  121. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
  122. package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
  123. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
  124. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
  125. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
  126. package/src/ui-tailwind/index.ts +9 -0
  127. package/src/ui-tailwind/page-chrome-model.ts +77 -5
  128. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
  129. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
  130. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
  131. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
  132. package/src/ui-tailwind/theme/tokens.ts +14 -0
  133. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
  134. package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
  135. package/src/validation/diagnostics.ts +1 -0
@@ -29,6 +29,8 @@ import type {
29
29
  SdtNode,
30
30
  ShapeNode,
31
31
  SmartArtPreviewNode,
32
+ BorderSpec,
33
+ TableBorders,
32
34
  TableCellBorders,
33
35
  TableNode,
34
36
  TextMark,
@@ -65,6 +67,14 @@ import { resolveHyperlinkRunFormatting } from "./hyperlink-color-resolver.ts";
65
67
  import { concretizeThemeColors, ThemeColorResolver } from "./theme-color-resolver.ts";
66
68
  import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
67
69
 
70
+ const SAFE_CSS_HEX_COLOR_RE = /^#?(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
71
+ const PICTURE_EFFECT_SCHEME_ALIASES: Record<string, string> = {
72
+ tx1: "dk1",
73
+ bg1: "lt1",
74
+ tx2: "dk2",
75
+ bg2: "lt2",
76
+ };
77
+
68
78
  interface ParagraphAccumulator {
69
79
  blockId: string;
70
80
  kind: "paragraph";
@@ -399,10 +409,37 @@ function createTableBlock(
399
409
  const rows: SurfaceTableRowSnapshot[] = [];
400
410
  const rowSpans = computeTableRowSpans(table);
401
411
  const resolvedTable = resolveTableStyleResolution(table, document.styles.tables);
412
+ // SOW gap G3 — build the theme color resolver once per table so every cell
413
+ // shading with `w:themeFill` can resolve the theme slot + tint/shade without
414
+ // reconstructing the resolver per cell.
415
+ const tableThemeResolver = document.subParts?.canonicalTheme
416
+ ? new ThemeColorResolver(document.subParts.canonicalTheme)
417
+ : undefined;
418
+
419
+ // SOW gap G5 — fold table-level borders into per-cell rendering. The CCEP
420
+ // SOW form tables ship only `w:tblBorders` (single sz=4 on every side + insideH
421
+ // + insideV) and no per-cell borders, so every interior edge must fall back
422
+ // to `insideH`/`insideV` and perimeter edges must fall back to outer sides.
423
+ const tableBorders = resolvedTable.tableResolved.borders ?? table.borders;
424
+ const totalColumns =
425
+ table.gridColumns.length > 0
426
+ ? table.gridColumns.length
427
+ : table.rows.reduce(
428
+ (max, r) =>
429
+ Math.max(
430
+ max,
431
+ (r.gridBefore ?? 0) +
432
+ r.cells.reduce((sum, c) => sum + (c.gridSpan ?? 1), 0) +
433
+ (r.gridAfter ?? 0),
434
+ ),
435
+ 0,
436
+ );
437
+ const totalRows = table.rows.length;
402
438
 
403
439
  for (const [rowIndex, row] of table.rows.entries()) {
404
440
  const cells: SurfaceTableCellSnapshot[] = [];
405
441
  const resolvedRow = resolvedTable.rows[rowIndex];
442
+ let columnCursor = row.gridBefore ?? 0;
406
443
  for (const [cellIndex, cell] of row.cells.entries()) {
407
444
  const cellContent: SurfaceBlockSnapshot[] = [];
408
445
  for (const child of cell.children) {
@@ -420,7 +457,30 @@ function createTableBlock(
420
457
  innerCursor = result.nextCursor;
421
458
  }
422
459
  const resolvedCell = resolvedRow?.cells[cellIndex];
423
- const cellBorders = resolveCellBorderStyles(resolvedCell?.borders ?? cell.borders);
460
+ const cellSpan = cell.gridSpan ?? 1;
461
+ const startColumn = columnCursor;
462
+ const endColumn = columnCursor + cellSpan - 1;
463
+ const position = {
464
+ isTopEdge: rowIndex === 0,
465
+ isBottomEdge: rowIndex === totalRows - 1,
466
+ isLeftEdge: startColumn === 0,
467
+ isRightEdge: endColumn === totalColumns - 1,
468
+ };
469
+ columnCursor = endColumn + 1;
470
+ const cellBorders = resolveCellBorderStyles(
471
+ resolvedCell?.borders ?? cell.borders,
472
+ tableBorders,
473
+ position,
474
+ );
475
+ // SOW gap G3 — resolve effective cell fill honoring theme references.
476
+ // `w:themeFill` wins when `w:fill` is absent or "auto"; the resolver
477
+ // applies `w:clrSchemeMapping` remap + byte-form tint/shade from
478
+ // `w:themeFillTint` / `w:themeFillShade`. Returned hex has no leading
479
+ // "#" to match the direct-fill convention below.
480
+ const effectiveFill = resolveCellShadingFill(
481
+ resolvedCell?.shading ?? cell.shading,
482
+ tableThemeResolver,
483
+ );
424
484
  // R2a: project the resolved conditional-format regions into CSS class
425
485
  // names so the NodeView can paint band colors via theme vars instead of
426
486
  // inline styles. Direct shading overrides still win at render time.
@@ -436,7 +496,7 @@ function createTableBlock(
436
496
  verticalMerge: cell.verticalMerge ?? null,
437
497
  colspan: cell.gridSpan ?? 1,
438
498
  rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
439
- ...(resolvedCell?.shading?.fill ? { backgroundColor: `#${resolvedCell.shading.fill}` } : {}),
499
+ ...(effectiveFill ? { backgroundColor: `#${effectiveFill}` } : {}),
440
500
  ...(resolvedCell?.verticalAlign ? { verticalAlign: resolvedCell.verticalAlign } : {}),
441
501
  ...(cellBorders.borderTop ? { borderTop: cellBorders.borderTop } : {}),
442
502
  ...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
@@ -487,6 +547,17 @@ function createTableBlock(
487
547
  }
488
548
  : undefined;
489
549
 
550
+ // SOW gap G1 — when the table's own width is expressed as a percent
551
+ // (`w:tblW w:type="pct"`), compute relative column proportions so the
552
+ // node-view can emit `<col style="width: NN%">` instead of absolute `pt`
553
+ // widths. This matters for the CCEP SOW, whose tables all use pct widths
554
+ // at both table and cell level: absolute pt columns inside a `width:100%`
555
+ // container drift against the container on zoom + re-layout.
556
+ const gridColumnsRelative = computeRelativeGridColumns(
557
+ table.gridColumns,
558
+ tr.widthType,
559
+ );
560
+
490
561
  return {
491
562
  block: {
492
563
  blockId: `table-${tableIndex}`,
@@ -495,6 +566,7 @@ function createTableBlock(
495
566
  to: innerCursor,
496
567
  styleId: table.styleId,
497
568
  gridColumns: table.gridColumns,
569
+ ...(gridColumnsRelative ? { gridColumnsRelative } : {}),
498
570
  ...(resolvedTable.table?.alignment ? { alignment: resolvedTable.table.alignment } : {}),
499
571
  tblLook: resolvedTable.effectiveTblLook,
500
572
  ...(tableResolvedAttr ? { tableResolved: tableResolvedAttr } : {}),
@@ -554,20 +626,148 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
554
626
  return rowSpans;
555
627
  }
556
628
 
629
+ /**
630
+ * SOW gap G3 — resolve the final paintable fill for a cell `w:shd`. Precedence
631
+ * matches Word: a concrete `w:fill` (non-"auto") wins; otherwise the theme
632
+ * reference in `w:themeFill` + `w:themeFillTint` / `w:themeFillShade` is
633
+ * resolved through the theme cascade. Returns a 6-digit hex WITHOUT a leading
634
+ * "#" to match the direct-fill convention, or undefined when no paintable
635
+ * color can be derived.
636
+ */
637
+ function resolveCellShadingFill(
638
+ shading:
639
+ | {
640
+ fill?: string;
641
+ themeFill?: string;
642
+ themeFillTint?: string;
643
+ themeFillShade?: string;
644
+ }
645
+ | undefined,
646
+ themeResolver: ThemeColorResolver | undefined,
647
+ ): string | undefined {
648
+ if (!shading) return undefined;
649
+ const direct = shading.fill;
650
+ if (direct && direct !== "auto") return direct;
651
+ const themeSlot = shading.themeFill;
652
+ if (!themeSlot || !themeResolver) return direct; // keep "auto" fallback if present
653
+ const resolved = themeResolver.resolveWordThemeColor(
654
+ themeSlot,
655
+ shading.themeFillTint,
656
+ shading.themeFillShade,
657
+ );
658
+ if (!resolved || resolved === "auto") return direct;
659
+ // Strip leading "#" if the resolver returned one.
660
+ return resolved.startsWith("#") ? resolved.slice(1) : resolved;
661
+ }
662
+
663
+ /**
664
+ * SOW gap G1 — derive relative column widths (percent 0–100, sum 100)
665
+ * from the canonical `gridColumns` (twips). Only populated when the table
666
+ * itself is sized in percent; other width types keep the absolute pt path
667
+ * in the node-view. Returns null when the grid is empty, all-zero, or the
668
+ * width type is not `pct`.
669
+ */
670
+ function computeRelativeGridColumns(
671
+ gridColumns: readonly number[],
672
+ widthType: "auto" | "dxa" | "pct" | "nil" | undefined,
673
+ ): number[] | null {
674
+ if (widthType !== "pct") return null;
675
+ if (gridColumns.length === 0) return null;
676
+ let total = 0;
677
+ for (const col of gridColumns) total += col > 0 ? col : 0;
678
+ if (total <= 0) return null;
679
+ return gridColumns.map((col) => (col > 0 ? (col / total) * 100 : 0));
680
+ }
681
+
682
+ /**
683
+ * Render a typed `BorderSpec` shape to a CSS shorthand string. Returns
684
+ * undefined for absent / "none" / "nil" specs so callers can keep their
685
+ * falsy-check pattern.
686
+ */
687
+ function borderSpecToCssShorthand(
688
+ spec: BorderSpec | undefined | null,
689
+ ): string | undefined {
690
+ if (!spec) return undefined;
691
+ if (spec.value === "none" || spec.value === "nil") return undefined;
692
+ const width = spec.size ? `${Math.max(1, Math.round(spec.size / 8))}px` : "1px";
693
+ const style =
694
+ spec.value === "double"
695
+ ? "double"
696
+ : spec.value === "dashed" || spec.value === "dashSmallGap"
697
+ ? "dashed"
698
+ : spec.value === "dotted"
699
+ ? "dotted"
700
+ : "solid";
701
+ const color = spec.color && spec.color !== "auto" ? `#${spec.color}` : "currentColor";
702
+ return `${width} ${style} ${color}`;
703
+ }
704
+
705
+ /**
706
+ * SOW gap G5 — border cascade for an individual cell. When the cell does not
707
+ * declare `w:tcBorders.<side>`, the renderer falls back to the table-level
708
+ * `w:tblBorders`: outer sides (`top`/`right`/`bottom`/`left`) apply when the
709
+ * cell sits on the corresponding table perimeter; `insideH`/`insideV` apply
710
+ * to interior edges. The CCEP SOW relies on this cascade — its form tables
711
+ * declare only `w:tblBorders` and expect every cell to carry visible 1px
712
+ * borders on all four sides.
713
+ *
714
+ * `position` is optional; when absent, the function falls back to the legacy
715
+ * cell-only behavior (no inside/outer cascade) so older callers keep working.
716
+ */
557
717
  function resolveCellBorderStyles(
558
718
  borders: TableCellBorders | undefined,
719
+ tableBorders?: TableBorders,
720
+ position?: {
721
+ isTopEdge: boolean;
722
+ isBottomEdge: boolean;
723
+ isLeftEdge: boolean;
724
+ isRightEdge: boolean;
725
+ },
559
726
  ): { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } {
560
- if (!borders) return {};
727
+ const pick = (
728
+ cellSide: BorderSpec | undefined,
729
+ tableOuter: BorderSpec | undefined,
730
+ tableInside: BorderSpec | undefined,
731
+ onEdge: boolean,
732
+ ): string | undefined => {
733
+ if (cellSide && cellSide.value !== "nil") {
734
+ const rendered = borderSpecToCssShorthand(cellSide);
735
+ if (rendered) return rendered;
736
+ if (cellSide.value === "none") return undefined; // explicit "off" — no fallback
737
+ }
738
+ const fallback = onEdge ? tableOuter : tableInside;
739
+ return borderSpecToCssShorthand(fallback);
740
+ };
741
+
561
742
  const result: { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } = {};
562
- const sides = [["top", "borderTop"], ["right", "borderRight"], ["bottom", "borderBottom"], ["left", "borderLeft"]] as const;
563
- for (const [side, key] of sides) {
564
- const spec = borders[side];
565
- if (!spec || spec.value === "none" || spec.value === "nil") continue;
566
- const width = spec.size ? `${Math.max(1, Math.round(spec.size / 8))}px` : "1px";
567
- const style = spec.value === "double" ? "double" : spec.value === "dashed" || spec.value === "dashSmallGap" ? "dashed" : spec.value === "dotted" ? "dotted" : "solid";
568
- const color = spec.color && spec.color !== "auto" ? `#${spec.color}` : "currentColor";
569
- result[key] = `${width} ${style} ${color}`;
570
- }
743
+ const top = pick(
744
+ borders?.top,
745
+ tableBorders?.top,
746
+ tableBorders?.insideH,
747
+ position?.isTopEdge ?? true,
748
+ );
749
+ const right = pick(
750
+ borders?.right,
751
+ tableBorders?.right,
752
+ tableBorders?.insideV,
753
+ position?.isRightEdge ?? true,
754
+ );
755
+ const bottom = pick(
756
+ borders?.bottom,
757
+ tableBorders?.bottom,
758
+ tableBorders?.insideH,
759
+ position?.isBottomEdge ?? true,
760
+ );
761
+ const left = pick(
762
+ borders?.left,
763
+ tableBorders?.left,
764
+ tableBorders?.insideV,
765
+ position?.isLeftEdge ?? true,
766
+ );
767
+ if (top) result.borderTop = top;
768
+ if (right) result.borderRight = right;
769
+ if (bottom) result.borderBottom = bottom;
770
+ if (left) result.borderLeft = left;
571
771
  return result;
572
772
  }
573
773
 
@@ -651,13 +851,14 @@ function createParagraphBlock(
651
851
  // by the placeholder path. Segment-level work inside
652
852
  // `appendInlineSegments` is suppressed symmetrically via the same
653
853
  // `cullBuild` flag, preserving cursor arithmetic.
654
- const effectiveNumbering = cullBuild
655
- ? undefined
656
- : resolveEffectiveParagraphNumbering(document, paragraph);
854
+ // Always resolve numbering so the counter advances for culled paragraphs too.
855
+ const effectiveNumbering = resolveEffectiveParagraphNumbering(document, paragraph);
657
856
  const resolvedNumbering =
658
857
  !cullBuild && effectiveNumbering
659
858
  ? numberingPrefixResolver.resolveDetailed(effectiveNumbering, paragraph)
660
- : null;
859
+ : effectiveNumbering
860
+ ? advanceNumberingCounterOnly(numberingPrefixResolver, effectiveNumbering)
861
+ : null;
661
862
 
662
863
  // Task 11: compute cascaded paragraph formatting (expensive — styles-catalog walk).
663
864
  const stylesCatalog = document.styles;
@@ -760,6 +961,14 @@ function createParagraphBlock(
760
961
  };
761
962
  }
762
963
 
964
+ function advanceNumberingCounterOnly(
965
+ resolver: NumberingPrefixResolver,
966
+ numbering: NonNullable<ParagraphNode["numbering"]>,
967
+ ): null {
968
+ resolver.resolve(numbering);
969
+ return null;
970
+ }
971
+
763
972
  function resolveEffectiveParagraphNumbering(
764
973
  document: CanonicalDocumentEnvelope,
765
974
  paragraph: ParagraphNode,
@@ -1100,7 +1309,7 @@ function appendInlineSegments(
1100
1309
  const mediaId = c.mediaId ?? `drawing-frame-${start}`;
1101
1310
  const state: "editable" | "missing" = c.mediaId ? "editable" : "missing";
1102
1311
  const anchor = surfaceAnchorFromGeometry(node.anchor);
1103
- const pictureEffects = surfacePictureEffectsFromContent(c);
1312
+ const pictureEffects = surfacePictureEffectsFromContent(c, themeResolver);
1104
1313
  paragraph.segments.push({
1105
1314
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
1106
1315
  kind: "image",
@@ -1303,6 +1512,7 @@ function surfaceAnchorFromGeometry(
1303
1512
  ...(anchor.allowOverlap !== undefined ? { allowOverlap: anchor.allowOverlap } : {}),
1304
1513
  ...(anchor.simplePos !== undefined ? { simplePos: anchor.simplePos } : {}),
1305
1514
  ...(anchor.docPr ? { docPr: { ...anchor.docPr } } : {}),
1515
+ ...(anchor.wrapPolygon ? { wrapPolygon: anchor.wrapPolygon } : {}),
1306
1516
  };
1307
1517
  }
1308
1518
 
@@ -1313,14 +1523,20 @@ function surfaceAnchorFromGeometry(
1313
1523
  */
1314
1524
  function surfacePictureEffectsFromContent(
1315
1525
  content: PictureContent,
1526
+ themeResolver?: ThemeColorResolver,
1316
1527
  ): SurfacePictureEffects | undefined {
1528
+ const outerShadow = resolveSurfacePictureShadow(content.outerShadow, themeResolver);
1529
+ const glow = resolveSurfacePictureGlow(content.glow, themeResolver);
1317
1530
  const has =
1318
1531
  content.srcRect !== undefined ||
1319
1532
  content.rotation !== undefined ||
1320
1533
  content.flipH !== undefined ||
1321
1534
  content.flipV !== undefined ||
1322
1535
  content.presetGeom !== undefined ||
1323
- content.stretch !== undefined;
1536
+ content.stretch !== undefined ||
1537
+ content.softEdgeRadius !== undefined ||
1538
+ outerShadow !== undefined ||
1539
+ glow !== undefined;
1324
1540
  if (!has) return undefined;
1325
1541
  return {
1326
1542
  ...(content.srcRect ? { srcRect: { ...content.srcRect } } : {}),
@@ -1329,9 +1545,63 @@ function surfacePictureEffectsFromContent(
1329
1545
  ...(content.flipV !== undefined ? { flipV: content.flipV } : {}),
1330
1546
  ...(content.presetGeom !== undefined ? { presetGeom: content.presetGeom } : {}),
1331
1547
  ...(content.stretch !== undefined ? { stretch: content.stretch } : {}),
1548
+ ...(content.softEdgeRadius !== undefined ? { softEdgeRadius: content.softEdgeRadius } : {}),
1549
+ ...(outerShadow ? { outerShadow } : {}),
1550
+ ...(glow ? { glow } : {}),
1332
1551
  };
1333
1552
  }
1334
1553
 
1554
+ function resolveSurfacePictureShadow(
1555
+ shadow: PictureContent["outerShadow"] | undefined,
1556
+ themeResolver?: ThemeColorResolver,
1557
+ ): SurfacePictureEffects["outerShadow"] | undefined {
1558
+ if (!shadow) return undefined;
1559
+ const color = resolveSurfacePictureEffectColor(shadow.color, shadow.colorType, themeResolver);
1560
+ if (!color) return undefined;
1561
+ return {
1562
+ blurRad: shadow.blurRad,
1563
+ dist: shadow.dist,
1564
+ dir: shadow.dir,
1565
+ color,
1566
+ colorType: "srgbClr",
1567
+ };
1568
+ }
1569
+
1570
+ function resolveSurfacePictureGlow(
1571
+ glow: PictureContent["glow"] | undefined,
1572
+ themeResolver?: ThemeColorResolver,
1573
+ ): SurfacePictureEffects["glow"] | undefined {
1574
+ if (!glow) return undefined;
1575
+ const color = resolveSurfacePictureEffectColor(glow.color, glow.colorType, themeResolver);
1576
+ if (!color) return undefined;
1577
+ return {
1578
+ radius: glow.radius,
1579
+ color,
1580
+ colorType: "srgbClr",
1581
+ };
1582
+ }
1583
+
1584
+ function resolveSurfacePictureEffectColor(
1585
+ color: string,
1586
+ colorType: "srgbClr" | "schemeClr",
1587
+ themeResolver?: ThemeColorResolver,
1588
+ ): string | undefined {
1589
+ if (colorType === "srgbClr") {
1590
+ return normalizeSafeCssHexColor(color);
1591
+ }
1592
+ const slot = PICTURE_EFFECT_SCHEME_ALIASES[color] ?? color;
1593
+ if (slot === "phClr") return undefined;
1594
+ const resolved = themeResolver?.resolveSchemeSlot(slot);
1595
+ return normalizeSafeCssHexColor(resolved);
1596
+ }
1597
+
1598
+ function normalizeSafeCssHexColor(value: string | undefined): string | undefined {
1599
+ if (!value) return undefined;
1600
+ const trimmed = value.trim();
1601
+ if (!SAFE_CSS_HEX_COLOR_RE.test(trimmed)) return undefined;
1602
+ return trimmed.replace(/^#/, "").toUpperCase();
1603
+ }
1604
+
1335
1605
  /**
1336
1606
  * V2c.5 — Extract the first paragraph's plain text from a parsed
1337
1607
  * `txbxBlocks` tree for the `txbxText` segment preview. The recursion
@@ -1790,6 +2060,10 @@ function summarizePreviewInline(node: InlineNode): string {
1790
2060
  return node.text ? `[VML: ${node.text}]` : "[Legacy VML drawing]";
1791
2061
  case "drawing_frame":
1792
2062
  return node.content.type === "picture" ? "[Image]" : "[Drawing]";
2063
+ case "ole_embed":
2064
+ return node.progId
2065
+ ? `[Embedded object: ${node.progId}]`
2066
+ : "[Embedded object]";
1793
2067
  }
1794
2068
  }
1795
2069
 
@@ -1860,6 +2134,7 @@ function toSurfaceResolvedNumbering(
1860
2134
  ? { textColumn: { ...numbering.geometry.textColumn } }
1861
2135
  : {}),
1862
2136
  },
2137
+ ...(numbering.picBulletMediaId ? { picBulletMediaId: numbering.picBulletMediaId } : {}),
1863
2138
  };
1864
2139
  }
1865
2140
 
@@ -210,6 +210,12 @@ export const tableNodeSpec: NodeSpec = {
210
210
  styleId: { default: null },
211
211
  propertiesXml: { default: null },
212
212
  gridColumns: { default: [] },
213
+ /**
214
+ * SOW gap G1 — relative column widths (percent 0–100) used for the
215
+ * `<colgroup>` when the table width itself is expressed as a percent.
216
+ * `null` = use absolute pt widths from `gridColumns`.
217
+ */
218
+ gridColumnsRelative: { default: null },
213
219
  alignment: { default: null },
214
220
  tblLookFirstRow: { default: false },
215
221
  tblLookLastRow: { default: false },
@@ -26,6 +26,7 @@
26
26
 
27
27
  import type { CanonicalRunFormatting, CanonicalTheme, ClrSchemeMappingSlot, ResolvedTheme } from "../model/canonical-document.ts";
28
28
  import { resolveThemeColor } from "../io/ooxml/parse-theme.ts";
29
+ import { GRADIENT_STOP_UNITS } from "./units.ts";
29
30
 
30
31
  /**
31
32
  * DrawingML color modifier (ECMA-376 §20.1.2.3.x).
@@ -40,7 +41,6 @@ export interface DrawingMlColorMod {
40
41
  value: number;
41
42
  }
42
43
 
43
- const DML_UNIT = 100_000;
44
44
 
45
45
  /**
46
46
  * Unified runtime theme color resolver.
@@ -251,7 +251,7 @@ function applyDmlMods(
251
251
  ): string {
252
252
  let rgb = parseHexToRgbDml(hex);
253
253
  for (const mod of mods) {
254
- const frac = mod.value / DML_UNIT;
254
+ const frac = mod.value / GRADIENT_STOP_UNITS;
255
255
  switch (mod.kind) {
256
256
  case "lumMod": {
257
257
  const hsl = rgbToHsl(rgb);
@@ -16,9 +16,18 @@
16
16
  /** EMU (English Metric Units) per CSS pixel at 96 dpi. */
17
17
  export const EMU_PER_PX = 9525;
18
18
 
19
+ /** EMU per inch — 914 400 EMU = 1 inch (ECMA-376 §20.1.2.2). */
20
+ export const EMU_PER_INCH = 914_400;
21
+
19
22
  /** OOXML rotation units per degree (`a:xfrm a:rot` = 60 000ths°). */
20
23
  export const ROTATION_UNITS_PER_DEGREE = 60000;
21
24
 
25
+ /** OOXML gradient-stop and crop-offset unit (100 000 = 100%). */
26
+ export const GRADIENT_STOP_UNITS = 100_000;
27
+
28
+ /** OOXML percentage parts unit (5 000 = 100%). Used for table pct widths. */
29
+ export const PERCENTAGE_PARTS = 5_000;
30
+
22
31
  /** OOXML picture-crop units per percent (`a:srcRect` uses 1/1000 of a percent). */
23
32
  export const SRCRECT_UNITS_PER_PERCENT = 1000;
24
33
 
@@ -387,15 +387,18 @@ export function attachScopeCardModel(
387
387
  }
388
388
 
389
389
  const workItemByScope = new Map<string, string>();
390
+ const anchorByScope = new Map<string, EditorAnchorProjection>();
390
391
  for (const scope of input.scopes ?? []) {
391
392
  if (scope.workItemId) {
392
393
  workItemByScope.set(scope.scopeId, scope.workItemId);
393
394
  }
395
+ anchorByScope.set(scope.scopeId, scope.anchor);
394
396
  }
395
397
 
396
398
  const models: ScopeCardModel[] = [];
397
399
  for (const segment of firstByScope.values()) {
398
400
  const workItemId = workItemByScope.get(segment.scopeId);
401
+ const anchor = anchorByScope.get(segment.scopeId);
399
402
  const issue = resolveIssueForScope(
400
403
  segment.scopeId,
401
404
  workItemId,
@@ -423,6 +426,7 @@ export function attachScopeCardModel(
423
426
  label: segment.label ?? "",
424
427
  posture: segment.posture,
425
428
  primaryAnchorRect,
429
+ ...(anchor ? { anchor } : {}),
426
430
  ...(issue ? { issue } : {}),
427
431
  suggestionGroupIds: suggestionGroups.map((group) => group.groupId),
428
432
  suggestionGroups,