@beyondwork/docx-react-component 1.0.37 → 1.0.39

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 (116) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +496 -1
  3. package/src/core/commands/section-layout-commands.ts +58 -0
  4. package/src/core/commands/table-grid.ts +431 -0
  5. package/src/core/commands/table-structure-commands.ts +845 -56
  6. package/src/core/commands/text-commands.ts +122 -2
  7. package/src/io/docx-session.ts +1 -0
  8. package/src/io/export/serialize-main-document.ts +2 -11
  9. package/src/io/export/serialize-numbering.ts +43 -10
  10. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  11. package/src/io/export/serialize-run-formatting.ts +90 -0
  12. package/src/io/export/serialize-styles.ts +212 -0
  13. package/src/io/export/serialize-tables.ts +74 -0
  14. package/src/io/export/table-properties-xml.ts +139 -4
  15. package/src/io/normalize/normalize-text.ts +15 -0
  16. package/src/io/ooxml/parse-fields.ts +10 -3
  17. package/src/io/ooxml/parse-footnotes.ts +60 -0
  18. package/src/io/ooxml/parse-headers-footers.ts +60 -0
  19. package/src/io/ooxml/parse-main-document.ts +137 -0
  20. package/src/io/ooxml/parse-numbering.ts +41 -1
  21. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  22. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  23. package/src/io/ooxml/parse-styles.ts +31 -0
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  26. package/src/io/ooxml/xml-element.ts +19 -0
  27. package/src/model/canonical-document.ts +117 -3
  28. package/src/runtime/collab/event-types.ts +165 -0
  29. package/src/runtime/collab/index.ts +22 -0
  30. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  31. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  32. package/src/runtime/document-layout.ts +4 -2
  33. package/src/runtime/document-navigation.ts +1 -1
  34. package/src/runtime/document-runtime.ts +248 -18
  35. package/src/runtime/layout/default-page-format.ts +96 -0
  36. package/src/runtime/layout/index.ts +47 -0
  37. package/src/runtime/layout/inert-layout-facet.ts +16 -0
  38. package/src/runtime/layout/layout-engine-instance.ts +100 -23
  39. package/src/runtime/layout/layout-invalidation.ts +14 -5
  40. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-graph.ts +55 -0
  43. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  44. package/src/runtime/layout/paginated-layout-engine.ts +484 -37
  45. package/src/runtime/layout/project-block-fragments.ts +225 -0
  46. package/src/runtime/layout/public-facet.ts +748 -16
  47. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  48. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  49. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  50. package/src/runtime/layout/table-render-plan.ts +249 -0
  51. package/src/runtime/numbering-prefix.ts +5 -0
  52. package/src/runtime/paragraph-style-resolver.ts +194 -0
  53. package/src/runtime/render/block-fragment-projection.ts +35 -0
  54. package/src/runtime/render/decoration-resolver.ts +189 -0
  55. package/src/runtime/render/index.ts +57 -0
  56. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  57. package/src/runtime/render/render-frame-types.ts +317 -0
  58. package/src/runtime/render/render-kernel.ts +759 -0
  59. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  60. package/src/runtime/surface-projection.ts +129 -9
  61. package/src/runtime/table-schema.ts +11 -0
  62. package/src/runtime/view-state.ts +67 -0
  63. package/src/runtime/workflow-markup.ts +1 -5
  64. package/src/runtime/workflow-rail-segments.ts +280 -0
  65. package/src/ui/WordReviewEditor.tsx +368 -19
  66. package/src/ui/editor-command-bag.ts +4 -0
  67. package/src/ui/editor-runtime-boundary.ts +16 -0
  68. package/src/ui/editor-shell-view.tsx +10 -0
  69. package/src/ui/editor-surface-controller.tsx +9 -1
  70. package/src/ui/headless/chrome-registry.ts +310 -15
  71. package/src/ui/headless/scoped-chrome-policy.ts +49 -1
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  75. package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
  76. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  77. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
  78. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
  79. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  80. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  81. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  82. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  83. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
  84. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  85. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
  86. package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
  87. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
  88. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  89. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
  90. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
  91. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  92. package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
  93. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
  94. package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
  95. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  96. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  97. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  98. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  99. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  100. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
  101. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  102. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  103. package/src/ui-tailwind/index.ts +29 -0
  104. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  105. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  106. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  107. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  108. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  109. package/src/ui-tailwind/theme/editor-theme.css +498 -163
  110. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
  111. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  112. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  113. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
  114. package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
  115. package/src/runtime/collab-review-sync.ts +0 -254
  116. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ CanonicalRunFormatting,
