@beyondwork/docx-react-component 1.0.95 → 1.0.97

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +33 -19
  3. package/src/api/v3/ui/_types.ts +11 -21
  4. package/src/api/v3/ui/chrome.ts +8 -9
  5. package/src/api/v3/ui/debug.ts +15 -77
  6. package/src/api/v3/ui/overlays-visibility.ts +9 -10
  7. package/src/api/v3/ui/overlays.ts +8 -75
  8. package/src/io/ooxml/parse-main-document.ts +30 -0
  9. package/src/io/ooxml/parse-picture.ts +14 -0
  10. package/src/io/ooxml/parse-shapes.ts +41 -1
  11. package/src/model/canonical-document.ts +17 -0
  12. package/src/runtime/document-runtime.ts +46 -1
  13. package/src/runtime/layout/layout-engine-version.ts +8 -1
  14. package/src/runtime/layout/page-story-resolver.ts +1 -0
  15. package/src/runtime/layout/paginated-layout-engine.ts +26 -10
  16. package/src/runtime/surface-projection.ts +114 -12
  17. package/src/runtime/workflow/rail/compose.ts +5 -0
  18. package/src/ui/WordReviewEditor.tsx +6 -10
  19. package/src/ui/editor-command-bag.ts +2 -0
  20. package/src/ui/ui-controller-factory.ts +2 -2
  21. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +12 -41
  22. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +3 -7
  23. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
  24. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -228
  25. package/src/ui-tailwind/debug/README.md +12 -50
  26. package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
  27. package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
  28. package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
  29. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
  30. package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
  31. package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
  32. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
  33. package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
  34. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +53 -53
  35. package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
  36. package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
  37. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
  38. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
  39. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
  40. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
  41. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
  42. package/src/ui-tailwind/theme/editor-theme.css +82 -84
  43. package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
@@ -10,7 +10,11 @@
10
10
  * preserved in the canonical node's rawXml field for lossless round-trip export.
11
11
  */
12
12
 
13
- import type { BlockNode, ShapeContent } from "../../model/canonical-document.ts";
13
+ import type {
14
+ BlockNode,
15
+ ShapeContent,
16
+ TextBoxBodyProperties,
17
+ } from "../../model/canonical-document.ts";
14
18
  import { parseFill } from "./parse-fill.ts";
