@beyondwork/docx-react-component 1.0.28 → 1.0.30

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 (92) hide show
  1. package/package.json +26 -37
  2. package/src/api/public-types.ts +531 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/index.ts +201 -79
  5. package/src/core/commands/table-structure-commands.ts +138 -5
  6. package/src/core/state/text-transaction.ts +370 -3
  7. package/src/index.ts +41 -0
  8. package/src/io/docx-session.ts +318 -25
  9. package/src/io/export/serialize-footnotes.ts +41 -46
  10. package/src/io/export/serialize-headers-footers.ts +36 -40
  11. package/src/io/export/serialize-main-document.ts +55 -89
  12. package/src/io/export/serialize-numbering.ts +104 -4
  13. package/src/io/export/serialize-runtime-revisions.ts +196 -2
  14. package/src/io/export/split-story-blocks-for-runtime-revisions.ts +252 -0
  15. package/src/io/export/table-properties-xml.ts +318 -0
  16. package/src/io/normalize/normalize-text.ts +34 -3
  17. package/src/io/ooxml/parse-comments.ts +6 -0
  18. package/src/io/ooxml/parse-footnotes.ts +69 -13
  19. package/src/io/ooxml/parse-headers-footers.ts +54 -11
  20. package/src/io/ooxml/parse-main-document.ts +112 -42
  21. package/src/io/ooxml/parse-numbering.ts +341 -26
  22. package/src/io/ooxml/parse-revisions.ts +118 -4
  23. package/src/io/ooxml/parse-styles.ts +176 -0
  24. package/src/io/ooxml/parse-tables.ts +34 -25
  25. package/src/io/ooxml/revision-boundaries.ts +127 -3
  26. package/src/io/ooxml/workflow-payload.ts +544 -0
  27. package/src/model/canonical-document.ts +91 -1
  28. package/src/model/snapshot.ts +112 -1
  29. package/src/preservation/store.ts +73 -3
  30. package/src/review/store/comment-store.ts +19 -1
  31. package/src/review/store/revision-actions.ts +29 -0
  32. package/src/review/store/revision-store.ts +12 -1
  33. package/src/review/store/revision-types.ts +11 -0
  34. package/src/runtime/context-analytics.ts +824 -0
  35. package/src/runtime/document-locations.ts +521 -0
  36. package/src/runtime/document-navigation.ts +14 -1
  37. package/src/runtime/document-outline.ts +440 -0
  38. package/src/runtime/document-runtime.ts +941 -45
  39. package/src/runtime/event-refresh-hints.ts +137 -0
  40. package/src/runtime/numbering-prefix.ts +67 -39
  41. package/src/runtime/page-layout-estimation.ts +100 -7
  42. package/src/runtime/resolved-numbering-geometry.ts +293 -0
  43. package/src/runtime/session-capabilities.ts +2 -2
  44. package/src/runtime/suggestions-snapshot.ts +137 -0
  45. package/src/runtime/surface-projection.ts +223 -27
  46. package/src/runtime/table-style-resolver.ts +409 -0
  47. package/src/runtime/view-state.ts +17 -1
  48. package/src/runtime/workflow-markup.ts +54 -14
  49. package/src/ui/WordReviewEditor.tsx +1269 -87
  50. package/src/ui/editor-command-bag.ts +7 -0
  51. package/src/ui/editor-runtime-boundary.ts +111 -10
  52. package/src/ui/editor-shell-view.tsx +17 -15
  53. package/src/ui/editor-surface-controller.tsx +5 -0
  54. package/src/ui/headless/selection-tool-context.ts +19 -0
  55. package/src/ui/headless/selection-tool-resolver.ts +752 -0
  56. package/src/ui/headless/selection-tool-types.ts +129 -0
  57. package/src/ui/headless/selection-toolbar-model.ts +10 -33
  58. package/src/ui/runtime-shortcut-dispatch.ts +365 -0
  59. package/src/ui-tailwind/chrome/chrome-preset-model.ts +107 -0
  60. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +15 -0
  61. package/src/ui-tailwind/chrome/review-queue-bar.tsx +97 -0
  62. package/src/ui-tailwind/chrome/tw-context-analytics-summary.tsx +122 -0
  63. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +1 -9
  64. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +1 -5
  65. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +8 -29
  66. package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +23 -0
  67. package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +35 -0
  68. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +37 -0
  69. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +298 -0
  70. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +116 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-suggestion.tsx +29 -0
  72. package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +27 -0
  73. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +3 -3
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +3 -3
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +86 -14
  76. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +57 -52
  77. package/src/ui-tailwind/editor-surface/pm-decorations.ts +36 -52
  78. package/src/ui-tailwind/editor-surface/pm-schema.ts +56 -5
  79. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +87 -24
  80. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
  81. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +135 -32
  82. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +74 -7
  83. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +17 -17
  84. package/src/ui-tailwind/review/tw-review-rail.tsx +19 -17
  85. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +10 -10
  86. package/src/ui-tailwind/status/tw-status-bar.tsx +10 -6
  87. package/src/ui-tailwind/theme/editor-theme.css +58 -40
  88. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -4
  89. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +250 -181
  90. package/src/ui-tailwind/tw-review-workspace.tsx +323 -280
  91. package/src/validation/compatibility-engine.ts +246 -2
  92. package/src/validation/docx-comment-proof.ts +24 -11
