@beyondwork/docx-react-component 1.0.13 → 1.0.15

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.
@@ -10,6 +10,55 @@ const HEX_COLOR_RE = /^[0-9A-Fa-f]{3,8}$/;
10
10
  const SAFE_FONT_RE = /^[A-Za-z0-9 ,\-'"]+$/;
11
11
  const SAFE_ALIGNMENT = new Set(["left", "center", "right", "justify", "start", "end"]);
12
12
 
13
+ function resolveHeadingLevel(
14
+ styleId: string | null,
15
+ outlineLevel: number | null,
16
+ ): number | null {
17
+ if (styleId) {
18
+ const normalized = styleId.toLowerCase();
19
+ const headingMatch = /^heading([1-6])$/.exec(normalized);
20
+ if (headingMatch) {
21
+ return Number.parseInt(headingMatch[1], 10);
22
+ }
23
+ if (normalized === "title") {
24
+ return 1;
25
+ }
26
+ if (normalized === "subtitle") {
27
+ return 2;
28
+ }
29
+ }
30
+
31
+ if (
32
+ typeof outlineLevel === "number" &&
33
+ Number.isInteger(outlineLevel) &&
34
+ outlineLevel >= 0 &&
35
+ outlineLevel <= 5
36
+ ) {
37
+ return outlineLevel + 1;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ function headingClassList(level: number): string[] {
44
+ switch (level) {
45
+ case 1:
46
+ return ["text-3xl", "font-semibold", "tracking-tight", "leading-tight"];
47
+ case 2:
48
+ return ["text-2xl", "font-semibold", "tracking-tight"];
49
+ case 3:
50
+ return ["text-xl", "font-medium"];
51
+ case 4:
52
+ return ["text-lg", "font-medium"];
53
+ case 5:
54
+ return ["text-base", "font-semibold", "uppercase", "tracking-[0.12em]"];
55
+ case 6:
56
+ return ["text-sm", "font-semibold", "uppercase", "tracking-[0.16em]"];
57
+ default:
58
+ return [];
59
+ }
60
+ }
61
+
13
62
  /** Validate a raw hex color string from OOXML (no leading #). Returns sanitized `#hex` or null. */
14
63
  function safeHexColor(raw: string | null | undefined): string | null {
15
64
  if (!raw || raw === "auto") return null;
@@ -45,6 +94,7 @@ export const editorSchema = new Schema({
45
94
  styleId: { default: null },
46
95
  numberingInstanceId: { default: null },
47
96
  numberingLevel: { default: null },
97
+ numberingPrefix: { default: null },
48
98
  alignment: { default: null },
49
99
  spacingBefore: { default: null },
50
100
  spacingAfter: { default: null },
@@ -58,6 +108,7 @@ export const editorSchema = new Schema({
58
108
  borderBottom: { default: null },
59
109
  borderLeft: { default: null },
60
110
  borderRight: { default: null },
111
+ outlineLevel: { default: null },
61
112
  bidi: { default: null },
62
113
  pageBreakBefore: { default: null },
63
114
  },
@@ -65,11 +116,10 @@ export const editorSchema = new Schema({
65
116
  toDOM(node) {
66
117
  const classes: string[] = ["leading-relaxed"];
67
118
  const styleId = node.attrs.styleId as string | null;
68
- if (styleId) {
69
- const lower = styleId.toLowerCase();
70
- if (lower === "heading1") classes.push("text-2xl font-medium");
71
- else if (lower === "heading2") classes.push("text-xl font-medium");
72
- else if (lower === "heading3") classes.push("text-lg font-medium");
119
+ const outlineLevel = node.attrs.outlineLevel as number | null;
120
+ const headingLevel = resolveHeadingLevel(styleId, outlineLevel);
121
+ if (headingLevel) {
122
+ classes.push(...headingClassList(headingLevel));
73
123
  }
74
124
  const attrs: Record<string, string> = { class: classes.join(" ") };
75
125
  const styles: string[] = [];
@@ -106,8 +156,50 @@ export const editorSchema = new Schema({
106
156
  if (pageBreak) styles.push("border-top: 2px dashed rgba(0,0,0,0.1); padding-top: 8px; margin-top: 16px");
107
157
  const bidi = node.attrs.bidi as boolean | null;
108
158
  if (bidi) attrs.dir = "rtl";
159
+ if (headingLevel) {
160
+ attrs["data-heading-level"] = String(headingLevel);
161
+ }
109
162
  if (styles.length > 0) attrs.style = styles.join("; ");
110
- return ["p", attrs, 0];
163
+ const numberingPrefix = node.attrs.numberingPrefix as string | null;
164
+ const numberingLevel = node.attrs.numberingLevel as number | null;
165
+ const children: Array<string | number | readonly unknown[]> = [];
166
+ if (pageBreak) {
167
+ children.push([
168
+ "span",
169
+ {
170
+ class:
171
+ "mb-2 inline-flex items-center gap-2 text-[10px] font-medium uppercase tracking-[0.18em] text-tertiary",
172
+ contenteditable: "false",
173
+ "data-page-break-before": "true",
174
+ },
175
+ "Page break",
176
+ ]);
177
+ }
178
+ if (numberingPrefix) {
179
+ const minWidth = Math.min(Math.max(numberingPrefix.length + 1, 4), 14);
180
+ children.push([
181
+ "span",
182
+ {
183
+ class:
184
+ "inline-flex select-none items-center justify-end text-tertiary font-[family-name:var(--font-legal-sans)]",
185
+ contenteditable: "false",
186
+ "data-numbering-prefix": numberingPrefix,
187
+ ...(typeof numberingLevel === "number"
188
+ ? { "data-numbering-level": String(numberingLevel) }
189
+ : {}),
190
+ style: `min-width: ${minWidth}ch; margin-right: 0.75rem; font-variant-numeric: tabular-nums;`,
191
+ },
192
+ numberingPrefix,
193
+ ]);
194
+ }
195
+ children.push([
196
+ "span",
197
+ {
198
+ class: "pm-paragraph-content",
199
+ },
200
+ 0,
201
+ ]);
202
+ return ["p", attrs, ...children];
111
203
  },
112
204
  },
113
205
 
@@ -132,13 +224,42 @@ export const editorSchema = new Schema({
132
224
  selectable: false,
133
225
  attrs: {
134
226
  tabWidth: { default: null },
227
+ leader: { default: null },
228
+ align: { default: null },
135
229
  },
136
230
  toDOM(node) {
137
231
  const width = node.attrs.tabWidth as number | null;
138
- if (width && width > 0) {
139
- return ["span", { style: `display: inline-block; width: ${width}px`, "data-node-type": "tab" }, "\u00A0"];
232
+ const leader = node.attrs.leader as string | null;
233
+ const align = node.attrs.align as string | null;
234
+ const styles = [
235
+ `display: inline-block`,
236
+ `width: ${width && width > 0 ? width : 32}px`,
237
+ `min-width: 8px`,
238
+ ];
239
+ if (leader === "dot" || leader === "middleDot") {
240
+ styles.push(
241
+ `background-image: radial-gradient(circle, currentColor 1px, transparent 1.25px)`,
242
+ `background-size: 6px 3px`,
243
+ `background-repeat: repeat-x`,
244
+ `background-position: left calc(100% - 2px)`,
245
+ `opacity: 0.55`,
246
+ );
247
+ } else if (leader === "hyphen") {
248
+ styles.push(`border-bottom: 1px dashed rgba(107,107,107,0.65)`);
249
+ } else if (leader === "underscore") {
250
+ styles.push(`border-bottom: 1px solid rgba(107,107,107,0.65)`);
251
+ } else if (leader === "heavy") {
252
+ styles.push(`border-bottom: 2px solid rgba(107,107,107,0.75)`);
140
253
  }
141
- return ["span", { class: "inline-block w-8", "data-node-type": "tab" }, "\u00A0"];
254
+ return [
255
+ "span",
256
+ {
257
+ style: styles.join("; "),
258
+ "data-node-type": "tab",
259
+ title: align ? `Tab stop · ${align}` : "Tab stop",
260
+ },
261
+ "\u00A0",
262
+ ];
142
263
  },
143
264
  },
144
265
 
@@ -366,19 +487,33 @@ export const editorSchema = new Schema({
366
487
  detail: { default: "" },
367
488
  },
368
489
  toDOM(node) {
490
+ const fragmentId = node.attrs.fragmentId as string;
491
+ const isPreview = fragmentId.startsWith("preview:");
369
492
  return [
370
493
  "div",
371
494
  {
372
- class: "border-l-2 border-dashed border-warning/30 pl-4 py-2 rounded-r bg-warning-soft/20 my-2",
495
+ class: isPreview
496
+ ? "my-3 rounded-xl border border-primary/15 bg-surface-raised/50 px-4 py-3"
497
+ : "border-l-2 border-dashed border-warning/30 pl-4 py-2 rounded-r bg-warning-soft/20 my-2",
373
498
  contenteditable: "false",
374
499
  "data-node-type": "opaque_block",
375
500
  },
376
501
  [
377
502
  "div",
378
- { class: "flex items-center gap-1.5 text-xs text-tertiary mb-1" },
379
- "\uD83D\uDD12 " + (node.attrs.label as string),
503
+ {
504
+ class: isPreview
505
+ ? "mb-2 text-[11px] uppercase tracking-[0.18em] text-tertiary"
506
+ : "flex items-center gap-1.5 text-xs text-tertiary mb-1",
507
+ },
508
+ `${isPreview ? "" : "\uD83D\uDD12 "}${node.attrs.label as string}`,
509
+ ],
510
+ [
511
+ "p",
512
+ {
513
+ class: isPreview ? "text-sm text-secondary whitespace-pre-wrap leading-relaxed" : "text-sm text-secondary whitespace-pre-wrap",
514
+ },
515
+ node.attrs.detail as string,
380
516
  ],
381
- ["p", { class: "text-sm text-secondary" }, node.attrs.detail as string],
382
517
  ];
383
518
  },
384
519
  },
@@ -100,7 +100,15 @@ function buildParagraph(
100
100
  ? ((prevStop as { pos?: number }).pos ?? (prevStop as { position?: number }).position ?? 0)
101
101
  : 0;
102
102
  const widthPx = Math.round((stopPos - prevPos) / 15);
103
- content.push(editorSchema.nodes.tab_char.create({ tabWidth: widthPx > 8 ? widthPx : null }));
103
+ const leader = (stop as { leader?: string }).leader ?? null;
104
+ const align = (stop as { val?: string }).val ?? null;
105
+ content.push(
106
+ editorSchema.nodes.tab_char.create({
107
+ tabWidth: widthPx > 8 ? widthPx : null,
108
+ leader,
109
+ align,
110
+ }),
111
+ );
104
112
  tabIndex++;
105
113
  } else {
106
114
  const nodes = buildInlineContent(segment);
@@ -113,6 +121,9 @@ function buildParagraph(
113
121
  styleId: block.styleId ?? null,
114
122
  numberingInstanceId: block.numbering?.numberingInstanceId ?? null,
115
123
  numberingLevel: block.numbering?.level ?? null,
124
+ numberingPrefix:
125
+ (block as typeof block & { numberingPrefix?: string }).numberingPrefix ??
126
+ null,
116
127
  alignment: block.alignment ?? null,
117
128
  spacingBefore: block.spacing?.before ?? null,
118
129
  spacingAfter: block.spacing?.after ?? null,
@@ -126,6 +137,7 @@ function buildParagraph(
126
137
  borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
127
138
  borderLeft: (block.borders as Record<string, unknown>)?.left ?? null,
128
139
  borderRight: (block.borders as Record<string, unknown>)?.right ?? null,
140
+ outlineLevel: block.outlineLevel ?? null,
129
141
  bidi: block.bidi ?? null,
130
142
  pageBreakBefore: block.pageBreakBefore ?? null,
131
143
  },
@@ -231,19 +243,9 @@ function buildTable(
231
243
  if (child.kind === "paragraph") {
232
244
  cellContent.push(buildParagraph(child as Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>));
233
245
  } else if (child.kind === "table") {
234
- cellContent.push(buildNestedTablePlaceholder(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
246
+ cellContent.push(buildTable(child as Extract<SurfaceBlockSnapshot, { kind: "table" }>));
235
247
  } else if (child.kind === "sdt_block") {
236
- cellContent.push(buildOpaqueBlock({
237
- blockId: child.blockId,
238
- kind: "opaque_block",
239
- from: child.from,
240
- to: child.to,
241
- fragmentId: child.blockId,
242
- warningId: child.blockId,
243
- label: child.alias ?? child.tag ?? "Content control",
244
- detail: "Structured content control remains read-only inside table cells.",
245
- state: "locked-preserve-only",
246
- }));
248
+ cellContent.push(buildSdtBlock(child as Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>));
247
249
  } else if (child.kind === "opaque_block") {
248
250
  cellContent.push(buildOpaqueBlock(child as Extract<SurfaceBlockSnapshot, { kind: "opaque_block" }>));
249
251
  }
@@ -276,22 +278,6 @@ function buildTable(
276
278
  );
277
279
  }
278
280
 
279
- function buildNestedTablePlaceholder(
280
- block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
281
- ): PMNode {
282
- return buildOpaqueBlock({
283
- blockId: block.blockId,
284
- kind: "opaque_block",
285
- from: block.from,
286
- to: block.to,
287
- fragmentId: block.blockId,
288
- warningId: block.blockId,
289
- label: "Nested table",
290
- detail: "Nested table remains read-only in the live ProseMirror cell surface.",
291
- state: "locked-preserve-only",
292
- });
293
- }
294
-
295
281
  function buildSdtBlock(
296
282
  block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
297
283
  ): PMNode {
@@ -27,6 +27,7 @@ export interface TwReviewWorkspaceProps {
27
27
  activeRevisionId?: string;
28
28
  showTrackedChanges: boolean;
29
29
  selectionPreview?: string | null;
30
+ addCommentDisabledReason?: string;
30
31
  onViewModeChange: (value: ViewMode) => void;
31
32
  onActiveRailTabChange: (value: ReviewRailTab) => void;
32
33
  onShowTrackedChangesChange: (show: boolean) => void;
@@ -92,6 +93,8 @@ export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
92
93
  <TwSelectionToolbar
93
94
  selectionPreview={props.selectionPreview}
94
95
  readOnly={snapshot.readOnly}
96
+ canAddComment={props.capabilities?.canAddComment}
97
+ disabledReason={props.addCommentDisabledReason}
95
98
  onAddComment={props.onAddComment}
96
99
  />
97
100
  </div>