@beyondwork/docx-react-component 1.0.41 → 1.0.43

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 (118) hide show
  1. package/package.json +38 -37
  2. package/src/api/awareness-identity-types.ts +35 -0
  3. package/src/api/comment-negotiation-types.ts +130 -0
  4. package/src/api/comment-presentation-types.ts +106 -0
  5. package/src/api/editor-state-types.ts +110 -0
  6. package/src/api/external-custody-types.ts +74 -0
  7. package/src/api/participants-types.ts +18 -0
  8. package/src/api/public-types.ts +541 -5
  9. package/src/api/scope-metadata-resolver-types.ts +88 -0
  10. package/src/core/commands/formatting-commands.ts +1 -1
  11. package/src/core/commands/index.ts +601 -9
  12. package/src/core/search/search-text.ts +15 -2
  13. package/src/index.ts +131 -1
  14. package/src/io/docx-session.ts +672 -2
  15. package/src/io/export/escape-xml-attribute.ts +26 -0
  16. package/src/io/export/external-send.ts +188 -0
  17. package/src/io/export/serialize-comments.ts +13 -16
  18. package/src/io/export/serialize-footnotes.ts +17 -24
  19. package/src/io/export/serialize-headers-footers.ts +17 -24
  20. package/src/io/export/serialize-main-document.ts +59 -62
  21. package/src/io/export/serialize-numbering.ts +20 -27
  22. package/src/io/export/serialize-runtime-revisions.ts +2 -9
  23. package/src/io/export/serialize-tables.ts +8 -15
  24. package/src/io/export/table-properties-xml.ts +25 -32
  25. package/src/io/import/external-reimport.ts +40 -0
  26. package/src/io/load-scheduler.ts +230 -0
  27. package/src/io/normalize/normalize-text.ts +83 -0
  28. package/src/io/ooxml/bw-xml.ts +244 -0
  29. package/src/io/ooxml/canonicalize-payload.ts +301 -0
  30. package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
  31. package/src/io/ooxml/comment-presentation-payload.ts +311 -0
  32. package/src/io/ooxml/external-custody-payload.ts +102 -0
  33. package/src/io/ooxml/participants-payload.ts +97 -0
  34. package/src/io/ooxml/payload-signature.ts +112 -0
  35. package/src/io/ooxml/workflow-payload-validator.ts +367 -0
  36. package/src/io/ooxml/workflow-payload.ts +317 -7
  37. package/src/runtime/awareness-identity.ts +173 -0
  38. package/src/runtime/collab/event-types.ts +27 -0
  39. package/src/runtime/collab-session-bridge.ts +157 -0
  40. package/src/runtime/collab-session-facet.ts +193 -0
  41. package/src/runtime/collab-session.ts +273 -0
  42. package/src/runtime/comment-negotiation-sync.ts +91 -0
  43. package/src/runtime/comment-negotiation.ts +158 -0
  44. package/src/runtime/comment-presentation.ts +223 -0
  45. package/src/runtime/document-runtime.ts +639 -124
  46. package/src/runtime/editor-state-channel.ts +544 -0
  47. package/src/runtime/editor-state-integration.ts +217 -0
  48. package/src/runtime/external-send-runtime.ts +117 -0
  49. package/src/runtime/layout/docx-font-loader.ts +11 -30
  50. package/src/runtime/layout/index.ts +2 -0
  51. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  52. package/src/runtime/layout/layout-engine-instance.ts +139 -14
  53. package/src/runtime/layout/page-graph.ts +79 -7
  54. package/src/runtime/layout/paginated-layout-engine.ts +441 -48
  55. package/src/runtime/layout/public-facet.ts +585 -14
  56. package/src/runtime/layout/table-row-split.ts +316 -0
  57. package/src/runtime/markdown-sanitizer.ts +132 -0
  58. package/src/runtime/participants.ts +134 -0
  59. package/src/runtime/perf-counters.ts +28 -0
  60. package/src/runtime/render/render-frame-types.ts +17 -0
  61. package/src/runtime/render/render-kernel.ts +172 -29
  62. package/src/runtime/resign-payload.ts +120 -0
  63. package/src/runtime/surface-projection.ts +10 -5
  64. package/src/runtime/tamper-gate.ts +157 -0
  65. package/src/runtime/workflow-markup.ts +80 -16
  66. package/src/runtime/workflow-rail-segments.ts +244 -5
  67. package/src/ui/WordReviewEditor.tsx +654 -45
  68. package/src/ui/editor-command-bag.ts +14 -0
  69. package/src/ui/editor-runtime-boundary.ts +111 -11
  70. package/src/ui/editor-shell-view.tsx +21 -0
  71. package/src/ui/editor-surface-controller.tsx +5 -0
  72. package/src/ui/headless/selection-helpers.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
  74. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  75. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
  76. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
  77. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
  78. package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
  79. package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
  80. package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
  81. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
  82. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  83. package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
  84. package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
  85. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
  86. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
  87. package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
  88. package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
  89. package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
  90. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
  91. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
  92. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
  93. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
  94. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
  95. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  96. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  97. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -1
  98. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
  99. package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
  100. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
  101. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
  102. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  103. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +10 -256
  104. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
  105. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
  106. package/src/ui-tailwind/index.ts +37 -1
  107. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  108. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  109. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  110. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  111. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  112. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  113. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  114. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  115. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
  116. package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
  117. package/src/ui-tailwind/theme/editor-theme.css +25 -0
  118. package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
