@beyondwork/docx-react-component 1.0.86 → 1.0.87
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 +49 -0
- package/src/api/v3/ui/chrome-composition.ts +2 -11
- package/src/api/v3/ui/chrome.ts +6 -8
- package/src/index.ts +5 -0
- package/src/io/export/serialize-main-document.ts +215 -6
- package/src/io/ooxml/parse-drawing.ts +15 -1
- package/src/io/ooxml/parse-fields.ts +410 -12
- package/src/model/canonical-document.ts +177 -2
- package/src/model/layout/page-layout-snapshot.ts +2 -0
- package/src/model/layout/runtime-page-graph-types.ts +6 -0
- package/src/preservation/store.ts +4 -5
- package/src/runtime/document-outline.ts +80 -0
- package/src/runtime/document-runtime.ts +338 -13
- package/src/runtime/formatting/field/page-number-format.ts +49 -0
- package/src/runtime/formatting/field/resolver.ts +61 -40
- package/src/runtime/layout/layout-engine-instance.ts +18 -1
- package/src/runtime/layout/layout-engine-version.ts +19 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
- package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
- package/src/runtime/layout/page-graph.ts +13 -2
- package/src/runtime/layout/paginated-layout-engine.ts +440 -117
- package/src/runtime/layout/project-block-fragments.ts +87 -4
- package/src/runtime/layout/resolve-page-fields.ts +8 -5
- package/src/runtime/layout/table-row-split.ts +97 -23
- package/src/runtime/surface-projection.ts +227 -27
- package/src/shell/session-bootstrap.ts +6 -1
- package/src/ui/WordReviewEditor.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +1 -0
- package/src/ui/headless/revision-decoration-model.ts +11 -13
- package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +32 -3
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -0
|
@@ -204,18 +204,16 @@ export function buildClassFromRevisionDisplay(
|
|
|
204
204
|
|
|
205
205
|
// Insertion underline (simple / all markup, insertion kind).
|
|
206
206
|
if (display.insertionUnderline) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// visual diff.
|
|
211
|
-
parts.push(
|
|
212
|
-
"underline decoration-insert/60 decoration-1 underline-offset-2 text-primary",
|
|
213
|
-
);
|
|
207
|
+
parts.push(display.markupMode === "all"
|
|
208
|
+
? "rounded-[2px] bg-insert-soft/35 px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2"
|
|
209
|
+
: "rounded-[2px] px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2");
|
|
214
210
|
}
|
|
215
211
|
|
|
216
212
|
// Strikethrough (deletion kind, simple or all markup).
|
|
217
213
|
if (display.strikethrough) {
|
|
218
|
-
parts.push(
|
|
214
|
+
parts.push(display.markupMode === "all"
|
|
215
|
+
? "rounded-[2px] bg-delete-soft/35 px-[1px] text-danger line-through decoration-danger decoration-2"
|
|
216
|
+
: "text-danger line-through decoration-danger decoration-2");
|
|
219
217
|
}
|
|
220
218
|
|
|
221
219
|
// De-emphasize (e.g. inactive revision in all-markup; reviewer
|
|
@@ -234,7 +232,7 @@ export function buildClassFromRevisionDisplay(
|
|
|
234
232
|
(display.kind === "formatting" || display.kind === "property-change")
|
|
235
233
|
) {
|
|
236
234
|
parts.push(
|
|
237
|
-
"underline decoration-accent/
|
|
235
|
+
"rounded-[2px] bg-accent-soft/70 px-[1px] underline decoration-accent/80 decoration-dotted decoration-2 underline-offset-2",
|
|
238
236
|
);
|
|
239
237
|
}
|
|
240
238
|
|
|
@@ -277,18 +275,18 @@ export function getRevisionHighlightClass(
|
|
|
277
275
|
return "";
|
|
278
276
|
case "simple-markup":
|
|
279
277
|
if (state.hasInsertions) {
|
|
280
|
-
return `underline decoration-insert
|
|
278
|
+
return `rounded-[2px] px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2${activeRing}`;
|
|
281
279
|
}
|
|
282
280
|
if (state.hasDeletions) {
|
|
283
|
-
return `text-
|
|
281
|
+
return `text-danger line-through decoration-danger decoration-2${activeRing}`;
|
|
284
282
|
}
|
|
285
283
|
return activeRing;
|
|
286
284
|
case "all-markup":
|
|
287
285
|
if (state.hasInsertions) {
|
|
288
|
-
return `
|
|
286
|
+
return `rounded-[2px] bg-insert-soft/35 px-[1px] text-primary underline decoration-insert decoration-2 underline-offset-2${activeRing}`;
|
|
289
287
|
}
|
|
290
288
|
if (state.hasDeletions) {
|
|
291
|
-
return `text-danger line-through decoration-danger
|
|
289
|
+
return `rounded-[2px] bg-delete-soft/35 px-[1px] text-danger line-through decoration-danger decoration-2${activeRing}`;
|
|
292
290
|
}
|
|
293
291
|
return activeRing;
|
|
294
292
|
}
|
|
@@ -17,11 +17,11 @@ export function isNarrowChromeViewport(viewportWidth?: number): boolean {
|
|
|
17
17
|
return typeof viewportWidth === "number" && viewportWidth <= NARROW_CHROME_MAX_WIDTH;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function getInitialReviewRailOpen(
|
|
20
|
+
export function getInitialReviewRailOpen(_input: {
|
|
21
21
|
viewportWidth?: number;
|
|
22
22
|
reviewRailAvailable: boolean;
|
|
23
23
|
}): boolean {
|
|
24
|
-
return
|
|
24
|
+
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function resolveResponsiveChromeState(
|
|
@@ -123,6 +123,7 @@ export function TwAlertBanner(
|
|
|
123
123
|
// 3. Workflow blocked — host policy refuses a command, per reasons.
|
|
124
124
|
if (workflowBlockedReasons.length > 0) {
|
|
125
125
|
const firstReason = workflowBlockedReasons[0]!;
|
|
126
|
+
const hint = getWorkflowBlockedHint(firstReason);
|
|
126
127
|
return renderBanner({
|
|
127
128
|
severity: "warning",
|
|
128
129
|
icon: (
|
|
@@ -131,6 +132,7 @@ export function TwAlertBanner(
|
|
|
131
132
|
message: (
|
|
132
133
|
<>
|
|
133
134
|
{firstReason.message}
|
|
135
|
+
{hint ? <span className="opacity-80"> {hint}</span> : null}
|
|
134
136
|
{workflowBlockedReasons.length > 1
|
|
135
137
|
? ` (+${workflowBlockedReasons.length - 1} more)`
|
|
136
138
|
: ""}
|
|
@@ -161,3 +163,28 @@ export function TwAlertBanner(
|
|
|
161
163
|
|
|
162
164
|
return null;
|
|
163
165
|
}
|
|
166
|
+
|
|
167
|
+
function getWorkflowBlockedHint(
|
|
168
|
+
reason: WorkflowBlockedCommandReason,
|
|
169
|
+
): string | null {
|
|
170
|
+
switch (reason.code) {
|
|
171
|
+
case "suggesting_unsupported":
|
|
172
|
+
return "Switch to Edit for this command, or insert plain text.";
|
|
173
|
+
case "workflow_comment_only":
|
|
174
|
+
return "Add a comment, or use an editing scope.";
|
|
175
|
+
case "outside_workflow_scope":
|
|
176
|
+
return "Move into an editable workflow scope.";
|
|
177
|
+
case "workflow_view_only":
|
|
178
|
+
case "document_viewing_mode":
|
|
179
|
+
case "document_read_only":
|
|
180
|
+
return "Open an editable copy or request edit access.";
|
|
181
|
+
case "workflow_preserve_only":
|
|
182
|
+
case "workflow_blocked_import":
|
|
183
|
+
case "protected_range":
|
|
184
|
+
case "unsupported_surface":
|
|
185
|
+
return "Use detail for the safe path.";
|
|
186
|
+
case "workflow_round_locked":
|
|
187
|
+
return "Wait for the round to unlock or request approval.";
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Transaction } from "prosemirror-state";
|
|
2
2
|
import type { EditorView } from "prosemirror-view";
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
TextCommandAck,
|
|
6
|
+
TextCommandRefreshClass,
|
|
7
|
+
} from "../../api/public-types.ts";
|
|
5
8
|
import type {
|
|
6
9
|
LocalEditSessionState,
|
|
7
10
|
PendingOp,
|
|
@@ -78,7 +81,11 @@ export interface FastTextEditLaneOptions {
|
|
|
78
81
|
/** Optional probe hooks for perf instrumentation. */
|
|
79
82
|
probe?: {
|
|
80
83
|
markPredicted(opId: string): void;
|
|
81
|
-
markReconciled(
|
|
84
|
+
markReconciled(
|
|
85
|
+
opId: string,
|
|
86
|
+
kind: TextCommandAck["kind"],
|
|
87
|
+
refreshClass: TextCommandRefreshClass,
|
|
88
|
+
): void;
|
|
82
89
|
};
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -96,6 +103,22 @@ function allocOpId(): string {
|
|
|
96
103
|
return `op-${Date.now().toString(36)}-${nextOpIdCounter}`;
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
export function getTextCommandRefreshClass(
|
|
107
|
+
ack: TextCommandAck,
|
|
108
|
+
): TextCommandRefreshClass {
|
|
109
|
+
if (ack.refreshClass) return ack.refreshClass;
|
|
110
|
+
switch (ack.kind) {
|
|
111
|
+
case "equivalent":
|
|
112
|
+
return "local-text-equivalent";
|
|
113
|
+
case "adjusted":
|
|
114
|
+
return "surface-only";
|
|
115
|
+
case "rejected":
|
|
116
|
+
return "blocked";
|
|
117
|
+
case "structural-divergence":
|
|
118
|
+
return "full-projection";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
export function createFastTextEditLane(
|
|
100
123
|
options: FastTextEditLaneOptions,
|
|
101
124
|
): FastTextEditLane {
|
|
@@ -136,9 +159,11 @@ export function createFastTextEditLane(
|
|
|
136
159
|
|
|
137
160
|
if (options.shouldBailBeforePredict?.(intent, fromRuntime, toRuntime)) {
|
|
138
161
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
162
|
+
const refreshClass = getTextCommandRefreshClass(ack);
|
|
139
163
|
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.bailBeforePredict);
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
incrementRefreshClassCounter(refreshClass);
|
|
165
|
+
markLaneDebugReconciled(debugEntry, ack.kind, refreshClass, true);
|
|
166
|
+
options.probe?.markReconciled(opId, ack.kind, refreshClass);
|
|
142
167
|
switch (ack.kind) {
|
|
143
168
|
case "equivalent":
|
|
144
169
|
options.session.advanceToRevision({
|
|
@@ -184,8 +209,10 @@ export function createFastTextEditLane(
|
|
|
184
209
|
op.predictedSelectionHead = view.state.selection.head;
|
|
185
210
|
|
|
186
211
|
const ack = options.dispatchRuntimeCommand(toRuntimeCommand(intent, opId));
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
const refreshClass = getTextCommandRefreshClass(ack);
|
|
213
|
+
markLaneDebugReconciled(debugEntry, ack.kind, refreshClass, false);
|
|
214
|
+
incrementRefreshClassCounter(refreshClass);
|
|
215
|
+
options.probe?.markReconciled(opId, ack.kind, refreshClass);
|
|
189
216
|
|
|
190
217
|
switch (ack.kind) {
|
|
191
218
|
case "equivalent":
|
|
@@ -284,6 +311,8 @@ interface LaneDebugEntry {
|
|
|
284
311
|
toRuntime: number;
|
|
285
312
|
/** Dispatch → reconcile observation. Filled by `markLaneDebugReconciled`. */
|
|
286
313
|
ackKind?: TextCommandAck["kind"];
|
|
314
|
+
/** Narrow refresh tier derived from the runtime ack. */
|
|
315
|
+
refreshClass?: TextCommandRefreshClass;
|
|
287
316
|
/** Wall-clock ms between `pushLaneDebug` and `markLaneDebugReconciled`. */
|
|
288
317
|
reconcileMs?: number;
|
|
289
318
|
/** Whether the lane short-circuited to dispatch-only (no predicted TX). */
|
|
@@ -336,10 +365,12 @@ function pushLaneDebug(
|
|
|
336
365
|
function markLaneDebugReconciled(
|
|
337
366
|
entry: LaneDebugEntry | null,
|
|
338
367
|
ackKind: TextCommandAck["kind"],
|
|
368
|
+
refreshClass: TextCommandRefreshClass,
|
|
339
369
|
bailed: boolean,
|
|
340
370
|
): void {
|
|
341
371
|
if (!entry) return;
|
|
342
372
|
entry.ackKind = ackKind;
|
|
373
|
+
entry.refreshClass = refreshClass;
|
|
343
374
|
entry.bailed = bailed;
|
|
344
375
|
const now =
|
|
345
376
|
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
@@ -348,6 +379,26 @@ function markLaneDebugReconciled(
|
|
|
348
379
|
entry.reconcileMs = now - entry.startedAtMs;
|
|
349
380
|
}
|
|
350
381
|
|
|
382
|
+
function incrementRefreshClassCounter(refreshClass: TextCommandRefreshClass): void {
|
|
383
|
+
switch (refreshClass) {
|
|
384
|
+
case "selection-only":
|
|
385
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshSelectionOnly);
|
|
386
|
+
return;
|
|
387
|
+
case "local-text-equivalent":
|
|
388
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshLocalTextEquivalent);
|
|
389
|
+
return;
|
|
390
|
+
case "surface-only":
|
|
391
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshSurfaceOnly);
|
|
392
|
+
return;
|
|
393
|
+
case "full-projection":
|
|
394
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshFullProjection);
|
|
395
|
+
return;
|
|
396
|
+
case "blocked":
|
|
397
|
+
incrementInvalidationCounter(PREDICTED_LANE_COUNTERS.refreshBlocked);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
351
402
|
function buildTxCompat(
|
|
352
403
|
view: EditorView,
|
|
353
404
|
_intent: PredictedIntent,
|
|
@@ -13,3 +13,20 @@ export function sliceBlocksForPage(
|
|
|
13
13
|
(b) => b.from < page.endOffset && b.to > page.startOffset,
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export function findBlockIndexRangeForPage(
|
|
18
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
19
|
+
page: Pick<DocumentPageSnapshot, "startOffset" | "endOffset">,
|
|
20
|
+
): { first: number; last: number } | null {
|
|
21
|
+
if (page.endOffset <= page.startOffset) return null;
|
|
22
|
+
let first = -1;
|
|
23
|
+
let last = -1;
|
|
24
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
25
|
+
const block = blocks[index]!;
|
|
26
|
+
if (block.from < page.endOffset && block.to > page.startOffset) {
|
|
27
|
+
if (first === -1) first = index;
|
|
28
|
+
last = index;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return first === -1 ? null : { first, last };
|
|
32
|
+
}
|
|
@@ -41,6 +41,11 @@ export const PREDICTED_LANE_COUNTERS = {
|
|
|
41
41
|
rollback: "predictions.rollback",
|
|
42
42
|
structuralDivergence: "predictions.structuralDivergence",
|
|
43
43
|
bailBeforePredict: "predictions.bailBeforePredict",
|
|
44
|
+
refreshSelectionOnly: "predictions.refresh.selectionOnly",
|
|
45
|
+
refreshLocalTextEquivalent: "predictions.refresh.localTextEquivalent",
|
|
46
|
+
refreshSurfaceOnly: "predictions.refresh.surfaceOnly",
|
|
47
|
+
refreshFullProjection: "predictions.refresh.fullProjection",
|
|
48
|
+
refreshBlocked: "predictions.refresh.blocked",
|
|
44
49
|
} as const;
|
|
45
50
|
|
|
46
51
|
export interface PerfProbeSample {
|
|
@@ -3,13 +3,47 @@ import { Plugin } from "prosemirror-state";
|
|
|
3
3
|
export interface ContextualInteractionCallbacks {
|
|
4
4
|
onCommentActivated?: (commentId: string) => void;
|
|
5
5
|
onRevisionActivated?: (revisionId: string) => void;
|
|
6
|
+
onRevisionHovered?: (revisionId: string | null) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findRevisionId(target: EventTarget | null): string | null {
|
|
10
|
+
const element = target as HTMLElement | null;
|
|
11
|
+
return element?.closest?.("[data-revision-id]")?.getAttribute("data-revision-id") ?? null;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
14
|
export function createContextualInteractionPlugin(
|
|
9
15
|
callbacks: ContextualInteractionCallbacks,
|
|
10
16
|
): Plugin {
|
|
17
|
+
let hoveredRevisionId: string | null = null;
|
|
18
|
+
|
|
11
19
|
return new Plugin({
|
|
12
20
|
props: {
|
|
21
|
+
handleDOMEvents: {
|
|
22
|
+
mouseover(_view, event) {
|
|
23
|
+
const revisionId = findRevisionId(event.target);
|
|
24
|
+
if (!revisionId || revisionId === hoveredRevisionId) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
hoveredRevisionId = revisionId;
|
|
28
|
+
callbacks.onRevisionHovered?.(revisionId);
|
|
29
|
+
return false;
|
|
30
|
+
},
|
|
31
|
+
mouseout(_view, event) {
|
|
32
|
+
const revisionId = findRevisionId(event.target);
|
|
33
|
+
if (!revisionId) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const relatedRevisionId = findRevisionId(event.relatedTarget);
|
|
37
|
+
if (relatedRevisionId === revisionId) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (hoveredRevisionId === revisionId) {
|
|
41
|
+
hoveredRevisionId = null;
|
|
42
|
+
callbacks.onRevisionHovered?.(null);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
13
47
|
handleClick(_view, _pos, event) {
|
|
14
48
|
const target = event.target as HTMLElement | null;
|
|
15
49
|
const commentId = target?.closest?.("[data-comment-id]")?.getAttribute("data-comment-id");
|
|
@@ -4,10 +4,12 @@ import type { CommentDecorationModel } from "../../ui/headless/comment-decoratio
|
|
|
4
4
|
import { getCommentHighlightClass, type MarkupDisplay } from "../../ui/headless/comment-decoration-model";
|
|
5
5
|
import type {
|
|
6
6
|
RevisionDecorationModel,
|
|
7
|
+
RevisionDecorationEntry,
|
|
7
8
|
RevisionDisplayFlags,
|
|
8
9
|
} from "../../ui/headless/revision-decoration-model";
|
|
9
10
|
import {
|
|
10
11
|
buildClassFromRevisionDisplay,
|
|
12
|
+
getAuthorColor,
|
|
11
13
|
getRevisionHighlightClass,
|
|
12
14
|
} from "../../ui/headless/revision-decoration-model";
|
|
13
15
|
import type {
|
|
@@ -46,6 +48,106 @@ type RailDecorationSpec = {
|
|
|
46
48
|
attrs: Record<string, string>;
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
function sanitizeRevisionAuthorColor(raw: unknown): string | null {
|
|
52
|
+
if (typeof raw !== "string") return null;
|
|
53
|
+
const value = raw.trim();
|
|
54
|
+
if (/^var\(--color-chart-categorical-[1-8]\)$/.test(value)) return value;
|
|
55
|
+
return sanitizeHostCssColor(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveRevisionAuthorColor(
|
|
59
|
+
rev: RevisionDecorationEntry,
|
|
60
|
+
display?: RevisionDisplayFlags,
|
|
61
|
+
): string | undefined {
|
|
62
|
+
return sanitizeRevisionAuthorColor(display?.authorColor) ?? getAuthorColor(rev.authorId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildRevisionAuthorStyle(
|
|
66
|
+
kind: RevisionDecorationEntry["kind"],
|
|
67
|
+
authorColor: string | undefined,
|
|
68
|
+
): string | undefined {
|
|
69
|
+
if (!authorColor) return undefined;
|
|
70
|
+
|
|
71
|
+
const backgroundStrength =
|
|
72
|
+
kind === "deletion" ? "8%" : kind === "insertion" ? "10%" : "9%";
|
|
73
|
+
return [
|
|
74
|
+
`--wre-revision-author: ${authorColor}`,
|
|
75
|
+
"color: var(--wre-revision-author)",
|
|
76
|
+
`background-color: color-mix(in srgb, var(--wre-revision-author) ${backgroundStrength}, transparent)`,
|
|
77
|
+
`text-decoration-color: var(--wre-revision-author)`,
|
|
78
|
+
"text-decoration-thickness: 2px",
|
|
79
|
+
"text-underline-offset: 2px",
|
|
80
|
+
"box-decoration-break: clone",
|
|
81
|
+
"-webkit-box-decoration-break: clone",
|
|
82
|
+
].join("; ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function labelRevisionKind(kind: RevisionDecorationEntry["kind"]): string {
|
|
86
|
+
switch (kind) {
|
|
87
|
+
case "insertion":
|
|
88
|
+
return "Insertion";
|
|
89
|
+
case "deletion":
|
|
90
|
+
return "Deletion";
|
|
91
|
+
case "formatting":
|
|
92
|
+
return "Formatting change";
|
|
93
|
+
case "move":
|
|
94
|
+
return "Move";
|
|
95
|
+
case "property-change":
|
|
96
|
+
return "Property change";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildRevisionInlineAttrs(
|
|
101
|
+
rev: RevisionDecorationEntry,
|
|
102
|
+
className: string,
|
|
103
|
+
display?: RevisionDisplayFlags,
|
|
104
|
+
): Record<string, string> {
|
|
105
|
+
const attrs: Record<string, string> = {
|
|
106
|
+
class: className,
|
|
107
|
+
"data-revision-id": rev.revisionId,
|
|
108
|
+
"data-revision-kind": rev.kind,
|
|
109
|
+
};
|
|
110
|
+
if (rev.authorId) {
|
|
111
|
+
attrs["data-revision-author-id"] = rev.authorId;
|
|
112
|
+
attrs.title = `${labelRevisionKind(rev.kind)} by ${rev.authorId}`;
|
|
113
|
+
}
|
|
114
|
+
if (rev.authorPaletteIndex !== undefined) {
|
|
115
|
+
attrs["data-revision-author-index"] = String(rev.authorPaletteIndex);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const style = buildRevisionAuthorStyle(
|
|
119
|
+
rev.kind,
|
|
120
|
+
resolveRevisionAuthorColor(rev, display),
|
|
121
|
+
);
|
|
122
|
+
if (style) {
|
|
123
|
+
attrs.style = style;
|
|
124
|
+
}
|
|
125
|
+
return attrs;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildRevisionBoundaryAttrs(
|
|
129
|
+
rev: RevisionDecorationEntry,
|
|
130
|
+
display?: RevisionDisplayFlags,
|
|
131
|
+
): Record<string, string> {
|
|
132
|
+
const attrs: Record<string, string> = {
|
|
133
|
+
class: "text-insert font-semibold",
|
|
134
|
+
"data-revision-id": rev.revisionId,
|
|
135
|
+
"data-revision-kind": rev.kind,
|
|
136
|
+
};
|
|
137
|
+
if (rev.authorId) {
|
|
138
|
+
attrs["data-revision-author-id"] = rev.authorId;
|
|
139
|
+
attrs.title = `${labelRevisionKind(rev.kind)} by ${rev.authorId}`;
|
|
140
|
+
}
|
|
141
|
+
if (rev.authorPaletteIndex !== undefined) {
|
|
142
|
+
attrs["data-revision-author-index"] = String(rev.authorPaletteIndex);
|
|
143
|
+
}
|
|
144
|
+
const authorColor = resolveRevisionAuthorColor(rev, display);
|
|
145
|
+
if (authorColor) {
|
|
146
|
+
attrs.style = `color: ${authorColor}`;
|
|
147
|
+
}
|
|
148
|
+
return attrs;
|
|
149
|
+
}
|
|
150
|
+
|
|
49
151
|
/**
|
|
50
152
|
* Validate and normalize a host-supplied CSS color before interpolating it
|
|
51
153
|
* into an inline-style string. Accepts only the narrow subset a
|
|
@@ -466,6 +568,7 @@ export function buildDecorations(
|
|
|
466
568
|
Decoration.inline(cleanPmFrom, cleanPmTo, {
|
|
467
569
|
class: "hidden",
|
|
468
570
|
"data-revision-id": rev.revisionId,
|
|
571
|
+
"data-revision-kind": rev.kind,
|
|
469
572
|
}),
|
|
470
573
|
);
|
|
471
574
|
revisionCount += 1;
|
|
@@ -480,17 +583,28 @@ export function buildDecorations(
|
|
|
480
583
|
// Suggestions styling is always shown regardless of showTrackedChanges toggle.
|
|
481
584
|
if (suggestionsEnabled) {
|
|
482
585
|
if (rev.kind === "insertion") {
|
|
586
|
+
const insertionClass =
|
|
587
|
+
buildClassFromRevisionDisplay(revDisplayFlags) ||
|
|
588
|
+
getRevisionHighlightClass(revisionModel, rev.from, rev.to, "all");
|
|
483
589
|
decorations.push(
|
|
484
|
-
Decoration.inline(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
590
|
+
Decoration.inline(
|
|
591
|
+
pmFrom,
|
|
592
|
+
pmTo,
|
|
593
|
+
buildRevisionInlineAttrs(rev, insertionClass, revDisplayFlags),
|
|
594
|
+
),
|
|
488
595
|
);
|
|
489
596
|
decorations.push(
|
|
490
597
|
Decoration.widget(pmFrom, () => {
|
|
491
598
|
const el = document.createElement("span");
|
|
492
599
|
el.textContent = "[";
|
|
493
|
-
|
|
600
|
+
const attrs = buildRevisionBoundaryAttrs(rev, revDisplayFlags);
|
|
601
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
602
|
+
if (name === "class") {
|
|
603
|
+
el.className = value;
|
|
604
|
+
} else {
|
|
605
|
+
el.setAttribute(name, value);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
494
608
|
el.setAttribute("contenteditable", "false");
|
|
495
609
|
return el;
|
|
496
610
|
}, { side: -1, key: `${rev.revisionId}-open` }),
|
|
@@ -499,30 +613,41 @@ export function buildDecorations(
|
|
|
499
613
|
Decoration.widget(pmTo, () => {
|
|
500
614
|
const el = document.createElement("span");
|
|
501
615
|
el.textContent = "]";
|
|
502
|
-
|
|
616
|
+
const attrs = buildRevisionBoundaryAttrs(rev, revDisplayFlags);
|
|
617
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
618
|
+
if (name === "class") {
|
|
619
|
+
el.className = value;
|
|
620
|
+
} else {
|
|
621
|
+
el.setAttribute(name, value);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
503
624
|
el.setAttribute("contenteditable", "false");
|
|
504
625
|
return el;
|
|
505
626
|
}, { side: 1, key: `${rev.revisionId}-close` }),
|
|
506
627
|
);
|
|
507
628
|
revisionCount += 1;
|
|
508
629
|
} else if (rev.kind === "deletion") {
|
|
630
|
+
const deletionClass =
|
|
631
|
+
buildClassFromRevisionDisplay(revDisplayFlags) ||
|
|
632
|
+
getRevisionHighlightClass(revisionModel, rev.from, rev.to, "all");
|
|
509
633
|
decorations.push(
|
|
510
|
-
Decoration.inline(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
634
|
+
Decoration.inline(
|
|
635
|
+
pmFrom,
|
|
636
|
+
pmTo,
|
|
637
|
+
buildRevisionInlineAttrs(rev, deletionClass, revDisplayFlags),
|
|
638
|
+
),
|
|
514
639
|
);
|
|
515
640
|
revisionCount += 1;
|
|
516
641
|
} else if (rev.kind === "property-change" || rev.kind === "formatting") {
|
|
517
642
|
const propertyChangeClass =
|
|
518
643
|
buildClassFromRevisionDisplay(revDisplayFlags) ||
|
|
519
|
-
"underline decoration-accent/
|
|
644
|
+
"rounded-[2px] bg-accent-soft/70 px-[1px] underline decoration-accent/80 decoration-dotted decoration-2 underline-offset-2";
|
|
520
645
|
decorations.push(
|
|
521
|
-
Decoration.inline(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
646
|
+
Decoration.inline(
|
|
647
|
+
pmFrom,
|
|
648
|
+
pmTo,
|
|
649
|
+
buildRevisionInlineAttrs(rev, propertyChangeClass, revDisplayFlags),
|
|
650
|
+
),
|
|
526
651
|
);
|
|
527
652
|
revisionCount += 1;
|
|
528
653
|
}
|
|
@@ -547,10 +672,11 @@ export function buildDecorations(
|
|
|
547
672
|
if (!cls) continue;
|
|
548
673
|
|
|
549
674
|
decorations.push(
|
|
550
|
-
Decoration.inline(
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
675
|
+
Decoration.inline(
|
|
676
|
+
pmFrom,
|
|
677
|
+
pmTo,
|
|
678
|
+
buildRevisionInlineAttrs(rev, cls, displayFlags),
|
|
679
|
+
),
|
|
554
680
|
);
|
|
555
681
|
revisionCount += 1;
|
|
556
682
|
}
|
|
@@ -87,13 +87,19 @@ function walkBlocks(
|
|
|
87
87
|
break;
|
|
88
88
|
}
|
|
89
89
|
case "opaque_block": {
|
|
90
|
+
const placeholderSize =
|
|
91
|
+
block.state === "placeholder-culled" &&
|
|
92
|
+
typeof block.placeholderSize === "number" &&
|
|
93
|
+
Number.isFinite(block.placeholderSize)
|
|
94
|
+
? Math.max(1, block.placeholderSize)
|
|
95
|
+
: 1;
|
|
90
96
|
entries.push({
|
|
91
97
|
runtimeStart: block.from,
|
|
92
98
|
pmStart: nextPmCursor,
|
|
93
99
|
runtimeEnd: block.to,
|
|
94
|
-
pmEnd: nextPmCursor +
|
|
100
|
+
pmEnd: nextPmCursor + placeholderSize,
|
|
95
101
|
});
|
|
96
|
-
nextPmCursor +=
|
|
102
|
+
nextPmCursor += placeholderSize;
|
|
97
103
|
break;
|
|
98
104
|
}
|
|
99
105
|
case "sdt_block": {
|