@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
@@ -0,0 +1,389 @@
1
+ /**
2
+ * In-flow page chrome as ProseMirror widget decorations.
3
+ *
4
+ * Every page boundary (between pages N and N+1) gets a widget whose DOM
5
+ * renders the full visible inter-page chrome:
6
+ * - the bottom edge + footer band of page N (with a PAGE/NUMPAGES read
7
+ * for page N)
8
+ * - a visible vertical gap with the inter-page "separator line" between
9
+ * the two frames (the frames themselves are rendered as CSS borders
10
+ * on the chrome widget's sections, so no absolute overlay is needed)
11
+ * - the top edge + header band of page N+1 (with a PAGE/NUMPAGES read
12
+ * for page N+1)
13
+ *
14
+ * Because the chrome lives INSIDE the PM flow, it cannot drift relative
15
+ * to paragraphs: whatever height the browser paints for the paragraphs
16
+ * naturally stacks with the widget's fixed height. No DOM measurement
17
+ * or absolute positioning is needed — this closes the alignment gap the
18
+ * earlier absolute-overlay approach suffered from.
19
+ *
20
+ * Double-clicking a band dispatches a custom event
21
+ * `wre-open-header-story-for-page` / `wre-open-footer-story-for-page`
22
+ * whose `detail.pageIndex` is the target page. The shell listens at the
23
+ * document level and routes to `runtime.openStory()`.
24
+ */
25
+
26
+ import { Decoration } from "prosemirror-view";
27
+ import type { RuntimePageGraph } from "../../runtime/layout/page-graph.ts";
28
+ import { resolvePageFieldDisplayText } from "../../runtime/layout/resolve-page-fields.ts";
29
+
30
+ export const PAGE_CHROME_DEFAULTS = {
31
+ headerBandPx: 32,
32
+ footerBandPx: 32,
33
+ interGapPx: 24,
34
+ } as const;
35
+
36
+ export function totalPageBreakGapPx(
37
+ dimensions: {
38
+ headerBandPx?: number;
39
+ footerBandPx?: number;
40
+ interGapPx?: number;
41
+ } = {},
42
+ ): number {
43
+ const header = dimensions.headerBandPx ?? PAGE_CHROME_DEFAULTS.headerBandPx;
44
+ const footer = dimensions.footerBandPx ?? PAGE_CHROME_DEFAULTS.footerBandPx;
45
+ const gap = dimensions.interGapPx ?? PAGE_CHROME_DEFAULTS.interGapPx;
46
+ return header + footer + gap;
47
+ }
48
+
49
+ export type PageChromePosture = "canvas" | "page";
50
+
51
+ export interface PageBreakDecorationInput {
52
+ graph: RuntimePageGraph | null;
53
+ /** Controls the visual weight of the chrome; canvas is minimal. */
54
+ posture: PageChromePosture;
55
+ /** Height in px of each page's header band. Default 32. */
56
+ headerBandPx?: number;
57
+ /** Height in px of each page's footer band. Default 32. */
58
+ footerBandPx?: number;
59
+ /** Visible gap between the footer of page N and the header of page N+1. */
60
+ interGapPx?: number;
61
+ /**
62
+ * Map a canonical runtime offset to a ProseMirror document offset. The
63
+ * surface's `PositionMap` provides this. Optional — when omitted, we
64
+ * use the runtime offset as-is (tests without a mapped PM surface).
65
+ */
66
+ runtimeToPmOffset?: (runtimeOffset: number) => number | null;
67
+ /**
68
+ * Optional per-page preview text for the header band (`pageId` →
69
+ * flattened first-paragraph text with PAGE / NUMPAGES resolved). When a
70
+ * preview is present it replaces the generic "Header" label in the band
71
+ * on that page. See `resolve-page-previews.ts`.
72
+ */
73
+ headerPreviewByPageId?: ReadonlyMap<string, string>;
74
+ /** Same shape for footers. */
75
+ footerPreviewByPageId?: ReadonlyMap<string, string>;
76
+ }
77
+
78
+ export function buildPageBreakDecorations(
79
+ input: PageBreakDecorationInput,
80
+ ): Decoration[] {
81
+ const { graph, posture, runtimeToPmOffset } = input;
82
+ if (!graph || graph.pages.length < 2) return [];
83
+
84
+ const headerBandPx =
85
+ input.headerBandPx ?? PAGE_CHROME_DEFAULTS.headerBandPx;
86
+ const footerBandPx =
87
+ input.footerBandPx ?? PAGE_CHROME_DEFAULTS.footerBandPx;
88
+ const interGapPx = input.interGapPx ?? PAGE_CHROME_DEFAULTS.interGapPx;
89
+
90
+ const decorations: Decoration[] = [];
91
+ for (let i = 1; i < graph.pages.length; i += 1) {
92
+ const prev = graph.pages[i - 1]!;
93
+ const next = graph.pages[i]!;
94
+ if (next.isBlankFiller) continue;
95
+ const runtimeOffset = next.startOffset;
96
+ const pmOffset = runtimeToPmOffset
97
+ ? runtimeToPmOffset(runtimeOffset)
98
+ : runtimeOffset;
99
+ if (pmOffset === null || pmOffset === undefined) continue;
100
+
101
+ const prevPageLabel = pageLabelForChrome(prev, graph, posture);
102
+ const nextPageLabel = pageLabelForChrome(next, graph, posture);
103
+
104
+ const prevFooterPreview =
105
+ input.footerPreviewByPageId?.get(prev.pageId) ?? "";
106
+ const nextHeaderPreview =
107
+ input.headerPreviewByPageId?.get(next.pageId) ?? "";
108
+
109
+ decorations.push(
110
+ Decoration.widget(
111
+ pmOffset,
112
+ () =>
113
+ buildChromeWidgetDom({
114
+ posture,
115
+ prevPageId: prev.pageId,
116
+ prevPageIndex: prev.pageIndex,
117
+ nextPageId: next.pageId,
118
+ nextPageIndex: next.pageIndex,
119
+ headerBandPx,
120
+ footerBandPx,
121
+ interGapPx,
122
+ prevPageLabel,
123
+ nextPageLabel,
124
+ hasPrevFooterStory: Boolean(prev.stories.footer),
125
+ hasNextHeaderStory: Boolean(next.stories.header),
126
+ prevFooterPreview,
127
+ nextHeaderPreview,
128
+ }),
129
+ {
130
+ side: -1,
131
+ key: `pb-${prev.pageId}-${next.pageId}-${posture}`,
132
+ ignoreSelection: true,
133
+ stopEvent: (event) => {
134
+ // Keep the dbl-click from bubbling into PM and stealing focus.
135
+ if (event.type === "mousedown" || event.type === "click") {
136
+ return true;
137
+ }
138
+ return false;
139
+ },
140
+ },
141
+ ),
142
+ );
143
+ }
144
+ return decorations;
145
+ }
146
+
147
+ /**
148
+ * The page label shown in both the footer band of page N and the header
149
+ * band of page N+1. Canvas mode uses a terser "N / M" format because the
150
+ * dotted line is already minimal; page mode uses the full "Page N of M".
151
+ */
152
+ function pageLabelForChrome(
153
+ page: { stories: { displayPageNumber: number } },
154
+ graph: RuntimePageGraph,
155
+ posture: PageChromePosture,
156
+ ): string {
157
+ const total = graph.contentPageCount;
158
+ if (posture === "canvas") {
159
+ return `${page.stories.displayPageNumber} / ${total}`;
160
+ }
161
+ return `Page ${page.stories.displayPageNumber} of ${total}`;
162
+ }
163
+
164
+ interface ChromeWidgetInput {
165
+ posture: PageChromePosture;
166
+ prevPageId: string;
167
+ prevPageIndex: number;
168
+ nextPageId: string;
169
+ nextPageIndex: number;
170
+ headerBandPx: number;
171
+ footerBandPx: number;
172
+ interGapPx: number;
173
+ prevPageLabel: string;
174
+ nextPageLabel: string;
175
+ hasPrevFooterStory: boolean;
176
+ hasNextHeaderStory: boolean;
177
+ prevFooterPreview: string;
178
+ nextHeaderPreview: string;
179
+ }
180
+
181
+ function buildChromeWidgetDom(input: ChromeWidgetInput): HTMLElement {
182
+ const root = document.createElement("div");
183
+ root.className = "wre-page-chrome-widget";
184
+ root.setAttribute("data-kind", "page-chrome-widget");
185
+ root.setAttribute("data-posture", input.posture);
186
+ root.setAttribute("data-prev-page-id", input.prevPageId);
187
+ root.setAttribute("data-next-page-id", input.nextPageId);
188
+ root.setAttribute("data-prev-page-index", String(input.prevPageIndex));
189
+ root.setAttribute("data-next-page-index", String(input.nextPageIndex));
190
+ root.contentEditable = "false";
191
+ root.setAttribute("aria-hidden", "false");
192
+ root.style.display = "block";
193
+ root.style.width = "100%";
194
+ root.style.userSelect = "none";
195
+
196
+ if (input.posture === "canvas") {
197
+ // Single dotted horizontal line with a small page-number callout.
198
+ root.style.height = `${input.interGapPx + 1}px`;
199
+ root.style.position = "relative";
200
+
201
+ const line = document.createElement("div");
202
+ line.className = "wre-page-chrome-canvas-seam";
203
+ line.style.position = "absolute";
204
+ line.style.left = "0";
205
+ line.style.right = "0";
206
+ line.style.top = `${Math.round(input.interGapPx / 2)}px`;
207
+ line.style.height = "0";
208
+ line.style.borderTop = "1px dotted var(--color-border, rgba(0,0,0,0.3))";
209
+ root.appendChild(line);
210
+
211
+ const badge = document.createElement("span");
212
+ badge.className = "wre-page-chrome-canvas-badge";
213
+ badge.setAttribute("data-kind", "canvas-seam-badge");
214
+ badge.textContent = input.nextPageLabel;
215
+ badge.style.position = "absolute";
216
+ badge.style.top = `${Math.round(input.interGapPx / 2) - 9}px`;
217
+ badge.style.left = "50%";
218
+ badge.style.transform = "translateX(-50%)";
219
+ badge.style.fontSize = "10px";
220
+ badge.style.letterSpacing = "0.12em";
221
+ badge.style.textTransform = "uppercase";
222
+ badge.style.color = "var(--color-text-tertiary, #6b7280)";
223
+ badge.style.backgroundColor =
224
+ "var(--color-surface, rgba(255,255,255,0.9))";
225
+ badge.style.padding = "0 8px";
226
+ root.appendChild(badge);
227
+ return root;
228
+ }
229
+
230
+ // PAGE-MODE chrome: footer band of prev + visible gap + header band of next.
231
+ root.style.height = `${
232
+ input.footerBandPx + input.interGapPx + input.headerBandPx
233
+ }px`;
234
+ root.style.position = "relative";
235
+
236
+ const footer = buildBand({
237
+ kind: "footer",
238
+ pageId: input.prevPageId,
239
+ pageIndex: input.prevPageIndex,
240
+ pageLabel: input.prevPageLabel,
241
+ bandPx: input.footerBandPx,
242
+ position: "top",
243
+ hasStory: input.hasPrevFooterStory,
244
+ previewText: input.prevFooterPreview,
245
+ });
246
+ root.appendChild(footer);
247
+
248
+ const separator = document.createElement("div");
249
+ separator.className = "wre-page-chrome-separator";
250
+ separator.style.position = "absolute";
251
+ separator.style.left = "0";
252
+ separator.style.right = "0";
253
+ separator.style.top = `${input.footerBandPx}px`;
254
+ separator.style.height = `${input.interGapPx}px`;
255
+ // Background: two subtle page-edge shadows mimicking real paper gap.
256
+ separator.style.background =
257
+ "linear-gradient(to bottom, rgba(0,0,0,0.045), rgba(0,0,0,0) 40%, rgba(0,0,0,0) 60%, rgba(0,0,0,0.035))";
258
+ root.appendChild(separator);
259
+
260
+ const header = buildBand({
261
+ kind: "header",
262
+ pageId: input.nextPageId,
263
+ pageIndex: input.nextPageIndex,
264
+ pageLabel: input.nextPageLabel,
265
+ bandPx: input.headerBandPx,
266
+ position: "bottom",
267
+ topOffsetPx: input.footerBandPx + input.interGapPx,
268
+ hasStory: input.hasNextHeaderStory,
269
+ previewText: input.nextHeaderPreview,
270
+ });
271
+ root.appendChild(header);
272
+
273
+ return root;
274
+ }
275
+
276
+ function buildBand(input: {
277
+ kind: "header" | "footer";
278
+ pageId: string;
279
+ pageIndex: number;
280
+ pageLabel: string;
281
+ bandPx: number;
282
+ position: "top" | "bottom";
283
+ topOffsetPx?: number;
284
+ hasStory: boolean;
285
+ previewText: string;
286
+ }): HTMLElement {
287
+ const band = document.createElement("div");
288
+ band.className = `wre-page-chrome-band wre-page-chrome-band-${input.kind}`;
289
+ band.setAttribute("data-band-kind", input.kind);
290
+ band.setAttribute("data-page-id", input.pageId);
291
+ band.setAttribute("data-page-index", String(input.pageIndex));
292
+ band.style.position = "absolute";
293
+ band.style.left = "0";
294
+ band.style.right = "0";
295
+ band.style.top = `${input.topOffsetPx ?? 0}px`;
296
+ band.style.height = `${input.bandPx}px`;
297
+ band.style.display = "flex";
298
+ band.style.alignItems = "center";
299
+ band.style.justifyContent = "space-between";
300
+ band.style.padding = "0 16px";
301
+ band.style.fontSize = "10px";
302
+ band.style.letterSpacing = "0.12em";
303
+ band.style.textTransform = "uppercase";
304
+ band.style.color = "var(--color-text-tertiary, #6b7280)";
305
+ band.style.backgroundColor =
306
+ "var(--color-surface-subtle, rgba(0,0,0,0.02))";
307
+ band.style.borderTop =
308
+ input.kind === "header"
309
+ ? "1px solid var(--color-border, rgba(0,0,0,0.08))"
310
+ : "none";
311
+ band.style.borderBottom =
312
+ input.kind === "footer"
313
+ ? "1px solid var(--color-border, rgba(0,0,0,0.08))"
314
+ : "none";
315
+ // Bands are interactive: double-click fires a custom event the shell
316
+ // forwards to `runtime.openStory()`.
317
+ band.style.pointerEvents = "auto";
318
+ band.style.cursor = input.hasStory ? "pointer" : "default";
319
+ band.title = input.hasStory
320
+ ? `Double-click to edit ${input.kind}`
321
+ : `No ${input.kind} defined for this page`;
322
+
323
+ const label = document.createElement("span");
324
+ label.className = "wre-page-chrome-band-label";
325
+ if (input.previewText && input.previewText.trim().length > 0) {
326
+ // Show the live content (with PAGE/NUMPAGES resolved) rather than the
327
+ // static "Header" / "Footer" placeholder. Band is compact so truncate
328
+ // at ~80 chars visually via CSS overflow; keep raw in textContent.
329
+ label.textContent = input.previewText;
330
+ label.style.textTransform = "none";
331
+ label.style.letterSpacing = "0";
332
+ label.style.fontSize = "11px";
333
+ label.style.color = "var(--color-text-secondary, #374151)";
334
+ label.style.overflow = "hidden";
335
+ label.style.textOverflow = "ellipsis";
336
+ label.style.whiteSpace = "nowrap";
337
+ label.style.maxWidth = "70%";
338
+ } else {
339
+ label.textContent = input.kind === "header" ? "Header" : "Footer";
340
+ }
341
+ band.appendChild(label);
342
+
343
+ const pageLabel = document.createElement("span");
344
+ pageLabel.className = "wre-page-chrome-band-page";
345
+ pageLabel.textContent = input.pageLabel;
346
+ band.appendChild(pageLabel);
347
+
348
+ if (input.hasStory) {
349
+ band.addEventListener("dblclick", (event) => {
350
+ event.stopPropagation();
351
+ event.preventDefault();
352
+ const eventName =
353
+ input.kind === "header"
354
+ ? "wre-open-header-story-for-page"
355
+ : "wre-open-footer-story-for-page";
356
+ // Use the band's owning document's `CustomEvent` constructor so the
357
+ // event passes through jsdom's instance-of check. In a real browser
358
+ // `band.ownerDocument.defaultView.CustomEvent` is the same as the
359
+ // global `CustomEvent`; in jsdom the two differ and the global one
360
+ // fails `dispatchEvent`'s internal Event-type convert step.
361
+ const view = band.ownerDocument?.defaultView as
362
+ | (Window & typeof globalThis)
363
+ | null;
364
+ const Ctor = view?.CustomEvent ?? CustomEvent;
365
+ band.dispatchEvent(
366
+ new Ctor(eventName, {
367
+ bubbles: true,
368
+ detail: { pageIndex: input.pageIndex, pageId: input.pageId },
369
+ }),
370
+ );
371
+ });
372
+ }
373
+
374
+ return band;
375
+ }
376
+
377
+ /**
378
+ * Resolve a `PAGE` or `NUMPAGES` value for a specific page, using the graph.
379
+ * Small re-export + convenience wrapper so the PM surface's field-atom
380
+ * renderer can swap the cached display text without importing the
381
+ * resolver module directly.
382
+ */
383
+ export function resolvePageFieldForPage(
384
+ family: "PAGE" | "NUMPAGES",
385
+ cachedText: string,
386
+ input: { page: RuntimePageGraph["pages"][number]; graph: RuntimePageGraph },
387
+ ): string {
388
+ return resolvePageFieldDisplayText(family, cachedText, input);
389
+ }
@@ -137,6 +137,7 @@ export const editorSchema = new Schema({
137
137
  numberingSuffix: { default: null },
138
138
  numberingMarkerWidth: { default: null },
139
139
  numberingMarkerJustification: { default: null },
140
+ numberingMarkerRunProperties: { default: null },
140
141
  alignment: { default: null },
141
142
  spacingBefore: { default: null },
142
143
  spacingAfter: { default: null },
@@ -266,10 +267,47 @@ export const editorSchema = new Schema({
266
267
  : numberingSuffix === "space"
267
268
  ? "0.5rem"
268
269
  : "0.75rem";
270
+
271
+ const markerRunProperties = node.attrs.numberingMarkerRunProperties as
272
+ | {
273
+ bold?: boolean;
274
+ italic?: boolean;
275
+ underline?: string;
276
+ fontSizeHalfPoints?: number;
277
+ colorHex?: string;
278
+ fontFamily?: string;
279
+ fontFamilyAscii?: string;
280
+ }
281
+ | null;
282
+
283
+ const baseClasses: string[] = ["inline-flex", "select-none", "items-center"];
284
+ if (!markerRunProperties) {
285
+ baseClasses.push("text-tertiary", "font-[family-name:var(--font-legal-sans)]");
286
+ }
287
+
269
288
  const prefixStyles = [
270
289
  `font-variant-numeric: tabular-nums`,
271
290
  `justify-content: ${resolveMarkerJustificationCss(numberingMarkerJustification)}`,
272
291
  ];
292
+
293
+ if (markerRunProperties) {
294
+ if (markerRunProperties.bold) prefixStyles.push("font-weight: bold");
295
+ if (markerRunProperties.italic) prefixStyles.push("font-style: italic");
296
+ if (markerRunProperties.underline && markerRunProperties.underline !== "none") {
297
+ prefixStyles.push("text-decoration: underline");
298
+ }
299
+ if (typeof markerRunProperties.fontSizeHalfPoints === "number") {
300
+ prefixStyles.push(`font-size: ${markerRunProperties.fontSizeHalfPoints / 2}pt`);
301
+ }
302
+ if (markerRunProperties.colorHex && markerRunProperties.colorHex !== "auto") {
303
+ prefixStyles.push(`color: #${markerRunProperties.colorHex.toLowerCase()}`);
304
+ }
305
+ const family = markerRunProperties.fontFamilyAscii ?? markerRunProperties.fontFamily;
306
+ if (family && SAFE_FONT_RE.test(family)) {
307
+ prefixStyles.push(`font-family: ${family}`);
308
+ }
309
+ }
310
+
273
311
  if (hasResolvedMarkerWidth) {
274
312
  const markerWidthPx = Math.max(1, Math.round(numberingMarkerWidth / 20));
275
313
  prefixStyles.push(
@@ -285,11 +323,11 @@ export const editorSchema = new Schema({
285
323
  `margin-right: ${fallbackMarginRight}`,
286
324
  );
287
325
  }
326
+
288
327
  children.push([
289
328
  "span",
290
329
  {
291
- class:
292
- "inline-flex select-none items-center text-tertiary font-[family-name:var(--font-legal-sans)]",
330
+ class: baseClasses.join(" "),
293
331
  contenteditable: "false",
294
332
  "data-numbering-prefix": numberingPrefix,
295
333
  ...(typeof numberingLevel === "number"