@@ -178,24 +178,24 @@ export const editorSchema = new Schema({
178
178
  const spacingBefore = node.attrs.spacingBefore as number | null;
179
179
  const contextualSpacingBefore = node.attrs.contextualSpacingBefore as boolean | null;
180
180
  if (contextualSpacingBefore) styles.push("margin-top: 0");
181
- else if (spacingBefore) styles.push(`margin-top: ${spacingBefore / 20}px`);
181
+ else if (spacingBefore) styles.push(`margin-top: ${spacingBefore / 20}pt`);
182
182
  const contextualSpacingAfter = node.attrs.contextualSpacingAfter as boolean | null;
183
183
  const spacingAfter = node.attrs.spacingAfter as number | null;
184
184
  if (contextualSpacingAfter) styles.push("margin-bottom: 0");
185
- else if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}px`);
185
+ else if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}pt`);
186
186
  const lineSpacing = node.attrs.lineSpacing as number | null;
187
187
  const lineRule = node.attrs.lineRule as string | null;
188
188
  if (lineSpacing && lineRule === "auto") styles.push(`line-height: ${lineSpacing / 240}`);
189
- else if (lineSpacing && lineRule === "exact") styles.push(`line-height: ${lineSpacing / 20}px`);
190
- else if (lineSpacing && lineRule === "atLeast") styles.push(`min-height: ${lineSpacing / 20}px`);
189
+ else if (lineSpacing && lineRule === "exact") styles.push(`line-height: ${lineSpacing / 20}pt`);
190
+ else if (lineSpacing && lineRule === "atLeast") styles.push(`min-height: ${lineSpacing / 20}pt`);
191
191
  const indentLeft = node.attrs.indentLeft as number | null;
192
- if (indentLeft) styles.push(`padding-left: ${indentLeft / 20}px`);
192
+ if (indentLeft) styles.push(`padding-left: ${indentLeft / 20}pt`);
193
193
  const indentRight = node.attrs.indentRight as number | null;
194
- if (indentRight) styles.push(`padding-right: ${indentRight / 20}px`);
194
+ if (indentRight) styles.push(`padding-right: ${indentRight / 20}pt`);
195
195
  const indentFirstLine = node.attrs.indentFirstLine as number | null;
196
196
  const indentHanging = node.attrs.indentHanging as number | null;
197
- if (indentHanging) styles.push(`text-indent: -${indentHanging / 20}px`);
198
- else if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}px`);
197
+ if (indentHanging) styles.push(`text-indent: -${indentHanging / 20}pt`);
198
+ else if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}pt`);
199
199
  const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
200
200
  if (shadingColor) styles.push(`background-color: ${shadingColor}`);
201
201
  for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
