@beyondwork/docx-react-component 1.0.18 → 1.0.20

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 (105) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +710 -4
  5. package/src/api/session-state.ts +60 -0
  6. package/src/core/commands/formatting-commands.ts +2 -1
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +19 -3
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +357 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +4 -1
  16. package/src/index.ts +51 -0
  17. package/src/io/docx-session.ts +623 -56
  18. package/src/io/export/serialize-comments.ts +104 -34
  19. package/src/io/export/serialize-footnotes.ts +198 -1
  20. package/src/io/export/serialize-headers-footers.ts +203 -10
  21. package/src/io/export/serialize-main-document.ts +285 -8
  22. package/src/io/export/serialize-numbering.ts +28 -7
  23. package/src/io/export/split-review-boundaries.ts +181 -19
  24. package/src/io/normalize/normalize-text.ts +144 -32
  25. package/src/io/ooxml/highlight-colors.ts +39 -0
  26. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  27. package/src/io/ooxml/parse-comments.ts +85 -19
  28. package/src/io/ooxml/parse-fields.ts +396 -0
  29. package/src/io/ooxml/parse-footnotes.ts +452 -22
  30. package/src/io/ooxml/parse-headers-footers.ts +657 -29
  31. package/src/io/ooxml/parse-inline-media.ts +30 -0
  32. package/src/io/ooxml/parse-main-document.ts +807 -20
  33. package/src/io/ooxml/parse-numbering.ts +7 -0
  34. package/src/io/ooxml/parse-revisions.ts +317 -38
  35. package/src/io/ooxml/parse-settings.ts +184 -0
  36. package/src/io/ooxml/parse-shapes.ts +25 -0
  37. package/src/io/ooxml/parse-styles.ts +463 -0
  38. package/src/io/ooxml/parse-theme.ts +32 -0
  39. package/src/legal/bookmarks.ts +44 -0
  40. package/src/legal/cross-references.ts +59 -1
  41. package/src/model/canonical-document.ts +250 -4
  42. package/src/model/cds-1.0.0.ts +13 -0
  43. package/src/model/snapshot.ts +87 -2
  44. package/src/review/store/revision-store.ts +6 -0
  45. package/src/review/store/revision-types.ts +1 -0
  46. package/src/runtime/document-layout.ts +332 -0
  47. package/src/runtime/document-navigation.ts +603 -0
  48. package/src/runtime/document-runtime.ts +1754 -78
  49. package/src/runtime/document-search.ts +145 -0
  50. package/src/runtime/numbering-prefix.ts +47 -26
  51. package/src/runtime/page-layout-estimation.ts +212 -0
  52. package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
  53. package/src/runtime/session-capabilities.ts +35 -3
  54. package/src/runtime/story-context.ts +164 -0
  55. package/src/runtime/story-targeting.ts +162 -0
  56. package/src/runtime/surface-projection.ts +324 -36
  57. package/src/runtime/table-schema.ts +89 -7
  58. package/src/runtime/view-state.ts +477 -0
  59. package/src/runtime/workflow-markup.ts +349 -0
  60. package/src/ui/WordReviewEditor.tsx +2469 -1344
  61. package/src/ui/browser-export.ts +52 -0
  62. package/src/ui/editor-command-bag.ts +120 -0
  63. package/src/ui/editor-runtime-boundary.ts +1422 -0
  64. package/src/ui/editor-shell-view.tsx +134 -0
  65. package/src/ui/editor-surface-controller.tsx +51 -0
  66. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  67. package/src/ui/headless/revision-decoration-model.ts +4 -4
  68. package/src/ui/headless/selection-helpers.ts +20 -0
  69. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  70. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  71. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  72. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  73. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  74. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  75. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  76. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  77. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
  78. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  79. package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
  80. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
  81. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  82. package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
  83. package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
  84. package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
  85. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
  86. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  87. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
  88. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  89. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  90. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
  91. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  92. package/src/ui-tailwind/index.ts +2 -1
  93. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  94. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  95. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  96. package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
  97. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  98. package/src/ui-tailwind/theme/editor-theme.css +127 -0
  99. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  100. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
  101. package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
  102. package/src/validation/compatibility-engine.ts +119 -24
  103. package/src/validation/compatibility-report.ts +1 -0
  104. package/src/validation/diagnostics.ts +1 -0
  105. package/src/validation/docx-comment-proof.ts +707 -0