@@ -1,10 +1,15 @@
1
1
  import React from "react";
2
2
 
3
- import type { StyleCatalogSnapshot } from "../../api/public-types";
3
+ import type {
4
+ StyleCatalogSnapshot,
5
+ TableOperationCapabilitySnapshot,
6
+ TableStructureContextSnapshot,
7
+ } from "../../api/public-types";
4
8
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
5
9
 
6
10
  export interface TwTableContextToolbarProps {
7
11
  disabled: boolean;
12
+ tableContext: TableStructureContextSnapshot | null;
8
13
  tableStyles: StyleCatalogSnapshot["tables"];
9
14
  onSetTableStyle?: (styleId: string) => void;
10
15
  onAddRowBefore?: () => void;
@@ -38,13 +43,25 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
38
43
  Table
39
44
  </span>
40
45
 
46
+ {props.tableContext?.currentCell.isHeader ? (
47
+ <span className="rounded bg-accent-soft px-2 py-1 text-[10px] font-medium text-accent">
48
+ Header row
49
+ </span>
50
+ ) : null}
51
+
41
52
  <select
42
53
  aria-label="Table style"
43
54
  className="h-8 rounded-md border border-border bg-canvas px-2 text-xs text-primary disabled:opacity-40"
44
- disabled={props.disabled || props.tableStyles.length === 0 || !props.onSetTableStyle}
55
+ disabled={
56
+ props.disabled ||
57
+ props.tableStyles.length === 0 ||
58
+ !props.onSetTableStyle ||
59
+ !props.tableContext?.operations.setTableStyle.enabled
60
+ }
45
61
  onMouseDown={preserveEditorSelectionMouseDown}
46
62
  onChange={(event) => props.onSetTableStyle?.(event.target.value)}
47
- defaultValue=""
63
+ value={props.tableContext?.currentStyleId ?? ""}
64
+ title={props.tableContext?.operations.setTableStyle.reason}
48
65
  >
49
66
  <option value="" disabled>Table style</option>
50
67
  {props.tableStyles.map((style) => (
@@ -54,28 +71,68 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
54
71
  ))}
55
72
  </select>
56
73
 
57
- <ToolbarButton ariaLabel="Add row above" disabled={props.disabled} onClick={props.onAddRowBefore}>
74
+ <ToolbarButton
75
+ ariaLabel="Add row above"
76
+ capability={props.tableContext?.operations.addRowBefore}
77
+ disabled={props.disabled}
78
+ onClick={props.onAddRowBefore}
79
+ >
58
80
  Row above
59
81
  </ToolbarButton>
60
- <ToolbarButton ariaLabel="Add row below" disabled={props.disabled} onClick={props.onAddRowAfter}>
82
+ <ToolbarButton
83
+ ariaLabel="Add row below"
84
+ capability={props.tableContext?.operations.addRowAfter}
85
+ disabled={props.disabled}
86
+ onClick={props.onAddRowAfter}
87
+ >
61
88
  Row below
62
89
  </ToolbarButton>
63
- <ToolbarButton ariaLabel="Delete row" disabled={props.disabled} onClick={props.onDeleteRow}>
90
+ <ToolbarButton
91
+ ariaLabel="Delete row"
92
+ capability={props.tableContext?.operations.deleteRow}
93
+ disabled={props.disabled}
94
+ onClick={props.onDeleteRow}
95
+ >
64
96
  Delete row
65
97
  </ToolbarButton>
66
- <ToolbarButton ariaLabel="Add column left" disabled={props.disabled} onClick={props.onAddColumnBefore}>
98
+ <ToolbarButton
99
+ ariaLabel="Add column left"
100
+ capability={props.tableContext?.operations.addColumnBefore}
101
+ disabled={props.disabled}
102
+ onClick={props.onAddColumnBefore}
103
+ >
67
104
  Column left
68
105
  </ToolbarButton>
69
- <ToolbarButton ariaLabel="Add column right" disabled={props.disabled} onClick={props.onAddColumnAfter}>
106
+ <ToolbarButton
107
+ ariaLabel="Add column right"
108
+ capability={props.tableContext?.operations.addColumnAfter}
109
+ disabled={props.disabled}
110
+ onClick={props.onAddColumnAfter}
111
+ >
70
112
  Column right
71
113
  </ToolbarButton>
72
- <ToolbarButton ariaLabel="Delete column" disabled={props.disabled} onClick={props.onDeleteColumn}>
114
+ <ToolbarButton
115
+ ariaLabel="Delete column"
116
+ capability={props.tableContext?.operations.deleteColumn}
117
+ disabled={props.disabled}
118
+ onClick={props.onDeleteColumn}
119
+ >
73
120
  Delete column
74
121
  </ToolbarButton>
75
- <ToolbarButton ariaLabel="Merge cells" disabled={props.disabled} onClick={props.onMergeCells}>
122
+ <ToolbarButton
123
+ ariaLabel="Merge cells"
124
+ capability={props.tableContext?.operations.mergeCells}
125
+ disabled={props.disabled}
126
+ onClick={props.onMergeCells}
127
+ >
76
128
  Merge
77
129
  </ToolbarButton>
78
- <ToolbarButton ariaLabel="Split cell" disabled={props.disabled} onClick={props.onSplitCell}>
130
+ <ToolbarButton
131
+ ariaLabel="Split cell"
132
+ capability={props.tableContext?.operations.splitCell}
133
+ disabled={props.disabled}
134
+ onClick={props.onSplitCell}
135
+ >
79
136
  Split
80
137
  </ToolbarButton>
81
138
 
@@ -86,16 +143,27 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
86
143
  key={color}
87
144
  type="button"
88
145
  aria-label={`Set cell fill ${color}`}
89
- disabled={props.disabled || !props.onSetCellBackground}
146
+ disabled={
147
+ props.disabled ||
148
+ !props.onSetCellBackground ||
149
+ !props.tableContext?.operations.setCellBackground.enabled
150
+ }
90
151
  onMouseDown={preserveEditorSelectionMouseDown}
91
152
  onClick={() => props.onSetCellBackground?.(color)}
92
153
  className="h-6 w-6 rounded border border-border disabled:opacity-40"
93
154
  style={{ backgroundColor: color }}
155
+ title={props.tableContext?.operations.setCellBackground.reason}
94
156
  />
95
157
  ))}
