@beyondwork/docx-react-component 1.0.37 → 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.
- package/package.json +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- 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/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- 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/parse-tables.ts +249 -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 +117 -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-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- 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 +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- 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/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- 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 +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- 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 +144 -62
- 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 -75
- 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 +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SurfaceBlockSnapshot,
|
|
5
|
+
SurfaceInlineSegment,
|
|
6
|
+
SurfaceTextMark,
|
|
7
|
+
} from "../../api/public-types.ts";
|
|
8
|
+
import type { CanonicalRunFormatting } from "../../model/canonical-document.ts";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;
|
|
15
|
+
const SAFE_FONT_RE = /^[A-Za-z0-9 ,\-'"]+$/;
|
|
16
|
+
const SAFE_ALIGNMENT = new Set(["left", "center", "right", "justify", "start", "end"]);
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helper utilities (mirror pm-schema.ts helpers)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function safeHexColor(raw: string | null | undefined): string | null {
|
|
23
|
+
if (!raw || raw === "auto") return null;
|
|
24
|
+
return HEX_COLOR_RE.test(raw) ? `#${raw}` : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve heading level from styleId or outlineLevel (outlineLevel 0 = Heading 1). */
|
|
28
|
+
function resolveHeadingLevel(styleId?: string, outlineLevel?: number): number | null {
|
|
29
|
+
if (styleId) {
|
|
30
|
+
const normalized = styleId.toLowerCase();
|
|
31
|
+
const compact = normalized.replace(/[\s_-]+/g, "");
|
|
32
|
+
const headingMatch = /^heading([1-6])$/.exec(compact);
|
|
33
|
+
if (headingMatch) {
|
|
34
|
+
return Number.parseInt(headingMatch[1], 10);
|
|
35
|
+
}
|
|
36
|
+
if (compact === "title") return 1;
|
|
37
|
+
if (compact === "subtitle") return 2;
|
|
38
|
+
if (compact === "tocheading") return 1;
|
|
39
|
+
if (/^(appendix|schedule|annex|exhibit|attachment)(heading|title)$/.test(compact)) return 2;
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
typeof outlineLevel === "number" &&
|
|
43
|
+
Number.isInteger(outlineLevel) &&
|
|
44
|
+
outlineLevel >= 0 &&
|
|
45
|
+
outlineLevel <= 5
|
|
46
|
+
) {
|
|
47
|
+
return outlineLevel + 1;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveMarkerJustificationCss(raw: string | undefined): string {
|
|
53
|
+
switch (raw) {
|
|
54
|
+
case "left":
|
|
55
|
+
return "flex-start";
|
|
56
|
+
case "center":
|
|
57
|
+
return "center";
|
|
58
|
+
case "right":
|
|
59
|
+
case "both":
|
|
60
|
+
case "distribute":
|
|
61
|
+
default:
|
|
62
|
+
return "flex-end";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Style builders
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/** Build CSSProperties for a paragraph block from spacing/indent/alignment. */
|
|
71
|
+
function buildParagraphStyle(
|
|
72
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>,
|
|
73
|
+
): React.CSSProperties {
|
|
74
|
+
const style: React.CSSProperties = {};
|
|
75
|
+
|
|
76
|
+
// Alignment — direct takes precedence over resolvedParagraphFormatting
|
|
77
|
+
const rawAlignment = block.alignment ?? block.resolvedParagraphFormatting?.alignment;
|
|
78
|
+
const safeAlign = rawAlignment === "both" ? "justify" : rawAlignment;
|
|
79
|
+
if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) {
|
|
80
|
+
style.textAlign = safeAlign as React.CSSProperties["textAlign"];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Spacing
|
|
84
|
+
const spacingBefore =
|
|
85
|
+
block.spacing?.before ?? block.resolvedParagraphFormatting?.spacing?.before;
|
|
86
|
+
const spacingAfter =
|
|
87
|
+
block.spacing?.after ?? block.resolvedParagraphFormatting?.spacing?.after;
|
|
88
|
+
const lineSpacing =
|
|
89
|
+
block.spacing?.line ?? block.resolvedParagraphFormatting?.spacing?.line;
|
|
90
|
+
const lineRule =
|
|
91
|
+
block.spacing?.lineRule ?? block.resolvedParagraphFormatting?.spacing?.lineRule;
|
|
92
|
+
|
|
93
|
+
if (spacingBefore != null) {
|
|
94
|
+
style.marginTop = `${spacingBefore / 20}px`;
|
|
95
|
+
}
|
|
96
|
+
if (spacingAfter != null) {
|
|
97
|
+
style.marginBottom = `${spacingAfter / 20}px`;
|
|
98
|
+
}
|
|
99
|
+
if (lineSpacing && lineRule === "auto") {
|
|
100
|
+
style.lineHeight = String(lineSpacing / 240);
|
|
101
|
+
} else if (lineSpacing && lineRule === "exact") {
|
|
102
|
+
style.lineHeight = `${lineSpacing / 20}px`;
|
|
103
|
+
} else if (lineSpacing && lineRule === "atLeast") {
|
|
104
|
+
style.minHeight = `${lineSpacing / 20}px`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Indentation
|
|
108
|
+
const indentLeft =
|
|
109
|
+
block.indentation?.left ?? block.resolvedParagraphFormatting?.indentation?.left;
|
|
110
|
+
const indentRight =
|
|
111
|
+
block.indentation?.right ?? block.resolvedParagraphFormatting?.indentation?.right;
|
|
112
|
+
const indentFirstLine =
|
|
113
|
+
block.indentation?.firstLine ?? block.resolvedParagraphFormatting?.indentation?.firstLine;
|
|
114
|
+
const indentHanging =
|
|
115
|
+
block.indentation?.hanging ?? block.resolvedParagraphFormatting?.indentation?.hanging;
|
|
116
|
+
|
|
117
|
+
if (indentLeft) style.paddingLeft = `${indentLeft / 20}px`;
|
|
118
|
+
if (indentRight) style.paddingRight = `${indentRight / 20}px`;
|
|
119
|
+
if (indentHanging) style.textIndent = `-${indentHanging / 20}px`;
|
|
120
|
+
else if (indentFirstLine) style.textIndent = `${indentFirstLine / 20}px`;
|
|
121
|
+
|
|
122
|
+
// Shading
|
|
123
|
+
const shadingFill = block.shading?.fill;
|
|
124
|
+
const shadingColor = safeHexColor(shadingFill);
|
|
125
|
+
if (shadingColor) style.backgroundColor = shadingColor;
|
|
126
|
+
|
|
127
|
+
// Page break visual indicator
|
|
128
|
+
if (block.pageBreakBefore) {
|
|
129
|
+
style.borderTop = "2px dashed rgba(0,0,0,0.1)";
|
|
130
|
+
style.paddingTop = "8px";
|
|
131
|
+
style.marginTop = "16px";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return style;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build CSSProperties for the numbering marker span. */
|
|
138
|
+
function buildMarkerStyle(
|
|
139
|
+
prefix: string,
|
|
140
|
+
suffix: "tab" | "space" | "nothing" | undefined,
|
|
141
|
+
markerRunProperties: CanonicalRunFormatting | undefined,
|
|
142
|
+
markerWidth: number | undefined,
|
|
143
|
+
markerJustification: string | undefined,
|
|
144
|
+
): React.CSSProperties {
|
|
145
|
+
const style: React.CSSProperties = {
|
|
146
|
+
fontVariantNumeric: "tabular-nums",
|
|
147
|
+
justifyContent: resolveMarkerJustificationCss(markerJustification),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (markerRunProperties) {
|
|
151
|
+
if (markerRunProperties.bold) style.fontWeight = "bold";
|
|
152
|
+
if (markerRunProperties.italic) style.fontStyle = "italic";
|
|
153
|
+
if (markerRunProperties.underline && markerRunProperties.underline !== "none") {
|
|
154
|
+
style.textDecoration = "underline";
|
|
155
|
+
}
|
|
156
|
+
if (typeof markerRunProperties.fontSizeHalfPoints === "number") {
|
|
157
|
+
style.fontSize = `${markerRunProperties.fontSizeHalfPoints / 2}pt`;
|
|
158
|
+
}
|
|
159
|
+
const colorHex = markerRunProperties.colorHex;
|
|
160
|
+
if (colorHex && colorHex !== "auto") {
|
|
161
|
+
style.color = `#${colorHex.toLowerCase()}`;
|
|
162
|
+
}
|
|
163
|
+
const family = markerRunProperties.fontFamilyAscii ?? markerRunProperties.fontFamily;
|
|
164
|
+
if (family && SAFE_FONT_RE.test(family)) {
|
|
165
|
+
style.fontFamily = family;
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
style.color = "var(--color-text-tertiary)";
|
|
169
|
+
style.fontFamily = "var(--font-legal-sans)";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hasResolvedMarkerWidth = typeof markerWidth === "number" && markerWidth > 0;
|
|
173
|
+
if (hasResolvedMarkerWidth) {
|
|
174
|
+
const markerWidthPx = Math.max(1, Math.round(markerWidth! / 20));
|
|
175
|
+
style.width = `${markerWidthPx}px`;
|
|
176
|
+
style.minWidth = `${markerWidthPx}px`;
|
|
177
|
+
style.flexBasis = `${markerWidthPx}px`;
|
|
178
|
+
style.marginRight = 0;
|
|
179
|
+
style.overflow = "visible";
|
|
180
|
+
} else {
|
|
181
|
+
const fallbackMinWidth = Math.min(Math.max(prefix.length + 1, 4), 14);
|
|
182
|
+
const fallbackMarginRight =
|
|
183
|
+
suffix === "nothing" ? "0.25rem" : suffix === "space" ? "0.5rem" : "0.75rem";
|
|
184
|
+
style.minWidth = `${fallbackMinWidth}ch`;
|
|
185
|
+
style.marginRight = fallbackMarginRight;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return style;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Build CSSProperties for a text segment from marks and markAttrs. */
|
|
192
|
+
function buildSegmentStyle(
|
|
193
|
+
marks: SurfaceTextMark[] | undefined,
|
|
194
|
+
markAttrs?: {
|
|
195
|
+
fontSize?: number;
|
|
196
|
+
textColor?: string;
|
|
197
|
+
fontFamily?: string;
|
|
198
|
+
backgroundColor?: string;
|
|
199
|
+
charSpacing?: number;
|
|
200
|
+
},
|
|
201
|
+
): React.CSSProperties {
|
|
202
|
+
const style: React.CSSProperties = {};
|
|
203
|
+
|
|
204
|
+
if (marks) {
|
|
205
|
+
if (marks.includes("bold")) style.fontWeight = "bold";
|
|
206
|
+
if (marks.includes("italic")) style.fontStyle = "italic";
|
|
207
|
+
if (marks.includes("underline")) style.textDecoration = "underline";
|
|
208
|
+
if (marks.includes("strikethrough") || marks.includes("doubleStrikethrough")) {
|
|
209
|
+
style.textDecoration = marks.includes("underline")
|
|
210
|
+
? "underline line-through"
|
|
211
|
+
: "line-through";
|
|
212
|
+
}
|
|
213
|
+
if (marks.includes("superscript")) {
|
|
214
|
+
style.verticalAlign = "super";
|
|
215
|
+
style.fontSize = "smaller";
|
|
216
|
+
}
|
|
217
|
+
if (marks.includes("subscript")) {
|
|
218
|
+
style.verticalAlign = "sub";
|
|
219
|
+
style.fontSize = "smaller";
|
|
220
|
+
}
|
|
221
|
+
if (marks.includes("allCaps")) style.textTransform = "uppercase";
|
|
222
|
+
if (marks.includes("smallCaps")) style.fontVariant = "small-caps";
|
|
223
|
+
if (marks.includes("vanish")) style.display = "none";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (markAttrs) {
|
|
227
|
+
if (markAttrs.fontSize) style.fontSize = `${markAttrs.fontSize}pt`;
|
|
228
|
+
if (markAttrs.textColor) style.color = markAttrs.textColor;
|
|
229
|
+
if (markAttrs.fontFamily && SAFE_FONT_RE.test(markAttrs.fontFamily)) {
|
|
230
|
+
style.fontFamily = markAttrs.fontFamily;
|
|
231
|
+
}
|
|
232
|
+
if (markAttrs.backgroundColor) style.backgroundColor = markAttrs.backgroundColor;
|
|
233
|
+
if (markAttrs.charSpacing) style.letterSpacing = `${markAttrs.charSpacing}px`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return style;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasStyleEntries(style: React.CSSProperties): boolean {
|
|
240
|
+
return Object.keys(style).length > 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Segment renderer
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/** Render a single inline segment. */
|
|
248
|
+
function renderSegment(seg: SurfaceInlineSegment): React.ReactNode {
|
|
249
|
+
switch (seg.kind) {
|
|
250
|
+
case "text": {
|
|
251
|
+
const style = buildSegmentStyle(seg.marks, seg.markAttrs);
|
|
252
|
+
const content = seg.text;
|
|
253
|
+
if (!hasStyleEntries(style)) {
|
|
254
|
+
return <React.Fragment key={seg.segmentId}>{content}</React.Fragment>;
|
|
255
|
+
}
|
|
256
|
+
return (
|
|
257
|
+
<span key={seg.segmentId} style={style}>
|
|
258
|
+
{content}
|
|
259
|
+
</span>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
case "tab":
|
|
263
|
+
return (
|
|
264
|
+
<span
|
|
265
|
+
key={seg.segmentId}
|
|
266
|
+
data-node-type="tab"
|
|
267
|
+
style={{ display: "inline-block", width: "32px", minWidth: "8px" }}
|
|
268
|
+
>
|
|
269
|
+
{"\u00A0"}
|
|
270
|
+
</span>
|
|
271
|
+
);
|
|
272
|
+
case "hard_break":
|
|
273
|
+
return <br key={seg.segmentId} />;
|
|
274
|
+
case "image":
|
|
275
|
+
return (
|
|
276
|
+
<span
|
|
277
|
+
key={seg.segmentId}
|
|
278
|
+
data-node-type="image"
|
|
279
|
+
style={{
|
|
280
|
+
display: "inline-block",
|
|
281
|
+
width: "48px",
|
|
282
|
+
height: "32px",
|
|
283
|
+
backgroundColor: "#e0e0e0",
|
|
284
|
+
verticalAlign: "middle",
|
|
285
|
+
margin: "0 4px",
|
|
286
|
+
borderRadius: "2px",
|
|
287
|
+
}}
|
|
288
|
+
title={seg.altText ?? "Image"}
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
case "field_ref":
|
|
292
|
+
return (
|
|
293
|
+
<span
|
|
294
|
+
key={seg.segmentId}
|
|
295
|
+
data-node-type="field_ref"
|
|
296
|
+
style={{ opacity: 0.6, fontSize: "0.85em" }}
|
|
297
|
+
>
|
|
298
|
+
[field]
|
|
299
|
+
</span>
|
|
300
|
+
);
|
|
301
|
+
case "note_ref":
|
|
302
|
+
return (
|
|
303
|
+
<span
|
|
304
|
+
key={seg.segmentId}
|
|
305
|
+
data-node-type="note_ref"
|
|
306
|
+
style={{ verticalAlign: "super", fontSize: "0.75em" }}
|
|
307
|
+
>
|
|
308
|
+
{seg.label}
|
|
309
|
+
</span>
|
|
310
|
+
);
|
|
311
|
+
case "opaque_inline":
|
|
312
|
+
return (
|
|
313
|
+
<span
|
|
314
|
+
key={seg.segmentId}
|
|
315
|
+
data-node-type="opaque_inline"
|
|
316
|
+
style={{ opacity: 0.6, fontSize: "0.85em" }}
|
|
317
|
+
>
|
|
318
|
+
{seg.displayText ?? seg.label}
|
|
319
|
+
</span>
|
|
320
|
+
);
|
|
321
|
+
default:
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Block renderers
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/** Render a paragraph block as <p>. */
|
|
331
|
+
function ParagraphBlock({
|
|
332
|
+
block,
|
|
333
|
+
}: {
|
|
334
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>;
|
|
335
|
+
}): React.ReactElement {
|
|
336
|
+
const headingLevel = resolveHeadingLevel(block.styleId, block.outlineLevel);
|
|
337
|
+
|
|
338
|
+
const classes: string[] = ["leading-relaxed"];
|
|
339
|
+
if (headingLevel) {
|
|
340
|
+
classes.push(...headingClassList(headingLevel));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const pStyle = buildParagraphStyle(block);
|
|
344
|
+
const attrs: React.HTMLAttributes<HTMLParagraphElement> & {
|
|
345
|
+
"data-heading-level"?: string;
|
|
346
|
+
"data-numbered"?: string;
|
|
347
|
+
"data-contextual-spacing"?: string;
|
|
348
|
+
} = {
|
|
349
|
+
className: classes.join(" "),
|
|
350
|
+
style: Object.keys(pStyle).length > 0 ? pStyle : undefined,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (headingLevel) {
|
|
354
|
+
attrs["data-heading-level"] = String(headingLevel);
|
|
355
|
+
}
|
|
356
|
+
if (block.numbering) {
|
|
357
|
+
attrs["data-numbered"] = "true";
|
|
358
|
+
}
|
|
359
|
+
if (block.contextualSpacing) {
|
|
360
|
+
attrs["data-contextual-spacing"] = "true";
|
|
361
|
+
}
|
|
362
|
+
if (block.bidi) {
|
|
363
|
+
attrs.dir = "rtl";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Numbering prefix span
|
|
367
|
+
const numberingPrefix = block.numberingPrefix;
|
|
368
|
+
const numberingSuffix = block.numberingSuffix;
|
|
369
|
+
const resolvedNumbering = block.resolvedNumbering;
|
|
370
|
+
const markerRunProperties = resolvedNumbering?.markerRunProperties;
|
|
371
|
+
const markerWidth = resolvedNumbering?.geometry?.markerLane?.width;
|
|
372
|
+
const markerJustification = resolvedNumbering?.geometry?.markerJustification;
|
|
373
|
+
|
|
374
|
+
const prefixSpan =
|
|
375
|
+
numberingPrefix != null ? (
|
|
376
|
+
<span
|
|
377
|
+
className={[
|
|
378
|
+
"inline-flex",
|
|
379
|
+
"select-none",
|
|
380
|
+
"items-center",
|
|
381
|
+
...(!markerRunProperties ? ["text-tertiary", "font-[family-name:var(--font-legal-sans)]"] : []),
|
|
382
|
+
].join(" ")}
|
|
383
|
+
contentEditable={false}
|
|
384
|
+
data-numbering-prefix={numberingPrefix}
|
|
385
|
+
{...(typeof resolvedNumbering?.level === "number"
|
|
386
|
+
? { "data-numbering-level": String(resolvedNumbering.level) }
|
|
387
|
+
: {})}
|
|
388
|
+
{...(numberingSuffix ? { "data-numbering-suffix": numberingSuffix } : {})}
|
|
389
|
+
style={buildMarkerStyle(
|
|
390
|
+
numberingPrefix,
|
|
391
|
+
numberingSuffix,
|
|
392
|
+
markerRunProperties,
|
|
393
|
+
markerWidth,
|
|
394
|
+
markerJustification,
|
|
395
|
+
)}
|
|
396
|
+
>
|
|
397
|
+
{numberingPrefix}
|
|
398
|
+
</span>
|
|
399
|
+
) : null;
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<p {...attrs}>
|
|
403
|
+
{prefixSpan}
|
|
404
|
+
<span className="pm-paragraph-content">
|
|
405
|
+
{block.segments.map((seg) => renderSegment(seg))}
|
|
406
|
+
</span>
|
|
407
|
+
</p>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function headingClassList(level: number): string[] {
|
|
412
|
+
switch (level) {
|
|
413
|
+
case 1:
|
|
414
|
+
return ["text-3xl", "font-semibold", "tracking-tight", "leading-tight"];
|
|
415
|
+
case 2:
|
|
416
|
+
return ["text-2xl", "font-semibold", "tracking-tight"];
|
|
417
|
+
case 3:
|
|
418
|
+
return ["text-xl", "font-medium"];
|
|
419
|
+
case 4:
|
|
420
|
+
return ["text-lg", "font-medium"];
|
|
421
|
+
case 5:
|
|
422
|
+
return ["text-base", "font-semibold", "uppercase", "tracking-[0.12em]"];
|
|
423
|
+
case 6:
|
|
424
|
+
return ["text-sm", "font-semibold", "uppercase", "tracking-[0.16em]"];
|
|
425
|
+
default:
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Render a table block as <table>. */
|
|
431
|
+
function TableBlock({
|
|
432
|
+
block,
|
|
433
|
+
}: {
|
|
434
|
+
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>;
|
|
435
|
+
}): React.ReactElement {
|
|
436
|
+
const tableStyle: React.CSSProperties = {
|
|
437
|
+
borderCollapse: "collapse",
|
|
438
|
+
width: "100%",
|
|
439
|
+
};
|
|
440
|
+
if (block.alignment === "center") {
|
|
441
|
+
tableStyle.marginLeft = "auto";
|
|
442
|
+
tableStyle.marginRight = "auto";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<table style={tableStyle} data-node-type="table">
|
|
447
|
+
<tbody>
|
|
448
|
+
{block.rows.map((row, rowIdx) => (
|
|
449
|
+
<tr
|
|
450
|
+
key={rowIdx}
|
|
451
|
+
style={
|
|
452
|
+
row.height != null && row.heightRule === "exact"
|
|
453
|
+
? { height: `${row.height / 20}px` }
|
|
454
|
+
: row.height != null && row.heightRule === "atLeast"
|
|
455
|
+
? { minHeight: `${row.height / 20}px` }
|
|
456
|
+
: undefined
|
|
457
|
+
}
|
|
458
|
+
>
|
|
459
|
+
{row.cells.map((cell, cellIdx) => {
|
|
460
|
+
if (cell.verticalMerge === "continue") {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
const cellStyle: React.CSSProperties = {};
|
|
464
|
+
if (cell.backgroundColor) cellStyle.backgroundColor = `#${cell.backgroundColor}`;
|
|
465
|
+
if (cell.verticalAlign) cellStyle.verticalAlign = cell.verticalAlign;
|
|
466
|
+
if (cell.borderTop) cellStyle.borderTop = cell.borderTop;
|
|
467
|
+
if (cell.borderRight) cellStyle.borderRight = cell.borderRight;
|
|
468
|
+
if (cell.borderBottom) cellStyle.borderBottom = cell.borderBottom;
|
|
469
|
+
if (cell.borderLeft) cellStyle.borderLeft = cell.borderLeft;
|
|
470
|
+
|
|
471
|
+
return (
|
|
472
|
+
<td
|
|
473
|
+
key={cellIdx}
|
|
474
|
+
colSpan={cell.colspan > 1 ? cell.colspan : undefined}
|
|
475
|
+
rowSpan={cell.rowspan > 1 ? cell.rowspan : undefined}
|
|
476
|
+
style={Object.keys(cellStyle).length > 0 ? cellStyle : undefined}
|
|
477
|
+
>
|
|
478
|
+
{cell.content.map((childBlock) => (
|
|
479
|
+
<BlockItem key={childBlock.blockId} block={childBlock} />
|
|
480
|
+
))}
|
|
481
|
+
</td>
|
|
482
|
+
);
|
|
483
|
+
})}
|
|
484
|
+
</tr>
|
|
485
|
+
))}
|
|
486
|
+
</tbody>
|
|
487
|
+
</table>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Render any block, dispatching to the appropriate component. */
|
|
492
|
+
function BlockItem({ block }: { block: SurfaceBlockSnapshot }): React.ReactElement | null {
|
|
493
|
+
switch (block.kind) {
|
|
494
|
+
case "paragraph":
|
|
495
|
+
return <ParagraphBlock block={block} />;
|
|
496
|
+
case "table":
|
|
497
|
+
return <TableBlock block={block} />;
|
|
498
|
+
case "sdt_block":
|
|
499
|
+
return (
|
|
500
|
+
<section data-node-type="sdt_block" style={{ margin: "8px 0" }}>
|
|
501
|
+
{block.children.map((child) => (
|
|
502
|
+
<BlockItem key={child.blockId} block={child} />
|
|
503
|
+
))}
|
|
504
|
+
</section>
|
|
505
|
+
);
|
|
506
|
+
case "opaque_block":
|
|
507
|
+
// Render as an uneditable placeholder
|
|
508
|
+
return (
|
|
509
|
+
<div
|
|
510
|
+
data-node-type="opaque_block"
|
|
511
|
+
style={{
|
|
512
|
+
opacity: 0.5,
|
|
513
|
+
borderLeft: "3px solid #aaa",
|
|
514
|
+
paddingLeft: "8px",
|
|
515
|
+
margin: "4px 0",
|
|
516
|
+
fontSize: "0.85em",
|
|
517
|
+
color: "#666",
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{block.label || "[Locked content]"}
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
default:
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// Public export
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* TwPageBlockView — read-only React renderer for a slice of SurfaceBlockSnapshot[].
|
|
534
|
+
*
|
|
535
|
+
* Used for pages 2+ in the page workspace. Page 1 uses the live PM editor;
|
|
536
|
+
* pages 2+ use this static view.
|
|
537
|
+
*
|
|
538
|
+
* Wraps output in a `.ProseMirror` container so it inherits pm-schema.css styles.
|
|
539
|
+
* The wrapper is `aria-hidden` because it is a visual-only duplicate of the
|
|
540
|
+
* live editor surface.
|
|
541
|
+
*/
|
|
542
|
+
export function TwPageBlockView({
|
|
543
|
+
blocks,
|
|
544
|
+
className,
|
|
545
|
+
}: {
|
|
546
|
+
blocks: SurfaceBlockSnapshot[];
|
|
547
|
+
className?: string;
|
|
548
|
+
}): React.ReactElement {
|
|
549
|
+
return (
|
|
550
|
+
<div
|
|
551
|
+
className={["ProseMirror", className].filter(Boolean).join(" ")}
|
|
552
|
+
aria-hidden="true"
|
|
553
|
+
>
|
|
554
|
+
{blocks.map((block) => (
|
|
555
|
+
<BlockItem key={block.blockId} block={block} />
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
}
|