2
3
  NumberingCatalog,
3
4
  NumberingLevelDefinition,
4
5
  NumberingLevelOverrideDefinition,
@@ -27,6 +28,7 @@ export interface ResolvedNumberingGeometry {
27
28
  firstLine?: number;
28
29
  hanging?: number;
29
30
  };
31
+ markerRunProperties?: CanonicalRunFormatting;
30
32
  }
31
33
 
32
34
  export interface ResolvedNumberingDefinitionSet {
@@ -67,7 +69,7 @@ export function resolveNumberingDefinitionSet(
67
69
  abstractDefinition,
68
70
  effectiveLevels,
69
71
  effectiveLevel,
70
- geometry: resolveNumberingGeometry(effectiveLevel.paragraphGeometry, paragraph),
72
+ geometry: resolveNumberingGeometry(effectiveLevel.paragraphGeometry, paragraph, effectiveLevel.runProperties),
71
73
  };
72
74
  }
73
75
 
@@ -110,6 +112,8 @@ function mergeLevelDefinition(
110
112
  base?.paragraphGeometry,
111
113
  override?.paragraphGeometry,
112
114
  );
115
+ const runProperties = override?.runProperties ?? base?.runProperties;
116
+ const restartAfterLevel = override?.restartAfterLevel ?? base?.restartAfterLevel;
113
117
 
114
118
  return {
115
119
  level,
@@ -124,6 +128,8 @@ function mergeLevelDefinition(
124
128
  : {}),
125
129
  ...(override?.suffix ?? base?.suffix ? { suffix: override?.suffix ?? base?.suffix } : {}),
126
130
  ...(paragraphGeometry ? { paragraphGeometry } : {}),
131
+ ...(runProperties ? { runProperties } : {}),
132
+ ...(restartAfterLevel !== undefined ? { restartAfterLevel } : {}),
127
133
  };
128
134
  }
129
135
 
