@beyondwork/docx-react-component 1.0.38 → 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 (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
17
+ import { recordPerfSample } from "../../ui-tailwind/editor-surface/perf-probe.ts";
17
18
  import type { EditorStoryTarget } from "../../api/public-types";
18
19
  import type {
19
20
  PublicPageNode,
@@ -165,6 +166,7 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
165
166
  }
166
167
 
167
168
  function buildFrame(options?: RenderFrameQueryOptions): RenderFrame {
169
+ const t0 = typeof performance !== "undefined" ? performance.now() : 0;
168
170
  const activeStory = options?.story ?? getActiveStory();
169
171
  const measurementFidelity = facet.getMeasurementFidelity();
170
172
  const rawPages = facet.getPages();
@@ -211,7 +213,7 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
211
213
  ? Number(extractRevisionFromPageId(filteredPages[0].pageId))
212
214
  : 0;
213
215
 
214
- return {
216
+ const frame: RenderFrame = {
215
217
  revision: Number.isFinite(revision) ? revision : 0,
216
218
  measurementFidelity,
217
219
  activeStory,
@@ -220,6 +222,8 @@ export function createRenderKernel(input: CreateRenderKernelInput): RenderKernel
220
222
  decorationIndex,
221
223
  anchorIndex,
222
224
  };
225
+ if (t0 > 0) recordPerfSample("render.frame_build", performance.now() - t0);
226
+ return frame;
223
227
  }
224
228
 
225
229
  return {
@@ -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) {