96
158
  </div>
97
159
 
98
- <ToolbarButton ariaLabel="Delete table" danger disabled={props.disabled} onClick={props.onDeleteTable}>
160
+ <ToolbarButton
161
+ ariaLabel="Delete table"
162
+ capability={props.tableContext?.operations.deleteTable}
163
+ danger
164
+ disabled={props.disabled}
165
+ onClick={props.onDeleteTable}
166
+ >
99
167
  Delete table
100
168
  </ToolbarButton>
101
169
  </div>
@@ -104,18 +172,22 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
104
172
 
105
173
  function ToolbarButton(props: {
106
174
  ariaLabel: string;
175
+ capability?: TableOperationCapabilitySnapshot;
107
176
  children: React.ReactNode;
108
177
  danger?: boolean;
109
178
  disabled: boolean;
110
179
  onClick?: () => void;
111
180
  }) {
181
+ const capabilityEnabled = props.capability?.enabled ?? true;
182
+ const title = !capabilityEnabled ? props.capability?.reason : undefined;
112
183
  return (
113
184
  <button
114
185
  type="button"
115
186
  aria-label={props.ariaLabel}
116
- disabled={props.disabled || !props.onClick}
187
+ disabled={props.disabled || !props.onClick || !capabilityEnabled}
117
188
  onMouseDown={preserveEditorSelectionMouseDown}
118
189
  onClick={props.onClick}
190
+ title={title}
119
191
  className={`inline-flex h-8 items-center rounded-md px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
120
192
  props.danger
121
193
  ? "text-danger hover:bg-danger/10"
@@ -1,5 +1,4 @@
1
1
  import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
2
- import { keymap } from "prosemirror-keymap";
3
2
  import { columnResizing, goToNextCell, isInTable, tableEditing } from "prosemirror-tables";
4
3
 
5
4
  import type { SelectionSnapshot } from "../../api/public-types";
@@ -7,6 +6,7 @@ import {
7
6
  createNodeSelectionSnapshot,
8
7
  createSelectionSnapshot,
9
8
  } from "../../ui/headless/selection-helpers";
9
+ import { resolveSurfaceShortcut } from "../../ui/runtime-shortcut-dispatch";
10
10
  import type { PositionMap } from "./pm-position-map";
11
11
 
12
12
  export interface CommandBridgeCallbacks {
@@ -74,8 +74,10 @@ export function createCommandBridgePlugins(
74
74
  }
75
75
 
76
76
  const { anchor, head } = view.state.selection;
77
+ const runtimeAnchor = posMap.pmToRuntime(anchor);
78
+ const runtimeHead = posMap.pmToRuntime(head);
77
79
  callbacks.onSelectionChange(
78
- createSelectionSnapshot(posMap.pmToRuntime(anchor), posMap.pmToRuntime(head)),
80
+ createSelectionSnapshot(runtimeAnchor, runtimeHead),
79
81
  );
80
82
  }
81
83
  },
@@ -122,55 +124,58 @@ export function createCommandBridgePlugins(
122
124
  },
123
125
  });
124
126
 
125
- // Keymap: intercept editing keys and dispatch runtime commands.
126
- const keymapPlugin = keymap({
127
- Backspace: () => {
128
- if (isComposing) return false;
129
- callbacks.onDeleteBackward();
130
- return true;
131
- },
132
- Delete: () => {
133
- if (isComposing) return false;
134
- callbacks.onDeleteForward();
135
- return true;
136
- },
137
- Enter: () => {
138
- if (isComposing) return false;
139
- callbacks.onSplitParagraph();
140
- return true;
141
- },
142
- "Shift-Enter": () => {
143
- if (isComposing) return false;
144
- callbacks.onInsertHardBreak();
145
- return true;
146
- },
147
- Tab: (state, dispatch, view) => {
148
- if (isComposing) return false;
149
- if (isInTable(state)) {
150
- return goToNextCell(1)(state, dispatch, view);
151
- }
152
- callbacks.onInsertTab();
153
- return true;
154
- },
155
- "Shift-Tab": (state, dispatch, view) => {
156
- if (isComposing) return false;
157
- if (isInTable(state)) {
158
- return goToNextCell(-1)(state, dispatch, view);
159
- }
160
- callbacks.onOutdentTab?.();
161
- return true;
162
- },
163
- "Mod-z": () => {
164
- callbacks.onUndo();
165
- return true;
166
- },
167
- "Mod-y": () => {
168
- callbacks.onRedo();
169
- return true;
170
- },
171
- "Shift-Mod-z": () => {
172
- callbacks.onRedo();
173
- return true;
127
+ // Keydown bridge: normalize mounted editor keys through the shared shortcut
128
+ // dispatcher so the PM layer and shell layer follow the same routing model.
129
+ const keydownPlugin = new Plugin({
130
+ props: {
131
+ handleKeyDown(view, event) {
132
+ if (isComposing) {
133
+ return false;
134
+ }
135
+
136
+ const resolution = resolveSurfaceShortcut(
137
+ {
138
+ key: event.key,
139
+ ctrlKey: event.ctrlKey,
140
+ metaKey: event.metaKey,
141
+ altKey: event.altKey,
142
+ shiftKey: event.shiftKey,
143
+ },
144
+ { inTable: isInTable(view.state) },
145
+ );
146
+
147
+ switch (resolution.kind) {
148
+ case "none":
149
+ return false;
150
+ case "delete-backward":
151
+ callbacks.onDeleteBackward();
152
+ return true;
153
+ case "delete-forward":
154
+ callbacks.onDeleteForward();
155
+ return true;
156
+ case "split-paragraph":
157
+ callbacks.onSplitParagraph();
158
+ return true;
159
+ case "insert-hard-break":
160
+ callbacks.onInsertHardBreak();
161
+ return true;
162
+ case "insert-tab":
163
+ callbacks.onInsertTab();
164
+ return true;
165
+ case "outdent-tab":
166
+ callbacks.onOutdentTab?.();
167
+ return true;
168
+ case "navigate-table-cell":
169
+ return goToNextCell(resolution.direction)(view.state, view.dispatch, view);
170
+ case "history":
171
+ if (resolution.history === "undo") {
172
+ callbacks.onUndo();
173
+ } else {
174
+ callbacks.onRedo();
175
+ }
176
+ return true;
177
+ }
178
+ },
174
179
  },
175
180
  });
176
181
 
@@ -180,5 +185,5 @@ export function createCommandBridgePlugins(
180
185
  const tablePlugin = tableEditing();
181
186
  const columnResizingPlugin = columnResizing();
182
187
 
183
- return [filterPlugin, selectionPlugin, inputPlugin, keymapPlugin, tablePlugin, columnResizingPlugin];
188
+ return [filterPlugin, selectionPlugin, inputPlugin, keydownPlugin, tablePlugin, columnResizingPlugin];
184
189
  }
@@ -9,6 +9,7 @@ import type {
9
9
  EditorStoryTarget,
10
10
  WorkflowBlockedCommandReason,
11
11
  WorkflowCandidateRange,
12
+ WorkflowMetadataMarkup,
12
13
  WorkflowScope,
13
14
  } from "../../api/public-types";
14
15
  import { MAIN_STORY_TARGET, storyTargetsEqual } from "../../core/selection/mapping.ts";
@@ -74,6 +75,10 @@ function getWorkflowCandidateRailClass(): string {
74
75
  return "wre-workflow-rail wre-workflow-rail-candidate";
75
76
  }
76
77
 
78
+ function getWorkflowMetadataInlineClass(): string {
79
+ return "wre-workflow-inline wre-workflow-inline-metadata";
80
+ }
81
+
77
82
  function getWorkflowBlockedInlineClass(reason: WorkflowBlockedCommandReason): string {
78
83
  if (reason.code === "workflow_blocked_import") {
79
84
  return "wre-workflow-inline wre-workflow-inline-blocked-import";
@@ -211,13 +216,11 @@ export function buildDecorations(
211
216
  workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[],
212
217
  activeWorkflowWorkItemId?: string | null,
213
218
  activeWorkflowScopeIds?: readonly string[],
214
- suggestionsEnabled = false,
219
+ workflowMetadata?: readonly WorkflowMetadataMarkup[],
215
220
  ): DecorationSet {
216
221
  const decorations: Decoration[] = [];
217
222
  const railRangeCache = new Map<string, Array<{ from: number; to: number }>>();
218
223
  const activeScopeIds = new Set(activeWorkflowScopeIds ?? []);
219
- // In suggestions mode, tracked changes are always shown regardless of the toggle.
220
- const effectiveShowTracked = suggestionsEnabled ? true : showTrackedChanges;
221
224
 
222
225
  // Walk comment threads and create inline decorations
223
226
  if (commentModel) {
@@ -251,7 +254,7 @@ export function buildDecorations(
251
254
  // Always hide deletions in clean mode (final-text semantics).
252
255
  // This is the critical behavior: "hide tracked changes" must show
253
256
  // the document as if accepted, not show deleted text as kept text.
254
- if (markupDisplay === "clean" && rev.kind === "deletion" && !suggestionsEnabled) {
257
+ if (markupDisplay === "clean" && rev.kind === "deletion") {
255
258
  const pmFrom = positionMap.runtimeToPm(rev.from);
256
259
  const pmTo = positionMap.runtimeToPm(rev.to);
257
260
  if (pmFrom < pmTo) {
@@ -266,48 +269,7 @@ export function buildDecorations(
266
269
  }
267
270
 
268
271
  // Skip visual styling when tracked changes display is off
269
- if (!effectiveShowTracked) continue;
270
-
271
- const pmFrom = positionMap.runtimeToPm(rev.from);
272
- const pmTo = positionMap.runtimeToPm(rev.to);
273
- if (pmFrom >= pmTo) continue;
274
-
275
- if (suggestionsEnabled) {
276
- if (rev.kind === "insertion") {
277
- decorations.push(
278
- Decoration.inline(pmFrom, pmTo, {
279
- class: "text-insert",
280
- "data-revision-id": rev.revisionId,
281
- }),
282
- );
283
- decorations.push(
284
- Decoration.widget(pmFrom, () => {
285
- const el = document.createElement("span");
286
- el.textContent = "[";
287
- el.className = "text-insert";
288
- el.setAttribute("contenteditable", "false");
289
- return el;
290
- }, { side: -1, key: `${rev.revisionId}-open` }),
291
- );
292
- decorations.push(
293
- Decoration.widget(pmTo, () => {
294
- const el = document.createElement("span");
295
- el.textContent = "]";
296
- el.className = "text-insert";
297
- el.setAttribute("contenteditable", "false");
298
- return el;
299
- }, { side: 1, key: `${rev.revisionId}-close` }),
300
- );
301
- } else if (rev.kind === "deletion") {
302
- decorations.push(
303
- Decoration.inline(pmFrom, pmTo, {
304
- class: "text-danger line-through decoration-danger/80 decoration-1",
305
- "data-revision-id": rev.revisionId,
306
- }),
307
- );
308
- }
309
- continue;
310
- }
272
+ if (!showTrackedChanges) continue;
311
273
 
312
274
  const cls = getRevisionHighlightClass(
313
275
  revisionModel,
@@ -317,12 +279,16 @@ export function buildDecorations(
317
279
  );
318
280
  if (!cls) continue;
319
281
 
320
- decorations.push(
321
- Decoration.inline(pmFrom, pmTo, {
322
- class: cls,
323
- "data-revision-id": rev.revisionId,
324
- }),
325
- );
282
+ const pmFrom = positionMap.runtimeToPm(rev.from);
283
+ const pmTo = positionMap.runtimeToPm(rev.to);
284
+ if (pmFrom < pmTo) {
285
+ decorations.push(
286
+ Decoration.inline(pmFrom, pmTo, {
287
+ class: cls,
288
+ "data-revision-id": rev.revisionId,
289
+ }),
290
+ );
291
+ }
326
292
  }
327
293
  }
328
294
 
@@ -365,6 +331,24 @@ export function buildDecorations(
365
331
  }
366
332
  }
367
333
 
334
+ if (workflowMetadata) {
335
+ for (const metadata of workflowMetadata) {
336
+ const metadataStoryTarget = metadata.storyTarget ?? MAIN_STORY_TARGET;
337
+ if (!storyTargetsEqual(metadataStoryTarget, activeStory)) continue;
338
+ const pmRange = buildAnchorPmRange(metadata.anchor, positionMap);
339
+ if (!pmRange || !pmRange.allowInline || pmRange.from >= pmRange.to) continue;
340
+
341
+ decorations.push(
342
+ Decoration.inline(pmRange.from, pmRange.to, {
343
+ class: getWorkflowMetadataInlineClass(),
344
+ ...(metadata.color ? { style: `--wre-workflow-metadata-color: ${metadata.color};` } : {}),
345
+ "data-workflow-entry-id": metadata.entryId,
346
+ "data-workflow-metadata-id": metadata.metadataId,
347
+ }),
348
+ );
349
+ }
350
+ }
351
+
368
352
  if (workflowCandidates) {
369
353
  for (const candidate of workflowCandidates) {
370
354
  const candidateStoryTarget = candidate.storyTarget ?? MAIN_STORY_TARGET;
@@ -99,6 +99,20 @@ function sanitizeLinkHref(raw: string | null | undefined): string | null {
99
99
  return null;
100
100
  }
101
101
 
102
+ function resolveMarkerJustificationCss(raw: string | null): string {
103
+ switch (raw) {
104
+ case "left":
105
+ return "flex-start";
106
+ case "center":
107
+ return "center";
108
+ case "right":
109
+ case "both":
110
+ case "distribute":
111
+ default:
112
+ return "flex-end";
113
+ }
114
+ }
115
+
102
116
  /**
103
117
  * ProseMirror schema for the supported live surface slice.
104
118
  *
@@ -121,6 +135,8 @@ export const editorSchema = new Schema({
121
135
  numberingLevel: { default: null },
122
136
  numberingPrefix: { default: null },
123
137
  numberingSuffix: { default: null },
138
+ numberingMarkerWidth: { default: null },
139
+ numberingMarkerJustification: { default: null },
124
140
  alignment: { default: null },
125
141
  spacingBefore: { default: null },
126
142
  spacingAfter: { default: null },
@@ -142,6 +158,7 @@ export const editorSchema = new Schema({
142
158
  outlineLevel: { default: null },
143
159
  bidi: { default: null },
144
160
  pageBreakBefore: { default: null },
161
+ hiddenTextOnly: { default: null },
145
162
  },
146
163
  parseDOM: [{ tag: "p" }],
147
164
  toDOM(node) {
@@ -191,6 +208,12 @@ export const editorSchema = new Schema({
191
208
  }
192
209
  const pageBreak = node.attrs.pageBreakBefore as boolean | null;
193
210
  if (pageBreak) styles.push("border-top: 2px dashed rgba(0,0,0,0.1); padding-top: 8px; margin-top: 16px");
211
+ const hiddenTextOnly = node.attrs.hiddenTextOnly as boolean | null;
212
+ if (hiddenTextOnly) {
213
+ attrs["data-hidden-paragraph"] = "true";
214
+ attrs["aria-hidden"] = "true";
215
+ styles.push("display: none");
216
+ }
194
217
  const bidi = node.attrs.bidi as boolean | null;
195
218
  if (bidi) attrs.dir = "rtl";
196
219
  if (headingLevel) {
@@ -218,6 +241,8 @@ export const editorSchema = new Schema({
218
241
  const numberingPrefix = node.attrs.numberingPrefix as string | null;
219
242
  const numberingLevel = node.attrs.numberingLevel as number | null;
220
243
  const numberingSuffix = node.attrs.numberingSuffix as "tab" | "space" | "nothing" | null;
244
+ const numberingMarkerWidth = node.attrs.numberingMarkerWidth as number | null;
245
+ const numberingMarkerJustification = node.attrs.numberingMarkerJustification as string | null;
221
246
  const children: Array<string | number | readonly unknown[]> = [];
222
247
  if (pageBreak) {
223
248
  children.push([
@@ -232,20 +257,46 @@ export const editorSchema = new Schema({
232
257
  ]);
233
258
  }
234
259
  if (numberingPrefix) {
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";
260
+ const hasResolvedMarkerWidth =
261
+ typeof numberingMarkerWidth === "number" && numberingMarkerWidth > 0;
262
+ const fallbackMinWidth = Math.min(Math.max(numberingPrefix.length + 1, 4), 14);
263
+ const fallbackMarginRight =
264
+ numberingSuffix === "nothing"
265
+ ? "0.25rem"
266
+ : numberingSuffix === "space"
267
+ ? "0.5rem"
268
+ : "0.75rem";
269
+ const prefixStyles = [
270
+ `font-variant-numeric: tabular-nums`,
271
+ `justify-content: ${resolveMarkerJustificationCss(numberingMarkerJustification)}`,
272
+ ];
273
+ if (hasResolvedMarkerWidth) {
274
+ const markerWidthPx = Math.max(1, Math.round(numberingMarkerWidth / 20));
275
+ prefixStyles.push(
276
+ `width: ${markerWidthPx}px`,
277
+ `min-width: ${markerWidthPx}px`,
278
+ `flex-basis: ${markerWidthPx}px`,
279
+ `margin-right: 0`,
280
+ `overflow: visible`,
281
+ );
282
+ } else {
283
+ prefixStyles.push(
284
+ `min-width: ${fallbackMinWidth}ch`,
285
+ `margin-right: ${fallbackMarginRight}`,
286
+ );
287
+ }
237
288
  children.push([
238
289
  "span",
239
290
  {
240
291
  class:
241
- "inline-flex select-none items-center justify-end text-tertiary font-[family-name:var(--font-legal-sans)]",
292
+ "inline-flex select-none items-center text-tertiary font-[family-name:var(--font-legal-sans)]",
242
293
  contenteditable: "false",
243
294
  "data-numbering-prefix": numberingPrefix,
244
295
  ...(typeof numberingLevel === "number"
245
296
  ? { "data-numbering-level": String(numberingLevel) }
246
297
  : {}),
247
298
  ...(numberingSuffix ? { "data-numbering-suffix": numberingSuffix } : {}),
248
- style: `min-width: ${minWidth}ch; margin-right: ${marginRight}; font-variant-numeric: tabular-nums;`,
299
+ style: prefixStyles.join("; "),
249
300
  },
250
301
  numberingPrefix,
251
302
  ]);
@@ -569,7 +620,7 @@ export const editorSchema = new Schema({
569
620
  return [
570
621
  "span",
571
622
  {
572
- class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-green-700 bg-green-50 border border-green-200",
623
+ class: "inline-flex items-center gap-1 mx-0.5 rounded border border-success/25 bg-success-soft px-1.5 py-0.5 text-xs text-success",
573
624
  "data-node-type": "shape_atom",
574
625
  contenteditable: "false",
575
626
  title: (node.attrs.detail as string) ?? label,