@beyondwork/docx-react-component 1.0.42 → 1.0.45

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 (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -30,7 +30,12 @@ import { resolvePageFieldDisplayText } from "../../runtime/layout/resolve-page-f
30
30
  export const PAGE_CHROME_DEFAULTS = {
31
31
  headerBandPx: 32,
32
32
  footerBandPx: 32,
33
- interGapPx: 24,
33
+ // L8 polish (2026-04-19): bumped from 24 → 48 so the workspace canvas
34
+ // between papers reads as a real break between discrete pages, not a
35
+ // seam inside one long paper. Phase D's discrete paper cards will
36
+ // re-tune once overlay-owned gaps ship; until then the widget spacer
37
+ // carries the full gap height.
38
+ interGapPx: 48,
34
39
  } as const;
35
40
 
36
41
  export function totalPageBreakGapPx(
@@ -73,6 +78,15 @@ export interface PageBreakDecorationInput {
73
78
  headerPreviewByPageId?: ReadonlyMap<string, string>;
74
79
  /** Same shape for footers. */
75
80
  footerPreviewByPageId?: ReadonlyMap<string, string>;
81
+ /**
82
+ * L7 Phase 2 Task 2.2.4a — optional per-page block-index range so the
83
+ * viewport-tracking hook can translate page visibility into block indices
84
+ * without re-walking the surface blocks. Keyed by pageIndex (0-based).
85
+ * When omitted, the chrome widgets are emitted without block-index
86
+ * attributes and the hook falls back to its ESTIMATED_BLOCKS_PER_PAGE
87
+ * heuristic.
88
+ */
89
+ blockIndexRangeByPageIndex?: ReadonlyMap<number, { first: number; last: number }>;
76
90
  }
77
91
 
78
92
  export function buildPageBreakDecorations(
@@ -88,6 +102,7 @@ export function buildPageBreakDecorations(
88
102
  const interGapPx = input.interGapPx ?? PAGE_CHROME_DEFAULTS.interGapPx;
89
103
 
90
104
  const decorations: Decoration[] = [];
105
+
91
106
  for (let i = 1; i < graph.pages.length; i += 1) {
92
107
  const prev = graph.pages[i - 1]!;
93
108
  const next = graph.pages[i]!;
@@ -106,6 +121,8 @@ export function buildPageBreakDecorations(
106
121
  const nextHeaderPreview =
107
122
  input.headerPreviewByPageId?.get(next.pageId) ?? "";
108
123
 
124
+ const nextBlockRange = input.blockIndexRangeByPageIndex?.get(next.pageIndex);
125
+
109
126
  decorations.push(
110
127
  Decoration.widget(
111
128
  pmOffset,
@@ -125,6 +142,8 @@ export function buildPageBreakDecorations(
125
142
  hasNextHeaderStory: Boolean(next.stories.header),
126
143
  prevFooterPreview,
127
144
  nextHeaderPreview,
145
+ nextPageFirstBlockIndex: nextBlockRange?.first ?? -1,
146
+ nextPageLastBlockIndex: nextBlockRange?.last ?? -1,
128
147
  }),
129
148
  {
130
149
  side: -1,
@@ -176,6 +195,16 @@ interface ChromeWidgetInput {
176
195
  hasNextHeaderStory: boolean;
177
196
  prevFooterPreview: string;
178
197
  nextHeaderPreview: string;
198
+ /**
199
+ * L7 Phase 2 Task 2.2.4a — inclusive first block index of the next page.
200
+ * -1 when block-index info was not supplied to the decoration builder.
201
+ */
202
+ nextPageFirstBlockIndex: number;
203
+ /**
204
+ * L7 Phase 2 Task 2.2.4a — inclusive last block index of the next page.
205
+ * -1 when block-index info was not supplied to the decoration builder.
206
+ */
207
+ nextPageLastBlockIndex: number;
179
208
  }
180
209
 
181
210
  // P14.c — cache the widget DOM by input identity. PM rebuilds the
@@ -205,6 +234,8 @@ function widgetCacheKey(input: ChromeWidgetInput): string {
205
234
  input.hasNextHeaderStory ? "1" : "0",
206
235
  input.prevFooterPreview,
207
236
  input.nextHeaderPreview,
237
+ input.nextPageFirstBlockIndex,
238
+ input.nextPageLastBlockIndex,
208
239
  ].join("\x1f");
209
240
  }
210
241
 
@@ -238,14 +269,27 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
238
269
  root.setAttribute("data-posture", input.posture);
239
270
  root.setAttribute("data-prev-page-id", input.prevPageId);
240
271
  root.setAttribute("data-next-page-id", input.nextPageId);
241
- root.setAttribute("data-prev-page-index", String(input.prevPageIndex));
242
- root.setAttribute("data-next-page-index", String(input.nextPageIndex));
272
+ // NOTE: data-prev-page-index and data-next-page-index were removed — they had
273
+ // no reader after data-page-frame was added (Task 2.2.4a). The page-id attrs
274
+ // above are still consumed by the chrome overlay (see line ~265 in this file).
243
275
  // P3.a: expose page-frame boundary markers so the page stack and tests
244
276
  // can enumerate pages without re-walking the render graph. Each widget
245
277
  // ends page N (`prev`) and starts page N+1 (`next`); the outer workspace
246
278
  // frame still accounts for the boundaries at both ends of the document.
247
279
  root.setAttribute("data-page-frame-end", input.prevPageId);
248
280
  root.setAttribute("data-page-frame-start", input.nextPageId);
281
+ // L7 Phase 2 Task 2.2.4a — per-page markers for the viewport-tracking hook
282
+ // (`useVisibleBlockRange`). Each boundary widget identifies the page that
283
+ // STARTS at this boundary (i.e. `nextPageIndex`). The hook's
284
+ // IntersectionObserver picks these up via `[data-page-frame]` to compute the
285
+ // visible block range. Block-index attributes are set only when the caller
286
+ // supplied a `blockIndexRangeByPageIndex` map (value ≥ 0); otherwise they
287
+ // are omitted so the hook's fallback estimate is used cleanly.
288
+ root.setAttribute("data-page-frame", String(input.nextPageIndex));
289
+ if (input.nextPageFirstBlockIndex >= 0) {
290
+ root.setAttribute("data-page-first-block-index", String(input.nextPageFirstBlockIndex));
291
+ root.setAttribute("data-page-last-block-index", String(input.nextPageLastBlockIndex));
292
+ }
249
293
  root.contentEditable = "false";
250
294
  root.setAttribute("aria-hidden", "false");
251
295
  root.style.display = "block";
@@ -286,175 +330,42 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
286
330
  return root;
287
331
  }
288
332
 
289
- // PAGE-MODE chrome: footer band of prev + visible gap + header band of next.
333
+ // L8 Phase B (2026-04-19): the page-posture widget is a **transparent
334
+ // spacer**. Inline band / gap / band chrome is retired — the real H/F
335
+ // bands are painted by `TwPageStackChromeLayer` as per-page overlays
336
+ // (P8.11). The spacer keeps the same total height as the retired stack
337
+ // so `TwPageStackOverlayLayer.measureWidgetsViaBoundingRect` continues to
338
+ // produce the same page-overlay rect math; L8 Phase D will take over
339
+ // painting the gap and reduce this to 0.
340
+ //
341
+ // L8 polish (2026-04-19): the spacer carries a subtle, borderless
342
+ // page-count label centered in the inter-page gap so the workspace
343
+ // canvas reads as "two papers with a page number between them" — no
344
+ // pill, no border, just small muted text.
290
345
  root.style.height = `${
291
346
  input.footerBandPx + input.interGapPx + input.headerBandPx
292
347
  }px`;
293
348
  root.style.position = "relative";
294
-
295
- const footer = buildBand({
296
- kind: "footer",
297
- pageId: input.prevPageId,
298
- pageIndex: input.prevPageIndex,
299
- pageLabel: input.prevPageLabel,
300
- bandPx: input.footerBandPx,
301
- position: "top",
302
- hasStory: input.hasPrevFooterStory,
303
- previewText: input.prevFooterPreview,
304
- });
305
- root.appendChild(footer);
306
-
307
- // P3.a: the inter-page gap is now a visible canvas strip that reads as
308
- // "the space between two papers", not a subtle gradient inside one white
309
- // page. The strip paints in the workspace canvas color (the same color
310
- // the page frames float on in page mode), with subtle drop/rise shadows
311
- // on either edge so the preceding footer reads as "bottom of page N's
312
- // paper" and the following header reads as "top of page N+1's paper".
313
- //
314
- // The visual goal: bringing the user closer to a Word-native perception
315
- // of distinct pages without requiring the PM editable tree to be split
316
- // into per-page subtrees (that lands in P3.b).
317
- const separator = document.createElement("div");
318
- separator.className = "wre-page-chrome-separator";
319
- separator.setAttribute("data-kind", "page-chrome-separator");
320
- separator.style.position = "absolute";
321
- separator.style.left = "0";
322
- separator.style.right = "0";
323
- separator.style.top = `${input.footerBandPx}px`;
324
- separator.style.height = `${input.interGapPx}px`;
325
- // Canvas color (same as the scroll root's bg-surface) so the strip reads
326
- // as "gap between two papers". Page mode and canvas mode both use the
327
- // same token so the UX remains consistent at any chrome preset.
328
- separator.style.backgroundColor = "var(--color-surface, #f1f5f9)";
329
- // Inner shadows on top/bottom: the previous footer's bottom edge gains
330
- // a subtle paper-edge shadow, and the next header's top edge likewise.
331
- // Inset shadows let us avoid touching the footer / header DOM while
332
- // keeping the shadows flush with the band borders.
333
- separator.style.boxShadow = [
334
- // Top edge — simulates the bottom shadow of page N's paper.
335
- "inset 0 1px 0 rgba(15, 23, 42, 0.06)",
336
- "inset 0 2px 3px -2px rgba(15, 23, 42, 0.12)",
337
- // Bottom edge — simulates the top shadow of page N+1's paper.
338
- "inset 0 -1px 0 rgba(15, 23, 42, 0.06)",
339
- "inset 0 -2px 3px -2px rgba(15, 23, 42, 0.12)",
340
- ].join(", ");
341
- root.appendChild(separator);
342
-
343
- const header = buildBand({
344
- kind: "header",
345
- pageId: input.nextPageId,
346
- pageIndex: input.nextPageIndex,
347
- pageLabel: input.nextPageLabel,
348
- bandPx: input.headerBandPx,
349
- position: "bottom",
350
- topOffsetPx: input.footerBandPx + input.interGapPx,
351
- hasStory: input.hasNextHeaderStory,
352
- previewText: input.nextHeaderPreview,
353
- });
354
- root.appendChild(header);
355
-
356
- return root;
357
- }
358
-
359
- function buildBand(input: {
360
- kind: "header" | "footer";
361
- pageId: string;
362
- pageIndex: number;
363
- pageLabel: string;
364
- bandPx: number;
365
- position: "top" | "bottom";
366
- topOffsetPx?: number;
367
- hasStory: boolean;
368
- previewText: string;
369
- }): HTMLElement {
370
- const band = document.createElement("div");
371
- band.className = `wre-page-chrome-band wre-page-chrome-band-${input.kind}`;
372
- band.setAttribute("data-band-kind", input.kind);
373
- band.setAttribute("data-page-id", input.pageId);
374
- band.setAttribute("data-page-index", String(input.pageIndex));
375
- band.style.position = "absolute";
376
- band.style.left = "0";
377
- band.style.right = "0";
378
- band.style.top = `${input.topOffsetPx ?? 0}px`;
379
- band.style.height = `${input.bandPx}px`;
380
- band.style.display = "flex";
381
- band.style.alignItems = "center";
382
- band.style.justifyContent = "space-between";
383
- band.style.padding = "0 16px";
384
- band.style.fontSize = "10px";
385
- band.style.letterSpacing = "0.12em";
386
- band.style.textTransform = "uppercase";
387
- band.style.color = "var(--color-text-tertiary, #6b7280)";
388
- band.style.backgroundColor =
389
- "var(--color-surface-subtle, rgba(0,0,0,0.02))";
390
- band.style.borderTop =
391
- input.kind === "header"
392
- ? "1px solid var(--color-border, rgba(0,0,0,0.08))"
393
- : "none";
394
- band.style.borderBottom =
395
- input.kind === "footer"
396
- ? "1px solid var(--color-border, rgba(0,0,0,0.08))"
397
- : "none";
398
- // Bands are interactive: double-click fires a custom event the shell
399
- // forwards to `runtime.openStory()`.
400
- band.style.pointerEvents = "auto";
401
- band.style.cursor = input.hasStory ? "pointer" : "default";
402
- band.title = input.hasStory
403
- ? `Double-click to edit ${input.kind}`
404
- : `No ${input.kind} defined for this page`;
349
+ root.style.pointerEvents = "none";
350
+ root.setAttribute("aria-hidden", "true");
405
351
 
406
352
  const label = document.createElement("span");
407
- label.className = "wre-page-chrome-band-label";
408
- if (input.previewText && input.previewText.trim().length > 0) {
409
- // Show the live content (with PAGE/NUMPAGES resolved) rather than the
410
- // static "Header" / "Footer" placeholder. Band is compact so truncate
411
- // at ~80 chars visually via CSS overflow; keep raw in textContent.
412
- label.textContent = input.previewText;
413
- label.style.textTransform = "none";
414
- label.style.letterSpacing = "0";
415
- label.style.fontSize = "11px";
416
- label.style.color = "var(--color-text-secondary, #374151)";
417
- label.style.overflow = "hidden";
418
- label.style.textOverflow = "ellipsis";
419
- label.style.whiteSpace = "nowrap";
420
- label.style.maxWidth = "70%";
421
- } else {
422
- label.textContent = input.kind === "header" ? "Header" : "Footer";
423
- }
424
- band.appendChild(label);
425
-
426
- const pageLabel = document.createElement("span");
427
- pageLabel.className = "wre-page-chrome-band-page";
428
- pageLabel.textContent = input.pageLabel;
429
- band.appendChild(pageLabel);
353
+ label.className = "wre-page-chrome-gap-label";
354
+ label.setAttribute("data-kind", "page-gap-label");
355
+ label.textContent = input.nextPageLabel;
356
+ label.style.position = "absolute";
357
+ label.style.left = "50%";
358
+ label.style.top = "50%";
359
+ label.style.transform = "translate(-50%, -50%)";
360
+ label.style.fontSize = "10px";
361
+ label.style.letterSpacing = "0.12em";
362
+ label.style.textTransform = "uppercase";
363
+ label.style.color = "var(--color-tertiary, #6b7280)";
364
+ // Intentionally no background, no border, no padding — the label floats
365
+ // transparently in the workspace-canvas gap.
366
+ root.appendChild(label);
430
367
 
431
- if (input.hasStory) {
432
- band.addEventListener("dblclick", (event) => {
433
- event.stopPropagation();
434
- event.preventDefault();
435
- const eventName =
436
- input.kind === "header"
437
- ? "wre-open-header-story-for-page"
438
- : "wre-open-footer-story-for-page";
439
- // Use the band's owning document's `CustomEvent` constructor so the
440
- // event passes through jsdom's instance-of check. In a real browser
441
- // `band.ownerDocument.defaultView.CustomEvent` is the same as the
442
- // global `CustomEvent`; in jsdom the two differ and the global one
443
- // fails `dispatchEvent`'s internal Event-type convert step.
444
- const view = band.ownerDocument?.defaultView as
445
- | (Window & typeof globalThis)
446
- | null;
447
- const Ctor = view?.CustomEvent ?? CustomEvent;
448
- band.dispatchEvent(
449
- new Ctor(eventName, {
450
- bubbles: true,
451
- detail: { pageIndex: input.pageIndex, pageId: input.pageId },
452
- }),
453
- );
454
- });
455
- }
456
-
457
- return band;
368
+ return root;
458
369
  }
459
370
 
460
371
  /**
@@ -160,9 +160,26 @@ export const editorSchema = new Schema({
160
160
  bidi: { default: null },
161
161
  pageBreakBefore: { default: null },
162
162
  hiddenTextOnly: { default: null },
163
+ placeholderCulled: { default: null },
164
+ blockId: { default: null },
163
165
  },
164
166
  parseDOM: [{ tag: "p" }],
165
167
  toDOM(node) {
168
+ // Viewport-culled placeholder paragraph — cheap size-preserving leaf.
169
+ if (node.attrs.placeholderCulled) {
170
+ return [
171
+ "div",
172
+ {
173
+ "data-node-type": "paragraph-placeholder",
174
+ "data-placeholder-culled": "true",
175
+ "data-placeholder-size": String(node.nodeSize),
176
+ "data-placeholder-block-id": node.attrs.blockId ?? "",
177
+ style: "min-height: 20px; contain: strict;",
178
+ "aria-hidden": "true",
179
+ },
180
+ 0,
181
+ ];
182
+ }
166
183
  const classes: string[] = ["leading-relaxed"];
167
184
  const styleId = node.attrs.styleId as string | null;
168
185
  const outlineLevel = node.attrs.outlineLevel as number | null;
@@ -634,16 +651,51 @@ export const editorSchema = new Schema({
634
651
  selectable: false,
635
652
  attrs: {
636
653
  previewMediaId: { default: null },
654
+ previewSrc: { default: null },
637
655
  detail: { default: null },
638
656
  },
639
657
  toDOM(node) {
658
+ const previewSrc = node.attrs.previewSrc as string | null;
659
+ const detail = (node.attrs.detail as string) ?? "Chart";
660
+ if (previewSrc) {
661
+ // Bitmap-backed: render the fallback image Word cached in mc:Fallback.
662
+ // The corner chip preserves the typed identity so agents and humans
663
+ // still see "this is a chart" at a glance.
664
+ return [
665
+ "span",
666
+ {
667
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
668
+ "data-node-type": "chart_atom",
669
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
670
+ contenteditable: "false",
671
+ title: detail,
672
+ },
673
+ [
674
+ "img",
675
+ {
676
+ src: previewSrc,
677
+ alt: detail,
678
+ class: "block max-w-full h-auto",
679
+ draggable: "false",
680
+ },
681
+ ],
682
+ [
683
+ "span",
684
+ {
685
+ 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",
686
+ "aria-hidden": "true",
687
+ },
688
+ "\uD83D\uDCC8 Chart",
689
+ ],
690
+ ];
691
+ }
640
692
  return [
641
693
  "span",
642
694
  {
643
695
  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",
644
696
  "data-node-type": "chart_atom",
645
697
  contenteditable: "false",
646
- title: (node.attrs.detail as string) ?? "Chart",
698
+ title: detail,
647
699
  },
648
700
  "\uD83D\uDCC8 Chart",
649
701
  ];
@@ -657,16 +709,48 @@ export const editorSchema = new Schema({
657
709
  selectable: false,
658
710
  attrs: {
659
711
  previewMediaId: { default: null },
712
+ previewSrc: { default: null },
660
713
  detail: { default: null },
661
714
  },
662
715
  toDOM(node) {
716
+ const previewSrc = node.attrs.previewSrc as string | null;
717
+ const detail = (node.attrs.detail as string) ?? "SmartArt";
718
+ if (previewSrc) {
719
+ return [
720
+ "span",
721
+ {
722
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
723
+ "data-node-type": "smartart_atom",
724
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
725
+ contenteditable: "false",
726
+ title: detail,
727
+ },
728
+ [
729
+ "img",
730
+ {
731
+ src: previewSrc,
732
+ alt: detail,
733
+ class: "block max-w-full h-auto",
734
+ draggable: "false",
735
+ },
736
+ ],
737
+ [
738
+ "span",
739
+ {
740
+ 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",
741
+ "aria-hidden": "true",
742
+ },
743
+ "\uD83D\uDDFA SmartArt",
744
+ ],
745
+ ];
746
+ }
663
747
  return [
664
748
  "span",
665
749
  {
666
750
  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",
667
751
  "data-node-type": "smartart_atom",
668
752
  contenteditable: "false",
669
- title: (node.attrs.detail as string) ?? "SmartArt",
753
+ title: detail,
670
754
  },
671
755
  "\uD83D\uDDFA SmartArt",
672
756
  ];
@@ -707,17 +791,50 @@ export const editorSchema = new Schema({
707
791
  attrs: {
708
792
  text: { default: "" },
709
793
  geometry: { default: null },
794
+ previewMediaId: { default: null },
795
+ previewSrc: { default: null },
710
796
  detail: { default: null },
711
797
  },
712
798
  toDOM(node) {
713
799
  const text = node.attrs.text as string;
800
+ const previewSrc = node.attrs.previewSrc as string | null;
801
+ const detail = (node.attrs.detail as string) ?? (text ? `WordArt: ${text}` : "WordArt");
802
+ if (previewSrc) {
803
+ return [
804
+ "span",
805
+ {
806
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
807
+ "data-node-type": "wordart_atom",
808
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
809
+ contenteditable: "false",
810
+ title: detail,
811
+ },
812
+ [
813
+ "img",
814
+ {
815
+ src: previewSrc,
816
+ alt: detail,
817
+ class: "block max-w-full h-auto",
818
+ draggable: "false",
819
+ },
820
+ ],
821
+ [
822
+ "span",
823
+ {
824
+ 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",
825
+ "aria-hidden": "true",
826
+ },
827
+ "\u2728 WordArt",
828
+ ],
829
+ ];
830
+ }
714
831
  return [
715
832
  "span",
716
833
  {
717
834
  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",
718
835
  "data-node-type": "wordart_atom",
719
836
  contenteditable: "false",
720
- title: (node.attrs.detail as string) ?? `WordArt: ${text}`,
837
+ title: detail,
721
838
  },
722
839
  "\u2728 " + (text || "WordArt"),
723
840
  ];
@@ -732,18 +849,51 @@ export const editorSchema = new Schema({
732
849
  attrs: {
733
850
  text: { default: null },
734
851
  shapeType: { default: null },
852
+ previewMediaId: { default: null },
853
+ previewSrc: { default: null },
735
854
  detail: { default: null },
736
855
  },
737
856
  toDOM(node) {
738
857
  const text = node.attrs.text as string | null;
739
858
  const label = text ? `VML: ${text}` : "VML shape";
859
+ const previewSrc = node.attrs.previewSrc as string | null;
860
+ const detail = (node.attrs.detail as string) ?? label;
861
+ if (previewSrc) {
862
+ return [
863
+ "span",
864
+ {
865
+ class: "relative inline-block align-baseline mx-0.5 max-w-full",
866
+ "data-node-type": "vml_atom",
867
+ "data-preview-media-id": (node.attrs.previewMediaId as string) ?? "",
868
+ contenteditable: "false",
869
+ title: detail,
870
+ },
871
+ [
872
+ "img",
873
+ {
874
+ src: previewSrc,
875
+ alt: detail,
876
+ class: "block max-w-full h-auto",
877
+ draggable: "false",
878
+ },
879
+ ],
880
+ [
881
+ "span",
882
+ {
883
+ 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",
884
+ "aria-hidden": "true",
885
+ },
886
+ "\u25A6 VML",
887
+ ],
888
+ ];
889
+ }
740
890
  return [
741
891
  "span",
742
892
  {
743
893
  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",
744
894
  "data-node-type": "vml_atom",
745
895
  contenteditable: "false",
746
- title: (node.attrs.detail as string) ?? label,
896
+ title: detail,
747
897
  },
748
898
  "\u25A6 " + label,
749
899
  ];
@@ -759,10 +909,26 @@ export const editorSchema = new Schema({
759
909
  warningId: { default: "" },
760
910
  label: { default: "Locked" },
761
911
  detail: { default: "" },
912
+ presentation: { default: "callout" },
913
+ placeholderSize: { default: null },
762
914
  },
763
915
  toDOM(node) {
764
916
  const fragmentId = node.attrs.fragmentId as string;
765
917
  const isPreview = fragmentId.startsWith("preview:");
918
+ const presentation = node.attrs.presentation as string;
919
+ if (presentation === "quiet-marker") {
920
+ return [
921
+ "div",
922
+ {
923
+ class: "block h-0 w-0 overflow-hidden",
924
+ contenteditable: "false",
925
+ "data-node-type": "opaque_block",
926
+ "data-block-presentation": "quiet-marker",
927
+ title: node.attrs.detail as string,
928
+ "aria-label": node.attrs.label as string,
929
+ },
930
+ ];
931
+ }
766
932
  return [
767
933
  "div",
768
934
  {