@beyondwork/docx-react-component 1.0.19 → 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.
- package/package.json +1 -1
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +850 -1315
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +174 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface ActiveObjectContext {
|
|
4
|
+
kind: "textbox" | "shape";
|
|
5
|
+
display: "inline" | "floating";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TwObjectContextToolbarProps {
|
|
9
|
+
activeObject: ActiveObjectContext;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function TwObjectContextToolbar(props: TwObjectContextToolbarProps) {
|
|
13
|
+
const label = props.activeObject.kind === "textbox" ? "Text box" : "Shape";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
data-testid="object-context-toolbar"
|
|
18
|
+
className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
|
|
19
|
+
>
|
|
20
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
21
|
+
Object
|
|
22
|
+
</span>
|
|
23
|
+
<span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
|
|
27
|
+
{props.activeObject.display}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="text-xs text-secondary">
|
|
30
|
+
Object selection is active.
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
2
|
import type { FocusEventHandler } from "react";
|
|
3
3
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
|
-
import { Bold, Italic, MessageSquare, Underline } from "lucide-react";
|
|
4
|
+
import { Baseline, Bold, Highlighter, Italic, MessageSquare, Underline } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
import type { SelectionToolbarModel } from "../../ui/headless/selection-toolbar-model";
|
|
7
7
|
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
@@ -14,6 +14,8 @@ export interface TwSelectionToolbarProps {
|
|
|
14
14
|
onToggleBold?: () => void;
|
|
15
15
|
onToggleItalic?: () => void;
|
|
16
16
|
onToggleUnderline?: () => void;
|
|
17
|
+
onSetTextColor?: (color: string) => void;
|
|
18
|
+
onSetHighlightColor?: (color: string | null) => void;
|
|
17
19
|
onAddComment?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -60,6 +62,20 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
60
62
|
disabled={formattingDisabled}
|
|
61
63
|
onClick={props.onToggleUnderline}
|
|
62
64
|
/>
|
|
65
|
+
<ToolbarActionButton
|
|
66
|
+
icon={<Baseline className="h-3.5 w-3.5" />}
|
|
67
|
+
label="Text color blue"
|
|
68
|
+
pressed={false}
|
|
69
|
+
disabled={formattingDisabled}
|
|
70
|
+
onClick={() => props.onSetTextColor?.("#1660a8")}
|
|
71
|
+
/>
|
|
72
|
+
<ToolbarActionButton
|
|
73
|
+
icon={<Highlighter className="h-3.5 w-3.5" />}
|
|
74
|
+
label="Highlight yellow"
|
|
75
|
+
pressed={false}
|
|
76
|
+
disabled={formattingDisabled}
|
|
77
|
+
onClick={() => props.onSetHighlightColor?.("#ffff00")}
|
|
78
|
+
/>
|
|
63
79
|
|
|
64
80
|
<div className="mx-0.5 h-4 w-px bg-border" />
|
|
65
81
|
|
|
@@ -86,9 +102,18 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
|
|
|
86
102
|
</Tooltip.Portal>
|
|
87
103
|
</Tooltip.Root>
|
|
88
104
|
|
|
89
|
-
{
|
|
105
|
+
{model.previewText ? (
|
|
90
106
|
<>
|
|
91
107
|
<div className="mx-0.5 h-4 w-px bg-border" />
|
|
108
|
+
<span className="max-w-[8rem] truncate text-[11px] text-secondary">
|
|
109
|
+
{model.previewText}
|
|
110
|
+
</span>
|
|
111
|
+
</>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{contextLabel ? (
|
|
115
|
+
<>
|
|
116
|
+
{!model.previewText ? <div className="mx-0.5 h-4 w-px bg-border" /> : null}
|
|
92
117
|
<span
|
|
93
118
|
className={`min-w-0 max-w-[11rem] truncate rounded-full px-2 py-0.5 text-[10px] font-medium tracking-[0.08em] ${
|
|
94
119
|
model.badges.some((badge) => badge.tone === "accent")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import type { StyleCatalogSnapshot } from "../../api/public-types";
|
|
4
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
5
|
+
|
|
6
|
+
export interface TwTableContextToolbarProps {
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
tableStyles: StyleCatalogSnapshot["tables"];
|
|
9
|
+
onSetTableStyle?: (styleId: string) => void;
|
|
10
|
+
onAddRowBefore?: () => void;
|
|
11
|
+
onAddRowAfter?: () => void;
|
|
12
|
+
onAddColumnBefore?: () => void;
|
|
13
|
+
onAddColumnAfter?: () => void;
|
|
14
|
+
onDeleteRow?: () => void;
|
|
15
|
+
onDeleteColumn?: () => void;
|
|
16
|
+
onMergeCells?: () => void;
|
|
17
|
+
onSplitCell?: () => void;
|
|
18
|
+
onSetCellBackground?: (color: string) => void;
|
|
19
|
+
onDeleteTable?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CELL_COLORS = [
|
|
23
|
+
"#ffffff",
|
|
24
|
+
"#f0f0ee",
|
|
25
|
+
"#dbeafe",
|
|
26
|
+
"#fef3c7",
|
|
27
|
+
"#dcfce7",
|
|
28
|
+
"#fce7f3",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-testid="table-context-toolbar"
|
|
35
|
+
className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
|
|
36
|
+
>
|
|
37
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
|
|
38
|
+
Table
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<select
|
|
42
|
+
aria-label="Table style"
|
|
43
|
+
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}
|
|
45
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
46
|
+
onChange={(event) => props.onSetTableStyle?.(event.target.value)}
|
|
47
|
+
defaultValue=""
|
|
48
|
+
>
|
|
49
|
+
<option value="" disabled>Table style</option>
|
|
50
|
+
{props.tableStyles.map((style) => (
|
|
51
|
+
<option key={style.styleId} value={style.styleId}>
|
|
52
|
+
{style.displayName}
|
|
53
|
+
</option>
|
|
54
|
+
))}
|
|
55
|
+
</select>
|
|
56
|
+
|
|
57
|
+
<ToolbarButton ariaLabel="Add row above" disabled={props.disabled} onClick={props.onAddRowBefore}>
|
|
58
|
+
Row above
|
|
59
|
+
</ToolbarButton>
|
|
60
|
+
<ToolbarButton ariaLabel="Add row below" disabled={props.disabled} onClick={props.onAddRowAfter}>
|
|
61
|
+
Row below
|
|
62
|
+
</ToolbarButton>
|
|
63
|
+
<ToolbarButton ariaLabel="Delete row" disabled={props.disabled} onClick={props.onDeleteRow}>
|
|
64
|
+
Delete row
|
|
65
|
+
</ToolbarButton>
|
|
66
|
+
<ToolbarButton ariaLabel="Add column left" disabled={props.disabled} onClick={props.onAddColumnBefore}>
|
|
67
|
+
Column left
|
|
68
|
+
</ToolbarButton>
|
|
69
|
+
<ToolbarButton ariaLabel="Add column right" disabled={props.disabled} onClick={props.onAddColumnAfter}>
|
|
70
|
+
Column right
|
|
71
|
+
</ToolbarButton>
|
|
72
|
+
<ToolbarButton ariaLabel="Delete column" disabled={props.disabled} onClick={props.onDeleteColumn}>
|
|
73
|
+
Delete column
|
|
74
|
+
</ToolbarButton>
|
|
75
|
+
<ToolbarButton ariaLabel="Merge cells" disabled={props.disabled} onClick={props.onMergeCells}>
|
|
76
|
+
Merge
|
|
77
|
+
</ToolbarButton>
|
|
78
|
+
<ToolbarButton ariaLabel="Split cell" disabled={props.disabled} onClick={props.onSplitCell}>
|
|
79
|
+
Split
|
|
80
|
+
</ToolbarButton>
|
|
81
|
+
|
|
82
|
+
<div className="flex items-center gap-1">
|
|
83
|
+
<span className="text-[11px] text-secondary">Fill</span>
|
|
84
|
+
{CELL_COLORS.map((color) => (
|
|
85
|
+
<button
|
|
86
|
+
key={color}
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label={`Set cell fill ${color}`}
|
|
89
|
+
disabled={props.disabled || !props.onSetCellBackground}
|
|
90
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
91
|
+
onClick={() => props.onSetCellBackground?.(color)}
|
|
92
|
+
className="h-6 w-6 rounded border border-border disabled:opacity-40"
|
|
93
|
+
style={{ backgroundColor: color }}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<ToolbarButton ariaLabel="Delete table" danger disabled={props.disabled} onClick={props.onDeleteTable}>
|
|
99
|
+
Delete table
|
|
100
|
+
</ToolbarButton>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ToolbarButton(props: {
|
|
106
|
+
ariaLabel: string;
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
danger?: boolean;
|
|
109
|
+
disabled: boolean;
|
|
110
|
+
onClick?: () => void;
|
|
111
|
+
}) {
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
aria-label={props.ariaLabel}
|
|
116
|
+
disabled={props.disabled || !props.onClick}
|
|
117
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
118
|
+
onClick={props.onClick}
|
|
119
|
+
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
|
+
props.danger
|
|
121
|
+
? "text-danger hover:bg-danger/10"
|
|
122
|
+
: "text-primary hover:bg-surface"
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{props.children}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
export type PerfProbeKind =
|
|
1
|
+
export type PerfProbeKind =
|
|
2
|
+
| "typing"
|
|
3
|
+
| "selection"
|
|
4
|
+
| "runtime.create"
|
|
5
|
+
| "snapshot.surface"
|
|
6
|
+
| "snapshot.compatibility"
|
|
7
|
+
| "snapshot.navigation"
|
|
8
|
+
| "pm.rebuild"
|
|
9
|
+
| "pm.decorations"
|
|
10
|
+
| "pm.mount"
|
|
11
|
+
| "shell.render"
|
|
12
|
+
| "workspace.chrome"
|
|
13
|
+
| "selection.sync";
|
|
2
14
|
|
|
3
15
|
export interface PerfProbeSample {
|
|
4
16
|
token: string;
|
|
@@ -18,14 +30,13 @@ interface PerfProbeState {
|
|
|
18
30
|
pending?: Record<string, PendingProbe>;
|
|
19
31
|
samples?: PerfProbeSample[];
|
|
20
32
|
maxSamples?: number;
|
|
33
|
+
invalidationCounts?: Record<string, number>;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
export interface PerfProbeSummary {
|
|
24
37
|
samples: PerfProbeSample[];
|
|
25
|
-
latest:
|
|
26
|
-
|
|
27
|
-
selection: PerfProbeSample | null;
|
|
28
|
-
};
|
|
38
|
+
latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>>;
|
|
39
|
+
invalidationCounts: Record<string, number>;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
declare global {
|
|
@@ -69,16 +80,47 @@ export function finishPerfProbe(token: string | null | undefined): PerfProbeSamp
|
|
|
69
80
|
recordedAt: Date.now(),
|
|
70
81
|
};
|
|
71
82
|
|
|
72
|
-
state
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
pushSample(state, sample);
|
|
84
|
+
|
|
85
|
+
return sample;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function recordPerfSample(
|
|
89
|
+
kind: PerfProbeKind,
|
|
90
|
+
durationMs = 0,
|
|
91
|
+
): PerfProbeSample | null {
|
|
92
|
+
const state = getEnabledState();
|
|
93
|
+
if (!state) {
|
|
94
|
+
return null;
|
|
77
95
|
}
|
|
78
96
|
|
|
97
|
+
const token = `${kind}-${state.nextToken ?? 0}`;
|
|
98
|
+
state.nextToken = (state.nextToken ?? 0) + 1;
|
|
99
|
+
const sample: PerfProbeSample = {
|
|
100
|
+
token,
|
|
101
|
+
kind,
|
|
102
|
+
durationMs,
|
|
103
|
+
recordedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
pushSample(state, sample);
|
|
79
106
|
return sample;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
export function incrementInvalidationCounter(
|
|
110
|
+
counter: string,
|
|
111
|
+
amount = 1,
|
|
112
|
+
): number {
|
|
113
|
+
const state = getEnabledState();
|
|
114
|
+
if (!state) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
state.invalidationCounts ??= {};
|
|
119
|
+
state.invalidationCounts[counter] =
|
|
120
|
+
(state.invalidationCounts[counter] ?? 0) + amount;
|
|
121
|
+
return state.invalidationCounts[counter]!;
|
|
122
|
+
}
|
|
123
|
+
|
|
82
124
|
export function getLatestPerfSummary(): PerfProbeSummary | null {
|
|
83
125
|
const state = getEnabledState();
|
|
84
126
|
const samples = state?.samples ?? [];
|
|
@@ -88,13 +130,22 @@ export function getLatestPerfSummary(): PerfProbeSummary | null {
|
|
|
88
130
|
|
|
89
131
|
return {
|
|
90
132
|
samples: [...samples],
|
|
91
|
-
latest:
|
|
92
|
-
|
|
93
|
-
selection: [...samples].reverse().find((sample) => sample.kind === "selection") ?? null,
|
|
94
|
-
},
|
|
133
|
+
latest: buildLatestSampleMap(samples),
|
|
134
|
+
invalidationCounts: { ...(state.invalidationCounts ?? {}) },
|
|
95
135
|
};
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
export function resetPerfProbeState(): void {
|
|
139
|
+
const state = getEnabledState();
|
|
140
|
+
if (!state) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
state.nextToken = 0;
|
|
144
|
+
state.pending = {};
|
|
145
|
+
state.samples = [];
|
|
146
|
+
state.invalidationCounts = {};
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
function getEnabledState(): PerfProbeState | null {
|
|
99
150
|
if (typeof window === "undefined") {
|
|
100
151
|
return null;
|
|
@@ -105,3 +156,24 @@ function getEnabledState(): PerfProbeState | null {
|
|
|
105
156
|
}
|
|
106
157
|
return state;
|
|
107
158
|
}
|
|
159
|
+
|
|
160
|
+
function pushSample(state: PerfProbeState, sample: PerfProbeSample): void {
|
|
161
|
+
state.samples ??= [];
|
|
162
|
+
state.samples.push(sample);
|
|
163
|
+
const maxSamples = state.maxSamples ?? 20;
|
|
164
|
+
if (state.samples.length > maxSamples) {
|
|
165
|
+
state.samples.splice(0, state.samples.length - maxSamples);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildLatestSampleMap(
|
|
170
|
+
samples: PerfProbeSample[],
|
|
171
|
+
): Partial<Record<PerfProbeKind, PerfProbeSample | null>> {
|
|
172
|
+
const latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>> = {};
|
|
173
|
+
for (const sample of [...samples].reverse()) {
|
|
174
|
+
if (latest[sample.kind] === undefined) {
|
|
175
|
+
latest[sample.kind] = sample;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return latest;
|
|
179
|
+
}
|
|
@@ -72,9 +72,9 @@ export function createCommandBridgePlugins(
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const {
|
|
75
|
+
const { anchor, head } = view.state.selection;
|
|
76
76
|
callbacks.onSelectionChange(
|
|
77
|
-
createSelectionSnapshot(posMap.pmToRuntime(
|
|
77
|
+
createSelectionSnapshot(posMap.pmToRuntime(anchor), posMap.pmToRuntime(head)),
|
|
78
78
|
);
|
|
79
79
|
}
|
|
80
80
|
},
|
|
@@ -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
|
}
|
|
@@ -29,7 +29,7 @@ export function buildPositionMap(surface: EditorSurfaceSnapshot): PositionMap {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
for (const entry of entries) {
|
|
32
|
-
if (runtimePos >= entry.runtimeStart && runtimePos
|
|
32
|
+
if (runtimePos >= entry.runtimeStart && runtimePos < entry.runtimeEnd) {
|
|
33
33
|
return entry.pmStart + (runtimePos - entry.runtimeStart);
|
|
34
34
|
}
|
|
35
35
|
if (runtimePos < entry.runtimeStart) {
|
|
@@ -16,14 +16,21 @@ function resolveHeadingLevel(
|
|
|
16
16
|
): number | null {
|
|
17
17
|
if (styleId) {
|
|
18
18
|
const normalized = styleId.toLowerCase();
|
|
19
|
-
const
|
|
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 (
|
|
24
|
+
if (compact === "title") {
|
|
24
25
|
return 1;
|
|
25
26
|
}
|
|
26
|
-
if (
|
|
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
|
*
|
|
@@ -101,9 +126,14 @@ export const editorSchema = new Schema({
|
|
|
101
126
|
spacingAfter: { default: null },
|
|
102
127
|
lineSpacing: { default: null },
|
|
103
128
|
lineRule: { default: null },
|
|
129
|
+
contextualSpacing: { default: null },
|
|
130
|
+
listContinuation: { default: null },
|
|
131
|
+
contextualSpacingBefore: { default: null },
|
|
132
|
+
contextualSpacingAfter: { default: null },
|
|
104
133
|
indentLeft: { default: null },
|
|
105
134
|
indentRight: { default: null },
|
|
106
135
|
indentFirstLine: { default: null },
|
|
136
|
+
indentHanging: { default: null },
|
|
107
137
|
shadingFill: { default: null },
|
|
108
138
|
borderTop: { default: null },
|
|
109
139
|
borderBottom: { default: null },
|
|
@@ -128,9 +158,13 @@ export const editorSchema = new Schema({
|
|
|
128
158
|
const safeAlign = alignment === "both" ? "justify" : alignment;
|
|
129
159
|
if (safeAlign && SAFE_ALIGNMENT.has(safeAlign)) styles.push(`text-align: ${safeAlign}`);
|
|
130
160
|
const spacingBefore = node.attrs.spacingBefore as number | null;
|
|
131
|
-
|
|
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;
|
|
132
165
|
const spacingAfter = node.attrs.spacingAfter as number | null;
|
|
133
|
-
if (
|
|
166
|
+
if (contextualSpacingAfter) styles.push("margin-bottom: 0");
|
|
167
|
+
else if (spacingAfter) styles.push(`margin-bottom: ${spacingAfter / 20}px`);
|
|
134
168
|
const lineSpacing = node.attrs.lineSpacing as number | null;
|
|
135
169
|
const lineRule = node.attrs.lineRule as string | null;
|
|
136
170
|
if (lineSpacing && lineRule === "auto") styles.push(`line-height: ${lineSpacing / 240}`);
|
|
@@ -141,7 +175,9 @@ export const editorSchema = new Schema({
|
|
|
141
175
|
const indentRight = node.attrs.indentRight as number | null;
|
|
142
176
|
if (indentRight) styles.push(`padding-right: ${indentRight / 20}px`);
|
|
143
177
|
const indentFirstLine = node.attrs.indentFirstLine as number | null;
|
|
144
|
-
|
|
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`);
|
|
145
181
|
const shadingColor = safeHexColor(node.attrs.shadingFill as string | null);
|
|
146
182
|
if (shadingColor) styles.push(`background-color: ${shadingColor}`);
|
|
147
183
|
for (const [side, attrName] of [["top", "borderTop"], ["bottom", "borderBottom"], ["left", "borderLeft"], ["right", "borderRight"]] as const) {
|
|
@@ -160,6 +196,24 @@ export const editorSchema = new Schema({
|
|
|
160
196
|
if (headingLevel) {
|
|
161
197
|
attrs["data-heading-level"] = String(headingLevel);
|
|
162
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
|
+
}
|
|
163
217
|
if (styles.length > 0) attrs.style = styles.join("; ");
|
|
164
218
|
const numberingPrefix = node.attrs.numberingPrefix as string | null;
|
|
165
219
|
const numberingLevel = node.attrs.numberingLevel as number | null;
|
|
@@ -277,10 +331,43 @@ export const editorSchema = new Schema({
|
|
|
277
331
|
state: { default: "editable" },
|
|
278
332
|
display: { default: "inline" },
|
|
279
333
|
detail: { default: null },
|
|
334
|
+
src: { default: null },
|
|
335
|
+
widthEmu: { default: null },
|
|
336
|
+
heightEmu: { default: null },
|
|
280
337
|
},
|
|
281
338
|
toDOM(node) {
|
|
282
339
|
const isMissing = node.attrs.state === "missing";
|
|
283
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
|
+
}
|
|
284
371
|
return [
|
|
285
372
|
"span",
|
|
286
373
|
{
|
|
@@ -380,6 +467,39 @@ export const editorSchema = new Schema({
|
|
|
380
467
|
},
|
|
381
468
|
},
|
|
382
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
|
+
|
|
383
503
|
table: tableNodeSpec,
|
|
384
504
|
table_row: tableRowNodeSpec,
|
|
385
505
|
table_cell: tableCellNodeSpec,
|
|
@@ -724,15 +844,26 @@ export const editorSchema = new Schema({
|
|
|
724
844
|
{
|
|
725
845
|
tag: "a[href]",
|
|
726
846
|
getAttrs(dom) {
|
|
727
|
-
return { href: (dom as HTMLElement).getAttribute("href") };
|
|
847
|
+
return { href: sanitizeLinkHref((dom as HTMLElement).getAttribute("href")) ?? "" };
|
|
728
848
|
},
|
|
729
849
|
},
|
|
730
850
|
],
|
|
731
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
|
+
}
|
|
732
863
|
return [
|
|
733
864
|
"a",
|
|
734
865
|
{
|
|
735
|
-
href
|
|
866
|
+
href,
|
|
736
867
|
class: "text-accent underline decoration-1 underline-offset-2",
|
|
737
868
|
target: "_blank",
|
|
738
869
|
rel: "noopener noreferrer",
|