@beyondwork/docx-react-component 1.0.38 → 1.0.40
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.
- package/package.json +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- 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"
|