15
19
  import {
16
20
  type XmlElementNode,
@@ -32,6 +36,8 @@ export interface ParsedWpsShape {
32
36
  text?: string;
33
37
  /** Raw txbxContent XML for structured re-rendering. */
34
38
  txbxContentXml?: string;
39
+ /** Text-box body layout from wps:bodyPr / a:bodyPr. */
40
+ textBoxBody?: TextBoxBodyProperties;
35
41
  /**
36
42
  * Parsed block-level structure from `w:txbxContent`, populated when a
37
43
  * `blockParser` callback is supplied (coord-02 §14 / coord-11 §22 —
@@ -97,6 +103,7 @@ export function parseShapeXml(
97
103
  const txbx = findFirstChild(wsp, "txbx");
98
104
  const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : null;
99
105
  const text = txbxContent ? extractAllText(txbxContent).trim() || undefined : undefined;
106
+ const textBoxBody = readTextBoxBody(wsp);
100
107
 
101
108
  // WordArt detection: geometry preset names that start with "text"
102
109
  if (prst && /^text/i.test(prst)) {
@@ -145,6 +152,7 @@ export function parseShapeXml(
145
152
  type: "shape",
146
153
  ...(isTextBox ? { isTextBox: true } : {}),
147
154
  ...(text ? { text } : {}),
155
+ ...(textBoxBody ? { textBoxBody } : {}),
148
156
  ...(txbxContentXml ? { txbxContentXml } : {}),
149
157
  ...(txbxBlocks && txbxBlocks.length > 0 ? { txbxBlocks } : {}),
150
158
  ...(prst ? { geometry: prst } : {}),
@@ -254,12 +262,14 @@ export function parseShapeContent(
254
262
 
255
263
  const fill = spPr ? readFill(spPr) : undefined;
256
264
  const line = spPr ? readLine(spPr) : undefined;
265
+ const textBoxBody = readTextBoxBody(wsp);
257
266
 
258
267
  // Text-box content — preserve raw XML for serialization + recurse via the
259
268
  // optional blockParser callback (CO4 F3.3) to populate txbxBlocks.
260
269
  const txbx = findFirstChild(wsp, "txbx");
261
270
  const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : undefined;
262
271
  const txbxContentXml = txbxContent ? extractRawXml(txbxContent) : undefined;
272
+ const text = txbxContent ? extractAllText(txbxContent).trim() || undefined : undefined;
263
273
 
264
274
  let txbxBlocks: ReadonlyArray<BlockNode> | undefined;
265
275
  if (txbxContentXml && blockParser) {
@@ -286,14 +296,37 @@ export function parseShapeContent(
286
296
 
287
297
  const result: ShapeContent = { type: "shape", rawXml: drawingRawXml };
288
298
  if (geometry) result.geometry = geometry;
299
+ if (text) result.text = text;
289
300
  if (fill) result.fill = fill;
290
301
  if (line) result.line = line;
291
302
  if (isTextBox) result.isTextBox = true;
303
+ if (textBoxBody) result.textBoxBody = textBoxBody;
292
304
  if (txbxContentXml) result.txbxContentXml = txbxContentXml;
293
305
  if (txbxBlocks && txbxBlocks.length > 0) result.txbxBlocks = txbxBlocks;
294
306
  return result;
295
307
  }
296
308
 
309
+ function readTextBoxBody(wsp: XmlElementNode): TextBoxBodyProperties | undefined {
310
+ const bodyPr = findFirstChild(wsp, "bodyPr");
311
+ if (!bodyPr) return undefined;
312
+
313
+ const result: TextBoxBodyProperties = {};
314
+ const anchor = bodyPr.attributes.anchor;
315
+ if (anchor === "t" || anchor === "ctr" || anchor === "b") {
316
+ result.anchor = anchor;
317
+ }
318
+ const left = readIntAttr(bodyPr, "lIns");
319
+ const top = readIntAttr(bodyPr, "tIns");
320
+ const right = readIntAttr(bodyPr, "rIns");
321
+ const bottom = readIntAttr(bodyPr, "bIns");
322
+ if (left !== undefined) result.insetLeftEmu = left;
323
+ if (top !== undefined) result.insetTopEmu = top;
324
+ if (right !== undefined) result.insetRightEmu = right;
325
+ if (bottom !== undefined) result.insetBottomEmu = bottom;
326
+
327
+ return Object.keys(result).length > 0 ? result : undefined;
328
+ }
329
+
297
330
  // F3.4 + P5 — readFill delegates to the shared `parseFill` primitive covering
298
331
  // solid / none / gradient / pattern. Lane 5 chart-style cascade can consume
299
332
  // the same parser via `src/io/ooxml/parse-fill.ts`.
@@ -320,3 +353,10 @@ function readLine(
320
353
  }
321
354
  return Object.keys(result).length > 0 ? result : undefined;
322
355
  }
356
+
357
+ function readIntAttr(node: XmlElementNode, attr: string): number | undefined {
358
+ const raw = node.attributes[attr];
359
+ if (raw === undefined) return undefined;
360
+ const parsed = parseInt(raw, 10);
361
+ return Number.isFinite(parsed) ? parsed : undefined;
362
+ }
@@ -1876,6 +1876,8 @@ export interface ShapeNode {
1876
1876
  text?: string;
1877
1877
  geometry?: string;
1878
1878
  isTextBox?: boolean;
1879
+ /** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
1880
+ textBoxBody?: TextBoxBodyProperties;
1879
1881
  /** Raw `<w:txbxContent>` XML, preserved for serialization + round-trip. */
1880
1882
  txbxContentXml?: string;
1881
1883
  /**
@@ -2001,6 +2003,8 @@ export interface PictureContent {
2001
2003
  packagePartName?: string;
2002
2004
  /** MIME resolved from the OPC media part, when known. */
2003
2005
  contentType?: string;
2006
+ /** DrawingML a:lum brightness/contrast adjustments on the blip. Values are OOXML fixed percentages. */
2007
+ lum?: { bright?: number; contrast?: number };
2004
2008
  srcRect?: { top: number; bottom: number; left: number; right: number };
2005
2009
  stretch?: boolean;
2006
2010
  /**
@@ -2032,6 +2036,8 @@ export interface PictureContent {
2032
2036
 
2033
2037
  export interface ShapeContent {
2034
2038
  type: "shape";
2039
+ /** Plain-text fallback extracted from `w:txbxContent` when present. */
2040
+ text?: string;
2035
2041
  geometry?: string;
2036
2042
  /**
2037
2043
  * Shape fill — solid, gradient, pattern, or none. srgbClr values on solid +
@@ -2061,6 +2067,8 @@ export interface ShapeContent {
2061
2067
  line?: { color?: string; widthEmu?: number; noLine?: boolean };
2062
2068
  /** True when the shape's geometry + txbxContent presence make it a text box. */
2063
2069
  isTextBox?: boolean;
2070
+ /** Text box body layout from `wps:bodyPr` / `a:bodyPr`. */
2071
+ textBoxBody?: TextBoxBodyProperties;
2064
2072
  /** Raw w:txbxContent XML, preserved for serialization + lossless round-trip. */
2065
2073
  txbxContentXml?: string;
2066
2074
  /**
@@ -2080,6 +2088,15 @@ export interface ShapeContent {
2080
2088
  rawXml: string;
2081
2089
  }
2082
2090
 
2091
+ export interface TextBoxBodyProperties {
2092
+ /** OOXML bodyPr anchor: top, center, or bottom. */
2093
+ anchor?: "t" | "ctr" | "b";
2094
+ insetLeftEmu?: number;
2095
+ insetTopEmu?: number;
2096
+ insetRightEmu?: number;
2097
+ insetBottomEmu?: number;
2098
+ }
2099
+
2083
2100
  export interface DrawingFrameNode {
2084
2101
  type: "drawing_frame";
2085
2102
  anchor: AnchorGeometry;
@@ -13,6 +13,7 @@ import {
13
13
  type EditorWarning as InternalEditorWarning,
14
14
  } from "../core/state/editor-state.ts";
15
15
  import {
16
+ createPlainText,
16
17
  logicalPositionToUnitIndex,
17
18
  parseTextStory,
18
19
  serializeTextStory,
@@ -3195,6 +3196,13 @@ export function createDocumentRuntime(
3195
3196
  replaceText(text, target, formatting) {
3196
3197
  try {
3197
3198
  const timestamp = clock();
3199
+ const selection = target ? createSelectionFromPublicAnchor(target) : state.selection;
3200
+ if (
3201
+ shouldPreserveEquivalentReplacement(formatting) &&
3202
+ replacementTextMatchesCurrentRange(state.document, activeStory, selection, text)
3203
+ ) {
3204
+ return;
3205
+ }
3198
3206
  applyTextCommandInActiveStory(
3199
3207
  {
3200
3208
  type: "text.insert",
@@ -3203,7 +3211,7 @@ export function createDocumentRuntime(
3203
3211
  origin: createOrigin("api", timestamp),
3204
3212
  },
3205
3213
  {
3206
- selection: target ? createSelectionFromPublicAnchor(target) : state.selection,
3214
+ selection,
3207
3215
  blockedCommandName: "replaceText",
3208
3216
  },
3209
3217
  );
@@ -6448,6 +6456,43 @@ function createSelectionFromPublicAnchor(
6448
6456
  }
6449
6457
  }
6450
6458
 
6459
+ function shouldPreserveEquivalentReplacement(formatting: TextFormattingDirective | undefined): boolean {
6460
+ return !formatting || formatting.mode === "match-replaced-range";
6461
+ }
6462
+
6463
+ function replacementTextMatchesCurrentRange(
6464
+ document: CanonicalDocumentEnvelope,
6465
+ activeStory: EditorStoryTarget,
6466
+ selection: import("../core/state/editor-state.ts").SelectionSnapshot,
6467
+ replacement: string,
6468
+ ): boolean {
6469
+ const from = Math.max(0, Math.min(selection.anchor, selection.head));
6470
+ const to = Math.max(0, Math.max(selection.anchor, selection.head));
6471
+ if (from === to) {
6472
+ return replacement.length === 0;
6473
+ }
6474
+
6475
+ const content = activeStory.kind === "main"
6476
+ ? document.content
6477
+ : {
6478
+ type: "doc" as const,
6479
+ children: [...getStoryBlocks(document, activeStory)],
6480
+ };
6481
+ const story = parseTextStory(content);
6482
+ if (from > story.size || to > story.size) {
6483
+ return false;
6484
+ }
6485
+
6486
+ const unitFrom = logicalPositionToUnitIndex(story.units, from, "after");
6487
+ const unitTo = logicalPositionToUnitIndex(story.units, to, "before");
6488
+ const selectedText = createPlainText({
6489
+ firstParagraph: story.firstParagraph,
6490
+ units: story.units.slice(unitFrom, unitTo),
6491
+ size: to - from,
6492
+ });
6493
+ return selectedText === replacement;
6494
+ }
6495
+
6451
6496
  /**
6452
6497
  * I2 Tier B Slice 4b — extract the selection range from a document as a
6453
6498
  * `CanonicalDocumentFragment`. The fragment preserves text + marks +
@@ -1018,8 +1018,15 @@
1018
1018
  * of reconstructing chapter-prefixed numbering in caller-local formulas.
1019
1019
  * Cache envelopes from v59 invalidate because page graph line-box,
1020
1020
  * fragment geometry, and page-numbering payloads changed.
1021
+ *
1022
+ * 61 — Page-story resolver + paginator refresh landed on react-refactor
1023
+ * alongside the floating-drawings-in-page-view fix (`70acfd9ae`). Both
1024
+ * `page-story-resolver.ts` and `paginated-layout-engine.ts` changed
1025
+ * without an upstream bump. Bump here so persisted cache envelopes
1026
+ * re-derive; no algorithm change beyond the floating-drawing clip
1027
+ * widening already covered by the resurrected tests.
1021
1028
  */
1022
- export const LAYOUT_ENGINE_VERSION = 60 as const;
1029
+ export const LAYOUT_ENGINE_VERSION = 61 as const;
1023
1030
 
1024
1031
  /**
1025
1032
  * Serialization schema version for the LayCache payload (the cache envelope
@@ -149,6 +149,7 @@ function resolveStoryVariant(
149
149
  sectionIndex: layout.sectionIndex,
150
150
  };
151
151
  }
152
+ return undefined;
152
153
  }
153
154
 
154
155
  // Even page variant when evenAndOddHeaders is active
@@ -2432,17 +2432,33 @@ function isOutOfFlowFrame(
2432
2432
  }
2433
2433
 
2434
2434
  function hasColumnBreak(block: SurfaceBlockSnapshot): boolean {
2435
- return block.kind === "paragraph" && block.segments.some(
2436
- (segment) =>
2437
- segment.kind === "opaque_inline" &&
2438
- segment.label === "Column break",
2439
- );
2435
+ if (block.kind === "paragraph") {
2436
+ return block.segments.some(
2437
+ (segment) =>
2438
+ segment.kind === "opaque_inline" &&
2439
+ segment.label === "Column break",
2440
+ );
2441
+ }
2442
+ return nestedBlocks(block).some(hasColumnBreak);
2440
2443
  }
2441
2444
 
2442
2445
  function hasPageBreak(block: SurfaceBlockSnapshot): boolean {
2443
- return block.kind === "paragraph" && block.segments.some(
2444
- (segment) =>
2445
- segment.kind === "opaque_inline" &&
2446
- segment.label === "Page break",
2447
- );
2446
+ if (block.kind === "paragraph") {
2447
+ return block.segments.some(
2448
+ (segment) =>
2449
+ segment.kind === "opaque_inline" &&
2450
+ segment.label === "Page break",
2451
+ );
2452
+ }
2453
+ return nestedBlocks(block).some(hasPageBreak);
2454
+ }
2455
+
2456
+ function nestedBlocks(block: SurfaceBlockSnapshot): readonly SurfaceBlockSnapshot[] {
2457
+ if (block.kind === "sdt_block") {
2458
+ return block.children;
2459
+ }
2460
+ if (block.kind === "table") {
2461
+ return block.rows.flatMap((row) => row.cells.flatMap((cell) => cell.content));
2462
+ }
2463
+ return [];
2448
2464
  }
@@ -49,6 +49,34 @@ import {
49
49
  isBlockedImportFeatureKey,
50
50
  } from "../preservation/store.ts";
51
51
  import { getStoryBlocks } from "./story-targeting.ts";
52
+
53
+ /**
54
+ * Refactor/11b Slice A — internal seam for the L1 PM Node identity cache.
55
+ *
56
+ * Surface-projection attaches a parallel `(BlockNode | null)[]` array
57
+ * alongside `EditorSurfaceSnapshot.blocks` via a globally-registered
58
+ * Symbol. `pm-state-from-snapshot` reads it through an inline mirror of
59
+ * the same Symbol — `Symbol.for` guarantees both sides resolve to the
60
+ * same symbol even though they live in different layers and never
61
+ * import each other. This avoids a cross-layer import that would blow
62
+ * the Layer 11 boundary register.
63
+ *
64
+ * The property is non-enumerable, so JSON.stringify and public
65
+ * consumers of `EditorSurfaceSnapshot` see no change in shape.
66
+ */
67
+ const CANONICAL_BLOCK_REFS_SYMBOL = Symbol.for("wre.canonical-block-refs");
68
+
69
+ function attachCanonicalBlockRefs(
70
+ snapshot: EditorSurfaceSnapshot,
71
+ refs: readonly (BlockNode | null)[],
72
+ ): void {
73
+ Object.defineProperty(snapshot, CANONICAL_BLOCK_REFS_SYMBOL, {
74
+ value: refs,
75
+ enumerable: false,
76
+ configurable: true,
77
+ writable: false,
78
+ });
79
+ }
52
80
  import {
53
81
  collectSectionContexts,
54
82
  findHeaderFooterDocumentEntry,
@@ -182,6 +210,12 @@ export function createEditorSurfaceSnapshot(
182
210
  children: [...getStoryBlocks(document, activeStory)],
183
211
  });
184
212
  const blocks: SurfaceBlockSnapshot[] = [];
213
+ // Refactor/11b Slice A — canonical BlockNode ref parallel to `blocks`.
214
+ // Feeds the L1 identity cache in `pm-state-from-snapshot` so unchanged
215
+ // canonical blocks reuse their PM Node instance across commits, which
216
+ // lets PM's `node.eq()` short-circuit `ViewDesc.update()`. `null` for
217
+ // placeholder-culled blocks — those are synthetic, not canonical.
218
+ const blockRefs: (BlockNode | null)[] = [];
185
219
  const lockedFragmentIds: string[] = [];
186
220
  // L03 boundary: construct one formatting context per projection pass.
187
221
  // The context owns the theme resolver + numbering prefix counter +
@@ -251,6 +285,7 @@ export function createEditorSurfaceSnapshot(
251
285
 
252
286
  if (isInViewport) {
253
287
  blocks.push(surfaceBlock.block);
288
+ blockRefs.push(root.children[index]);
254
289
  lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
255
290
  } else {
256
291
  // Replace with size-preserving placeholder. from/to track the SAME
@@ -270,6 +305,8 @@ export function createEditorSurfaceSnapshot(
270
305
  placeholderSize,
271
306
  state: "placeholder-culled",
272
307
  } as SurfaceBlockSnapshot);
308
+ // Placeholder has no canonical ref — L1 cache will skip it.
309
+ blockRefs.push(null);
273
310
  // Do NOT push lockedFragmentIds — placeholder has no real fragment.
274
311
  }
275
312
 
@@ -317,7 +354,7 @@ export function createEditorSurfaceSnapshot(
317
354
  }
318
355
  }
319
356
 
320
- return {
357
+ const snapshot: EditorSurfaceSnapshot = {
321
358
  storySize: cursor,
322
359
  plainText: createPlainText(blocks),
323
360
  blocks,
@@ -325,6 +362,8 @@ export function createEditorSurfaceSnapshot(
325
362
  secondaryStories,
326
363
  viewportBlockRanges,
327
364
  };
365
+ attachCanonicalBlockRefs(snapshot, blockRefs);
366
+ return snapshot;
328
367
  }
329
368
 
330
369
  /**
@@ -1427,7 +1466,7 @@ function appendInlineSegments(
1427
1466
  return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
1428
1467
  }
1429
1468
  case "chart_preview": {
1430
- const parsedChartId = registerParsedChartPreview(node);
1469
+ const parsedChartId = registerParsedChartPreview(node, document);
1431
1470
  return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
1432
1471
  previewMediaId: node.previewMediaId,
1433
1472
  parsedChartId,
@@ -1438,6 +1477,9 @@ function appendInlineSegments(
1438
1477
  previewMediaId: node.previewMediaId,
1439
1478
  });
1440
1479
  case "shape":
1480
+ if (isMicrosoftSensitivityLabelShape(node)) {
1481
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
1482
+ }
1441
1483
  if (promoteSecondaryStoryTextBoxes && node.isTextBox && node.text) {
1442
1484
  return appendTextBoxSegment(
1443
1485
  paragraph,
@@ -1452,6 +1494,9 @@ function appendInlineSegments(
1452
1494
  case "wordart":
1453
1495
  return appendComplexPreviewSegment(paragraph, node, start, "WordArt", createWordArtDetail(node));
1454
1496
  case "vml_shape":
1497
+ if (isMicrosoftSensitivityLabelShape(node)) {
1498
+ return { nextCursor: start + 1, lockedFragmentIds: [] };
1499
+ }
1455
1500
  if (promoteSecondaryStoryTextBoxes && shouldRenderSecondaryStoryVmlTextBox(node)) {
1456
1501
  return appendTextBoxSegment(
1457
1502
  paragraph,
@@ -1487,7 +1532,8 @@ function appendInlineSegments(
1487
1532
  const label = c.isTextBox ? "Text box" : "Drawing shape";
1488
1533
  const detail = `DrawingFrame shape (${node.anchor.wrapMode}).`;
1489
1534
  const anchor = surfaceAnchorFromGeometry(node.anchor);
1490
- const txbxText = c.isTextBox ? extractTxbxFirstText(c.txbxBlocks) : undefined;
1535
+ const txbxTextSegment = c.isTextBox ? extractTxbxFirstTextSegment(c.txbxBlocks) : undefined;
1536
+ const txbxText = txbxTextSegment?.text ?? (c.isTextBox ? c.text : undefined);
1491
1537
  const surfaceFill = c.fill;
1492
1538
  paragraph.segments.push({
1493
1539
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -1501,12 +1547,17 @@ function appendInlineSegments(
1501
1547
  ...(surfaceFill ? { fill: surfaceFill } : {}),
1502
1548
  ...(c.line ? { line: c.line } : {}),
1503
1549
  ...(c.isTextBox ? { isTextBox: true } : {}),
1550
+ ...(c.textBoxBody ? { textBoxBody: c.textBoxBody } : {}),
1504
1551
  ...(txbxText ? { txbxText } : {}),
1552
+ ...(txbxTextSegment?.marks && txbxTextSegment.marks.length > 0
1553
+ ? { txbxMarks: txbxTextSegment.marks }
1554
+ : {}),
1555
+ ...(txbxTextSegment?.markAttrs ? { txbxMarkAttrs: txbxTextSegment.markAttrs } : {}),
1505
1556
  });
1506
1557
  return { nextCursor: start + 1, lockedFragmentIds: [] };
1507
1558
  }
1508
1559
  if (c.type === "chart_preview") {
1509
- const parsedChartId = registerParsedChartPreview(c);
1560
+ const parsedChartId = registerParsedChartPreview(c, document);
1510
1561
  return appendComplexPreviewSegment(
1511
1562
  paragraph,
1512
1563
  c,
@@ -1752,7 +1803,10 @@ function appendInlineSegments(
1752
1803
  }
1753
1804
  }
1754
1805
 
1755
- function registerParsedChartPreview(node: ChartPreviewNode): string | undefined {
1806
+ function registerParsedChartPreview(
1807
+ node: ChartPreviewNode,
1808
+ document: CanonicalDocumentEnvelope,
1809
+ ): string | undefined {
1756
1810
  if (!node.parsedData) return undefined;
1757
1811
  const parsedChartId = stableChartId(node.rawXml);
1758
1812
  // Always call `set` (even when the entry exists) so the active
@@ -1763,7 +1817,7 @@ function registerParsedChartPreview(node: ChartPreviewNode): string | undefined
1763
1817
  model: node.parsedData,
1764
1818
  widthPx,
1765
1819
  heightPx,
1766
- theme: undefined,
1820
+ theme: document.subParts?.resolvedTheme,
1767
1821
  });
1768
1822
  return parsedChartId;
1769
1823
  }
@@ -1898,6 +1952,7 @@ function surfacePictureEffectsFromContent(
1898
1952
  const outerShadow = resolveSurfacePictureShadow(content.outerShadow, themeResolver);
1899
1953
  const glow = resolveSurfacePictureGlow(content.glow, themeResolver);
1900
1954
  const has =
1955
+ content.lum !== undefined ||
1901
1956
  content.srcRect !== undefined ||
1902
1957
  content.rotation !== undefined ||
1903
1958
  content.flipH !== undefined ||
@@ -1909,6 +1964,7 @@ function surfacePictureEffectsFromContent(
1909
1964
  glow !== undefined;
1910
1965
  if (!has) return undefined;
1911
1966
  return {
1967
+ ...(content.lum ? { lum: { ...content.lum } } : {}),
1912
1968
  ...(content.srcRect ? { srcRect: { ...content.srcRect } } : {}),
1913
1969
  ...(content.rotation !== undefined ? { rotation: content.rotation } : {}),
1914
1970
  ...(content.flipH !== undefined ? { flipH: content.flipH } : {}),
@@ -2007,16 +2063,51 @@ function flattenSurfaceFieldDisplayText(
2007
2063
  * and read the immediate run content. Returns `undefined` when no text
2008
2064
  * is present.
2009
2065
  */
2010
- function extractTxbxFirstText(
2066
+ function extractTxbxFirstTextSegment(
2011
2067
  blocks: ShapeContent["txbxBlocks"],
2012
- ): string | undefined {
2068
+ ): {
2069
+ text: string;
2070
+ marks?: SurfaceTextMark[];
2071
+ markAttrs?: {
2072
+ backgroundColor?: string;
2073
+ charSpacing?: number;
2074
+ kerning?: number;
2075
+ textFill?: string;
2076
+ fontFamily?: string;
2077
+ fontSize?: number;
2078
+ textColor?: string;
2079
+ };
2080
+ } | undefined {
2013
2081
  if (!blocks || blocks.length === 0) return undefined;
2014
2082
  for (const block of blocks) {
2015
2083
  if (block.type !== "paragraph") continue;
2016
- const runs = (block as { runs?: ReadonlyArray<{ text?: string }> }).runs;
2017
- if (!runs) continue;
2018
- const text = runs.map((r) => r.text ?? "").join("").trim();
2019
- if (text) return text;
2084
+ const children = (block as { children?: ReadonlyArray<{ type?: string; text?: string; marks?: TextMark[] }> }).children;
2085
+ const legacyRuns = (block as { runs?: ReadonlyArray<{ text?: string; marks?: TextMark[] }> }).runs;
2086
+ if (!children && !legacyRuns) continue;
2087
+ let firstMarks: TextMark[] | undefined;
2088
+ const text = (children
2089
+ ? children
2090
+ .map((child) => {
2091
+ if (child.type !== "text") return "";
2092
+ if (!firstMarks && child.marks) firstMarks = child.marks;
2093
+ return child.text ?? "";
2094
+ })
2095
+ .join("")
2096
+ : legacyRuns
2097
+ ?.map((run) => {
2098
+ if (!firstMarks && run.marks) firstMarks = run.marks;
2099
+ return run.text ?? "";
2100
+ })
2101
+ .join("") ?? ""
2102
+ ).trim();
2103
+ if (text) {
2104
+ const cloned = firstMarks ? cloneMarks(firstMarks) : undefined;
2105
+ return {
2106
+ text,
2107
+ ...(cloned?.marks && cloned.marks.length > 0 ? { marks: cloned.marks } : {}),
2108
+ ...(cloned?.markAttrs ? { markAttrs: cloned.markAttrs } : {}),
2109
+ };
2110
+ }
2020
2111
  }
2021
2112
  return undefined;
2022
2113
  }
@@ -2072,6 +2163,17 @@ function shouldRenderSecondaryStoryVmlTextBox(node: VmlShapeNode): boolean {
2072
2163
  return Boolean(node.text) && (!node.shapeType || /_x0000_t202$/iu.test(node.shapeType));
2073
2164
  }
2074
2165
 
2166
+ function isMicrosoftSensitivityLabelShape(
2167
+ node: ShapeNode | VmlShapeNode,
2168
+ ): boolean {
2169
+ if (!/classification/i.test(node.text ?? "")) {
2170
+ return false;
2171
+ }
2172
+ return /\b(?:id|name)="MSIPCM/iu.test(node.rawXml) ||
2173
+ /\b(?:alt|descr)="[^"]*&quot;Placement&quot;:&quot;Footer&quot;/iu.test(node.rawXml) ||
2174
+ /"Placement"\s*:\s*"Footer"/iu.test(node.rawXml);
2175
+ }
2176
+
2075
2177
  function createChartDetail(node: ChartPreviewNode): string {
2076
2178
  const parts = ["Embedded chart."];
2077
2179
  if (node.previewMediaId) {
@@ -94,6 +94,11 @@ export function collectScopeRailSegments(
94
94
  const activeIds = new Set(input.activeWorkItemScopeIds ?? []);
95
95
 
96
96
  for (const scope of input.scopes ?? []) {
97
+ // Invisible scopes are runtime/agent context only. They may still
98
+ // participate in guard decisions, but they must not surface as rail,
99
+ // card, or body-tint chrome.
100
+ if (scope.visibility === "invisible") continue;
101
+
97
102
  const range = anchorToRuntimeRange(scope.anchor);
98
103
  if (!range) continue;
99
104
  const storyTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
@@ -187,7 +187,6 @@ import {
187
187
  withExportDelivery,
188
188
  } from "./browser-export";
189
189
  import { EditorShellView } from "./editor-shell-view.tsx";
190
- import { TwDebugPresentation } from "../ui-tailwind/debug/index.ts";
191
190
  import { shellPasteFragmentParser as SHELL_PASTE_FRAGMENT_PARSER } from "../shell/paste-adapter.ts";
192
191
  import { EditorSurfaceController } from "./editor-surface-controller.tsx";
193
192
  import type { EditorActionHostCallbacks } from "../ui-tailwind/chrome/editor-action-registry";
@@ -1061,7 +1060,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1061
1060
  chromeControllerRef,
1062
1061
  commandPaletteDisabled,
1063
1062
  customSelectionTools,
1064
- debugMode = "off",
1065
1063
  } = props;
1066
1064
 
1067
1065
  const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
@@ -1140,7 +1138,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1140
1138
  hostPosture: {
1141
1139
  reviewMode: reviewMode === "review" ? "reviewer" : "author",
1142
1140
  markupDisplay: normalizeHostMarkupDisplay(markupDisplay),
1143
- debugMode,
1144
1141
  chromePreset,
1145
1142
  },
1146
1143
  chromePresetInput: {
@@ -1261,7 +1258,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1261
1258
  api,
1262
1259
  reviewMode,
1263
1260
  markupDisplay,
1264
- debugMode,
1265
1261
  chromePreset,
1266
1262
  readOnly,
1267
1263
  ]);
@@ -3395,6 +3391,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3395
3391
  activeRuntime.rejectAllChanges();
3396
3392
  setActiveRailTab("changes");
3397
3393
  },
3394
+ onAcceptSuggestionGroup: (groupId: string) =>
3395
+ applySuggestionGroupAction(activeRuntime, groupId, "accept"),
3396
+ onRejectSuggestionGroup: (groupId: string) =>
3397
+ applySuggestionGroupAction(activeRuntime, groupId, "reject"),
3398
3398
  onCloseStory: () => {
3399
3399
  activeRuntime.closeStory();
3400
3400
  },
@@ -3907,10 +3907,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3907
3907
  });
3908
3908
  }}
3909
3909
  onScopeAcceptSuggestionGroup={(payload) => {
3910
- applySuggestionGroupAction(activeRuntime, payload.groupId, "accept");
3910
+ commands.onAcceptSuggestionGroup?.(payload.groupId);
3911
3911
  }}
3912
3912
  onScopeRejectSuggestionGroup={(payload) => {
3913
- applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
3913
+ commands.onRejectSuggestionGroup?.(payload.groupId);
3914
3914
  }}
3915
3915
  mediaPreviews={mediaPreviews}
3916
3916
  onActivateFloatingImage={(payload) => {
@@ -3959,10 +3959,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
3959
3959
  }}
3960
3960
  />
3961
3961
  <TwRuntimeReplDialog runtime={activeRuntime} editorRef={editorRefForRepl} />
3962
- <TwDebugPresentation
3963
- mode={debugMode}
3964
- sessionId={documentId}
3965
- />
3966
3962
  </>
3967
3963
  </OverlayAnchorBridgeProvider>
3968
3964
  </UiShellChannelsProvider>
@@ -104,6 +104,8 @@ export interface EditorCommandBag {
104
104
  onRejectRevision(revisionId: string): void;
105
105
  onAcceptAllChanges(): void;
106
106
  onRejectAllChanges(): void;
107
+ onAcceptSuggestionGroup?(groupId: string): void;
108
+ onRejectSuggestionGroup?(groupId: string): void;
107
109
  onCloseStory?(): void;
108
110
  /**
109
111
  * @deprecated P8.11 — see the matching prop on `TwReviewWorkspaceProps`.
@@ -79,7 +79,7 @@ let nextControllerId = 0;
79
79
  export interface ShellUiControllerDeps {
80
80
  /**
81
81
  * Current host-provided posture slice — reviewMode / markupDisplay /
82
- * debugMode / chromePreset. MUST read through a live ref so the factory
82
+ * chromePreset. MUST read through a live ref so the factory
83
83
  * closure returns current render state, not stale construction-time state.
84
84
  */
85
85
  readonly getHostPosture?: () => ChromeHostPosture | undefined;
@@ -95,7 +95,7 @@ export interface ShellUiControllerDeps {
95
95
  readonly getOverlayAnchor?: (query: OverlayAnchorQuery) => GeometryRect | null;
96
96
  /**
97
97
  * Posture-change stream. Fires when any of the inputs to `ChromePosture`
98
- * change — reviewMode / markupDisplay / debugMode / chromePreset (host),
98
+ * change — reviewMode / markupDisplay / chromePreset (host),
99
99
  * effectiveMode / blockedReasons / documentMode / readOnly (runtime).
100
100
  * Returns an unsubscribe function.
101
101
  *