@@ -4,6 +4,8 @@ import type { CommentDecorationModel } from "../../ui/headless/comment-decoratio
4
4
  import { getCommentHighlightClass, type MarkupDisplay } from "../../ui/headless/comment-decoration-model";
5
5
  import type { RevisionDecorationModel } from "../../ui/headless/revision-decoration-model";
6
6
  import { getRevisionHighlightClass } from "../../ui/headless/revision-decoration-model";
7
+ import type { EditorStoryTarget, WorkflowScope } from "../../api/public-types";
8
+ import { MAIN_STORY_TARGET, storyTargetsEqual } from "../../core/selection/mapping.ts";
7
9
  import type { PositionMap } from "./pm-position-map";
8
10
  import type { Node as PMNode } from "prosemirror-model";
9
11
 
@@ -21,6 +23,8 @@ export function buildDecorations(
21
23
  revisionModel: RevisionDecorationModel | undefined,
22
24
  markupDisplay: MarkupDisplay,
23
25
  showTrackedChanges = true,
26
+ workflowScopes?: readonly WorkflowScope[],
27
+ activeStory: EditorStoryTarget = MAIN_STORY_TARGET,
24
28
  ): DecorationSet {
25
29
  const decorations: Decoration[] = [];
26
30
 
@@ -94,5 +98,36 @@ export function buildDecorations(
94
98
  }
95
99
  }
96
100
 
101
+ // Walk workflow scopes and create inline decorations for scope emphasis.
102
+ if (workflowScopes) {
103
+ for (const scope of workflowScopes) {
104
+ const scopeStoryTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
105
+ if (!storyTargetsEqual(scopeStoryTarget, activeStory)) continue;
106
+ if (scope.anchor.kind === "detached") continue;
107
+ const from = scope.anchor.kind === "range" ? scope.anchor.from : scope.anchor.at;
108
+ const to = scope.anchor.kind === "range" ? scope.anchor.to : scope.anchor.at;
109
+ const pmFrom = positionMap.runtimeToPm(from);
110
+ const pmTo = positionMap.runtimeToPm(to);
111
+ if (pmFrom >= pmTo) continue;
112
+
113
+ const modeClass =
114
+ scope.mode === "edit"
115
+ ? "bg-blue-50/40 ring-1 ring-blue-200/50"
116
+ : scope.mode === "suggest"
117
+ ? "bg-amber-50/40 ring-1 ring-amber-200/50"
118
+ : scope.mode === "comment"
119
+ ? "bg-green-50/40 ring-1 ring-green-200/50"
120
+ : "bg-gray-50/40 ring-1 ring-gray-200/50";
121
+
122
+ decorations.push(
123
+ Decoration.inline(pmFrom, pmTo, {
124
+ class: modeClass,
125
+ "data-workflow-scope-id": scope.scopeId,
126
+ "data-workflow-scope-mode": scope.mode,
127
+ }),
128
+ );
129
+ }
130
+ }
131
+
97
132
  return DecorationSet.create(doc, decorations);
98
133
  }
@@ -25,11 +25,11 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
25
25
  return entries[0]?.pmStart ?? 1;
26
26
  }
27
27
  if (runtimePos >= runtimeStorySize) {
28
- return pmDocSize - 1;
28
+ return entries[entries.length - 1]?.pmEnd ?? pmDocSize - 1;
29
29
  }
30
30
 