@@ -179,6 +185,7 @@ function mergeLevelParagraphGeometry(
179
185
  function resolveNumberingGeometry(
180
186
  levelGeometry: NumberingLevelParagraphGeometry | undefined,
181
187
  paragraph: Pick<ParagraphNode, "spacing" | "indentation" | "tabStops"> | undefined,
188
+ levelRunProperties: CanonicalRunFormatting | undefined,
182
189
  ): ResolvedNumberingGeometry {
183
190
  const spacing = mergeParagraphSpacing(levelGeometry?.spacing, paragraph?.spacing);
184
191
  const indentation = mergeParagraphIndentation(levelGeometry?.indentation, paragraph?.indentation);
@@ -197,6 +204,7 @@ function resolveNumberingGeometry(
197
204
  ...(tabStops && tabStops.length > 0 ? { tabStops } : {}),
198
205
  ...(markerLane ? { markerLane } : {}),
199
206
  ...(textColumn ? { textColumn } : {}),
207
+ ...(levelRunProperties ? { markerRunProperties: levelRunProperties } : {}),
200
208
  };
201
209
  }
202
210
 
@@ -46,6 +46,12 @@ import {
46
46
  resolveSectionVariants,
47
47
  } from "./story-context.ts";
48
48
  import { resolveTableStyleResolution } from "./table-style-resolver.ts";
49
+ import {
50
+ resolveEffectiveParagraphFormatting,
51
+ resolveEffectiveRunFormatting,
52
+ resolveNumberingMarkerRunFormatting,
53
+ } from "./paragraph-style-resolver.ts";
54
+ import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
49
55
 
50
56
  interface ParagraphAccumulator {
51
57
  blockId: string;
@@ -57,6 +63,7 @@ interface ParagraphAccumulator {
57
63
  numberingPrefix?: string;
58
64
  numberingSuffix?: "tab" | "space" | "nothing";
59
65
  resolvedNumbering?: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"];
66
+ resolvedParagraphFormatting?: CanonicalParagraphFormatting;
60
67
  contextualSpacing?: boolean;
61
68
  segments: SurfaceInlineSegment[];
62
69
  }
@@ -341,6 +348,16 @@ function createTableBlock(
341
348
  }
342
349
  const resolvedCell = resolvedRow?.cells[cellIndex];
343
350
  const cellBorders = resolveCellBorderStyles(resolvedCell?.borders ?? cell.borders);
351
+ // R2a: project the resolved conditional-format regions into CSS class
352
+ // names so the NodeView can paint band colors via theme vars instead of
353
+ // inline styles. Direct shading overrides still win at render time.
354
+ const bandClasses =
355
+ resolvedCell?.activeConditionalRegions &&
356
+ resolvedCell.activeConditionalRegions.length > 0
357
+ ? resolvedCell.activeConditionalRegions
358
+ .map((region) => `band-${region}`)
359
+ .join(" ")
360
+ : null;
344
361
  cells.push({
345
362
  gridSpan: cell.gridSpan ?? 1,
346
363
  verticalMerge: cell.verticalMerge ?? null,
@@ -352,6 +369,7 @@ function createTableBlock(
352
369
  ...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
353
370
  ...(cellBorders.borderBottom ? { borderBottom: cellBorders.borderBottom } : {}),
354
371
  ...(cellBorders.borderLeft ? { borderLeft: cellBorders.borderLeft } : {}),
372
+ ...(bandClasses ? { bandClasses } : {}),
355
373
  content: cellContent,
356
374
  });
357
375
  }
@@ -365,6 +383,7 @@ function createTableBlock(
365
383
  ...(resolvedRow?.style.height !== undefined ? { height: resolvedRow.style.height } : {}),
366
384
  ...(resolvedRow?.style.heightRule ? { heightRule: resolvedRow.style.heightRule } : {}),
367
385
  ...(headerLike ? { isHeader: true } : {}),
386
+ ...(row.cantSplit ? { cantSplit: true } : {}),
368
387
  });
369
388
  }
370
389
 