@@ -309,11 +309,13 @@ export const editorSchema = new Schema({
309
309
  }
310
310
 
311
311
  if (hasResolvedMarkerWidth) {
312
- const markerWidthPx = Math.max(1, Math.round(numberingMarkerWidth / 20));
312
+ // P13.a: emit marker geometry in pt (twips/20 == pt) so it
313
+ // self-scales under CSS `zoom` and matches Word's intent.
314
+ const markerWidthPt = Math.max(1, numberingMarkerWidth / 20);
313
315
  prefixStyles.push(
314
- `width: ${markerWidthPx}px`,
315
- `min-width: ${markerWidthPx}px`,
316
- `flex-basis: ${markerWidthPx}px`,
316
+ `width: ${markerWidthPt}pt`,
317
+ `min-width: ${markerWidthPt}pt`,
318
+ `flex-basis: ${markerWidthPt}pt`,
317
319
  `margin-right: 0`,
318
320
  `overflow: visible`,
319
321
  );
@@ -632,16 +634,51 @@ export const editorSchema = new Schema({
632
634
  selectable: false,
633
635
  attrs: {
634
636
  previewMediaId: { default: null },
637
+ previewSrc: { default: null },
635
638
  detail: { default: null },
636
639
  },
637
640
  toDOM(node) {
641
+ const previewSrc = node.attrs.previewSrc as string | null;
642
+ const detail = (node.attrs.detail as string) ?? "Chart";
643
+ if (previewSrc) {
644
+ // Bitmap-backed: render the fallback image Word cached in mc:Fallback.
645
+ // The corner chip preserves the typed identity so agents and humans
646
+ // still see "this is a chart" at a glance.
647
+ return [
648
+ "span",
649
+ {
650
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
651
+ "data-node-type": "chart_atom",
652
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
653
+ contenteditable: "false",
654
+ title: detail,
655
+ },
656
+ [
657
+ "img",
658
+ {
659
+ src: previewSrc,
660
+ alt: detail,
661
+ class: "block max-w-full h-auto",
662
+ draggable: "false",
663
+ },
664
+ ],
665
+ [
666
+ "span",
667
+ {
668
+ class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-blue-200 bg-blue-50/90 px-1 py-0.5 text-[10px] text-blue-700",
669
+ "aria-hidden": "true",
670
+ },
671
+ "\uD83D\uDCC8 Chart",
672
+ ],
673
+ ];
674
+ }
638
675
  return [
639
676
  "span",
640
677
  {
641
678
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-blue-700 bg-blue-50 border border-blue-200",
642
679
  "data-node-type": "chart_atom",
643
680
  contenteditable: "false",
644
- title: (node.attrs.detail as string) ?? "Chart",
681
+ title: detail,
645
682
  },
646
683
  "\uD83D\uDCC8 Chart",
647
684
  ];
@@ -655,16 +692,48 @@ export const editorSchema = new Schema({
655
692
  selectable: false,
656
693
  attrs: {
657
694
  previewMediaId: { default: null },
695
+ previewSrc: { default: null },
658
696
  detail: { default: null },
659
697
  },
660
698
  toDOM(node) {
699
+ const previewSrc = node.attrs.previewSrc as string | null;
700
+ const detail = (node.attrs.detail as string) ?? "SmartArt";
701
+ if (previewSrc) {
702
+ return [
703
+ "span",
704
+ {
705
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
706
+ "data-node-type": "smartart_atom",
707
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
708
+ contenteditable: "false",
709
+ title: detail,
710
+ },
711
+ [
712
+ "img",
713
+ {
714
+ src: previewSrc,
715
+ alt: detail,
716
+ class: "block max-w-full h-auto",
717
+ draggable: "false",
718
+ },
719
+ ],
720
+ [
721
+ "span",
722
+ {
723
+ class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-purple-200 bg-purple-50/90 px-1 py-0.5 text-[10px] text-purple-700",
724
+ "aria-hidden": "true",
725
+ },
726
+ "\uD83D\uDDFA SmartArt",
727
+ ],
728
+ ];
729
+ }
661
730
  return [
662
731
  "span",
663
732
  {
664
733
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-purple-700 bg-purple-50 border border-purple-200",
665
734
  "data-node-type": "smartart_atom",
666
735
  contenteditable: "false",
667
- title: (node.attrs.detail as string) ?? "SmartArt",
736
+ title: detail,
668
737
  },
669
738
  "\uD83D\uDDFA SmartArt",
670
739
  ];
@@ -705,17 +774,50 @@ export const editorSchema = new Schema({
705
774
  attrs: {
706
775
  text: { default: "" },
707
776
  geometry: { default: null },
777
+ previewMediaId: { default: null },
778
+ previewSrc: { default: null },
708
779
  detail: { default: null },
709
780
  },
710
781
  toDOM(node) {
711
782
  const text = node.attrs.text as string;
783
+ const previewSrc = node.attrs.previewSrc as string | null;
784
+ const detail = (node.attrs.detail as string) ?? (text ? `WordArt: ${text}` : "WordArt");
785
+ if (previewSrc) {
786
+ return [
787
+ "span",
788
+ {
789
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
790
+ "data-node-type": "wordart_atom",
791
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
792
+ contenteditable: "false",
793
+ title: detail,
794
+ },
795
+ [
796
+ "img",
797
+ {
798
+ src: previewSrc,
799
+ alt: detail,
800
+ class: "block max-w-full h-auto",
801
+ draggable: "false",
802
+ },
803
+ ],
804
+ [
805
+ "span",
806
+ {
807
+ class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-orange-200 bg-orange-50/90 px-1 py-0.5 text-[10px] text-orange-700",
808
+ "aria-hidden": "true",
809
+ },
810
+ "\u2728 WordArt",
811
+ ],
812
+ ];
813
+ }
712
814
  return [
713
815
  "span",
714
816
  {
715
817
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-orange-700 bg-orange-50 border border-orange-200 font-medium italic",
716
818
  "data-node-type": "wordart_atom",
717
819
  contenteditable: "false",
718
- title: (node.attrs.detail as string) ?? `WordArt: ${text}`,
820
+ title: detail,
719
821
  },
720
822
  "\u2728 " + (text || "WordArt"),
721
823
  ];
@@ -730,18 +832,51 @@ export const editorSchema = new Schema({
730
832
  attrs: {
731
833
  text: { default: null },
732
834
  shapeType: { default: null },
835
+ previewMediaId: { default: null },
836
+ previewSrc: { default: null },
733
837
  detail: { default: null },
734
838
  },
735
839
  toDOM(node) {
736
840
  const text = node.attrs.text as string | null;
737
841
  const label = text ? `VML: ${text}` : "VML shape";
842
+ const previewSrc = node.attrs.previewSrc as string | null;
843
+ const detail = (node.attrs.detail as string) ?? label;
844
+ if (previewSrc) {
845
+ return [
846
+ "span",
847
+ {
848
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
849
+ "data-node-type": "vml_atom",
850
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
851
+ contenteditable: "false",
852
+ title: detail,
853
+ },
854
+ [
855
+ "img",
856
+ {
857
+ src: previewSrc,
858
+ alt: detail,
859
+ class: "block max-w-full h-auto",
860
+ draggable: "false",
861
+ },
862
+ ],
863
+ [
864
+ "span",
865
+ {
866
+ class: "absolute top-1 right-1 inline-flex items-center gap-0.5 rounded border border-gray-300 bg-gray-100/90 px-1 py-0.5 text-[10px] text-gray-600",
867
+ "aria-hidden": "true",
868
+ },
869
+ "\u25A6 VML",
870
+ ],
871
+ ];
872
+ }
738
873
  return [
739
874
  "span",
740
875
  {
741
876
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-gray-600 bg-gray-100 border border-gray-300",
742
877
  "data-node-type": "vml_atom",
743
878
  contenteditable: "false",
744
- title: (node.attrs.detail as string) ?? label,
879
+ title: detail,
745
880
  },
746
881
  "\u25A6 " + label,
747
882
  ];
@@ -757,10 +892,25 @@ export const editorSchema = new Schema({
757
892
  warningId: { default: "" },
758
893
  label: { default: "Locked" },
759
894
  detail: { default: "" },
895
+ presentation: { default: "callout" },
760
896
  },
761
897
  toDOM(node) {
762
898
  const fragmentId = node.attrs.fragmentId as string;
763
899
  const isPreview = fragmentId.startsWith("preview:");
900
+ const presentation = node.attrs.presentation as string;
901
+ if (presentation === "quiet-marker") {
902
+ return [
903
+ "div",
904
+ {
905
+ class: "block h-0 w-0 overflow-hidden",
906
+ contenteditable: "false",
907
+ "data-node-type": "opaque_block",
908
+ "data-block-presentation": "quiet-marker",
909
+ title: node.attrs.detail as string,
910
+ "aria-label": node.attrs.label as string,
911
+ },
912
+ ];
913
+ }
764
914
  return [
765
915
  "div",
766
916
  {
@@ -889,7 +1039,7 @@ export const editorSchema = new Schema({
889
1039
  attrs: { value: { default: 0 } },
890
1040
  toDOM(mark) {
891
1041
  const twips = mark.attrs.value as number;
892
- return ["span", { style: `letter-spacing: ${twips / 20}px` }, 0];
1042
+ return ["span", { style: `letter-spacing: ${twips / 20}pt` }, 0];
893
1043
  },
894
1044
  },
895
1045
  font_kerning: {
@@ -268,7 +268,7 @@ function buildPMBlocks(
268
268
  } else if (block.kind === "sdt_block") {
269
269
  nodes.push(buildSdtBlock(block, mediaPreviews, showUnsupportedObjectPreviews));
270
270
  } else {
271
- nodes.push(buildOpaqueBlock(block));
271
+ nodes.push(buildOpaqueBlock(block, showUnsupportedObjectPreviews));
272
272
  }
273
273
  }
274
274
 
@@ -366,7 +366,13 @@ function buildParagraph(
366
366
  indentRight: paragraphLayout.indentation?.right ?? null,
367
367
  indentFirstLine: paragraphLayout.indentation?.firstLine ?? null,
368
368
  indentHanging: paragraphLayout.indentation?.hanging ?? null,
369
- numberingMarkerWidth: paragraphLayout.markerLane?.width ?? null,
369
+ numberingMarkerWidth:
370
+ paragraphLayout.markerLane?.width ??
371
+ paragraphLayout.indentation?.hanging ??
372
+ (paragraphLayout.indentation?.firstLine !== undefined &&
373
+ paragraphLayout.indentation.firstLine < 0
374
+ ? Math.abs(paragraphLayout.indentation.firstLine)
375
+ : null),
370
376
  numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
371
377
  numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
372
378
  shadingFill: block.shading?.fill ?? cascade?.shading?.fill ?? null,
@@ -453,7 +459,7 @@ function buildInlineContent(
453
459
  }
454
460
 
455
461
  case "opaque_inline":
456
- return [buildOpaqueInlineOrComplexAtom(segment, showUnsupportedObjectPreviews)];
462
+ return [buildOpaqueInlineOrComplexAtom(segment, mediaPreviews, showUnsupportedObjectPreviews)];
457
463
 
458
464
  case "note_ref": {
459
465
  const text = editorSchema.text(
@@ -602,6 +608,7 @@ const UNSUPPORTED_COMPLEX_PREVIEW_LABELS = new Set<string>([
602
608
  */
603
609
  function buildOpaqueInlineOrComplexAtom(
604
610
  segment: Extract<import("../../api/public-types").SurfaceInlineSegment, { kind: "opaque_inline" }>,
611
+ mediaPreviews: Record<string, MediaPreviewDescriptor>,
605
612
  showUnsupportedObjectPreviews: boolean,
606
613
  ): PMNode {
607
614
  const label = segment.label;
@@ -618,11 +625,30 @@ function buildOpaqueInlineOrComplexAtom(
618
625
  });
619
626
  }
620
627
 
621
- if (showUnsupportedObjectPreviews && label === "Embedded chart") {
622
- return editorSchema.nodes.chart_atom.create({ detail });
628
+ // Bitmap-backed complex objects always upgrade to the typed atom so the
629
+ // reviewer sees Word's own cached rendering regardless of the debug-preview
630
+ // flag. The flag still gates the badge-only path for shape/wordart/vml
631
+ // families (below) which are decoration-weight. Chart and SmartArt are
632
+ // *always* rendered as typed atoms regardless of flag — a silent quiet
633
+ // marker over a chart leaves the reviewer with no signal that data is
634
+ // missing, which is worse than the small cost of an always-visible chip.
635
+ const previewSrc = segment.previewMediaId
636
+ ? mediaPreviews[segment.previewMediaId]?.src ?? null
637
+ : null;
638
+
639
+ if (label === "Embedded chart") {
640
+ return editorSchema.nodes.chart_atom.create({
641
+ previewMediaId: segment.previewMediaId ?? null,
642
+ previewSrc,
643
+ detail,
644
+ });
623
645
  }
624
- if (showUnsupportedObjectPreviews && label === "SmartArt diagram") {
625
- return editorSchema.nodes.smartart_atom.create({ detail });
646
+ if (label === "SmartArt diagram") {
647
+ return editorSchema.nodes.smartart_atom.create({
648
+ previewMediaId: segment.previewMediaId ?? null,
649
+ previewSrc,
650
+ detail,
651
+ });
626
652
  }
627
653
  if (showUnsupportedObjectPreviews && (label === "Drawing shape" || label === "Text box")) {
628
654
  const textMatch = /(?:Text content|Content): "([^"]+)"/.exec(detail);
@@ -672,12 +698,14 @@ function buildOpaqueInlineOrComplexAtom(
672
698
 
673
699
  function buildOpaqueBlock(
674
700
  block: Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>,
701
+ showUnsupportedObjectPreviews: boolean,
675
702
  ): PMNode {
676
703
  return editorSchema.nodes.opaque_block.create({
677
704
  fragmentId: block.fragmentId,
678
705
  warningId: block.warningId,
679
706
  label: block.label,
680
707
  detail: block.detail,
708
+ presentation: showUnsupportedObjectPreviews ? "callout" : "quiet-marker",
681
709
  });
682
710
  }
683
711
 
@@ -12,6 +12,23 @@ import {
12
12
  type RemoteCursorState,
13
13
  } from "../../runtime/collab/remote-cursor-awareness";
14
14
 
15
+ /**
16
+ * Remote `cursor.color` comes straight off Yjs Awareness — another peer
17
+ * can set it to anything. Validate to a narrow CSS-color allowlist
18
+ * before interpolating into an inline style, otherwise a malicious peer
19
+ * could inject arbitrary CSS declarations via `; background: url(...)`.
20
+ * Returns a fallback neutral color when the input is rejected.
21
+ */
22
+ const SAFE_REMOTE_CURSOR_FALLBACK = "#6b7280";
23
+ function safeRemoteCursorColor(raw: unknown): string {
24
+ if (typeof raw !== "string") return SAFE_REMOTE_CURSOR_FALLBACK;
25
+ const value = raw.trim();
26
+ if (value.length === 0 || value.length > 32) return SAFE_REMOTE_CURSOR_FALLBACK;
27
+ if (/^#[0-9a-fA-F]{3}$|^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{8}$/.test(value)) return value;
28
+ if (/^rgba?\(\s*\d+(\.\d+)?%?(\s*,\s*\d+(\.\d+)?%?){2,3}\s*\)$/.test(value)) return value;
29
+ return SAFE_REMOTE_CURSOR_FALLBACK;
30
+ }
31
+
15
32
  interface RemoteCursorPluginState {
16
33
  cursors: RemoteCursorState[];
17
34
  }
@@ -68,7 +85,7 @@ export function createRemoteCursorPlugin(
68
85
  if (from !== to) {
69
86
  decorations.push(
70
87
  Decoration.inline(from, to, {
71
- style: `background-color: ${cursor.color}33;`,
88
+ style: `background-color: ${safeRemoteCursorColor(cursor.color)}33;`,
72
89
  class: "remote-cursor-selection",
73
90
  }),
74
91
  );
@@ -149,7 +166,7 @@ function createCursorWidget(cursor: RemoteCursorState): HTMLElement {
149
166
  top: -0.2em;
150
167
  width: 2px;
151
168
  height: 1.2em;
152
- background-color: ${cursor.color};
169
+ background-color: ${safeRemoteCursorColor(cursor.color)};
153
170
  border-radius: 1px;
154
171
  `;
155
172
  container.appendChild(caret);
@@ -163,7 +180,7 @@ function createCursorWidget(cursor: RemoteCursorState): HTMLElement {
163
180
  font-family: system-ui, -apple-system, sans-serif;
164
181
  font-weight: 500;
165
182
  color: white;
166
- background-color: ${cursor.color};
183
+ background-color: ${safeRemoteCursorColor(cursor.color)};
167
184
  padding: 1px 4px;
168
185
  border-radius: 3px;
169
186
  white-space: nowrap;