31
31
  for (const entry of entries) {
32
- if (runtimePos >= entry.runtimeStart && runtimePos <= entry.runtimeEnd) {
32
+ if (runtimePos >= entry.runtimeStart && runtimePos < entry.runtimeEnd) {
33
33
  return entry.pmStart + (runtimePos - entry.runtimeStart);
34
34
  }
35
35
  if (runtimePos < entry.runtimeStart) {
@@ -75,7 +75,7 @@ function walkBlocks(
75
75
  for (const block of blocks) {
76
76
  switch (block.kind) {
77
77
  case "paragraph": {
78
- const pmContentStart = nextPmCursor + 1;
78
+ const pmContentStart = nextPmCursor;
79
79
  const runtimeLength = block.to - block.from;
80
80
  entries.push({
81
81
  runtimeStart: block.from,
@@ -16,14 +16,21 @@ function resolveHeadingLevel(
16
16
  ): number | null {
17
17
  if (styleId) {
18
18
  const normalized = styleId.toLowerCase();
19
- const headingMatch = /^heading([1-6])$/.exec(normalized);
19
+ const compact = normalized.replace(/[\s_-]+/g, "");
20
+ const headingMatch = /^heading([1-6])$/.exec(compact);
20
21
  if (headingMatch) {
21
22
  return Number.parseInt(headingMatch[1], 10);
22
23
  }
23
- if (normalized === "title") {
24
+ if (compact === "title") {
24
25
  return 1;
25
26
  }
26
- if (normalized === "subtitle") {
27
+ if (compact === "subtitle") {
28
+ return 2;
29
+ }
30
+ if (compact === "tocheading") {
31
+ return 1;
32
+ }
33
+ if (/^(appendix|schedule|annex|exhibit|attachment)(heading|title)$/.test(compact)) {
27
34
  return 2;
28
35
  }
29
36
  }
@@ -74,6 +81,24 @@ function safeCssColor(raw: string | null | undefined): string | null {
74
81
  return null;
75
82
  }
76
83
 
84
+ function sanitizeLinkHref(raw: string | null | undefined): string | null {
85
+ if (!raw) return null;
86
+ const trimmed = raw.trim();
87
+ if (trimmed.startsWith("#")) {
88
+ return trimmed;
89
+ }
90
+ const lower = trimmed.toLowerCase();
91
+ if (
92
+ lower.startsWith("http://") ||
93
+ lower.startsWith("https://") ||
94
+ lower.startsWith("mailto:") ||
95
+ lower.startsWith("tel:")
96
+ ) {
97
+ return trimmed;
98
+ }
99
+ return null;
100
+ }
101
+
77
102
  /**
78
103
  * ProseMirror schema for the supported live surface slice.
79
104
  *
@@ -95,14 +120,20 @@ export const editorSchema = new Schema({
95
120
  numberingInstanceId: { default: null },
96
121
  numberingLevel: { default: null },
97
122
  numberingPrefix: { default: null },
123
+ numberingSuffix: { default: null },
98
124
  alignment: { default: null },
99
125
  spacingBefore: { default: null },
100
126
  spacingAfter: { default: null },
101
127
  lineSpacing: { default: null },
102
128
  lineRule: { default: null },
129
+ contextualSpacing: { default: null },
130
+ listContinuation: { default: null },
131
+ contextualSpacingBefore: { default: null },
132
+ contextualSpacingAfter: { default: null },
103
133
  indentLeft: { default: null },
104
134
  indentRight: { default: null },
105
135
  indentFirstLine: { default: null },
136
+ indentHanging: { default: null },
106
137
  shadingFill: { default: null },
107
138
  borderTop: { default: null },
108
139
  borderBottom: { default: null },
@@ -127,9 +158,13 @@ export const editorSchema = new Schema({
127
158
  const safeAlign = alignment === "both" ? "justify" : alignment;
128
159
  if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) styles.push(`text-align: ${safeAlign}`);
129
160
  const spacingBefore = node.attrs.spacingBefore as number | null;
130
- if (spacingBefore) styles.push(`margin-top: ${spacingBefore / 20}px`);
161
+ const contextualSpacingBefore = node.attrs.contextualSpacingBefore as boolean | null;
162
+ if (contextualSpacingBefore) styles.push("margin-top: 0");
163
+ else if (spacingBefore) styles.push(`margin-top: ${spacingBefore / 20}px`);
164
+ const contextualSpacingAfter = node.attrs.contextualSpacingAfter as boolean | null;
131
165
  const spacingAfter = node.attrs.spacingAfter as number | null;
132
- if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}px`);
166
+ if (contextualSpacingAfter) styles.push("margin-bottom: 0");
167
+ else if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}px`);
133
168
  const lineSpacing = node.attrs.lineSpacing as number | null;
134
169
  const lineRule = node.attrs.lineRule as string | null;
135
170
  if (lineSpacing && lineRule === "auto") styles.push(`line-height: ${lineSpacing / 240}`);
@@ -140,7 +175,9 @@ export const editorSchema = new Schema({
140
175
  const indentRight = node.attrs.indentRight as number | null;
141
176
  if (indentRight) styles.push(`padding-right: ${indentRight / 20}px`);
142
177
  const indentFirstLine = node.attrs.indentFirstLine as number | null;
143
- if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}px`);
178
+ const indentHanging = node.attrs.indentHanging as number | null;
179
+ if (indentHanging) styles.push(`text-indent: -${indentHanging / 20}px`);
180
+ else if (indentFirstLine) styles.push(`text-indent: ${indentFirstLine / 20}px`);
144
181
  const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
145
182
  if (shadingColor) styles.push(`background-color: ${shadingColor}`);
146
183
  for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
@@ -159,9 +196,28 @@ export const editorSchema = new Schema({
159
196
  if (headingLevel) {
160
197
  attrs["data-heading-level"] = String(headingLevel);
161
198
  }
199
+ const isNumbered = node.attrs.numberingInstanceId !== null;
200
+ if (isNumbered) {
201
+ attrs["data-numbered"] = "true";
202
+ }
203
+ const contextualSpacing = node.attrs.contextualSpacing as boolean | null;
204
+ if (contextualSpacing) {
205
+ attrs["data-contextual-spacing"] = "true";
206
+ }
207
+ const listContinuation = node.attrs.listContinuation as boolean | null;
208
+ if (listContinuation) {
209
+ attrs["data-list-continuation"] = "true";
210
+ }
211
+ if (contextualSpacingBefore) {
212
+ attrs["data-contextual-spacing-before"] = "true";
213
+ }
214
+ if (contextualSpacingAfter) {
215
+ attrs["data-contextual-spacing-after"] = "true";
216
+ }
162
217
  if (styles.length > 0) attrs.style = styles.join("; ");
163
218
  const numberingPrefix = node.attrs.numberingPrefix as string | null;
164
219
  const numberingLevel = node.attrs.numberingLevel as number | null;
220
+ const numberingSuffix = node.attrs.numberingSuffix as "tab" | "space" | "nothing" | null;
165
221
  const children: Array<string | number | readonly unknown[]> = [];
166
222
  if (pageBreak) {
167
223
  children.push([
@@ -177,6 +233,7 @@ export const editorSchema = new Schema({
177
233
  }
178
234
  if (numberingPrefix) {
179
235
  const minWidth = Math.min(Math.max(numberingPrefix.length + 1, 4), 14);
236
+ const marginRight = numberingSuffix === "nothing" ? "0.25rem" : numberingSuffix === "space" ? "0.5rem" : "0.75rem";
180
237
  children.push([
181
238
  "span",
182
239
  {
@@ -187,7 +244,8 @@ export const editorSchema = new Schema({
187
244
  ...(typeof numberingLevel === "number"
188
245
  ? { "data-numbering-level": String(numberingLevel) }
189
246
  : {}),
190
- style: `min-width: ${minWidth}ch; margin-right: 0.75rem; font-variant-numeric: tabular-nums;`,
247
+ ...(numberingSuffix ? { "data-numbering-suffix": numberingSuffix } : {}),
248
+ style: `min-width: ${minWidth}ch; margin-right: ${marginRight}; font-variant-numeric: tabular-nums;`,
191
249
  },
192
250
  numberingPrefix,
193
251
  ]);
@@ -273,10 +331,43 @@ export const editorSchema = new Schema({
273
331
  state: { default: "editable" },
274
332
  display: { default: "inline" },
275
333
  detail: { default: null },
334
+ src: { default: null },
335
+ widthEmu: { default: null },
336
+ heightEmu: { default: null },
276
337
  },
277
338
  toDOM(node) {
278
339
  const isMissing = node.attrs.state === "missing";
279
340
  const isFloating = node.attrs.display === "floating";
341
+ const src = node.attrs.src as string | null;
342
+ const widthEmu = node.attrs.widthEmu as number | null;
343
+ const heightEmu = node.attrs.heightEmu as number | null;
344
+ if (!isMissing && src) {
345
+ const widthPx = widthEmu ? Math.max(24, Math.round(widthEmu / 9525)) : undefined;
346
+ const heightPx = heightEmu ? Math.max(24, Math.round(heightEmu / 9525)) : undefined;
347
+ const style = [
348
+ "display:inline-block",
349
+ "vertical-align:middle",
350
+ "margin:0 4px",
351
+ widthPx ? `width:${widthPx}px` : "",
352
+ heightPx ? `height:${heightPx}px` : "",
353
+ ].filter(Boolean).join(";");
354
+ return [
355
+ "span",
356
+ {
357
+ class: "inline-flex items-center rounded",
358
+ "data-node-type": "image",
359
+ title: (node.attrs.detail as string) ?? (node.attrs.altText as string) ?? "Image",
360
+ },
361
+ [
362
+ "img",
363
+ {
364
+ src,
365
+ alt: (node.attrs.altText as string) ?? "",
366
+ style,
367
+ },
368
+ ],
369
+ ];
370
+ }
280
371
  return [
281
372
  "span",
282
373
  {
@@ -300,14 +391,28 @@ export const editorSchema = new Schema({
300
391
  alias: { default: null },
301
392
  tag: { default: null },
302
393
  lock: { default: null },
394
+ checkboxChecked: { default: null },
395
+ dateValue: { default: null },
396
+ dropdownItems: { default: null },
397
+ comboBoxItems: { default: null },
398
+ showingPlcHdr: { default: false },
303
399
  },
304
400
  toDOM(node) {
305
- const meta = [node.attrs.alias, node.attrs.tag, node.attrs.sdtType].filter(Boolean).join(" · ");
401
+ const sdtType = node.attrs.sdtType as string | null;
402
+ const typeLabel = sdtType === "checkbox" ? "\u2611 Checkbox"
403
+ : sdtType === "date" ? "\uD83D\uDCC5 Date"
404
+ : sdtType === "dropDownList" ? "\u25BE Dropdown"
405
+ : sdtType === "comboBox" ? "\u25BE Combo box"
406
+ : sdtType === "plainText" ? "\u270E Plain text"
407
+ : sdtType === "richText" ? "\u270E Rich text"
408
+ : sdtType ?? undefined;
409
+ const meta = [node.attrs.alias, node.attrs.tag, typeLabel].filter(Boolean).join(" \u00B7 ");
306
410
  return [
307
411
  "section",
308
412
  {
309
413
  class: "my-2 rounded-xl border border-primary/15 bg-surface-raised/60 px-3 py-2",
310
414
  "data-node-type": "sdt_block",
415
+ ...(sdtType ? { "data-sdt-type": sdtType } : {}),
311
416
  },
312
417
  [
313
418
  "div",
@@ -332,13 +437,29 @@ export const editorSchema = new Schema({
332
437
  warningId: { default: "" },
333
438
  label: { default: "Locked" },
334
439
  detail: { default: "" },
440
+ presentation: { default: "inline-chip" },
335
441
  },
336
442
  toDOM(node) {
443
+ const presentation = node.attrs.presentation as string;
444
+ if (presentation === "quiet-marker") {
445
+ return [
446
+ "span",
447
+ {
448
+ class: "inline-block h-0 w-0 overflow-hidden align-baseline",
449
+ "data-node-type": "opaque_inline",
450
+ "data-inline-presentation": "quiet-marker",
451
+ contenteditable: "false",
452
+ title: node.attrs.detail as string,
453
+ "aria-label": node.attrs.label as string,
454
+ },
455
+ ];
456
+ }
337
457
  return [
338
458
  "span",
339
459
  {
340
460
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-comment bg-warning-soft",
341
461
  "data-node-type": "opaque_inline",
462
+ "data-inline-presentation": "inline-chip",
342
463
  title: node.attrs.detail as string,
343
464
  },
344
465
  "\uD83D\uDD12 " + (node.attrs.label as string),
@@ -346,6 +467,39 @@ export const editorSchema = new Schema({
346
467
  },
347
468
  },
348
469
 
470
+ field_ref_atom: {
471
+ inline: true,
472
+ group: "inline",
473
+ atom: true,
474
+ selectable: false,
475
+ attrs: {
476
+ fieldFamily: { default: "REF" },
477
+ fieldTarget: { default: null },
478
+ instruction: { default: "" },
479
+ refreshStatus: { default: "stale" },
480
+ label: { default: "Field" },
481
+ },
482
+ toDOM(node) {
483
+ const refreshStatus = node.attrs.refreshStatus as string;
484
+ const statusClass =
485
+ refreshStatus === "current"
486
+ ? "text-blue-700 bg-blue-50 border-blue-200"
487
+ : refreshStatus === "unresolvable"
488
+ ? "text-amber-800 bg-amber-50 border-amber-200"
489
+ : "text-slate-700 bg-slate-50 border-slate-200";
490
+ return [
491
+ "span",
492
+ {
493
+ class: `inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs border ${statusClass}`,
494
+ "data-node-type": "field_ref_atom",
495
+ "data-field-family": node.attrs.fieldFamily as string,
496
+ title: node.attrs.instruction as string,
497
+ },
498
+ (node.attrs.label as string) || "Field",
499
+ ];
500
+ },
501
+ },
502
+
349
503
  table: tableNodeSpec,
350
504
  table_row: tableRowNodeSpec,
351
505
  table_cell: tableCellNodeSpec,
@@ -403,7 +557,7 @@ export const editorSchema = new Schema({
403
557
  inline: true,
404
558
  group: "inline",
405
559
  atom: true,
406
- selectable: false,
560
+ selectable: true,
407
561
  attrs: {
408
562
  text: { default: null },
409
563
  geometry: { default: null },
@@ -454,7 +608,7 @@ export const editorSchema = new Schema({
454
608
  inline: true,
455
609
  group: "inline",
456
610
  atom: true,
457
- selectable: false,
611
+ selectable: true,
458
612
  attrs: {
459
613
  text: { default: null },
460
614
  shapeType: { default: null },
@@ -551,7 +705,15 @@ export const editorSchema = new Schema({
551
705
  },
552
706
  vanish: {
553
707
  toDOM() {
554
- return ["span", { style: "opacity: 0.3; text-decoration: underline dotted; text-decoration-color: rgba(0,0,0,0.3)" }, 0];
708
+ return [
709
+ "span",
710
+ {
711
+ style: "display: none",
712
+ "data-hidden-text": "true",
713
+ "aria-hidden": "true",
714
+ },
715
+ 0,
716
+ ];
555
717
  },
556
718
  },
557
719
  emboss: {
@@ -682,15 +844,26 @@ export const editorSchema = new Schema({
682
844
  {
683
845
  tag: "a[href]",
684
846
  getAttrs(dom) {
685
- return { href: (dom as HTMLElement).getAttribute("href") };
847
+ return { href: sanitizeLinkHref((dom as HTMLElement).getAttribute("href")) ?? "" };
686
848
  },
687
849
  },
688
850
  ],
689
851
  toDOM(mark) {
852
+ const href = sanitizeLinkHref(mark.attrs.href as string);
853
+ if (!href) {
854
+ return [
855
+ "span",
856
+ {
857
+ class: "text-accent underline decoration-1 underline-offset-2",
858
+ "data-invalid-link": "true",
859
+ },
860
+ 0,
861
+ ];
862
+ }
690
863
  return [
691
864
  "a",
692
865
  {
693
- href: mark.attrs.href as string,
866
+ href,
694
867
  class: "text-accent underline decoration-1 underline-offset-2",
695
868
  target: "_blank",
696
869
  rel: "noopener noreferrer",