@@ -523,6 +542,26 @@ function createParagraphBlock(
523
542
  const resolvedNumbering = effectiveNumbering
524
543
  ? numberingPrefixResolver.resolveDetailed(effectiveNumbering, paragraph)
525
544
  : null;
545
+
546
+ // Task 11: compute cascaded paragraph formatting
547
+ const stylesCatalog = document.styles;
548
+ const directParagraphFormatting = buildDirectParagraphFormattingFromNode(paragraph);
549
+ const resolvedParagraphFormatting = resolveEffectiveParagraphFormatting(
550
+ { styleId: paragraph.styleId, direct: directParagraphFormatting },
551
+ stylesCatalog,
552
+ );
553
+
554
+ // Task 11: compute cascaded marker run formatting
555
+ const markerRunProperties = effectiveNumbering
556
+ ? resolveNumberingMarkerRunFormatting(
557
+ {
558
+ paragraphStyleId: paragraph.styleId,
559
+ levelRunProperties: resolvedNumbering?.markerRunProperties,
560
+ },
561
+ stylesCatalog,
562
+ )
563
+ : undefined;
564
+
526
565
  const accumulator: ParagraphAccumulator = {
527
566
  blockId: `paragraph-${paragraphIndex}`,
528
567
  kind: "paragraph",
@@ -536,9 +575,17 @@ function createParagraphBlock(
536
575
  ...(resolvedNumbering.text !== null && resolvedNumbering.suffix
537
576
  ? { numberingSuffix: resolvedNumbering.suffix }
538
577
  : {}),
539
- resolvedNumbering: toSurfaceResolvedNumbering(resolvedNumbering),
578
+ resolvedNumbering: {
579
+ ...toSurfaceResolvedNumbering(resolvedNumbering),
580
+ ...(markerRunProperties && Object.keys(markerRunProperties).length > 0
581
+ ? { markerRunProperties }
582
+ : {}),
583
+ },
540
584
  }
541
585
  : {}),
586
+ ...(resolvedParagraphFormatting && Object.keys(resolvedParagraphFormatting).length > 0
587
+ ? { resolvedParagraphFormatting }
588
+ : {}),
542
589
  ...(paragraph.alignment ? { alignment: paragraph.alignment } : {}),
543
590
  ...(paragraph.spacing ? { spacing: paragraph.spacing } : {}),
544
591
  ...(paragraph.contextualSpacing !== undefined
@@ -683,6 +730,48 @@ function resolveStyleLinkedNumberingLevel(
683
730
  return undefined;
684
731
  }
685
732
 
733
+ function buildDirectRunFormattingFromMarks(
734
+ marks: SurfaceTextMark[] | undefined,
735
+ markAttrs: {
736
+ backgroundColor?: string;
737
+ charSpacing?: number;
738
+ kerning?: number;
739
+ textFill?: string;
740
+ fontFamily?: string;
741
+ fontSize?: number;
742
+ textColor?: string;
743
+ } | undefined,
744
+ ): CanonicalRunFormatting | undefined {
745
+ const direct: CanonicalRunFormatting = {};
746
+ if (marks) {
747
+ if (marks.includes("bold")) direct.bold = true;
748
+ if (marks.includes("italic")) direct.italic = true;
749
+ if (marks.includes("underline")) direct.underline = "single";
750
+ if (marks.includes("strikethrough")) direct.strikethrough = true;
751
+ if (marks.includes("doubleStrikethrough")) direct.doubleStrikethrough = true;
752
+ if (marks.includes("vanish")) direct.vanish = true;
753
+ if (marks.includes("allCaps")) direct.allCaps = true;
754
+ if (marks.includes("smallCaps")) direct.smallCaps = true;
755
+ }
756
+ if (markAttrs) {
757
+ if (markAttrs.fontFamily) {
758
+ direct.fontFamily = markAttrs.fontFamily;
759
+ direct.fontFamilyAscii = markAttrs.fontFamily;
760
+ }
761
+ if (typeof markAttrs.fontSize === "number") {
762
+ // markAttrs.fontSize is already in half-points from cloneMarks
763
+ direct.fontSizeHalfPoints = markAttrs.fontSize;
764
+ }
765
+ if (markAttrs.textColor) {
766
+ direct.colorHex = markAttrs.textColor.replace(/^#/, "");
767
+ }
768
+ if (markAttrs.backgroundColor) {
769
+ direct.highlight = markAttrs.backgroundColor;
770
+ }
771
+ }
772
+ return Object.keys(direct).length > 0 ? direct : undefined;
773
+ }
774
+
686
775
  function appendInlineSegments(
687
776
  paragraph: ParagraphAccumulator,
688
777
  node: InlineNode,
@@ -692,23 +781,33 @@ function appendInlineSegments(
692
781
  hyperlinkHref?: string,
693
782
  ): { nextCursor: number; lockedFragmentIds: string[] } {
694
783
  switch (node.type) {
695
- case "text":
784
+ case "text": {
785
+ const cloned = node.marks ? cloneMarks(node.marks) : { marks: [] as SurfaceTextMark[] };
786
+ const directRunFormatting = buildDirectRunFormattingFromMarks(
787
+ cloned.marks.length > 0 ? cloned.marks : undefined,
788
+ cloned.markAttrs,
789
+ );
790
+ const resolvedRunFormatting = resolveEffectiveRunFormatting(
791
+ {
792
+ paragraphStyleId: paragraph.styleId,
793
+ characterStyleId: undefined,
794
+ direct: directRunFormatting,
795
+ },
796
+ document.styles,
797
+ );
696
798
  paragraph.segments.push({
697
799
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
698
800
  kind: "text",
699
801
  from: start,
700
802
  to: start + Array.from(node.text).length,
701
803
  text: node.text,
702
- ...(node.marks ? (() => {
703
- const result = cloneMarks(node.marks);
704
- return {
705
- ...(result.marks.length > 0 ? { marks: result.marks } : {}),
706
- ...(result.markAttrs ? { markAttrs: result.markAttrs } : {}),
707
- };
708
- })() : {}),
804
+ ...(cloned.marks.length > 0 ? { marks: cloned.marks } : {}),
805
+ ...(cloned.markAttrs ? { markAttrs: cloned.markAttrs } : {}),
806
+ ...(Object.keys(resolvedRunFormatting).length > 0 ? { resolvedRunFormatting } : {}),
709
807
  ...(hyperlinkHref ? { hyperlinkHref } : {}),
710
808
  });
711
809
  return { nextCursor: start + Array.from(node.text).length, lockedFragmentIds: [] };
810
+ }
712
811
  case "tab":
713
812
  paragraph.segments.push({
714
813
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -1368,6 +1467,27 @@ function toSurfaceTabStop(
1368
1467
  };
1369
1468
  }
1370
1469
 
1470
+ function buildDirectParagraphFormattingFromNode(
1471
+ paragraph: ParagraphNode,
1472
+ ): CanonicalParagraphFormatting | undefined {
1473
+ const direct: CanonicalParagraphFormatting = {};
1474
+ if (paragraph.spacing) direct.spacing = paragraph.spacing;
1475
+ if (paragraph.indentation) direct.indentation = paragraph.indentation;
1476
+ if (paragraph.alignment) direct.alignment = paragraph.alignment;
1477
+ if (paragraph.borders) direct.borders = paragraph.borders;
1478
+ if (paragraph.shading) direct.shading = paragraph.shading;
1479
+ if (paragraph.tabStops && paragraph.tabStops.length > 0) direct.tabStops = [...paragraph.tabStops];
1480
+ if (paragraph.contextualSpacing !== undefined) direct.contextualSpacing = paragraph.contextualSpacing;
1481
+ if (paragraph.keepNext !== undefined) direct.keepNext = paragraph.keepNext;
1482
+ if (paragraph.keepLines !== undefined) direct.keepLines = paragraph.keepLines;
1483
+ if (paragraph.widowControl !== undefined) direct.widowControl = paragraph.widowControl;
1484
+ if (paragraph.pageBreakBefore !== undefined) direct.pageBreakBefore = paragraph.pageBreakBefore;
1485
+ if (paragraph.outlineLevel !== undefined) direct.outlineLevel = paragraph.outlineLevel;
1486
+ if (paragraph.bidi !== undefined) direct.bidi = paragraph.bidi;
1487
+ if (paragraph.suppressLineNumbers !== undefined) direct.suppressLineNumbers = paragraph.suppressLineNumbers;
1488
+ return Object.keys(direct).length > 0 ? direct : undefined;
1489
+ }
1490
+
1371
1491
  function toSurfaceResolvedNumbering(
1372
1492
  numbering: NumberingPrefixResult,
1373
1493
  ): NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"]> {
@@ -42,6 +42,7 @@ type TableCellAttrs = {
42
42
  borderRight?: string | null;
43
43
  borderBottom?: string | null;
44
44
  borderLeft?: string | null;
45
+ bandClasses?: string | null;
45
46
  };
46
47
 
47
48
  function resolveRenderedColspan(attrs: {
@@ -96,6 +97,7 @@ function getCellAttrs(dom: HTMLElement): TableCellAttrs {
96
97
  borderRight: dom.getAttribute("data-border-right"),
97
98
  borderBottom: dom.getAttribute("data-border-bottom"),
98
99
  borderLeft: dom.getAttribute("data-border-left"),
100
+ bandClasses: dom.getAttribute("data-band-classes"),
99
101
  };
100
102
  }
101
103
 
@@ -129,6 +131,11 @@ function setCellDomAttrs(nodeAttrs: TableCellAttrs, className: string): Record<s
129
131
  if (nodeAttrs.borderRight) attrs["data-border-right"] = nodeAttrs.borderRight;
130
132
  if (nodeAttrs.borderBottom) attrs["data-border-bottom"] = nodeAttrs.borderBottom;
131
133
  if (nodeAttrs.borderLeft) attrs["data-border-left"] = nodeAttrs.borderLeft;
134
+ if (nodeAttrs.bandClasses) {
135
+ attrs["data-band-classes"] = nodeAttrs.bandClasses;
136
+ // Concatenate band classes onto the base `class` so Tailwind's @apply resolves at parse time.
137
+ attrs.class = `${attrs.class} ${nodeAttrs.bandClasses}`;
138
+ }
132
139
 
133
140
  const styles: string[] = [];
134
141
  const bgColor = safeCssColor(nodeAttrs.backgroundColor);
@@ -181,6 +188,8 @@ const tableCellSpecAttrs = {
181
188
  borderRight: { default: null },
182
189
  borderBottom: { default: null },
183
190
  borderLeft: { default: null },
191
+ /** R2b: space-joined band classes ("band-firstRow band-band1Horz") from the resolved style. */
192
+ bandClasses: { default: null },
184
193
  } as const;
185
194
 
186
195
  export const tableNodeSpec: NodeSpec = {
@@ -199,6 +208,8 @@ export const tableNodeSpec: NodeSpec = {
199
208
  tblLookLastColumn: { default: false },
200
209
  tblLookNoHBand: { default: false },
201
210
  tblLookNoVBand: { default: false },
211
+ /** R2d: raw `w:tblLook/@w:val` hex preserved verbatim so vendor-extended bits survive round-trip. */
212
+ tblLookVal: { default: null },
202
213
  },
203
214
  parseDOM: [{ tag: "table" }],
204
215
  toDOM(node) {
@@ -19,13 +19,17 @@ import type {
19
19
  ActiveListContext,
20
20
  ActiveNoteContext,
21
21
  CaretAffinity,
22
+ ChromePinsState,
23
+ ChromePinSurface,
22
24
  DocumentMode,
25
+ EditorRole,
23
26
  EditorStoryTarget,
24
27
  EditorSurfaceSnapshot,
25
28
  EditorViewStateSnapshot,
26
29
  LayoutMeasurement,
27
30
  PageLayoutSnapshot,
28
31
  PageRegionHitTest,
32
+ PinState,
29
33
  SelectionSnapshot,
30
34
  SurfaceBlockSnapshot,
31
35
  SurfaceInlineSegment,
@@ -44,6 +48,20 @@ export interface ViewState {
44
48
  caretAffinity: CaretAffinity;
45
49
  activePageRegion: PageRegionHitTest | null;
46
50
  activeObjectFrame: LayoutMeasurement["objectFrame"] | null;
51
+ /**
52
+ * Role-scoped chrome dimension (spec §6.4). Host apps drive the role via
53
+ * `setEditorRole`; the mounted shell reads this to pick a per-role
54
+ * toolbar action set. Independent of `viewMode` — one user session may
55
+ * start in "review" role and switch to "editor" without changing
56
+ * workspace or document mode.
57
+ */
58
+ editorRole: EditorRole;
59
+ /**
60
+ * Pin state for detachable chrome surfaces (topnav, selection tier).
61
+ * Lives here so it survives snapshot rebuilds within one session.
62
+ * Absent key ⇒ docked default.
63
+ */
64
+ chromePins: ChromePinsState;
47
65
  }
48
66
 
49
67
  const MIN_ZOOM_PERCENT = 50;
@@ -58,6 +76,8 @@ const DEFAULT_VIEW_STATE: ViewState = {
58
76
  caretAffinity: "none",
59
77
  activePageRegion: null,
60
78
  activeObjectFrame: null,
79
+ editorRole: "editor",
80
+ chromePins: {},
61
81
  };
62
82
 
63
83
  export function createViewState(initial?: Partial<ViewState>): ViewState {
@@ -113,6 +133,37 @@ export function setActiveObjectFrame(
113
133
  return { ...state, activeObjectFrame: frame };
114
134
  }
115
135
 
136
+ export function setEditorRole(state: ViewState, role: EditorRole): ViewState {
137
+ if (state.editorRole === role) return state;
138
+ return { ...state, editorRole: role };
139
+ }
140
+
141
+ export function setChromePin(
142
+ state: ViewState,
143
+ surface: ChromePinSurface,
144
+ pin: PinState | null,
145
+ ): ViewState {
146
+ const next: ChromePinsState = { ...state.chromePins };
147
+ if (pin === null) {
148
+ if (next[surface] === undefined) {
149
+ return state;
150
+ }
151
+ delete next[surface];
152
+ } else {
153
+ const current = next[surface];
154
+ if (
155
+ current &&
156
+ current.detached === pin.detached &&
157
+ current.offset.x === pin.offset.x &&
158
+ current.offset.y === pin.offset.y
159
+ ) {
160
+ return state;
161
+ }
162
+ next[surface] = pin;
163
+ }
164
+ return { ...state, chromePins: next };
165
+ }
166
+
116
167
  /**
117
168
  * Derive list context from the surface block at the current selection head.
118
169
  */
@@ -219,9 +270,25 @@ export function createEditorViewStateSnapshot(
219
270
  activeObjectFrame: derivedViewState.activeObjectFrame,
220
271
  measurement,
221
272
  isFocused: derivedViewState.isFocused,
273
+ editorRole: derivedViewState.editorRole,
274
+ chromePins: cloneChromePins(derivedViewState.chromePins),
222
275
  };
223
276
  }
224
277
 
278
+ function cloneChromePins(pins: ChromePinsState): ChromePinsState {
279
+ const out: ChromePinsState = {};
280
+ for (const key of Object.keys(pins) as ChromePinSurface[]) {
281
+ const pin = pins[key];
282
+ if (pin) {
283
+ out[key] = {
284
+ detached: pin.detached,
285
+ offset: { x: pin.offset.x, y: pin.offset.y },
286
+ };
287
+ }
288
+ }
289
+ return out;
290
+ }
291
+
225
292
  // ---------------------------------------------------------------------------
226
293
  // Internal helpers
227
294
  // ---------------------------------------------------------------------------
@@ -341,11 +341,7 @@ function collectOpaqueFragmentMarkup(
341
341
  const seen = new Set(existing.map((item) => item.fragmentId));
342
342
 
343
343
  return Object.values(preservation.opaqueFragments)
344
- .filter(
345
- (fragment) =>
346
- !seen.has(fragment.fragmentId)
347
- && fragment.packagePartName === "/word/document.xml",
348
- )
344
+ .filter((fragment) => !seen.has(fragment.fragmentId))
349
345
  .map((fragment) => {
350
346
  const descriptor = describeOpaqueFragment(fragment);
351
347
  const blockedReasonCode = isBlockedImportFeatureKey(descriptor.featureKey)