@beyondwork/docx-react-component 1.0.19 → 1.0.21
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 +44 -25
- 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 +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -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 +237 -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 +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -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 +130 -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
|
@@ -16,6 +16,9 @@ import type {
|
|
|
16
16
|
SearchOptions,
|
|
17
17
|
SearchResultSnapshot,
|
|
18
18
|
SelectionSnapshot,
|
|
19
|
+
WorkflowBlockedCommandReason,
|
|
20
|
+
WorkflowCandidateRange,
|
|
21
|
+
WorkflowScope,
|
|
19
22
|
} from "../../api/public-types";
|
|
20
23
|
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
21
24
|
import { searchDocument } from "../../runtime/document-search.ts";
|
|
@@ -38,7 +41,12 @@ import {
|
|
|
38
41
|
} from "./pm-command-bridge";
|
|
39
42
|
import { buildDecorations } from "./pm-decorations";
|
|
40
43
|
import { createContextualInteractionPlugin } from "./pm-contextual-ui";
|
|
41
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
finishPerfProbe,
|
|
46
|
+
incrementInvalidationCounter,
|
|
47
|
+
recordPerfSample,
|
|
48
|
+
startPerfProbe,
|
|
49
|
+
} from "./perf-probe";
|
|
42
50
|
import type { PositionMap } from "./pm-position-map";
|
|
43
51
|
import {
|
|
44
52
|
clearSearch as clearSearchPlugin,
|
|
@@ -47,8 +55,13 @@ import {
|
|
|
47
55
|
performSearch,
|
|
48
56
|
searchPluginKey,
|
|
49
57
|
} from "./search-plugin";
|
|
58
|
+
import {
|
|
59
|
+
createSurfaceDecorationKey,
|
|
60
|
+
createSurfaceDocumentBuildKey,
|
|
61
|
+
} from "./surface-build-keys";
|
|
50
62
|
import { tableNodeViews } from "./tw-table-node-view";
|
|
51
63
|
import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
|
|
64
|
+
import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
|
|
52
65
|
|
|
53
66
|
/**
|
|
54
67
|
* Same props interface as the legacy TwEditorSurface — drop-in replacement.
|
|
@@ -77,6 +90,10 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
77
90
|
onCommentActivated?: (commentId: string) => void;
|
|
78
91
|
onRevisionActivated?: (revisionId: string) => void;
|
|
79
92
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
93
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
94
|
+
workflowScopes?: readonly WorkflowScope[];
|
|
95
|
+
workflowCandidates?: readonly WorkflowCandidateRange[];
|
|
96
|
+
workflowBlockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -97,6 +114,17 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
97
114
|
onBlur,
|
|
98
115
|
} = props;
|
|
99
116
|
const surface = snapshot.surface;
|
|
117
|
+
const mediaPreviewKey = useMemo(
|
|
118
|
+
() =>
|
|
119
|
+
Object.entries(props.mediaPreviews ?? {})
|
|
120
|
+
.sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
|
|
121
|
+
.map(
|
|
122
|
+
([mediaId, preview]) =>
|
|
123
|
+
`${mediaId}:${preview.widthEmu ?? ""}:${preview.heightEmu ?? ""}:${preview.src}`,
|
|
124
|
+
)
|
|
125
|
+
.join("|"),
|
|
126
|
+
[props.mediaPreviews],
|
|
127
|
+
);
|
|
100
128
|
|
|
101
129
|
const canEdit = Boolean(
|
|
102
130
|
surface && snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError,
|
|
@@ -109,7 +137,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
109
137
|
const activeSearchRef = useRef<{ query: string; options: SearchOptions } | null>(null);
|
|
110
138
|
const pendingTypingProbeRef = useRef<string | null>(null);
|
|
111
139
|
const pendingSelectionProbeRef = useRef<string | null>(null);
|
|
112
|
-
const
|
|
140
|
+
const documentBuildKeyRef = useRef<string | null>(null);
|
|
141
|
+
const decorationBuildKeyRef = useRef<string | null>(null);
|
|
113
142
|
const suppressSelectionEchoRef = useRef(false);
|
|
114
143
|
const selectionToolbarFrameRef = useRef<number | null>(null);
|
|
115
144
|
const lastSelectionToolbarMeasurementRef = useRef<{
|
|
@@ -158,6 +187,38 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
158
187
|
() => createRevisionDecorationModel(snapshot.trackedChanges, props.activeRevisionId),
|
|
159
188
|
[snapshot.trackedChanges, props.activeRevisionId],
|
|
160
189
|
);
|
|
190
|
+
const documentBuildKey = useMemo(
|
|
191
|
+
() =>
|
|
192
|
+
createSurfaceDocumentBuildKey({
|
|
193
|
+
surface,
|
|
194
|
+
activeStory: snapshot.activeStory,
|
|
195
|
+
mediaPreviewKey,
|
|
196
|
+
}),
|
|
197
|
+
[mediaPreviewKey, snapshot.activeStory, surface],
|
|
198
|
+
);
|
|
199
|
+
const decorationBuildKey = useMemo(
|
|
200
|
+
() =>
|
|
201
|
+
createSurfaceDecorationKey({
|
|
202
|
+
markupDisplay,
|
|
203
|
+
showTrackedChanges,
|
|
204
|
+
canEdit,
|
|
205
|
+
activeCommentId: snapshot.comments.activeCommentId,
|
|
206
|
+
activeRevisionId: props.activeRevisionId,
|
|
207
|
+
workflowScopeSignature: JSON.stringify(props.workflowScopes ?? []),
|
|
208
|
+
workflowCandidateSignature: JSON.stringify(props.workflowCandidates ?? []),
|
|
209
|
+
workflowBlockedSignature: JSON.stringify(props.workflowBlockedReasons ?? []),
|
|
210
|
+
}),
|
|
211
|
+
[
|
|
212
|
+
canEdit,
|
|
213
|
+
markupDisplay,
|
|
214
|
+
props.activeRevisionId,
|
|
215
|
+
props.workflowCandidates,
|
|
216
|
+
props.workflowBlockedReasons,
|
|
217
|
+
props.workflowScopes,
|
|
218
|
+
showTrackedChanges,
|
|
219
|
+
snapshot.comments.activeCommentId,
|
|
220
|
+
],
|
|
221
|
+
);
|
|
161
222
|
|
|
162
223
|
// Create PM plugins (stable across renders — callbacks accessed via ref)
|
|
163
224
|
const plugins = useMemo(() => {
|
|
@@ -174,6 +235,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
174
235
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
175
236
|
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
176
237
|
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
238
|
+
isSelectionSyncSuppressed: () =>
|
|
239
|
+
callbacksRef.current?.isSelectionSyncSuppressed?.() ?? false,
|
|
177
240
|
}),
|
|
178
241
|
createContextualInteractionPlugin({
|
|
179
242
|
onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
|
|
@@ -183,37 +246,48 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
183
246
|
];
|
|
184
247
|
}, [props.onCommentActivated, props.onRevisionActivated]);
|
|
185
248
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// it will be created when the runtime provides a real snapshot.
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
if (!mountRef.current || !surface) return;
|
|
191
|
-
|
|
192
|
-
const surfaceBuildKey = JSON.stringify({
|
|
193
|
-
revisionToken: snapshot.revisionToken,
|
|
194
|
-
activeStory: snapshot.activeStory,
|
|
195
|
-
markupDisplay,
|
|
196
|
-
canEdit,
|
|
197
|
-
showTrackedChanges,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
if (viewRef.current && surfaceBuildKeyRef.current === surfaceBuildKey) {
|
|
201
|
-
const positionMap = positionMapRef.current;
|
|
202
|
-
if (!positionMap) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
249
|
+
const applyDecorationProps = useCallback(
|
|
250
|
+
(view: EditorView, positionMap: PositionMap): void => {
|
|
205
251
|
const decorations = buildDecorations(
|
|
206
|
-
|
|
252
|
+
view.state.doc,
|
|
207
253
|
positionMap,
|
|
208
254
|
commentModel,
|
|
209
255
|
revisionModel,
|
|
210
256
|
markupDisplay,
|
|
211
257
|
showTrackedChanges,
|
|
258
|
+
props.workflowScopes,
|
|
259
|
+
snapshot.activeStory,
|
|
260
|
+
props.workflowCandidates,
|
|
261
|
+
props.workflowBlockedReasons,
|
|
212
262
|
);
|
|
213
|
-
|
|
263
|
+
view.setProps({
|
|
214
264
|
editable: () => canEdit,
|
|
215
265
|
decorations: () => decorations,
|
|
216
266
|
});
|
|
267
|
+
decorationBuildKeyRef.current = decorationBuildKey;
|
|
268
|
+
recordPerfSample("pm.decorations");
|
|
269
|
+
incrementInvalidationCounter("pm.laneB.decorationUpdates");
|
|
270
|
+
},
|
|
271
|
+
[
|
|
272
|
+
canEdit,
|
|
273
|
+
commentModel,
|
|
274
|
+
decorationBuildKey,
|
|
275
|
+
markupDisplay,
|
|
276
|
+
revisionModel,
|
|
277
|
+
showTrackedChanges,
|
|
278
|
+
props.workflowBlockedReasons,
|
|
279
|
+
props.workflowCandidates,
|
|
280
|
+
props.workflowScopes,
|
|
281
|
+
props.workflowCandidates,
|
|
282
|
+
props.workflowBlockedReasons,
|
|
283
|
+
],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Create or update the PM document only when the structural key changes.
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!mountRef.current || !surface) return;
|
|
289
|
+
|
|
290
|
+
if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
|
|
217
291
|
return;
|
|
218
292
|
}
|
|
219
293
|
|
|
@@ -221,9 +295,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
221
295
|
surface,
|
|
222
296
|
snapshot.selection,
|
|
223
297
|
plugins,
|
|
298
|
+
props.mediaPreviews,
|
|
224
299
|
);
|
|
225
300
|
positionMapRef.current = positionMap;
|
|
226
|
-
|
|
227
301
|
const decorations = buildDecorations(
|
|
228
302
|
state.doc,
|
|
229
303
|
positionMap,
|
|
@@ -231,7 +305,13 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
231
305
|
revisionModel,
|
|
232
306
|
markupDisplay,
|
|
233
307
|
showTrackedChanges,
|
|
308
|
+
props.workflowScopes,
|
|
309
|
+
snapshot.activeStory,
|
|
310
|
+
props.workflowCandidates,
|
|
311
|
+
props.workflowBlockedReasons,
|
|
234
312
|
);
|
|
313
|
+
recordPerfSample("pm.rebuild");
|
|
314
|
+
incrementInvalidationCounter("pm.laneA.rebuilds");
|
|
235
315
|
|
|
236
316
|
if (!viewRef.current) {
|
|
237
317
|
// First time surface is available — create the EditorView
|
|
@@ -246,19 +326,16 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
246
326
|
},
|
|
247
327
|
});
|
|
248
328
|
viewRef.current = view;
|
|
329
|
+
recordPerfSample("pm.mount");
|
|
249
330
|
} else {
|
|
250
|
-
// View exists — update state and decorations
|
|
251
|
-
viewRef.current.setProps({
|
|
252
|
-
editable: () => canEdit,
|
|
253
|
-
decorations: () => decorations,
|
|
254
|
-
});
|
|
255
331
|
suppressSelectionEchoRef.current = true;
|
|
256
332
|
viewRef.current.updateState(state);
|
|
257
333
|
queueMicrotask(() => {
|
|
258
334
|
suppressSelectionEchoRef.current = false;
|
|
259
335
|
});
|
|
260
336
|
}
|
|
261
|
-
|
|
337
|
+
documentBuildKeyRef.current = documentBuildKey;
|
|
338
|
+
applyDecorationProps(viewRef.current, positionMap);
|
|
262
339
|
|
|
263
340
|
if (activeSearchRef.current) {
|
|
264
341
|
applySearch(
|
|
@@ -271,16 +348,29 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
271
348
|
pendingTypingProbeRef.current = null;
|
|
272
349
|
}
|
|
273
350
|
}, [
|
|
274
|
-
|
|
275
|
-
|
|
351
|
+
applyDecorationProps,
|
|
352
|
+
documentBuildKey,
|
|
276
353
|
surface,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
canEdit,
|
|
281
|
-
showTrackedChanges,
|
|
354
|
+
snapshot.selection,
|
|
355
|
+
plugins,
|
|
356
|
+
props.mediaPreviews,
|
|
282
357
|
]);
|
|
283
358
|
|
|
359
|
+
// Update decorations and editability without rebuilding the PM document.
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
const view = viewRef.current;
|
|
362
|
+
const positionMap = positionMapRef.current;
|
|
363
|
+
if (!view || !surface || !positionMap) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (decorationBuildKeyRef.current === decorationBuildKey) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
applyDecorationProps(view, positionMap);
|
|
372
|
+
}, [applyDecorationProps, decorationBuildKey, surface]);
|
|
373
|
+
|
|
284
374
|
useEffect(() => {
|
|
285
375
|
const view = viewRef.current;
|
|
286
376
|
const positionMap = positionMapRef.current;
|
|
@@ -299,6 +389,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
299
389
|
|
|
300
390
|
suppressSelectionEchoRef.current = true;
|
|
301
391
|
view.dispatch(view.state.tr.setSelection(nextSelection));
|
|
392
|
+
recordPerfSample("selection.sync");
|
|
302
393
|
queueMicrotask(() => {
|
|
303
394
|
suppressSelectionEchoRef.current = false;
|
|
304
395
|
});
|
|
@@ -356,11 +447,41 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
356
447
|
|
|
357
448
|
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
358
449
|
const view = viewRef.current;
|
|
450
|
+
const hiddenDeletionRanges =
|
|
451
|
+
markupDisplay === "clean"
|
|
452
|
+
? snapshot.trackedChanges.revisions
|
|
453
|
+
.filter(
|
|
454
|
+
(
|
|
455
|
+
revision,
|
|
456
|
+
): revision is typeof revision & {
|
|
457
|
+
anchor: Extract<typeof revision.anchor, { kind: "range" }>;
|
|
458
|
+
} =>
|
|
459
|
+
revision.kind === "deletion" &&
|
|
460
|
+
revision.status === "active" &&
|
|
461
|
+
revision.anchor.kind === "range",
|
|
462
|
+
)
|
|
463
|
+
.map((revision) => ({
|
|
464
|
+
from: revision.anchor.from,
|
|
465
|
+
to: revision.anchor.to,
|
|
466
|
+
}))
|
|
467
|
+
: [];
|
|
359
468
|
if (view) {
|
|
360
|
-
const rawResults = performSearch(view.state, query, options)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
469
|
+
const rawResults = performSearch(view.state, query, options)
|
|
470
|
+
.filter((result) => {
|
|
471
|
+
if (hiddenDeletionRanges.length === 0) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
const positionMap = positionMapRef.current;
|
|
475
|
+
if (!positionMap) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
const runtimeFrom = positionMap.pmToRuntime(result.from);
|
|
479
|
+
const runtimeTo = positionMap.pmToRuntime(result.to);
|
|
480
|
+
return !hiddenDeletionRanges.some(
|
|
481
|
+
(range) => runtimeFrom < range.to && runtimeTo > range.from,
|
|
482
|
+
);
|
|
483
|
+
})
|
|
484
|
+
.slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
364
485
|
view.dispatch(
|
|
365
486
|
view.state.tr.setMeta(searchPluginKey, {
|
|
366
487
|
results: rawResults,
|
|
@@ -369,16 +490,37 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
369
490
|
);
|
|
370
491
|
}
|
|
371
492
|
|
|
372
|
-
return
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
493
|
+
return filterHiddenDeletionSearchResults(
|
|
494
|
+
searchDocument(
|
|
495
|
+
props.canonicalDocument,
|
|
496
|
+
snapshot.selection,
|
|
497
|
+
snapshot.activeStory,
|
|
498
|
+
props.documentNavigation,
|
|
499
|
+
query,
|
|
500
|
+
options,
|
|
501
|
+
),
|
|
502
|
+
hiddenDeletionRanges,
|
|
379
503
|
);
|
|
380
504
|
}
|
|
381
505
|
|
|
506
|
+
function filterHiddenDeletionSearchResults(
|
|
507
|
+
results: SearchResultSnapshot[],
|
|
508
|
+
hiddenRanges: Array<{ from: number; to: number }>,
|
|
509
|
+
): SearchResultSnapshot[] {
|
|
510
|
+
if (hiddenRanges.length === 0) {
|
|
511
|
+
return results;
|
|
512
|
+
}
|
|
513
|
+
return results.filter((result) => {
|
|
514
|
+
const anchor = result.anchor;
|
|
515
|
+
if (anchor.kind !== "range") {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
return !hiddenRanges.some(
|
|
519
|
+
(range) => anchor.from < range.to && anchor.to > range.from,
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
382
524
|
function clearLiveSearch(): void {
|
|
383
525
|
const view = viewRef.current;
|
|
384
526
|
if (!view) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentNavigationSnapshot,
|
|
3
|
+
PageLayoutSnapshot,
|
|
4
|
+
SurfaceBlockSnapshot,
|
|
5
|
+
} from "../api/public-types.ts";
|
|
6
|
+
|
|
7
|
+
export interface LineMarker {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
topPx: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function computeLineMarkersIfEnabled(input: {
|
|
14
|
+
pageLayout: PageLayoutSnapshot | undefined;
|
|
15
|
+
surfaceBlocks: readonly SurfaceBlockSnapshot[];
|
|
16
|
+
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>;
|
|
17
|
+
buildLineNumberMarkers: (
|
|
18
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
19
|
+
pages: ReadonlyArray<DocumentNavigationSnapshot["pages"][number]>,
|
|
20
|
+
) => LineMarker[];
|
|
21
|
+
}): LineMarker[] {
|
|
22
|
+
if (!input.pageLayout?.lineNumbering) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return input.buildLineNumberMarkers(input.surfaceBlocks, input.pages);
|
|
27
|
+
}
|
|
@@ -50,7 +50,7 @@ export function TwCommentSidebar(props: TwCommentSidebarProps) {
|
|
|
50
50
|
))}
|
|
51
51
|
</div>
|
|
52
52
|
) : (
|
|
53
|
-
<div className="rounded-
|
|
53
|
+
<div className="rounded-lg border border-dashed border-border bg-surface/60 px-2.5 py-3 text-[10px] leading-4 text-tertiary">
|
|
54
54
|
No comment threads yet. Select text and add one from the toolbar.
|
|
55
55
|
</div>
|
|
56
56
|
)}
|
|
@@ -93,7 +93,7 @@ function CommentThreadCard(props: {
|
|
|
93
93
|
role="button"
|
|
94
94
|
tabIndex={0}
|
|
95
95
|
className={[
|
|
96
|
-
"cursor-pointer rounded-
|
|
96
|
+
"cursor-pointer rounded-lg border px-2 py-1.5 transition-colors",
|
|
97
97
|
focusRingClass,
|
|
98
98
|
isActive
|
|
99
99
|
? "border-accent/25 bg-accent-soft/35"
|
|
@@ -125,7 +125,7 @@ function CommentThreadCard(props: {
|
|
|
125
125
|
|
|
126
126
|
{/* Excerpt — anchored text from document */}
|
|
127
127
|
{showExcerpt ? (
|
|
128
|
-
<p className="mb-1 rounded-md border-l-2 border-comment/25 bg-comment-soft/30 px-2 py-1 text-[9px] leading-4 text-comment/80 italic line-clamp-2">
|
|
128
|
+
<p className="mb-1 rounded-md border-l-2 border-comment/25 bg-comment-soft/30 px-2 py-1 text-[9px] leading-4 text-comment/80 italic whitespace-pre-wrap break-words line-clamp-2">
|
|
129
129
|
{thread.excerpt}
|
|
130
130
|
</p>
|
|
131
131
|
) : null}
|
|
@@ -140,7 +140,7 @@ function CommentThreadCard(props: {
|
|
|
140
140
|
/>
|
|
141
141
|
) : leadEntry?.body ? (
|
|
142
142
|
<p
|
|
143
|
-
className="text-[10px] leading-[1.
|
|
143
|
+
className="text-[10px] leading-[1.1rem] text-secondary whitespace-pre-wrap break-words line-clamp-4"
|
|
144
144
|
data-comment-thread-body="true"
|
|
145
145
|
>
|
|
146
146
|
{leadEntry.body}
|
|
@@ -159,13 +159,13 @@ function CommentThreadCard(props: {
|
|
|
159
159
|
|
|
160
160
|
{/* Reply entries (compact) */}
|
|
161
161
|
{thread.entries.slice(1).map((entry) => (
|
|
162
|
-
<div key={entry.entryId} className="mt-1 border-
|
|
162
|
+
<div key={entry.entryId} className="mt-1.5 ml-4 border-l border-border/50 pl-2.5">
|
|
163
163
|
<div className="mb-0.5 flex items-center gap-1">
|
|
164
164
|
<span className="text-[9px] font-medium text-secondary">{entry.authorId}</span>
|
|
165
165
|
<span className="text-[9px] text-tertiary">{formatCommentDate(entry.createdAt)}</span>
|
|
166
166
|
</div>
|
|
167
167
|
<p
|
|
168
|
-
className="text-[10px] leading-4 text-secondary line-clamp-
|
|
168
|
+
className="text-[10px] leading-4 text-secondary whitespace-pre-wrap break-words line-clamp-3"
|
|
169
169
|
data-comment-reply-body="true"
|
|
170
170
|
>
|
|
171
171
|
{entry.body}
|
|
@@ -180,7 +180,7 @@ function CommentThreadCard(props: {
|
|
|
180
180
|
) : null}
|
|
181
181
|
|
|
182
182
|
{/* Inline actions — compact, horizontal */}
|
|
183
|
-
<div className="mt-1
|
|
183
|
+
<div className="mt-1 flex items-center gap-0.5">
|
|
184
184
|
{thread.status === "open" && (
|
|
185
185
|
<>
|
|
186
186
|
<button
|
|
@@ -5,15 +5,17 @@ import type {
|
|
|
5
5
|
CompatibilityFeatureEntry,
|
|
6
6
|
CompatibilityPanelSnapshot,
|
|
7
7
|
EditorWarning,
|
|
8
|
+
WorkflowBlockedCommandReason,
|
|
8
9
|
} from "../../api/public-types";
|
|
9
10
|
|
|
10
11
|
export interface TwHealthPanelProps {
|
|
11
12
|
compatibility: CompatibilityPanelSnapshot;
|
|
12
13
|
warnings: EditorWarning[];
|
|
14
|
+
blockedReasons?: WorkflowBlockedCommandReason[];
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function TwHealthPanel(props: TwHealthPanelProps) {
|
|
16
|
-
const { compatibility, warnings } = props;
|
|
18
|
+
const { compatibility, warnings, blockedReasons = [] } = props;
|
|
17
19
|
const supportedCount = compatibility.featureEntries.filter(
|
|
18
20
|
(e) => e.featureClass === "supported-roundtrip",
|
|
19
21
|
).length;
|
|
@@ -80,7 +82,34 @@ export function TwHealthPanel(props: TwHealthPanelProps) {
|
|
|
80
82
|
</div>
|
|
81
83
|
))}
|
|
82
84
|
|
|
83
|
-
{
|
|
85
|
+
{blockedReasons.length > 0 ? (
|
|
86
|
+
<>
|
|
87
|
+
<div className="border-t border-border mt-2 pt-2">
|
|
88
|
+
<p className="text-xs font-medium text-tertiary mb-1">Workflow blocked reasons</p>
|
|
89
|
+
</div>
|
|
90
|
+
{blockedReasons.map((reason, index) => (
|
|
91
|
+
<div key={`blocked-${index}`} className="flex rounded-lg transition-colors hover:bg-surface">
|
|
92
|
+
<div className="w-0.5 shrink-0 rounded-l-lg bg-amber-400" />
|
|
93
|
+
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
94
|
+
<ShieldAlert className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />
|
|
95
|
+
<div className="flex-1 min-w-0">
|
|
96
|
+
<div className="flex items-start justify-between gap-2">
|
|
97
|
+
<span className="text-sm font-medium text-primary">{reason.message}</span>
|
|
98
|
+
<span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium text-amber-700 bg-amber-100">
|
|
99
|
+
{reason.code.replace(/_/g, " ")}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
{reason.scopeId ? (
|
|
103
|
+
<p className="text-xs text-tertiary mt-0.5">scope: {reason.scopeId}</p>
|
|
104
|
+
) : null}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</>
|
|
110
|
+
) : null}
|
|
111
|
+
|
|
112
|
+
{compatibility.featureEntries.length === 0 && warnings.length === 0 && blockedReasons.length === 0 ? (
|
|
84
113
|
<p className="text-xs text-tertiary py-4">
|
|
85
114
|
No compatibility entries or warnings to display.
|
|
86
115
|
</p>
|
|
@@ -52,7 +52,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
52
52
|
return (
|
|
53
53
|
<aside
|
|
54
54
|
aria-label="Review rail"
|
|
55
|
-
className="flex w-[
|
|
55
|
+
className="flex w-[304px] shrink-0 flex-col border-l border-border bg-canvas"
|
|
56
56
|
>
|
|
57
57
|
<Tabs.Root
|
|
58
58
|
value={props.activeTab}
|
|
@@ -79,7 +79,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
79
79
|
|
|
80
80
|
<ScrollArea.Root className="flex-1 min-h-0">
|
|
81
81
|
<ScrollArea.Viewport className="h-full w-full">
|
|
82
|
-
<Tabs.Content value="comments" className="p-
|
|
82
|
+
<Tabs.Content value="comments" className="p-2.5 outline-none">
|
|
83
83
|
<TwCommentSidebar
|
|
84
84
|
currentUserId={props.currentUserId}
|
|
85
85
|
comments={props.comments}
|
|
@@ -92,7 +92,7 @@ export function TwReviewRail(props: TwReviewRailProps) {
|
|
|
92
92
|
/>
|
|
93
93
|
</Tabs.Content>
|
|
94
94
|
|
|
95
|
-
<Tabs.Content value="changes" className="p-
|
|
95
|
+
<Tabs.Content value="changes" className="p-2.5 outline-none">
|
|
96
96
|
<TwRevisionSidebar
|
|
97
97
|
trackedChanges={props.trackedChanges}
|
|
98
98
|
markupDisplay={props.markupDisplay}
|
|
@@ -28,16 +28,16 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<div className="outline-none">
|
|
31
|
-
<p className="text-
|
|
31
|
+
<p className="mb-2 text-[10px] text-tertiary">
|
|
32
32
|
{trackedChanges.pendingChangeIds.length} active · {trackedChanges.acceptedChangeIds.length} accepted · {trackedChanges.preserveOnlyChangeIds.length} preserve-only
|
|
33
33
|
</p>
|
|
34
34
|
|
|
35
35
|
{/* Bulk actions */}
|
|
36
|
-
<div className="flex gap-1
|
|
36
|
+
<div className="mb-2 flex gap-1">
|
|
37
37
|
<button
|
|
38
38
|
type="button"
|
|
39
39
|
disabled={actionablePendingCount === 0}
|
|
40
|
-
className="inline-flex items-center gap-1 rounded-md px-2
|
|
40
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-semibold text-accent hover:bg-accent-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
41
41
|
onClick={props.onAcceptAllChanges}
|
|
42
42
|
>
|
|
43
43
|
Accept all ({actionablePendingCount})
|
|
@@ -45,7 +45,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
45
45
|
<button
|
|
46
46
|
type="button"
|
|
47
47
|
disabled={actionablePendingCount === 0}
|
|
48
|
-
className="inline-flex items-center gap-1 rounded-md px-2
|
|
48
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] text-secondary hover:bg-surface transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
49
49
|
onClick={props.onRejectAllChanges}
|
|
50
50
|
>
|
|
51
51
|
Reject all
|
|
@@ -76,14 +76,14 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
76
76
|
: rev.kind === "deletion" ? "bg-danger"
|
|
77
77
|
: "bg-tertiary"
|
|
78
78
|
}`} />
|
|
79
|
-
<div className="p-2
|
|
80
|
-
<div className="flex items-start justify-between gap-2
|
|
81
|
-
<span className="text-
|
|
79
|
+
<div className="p-2 flex-1 min-w-0">
|
|
80
|
+
<div className="mb-0.5 flex items-start justify-between gap-2">
|
|
81
|
+
<span className="text-[11px] font-medium text-primary">{rev.anchorLabel}</span>
|
|
82
82
|
<RevisionBadge status={rev.status} actionability={rev.actionability} />
|
|
83
83
|
</div>
|
|
84
|
-
<p className="text-
|
|
84
|
+
<p className="mb-1 text-[10px] text-tertiary">{rev.authorId} · {rev.createdAt}</p>
|
|
85
85
|
{rev.excerpt ? (
|
|
86
|
-
<p className={`text-
|
|
86
|
+
<p className={`text-[11px] ${
|
|
87
87
|
rev.kind === "insertion" ? "text-insert"
|
|
88
88
|
: rev.kind === "deletion" ? "text-danger line-through"
|
|
89
89
|
: "text-secondary"
|
|
@@ -91,18 +91,18 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
91
91
|
{rev.excerpt}
|
|
92
92
|
</p>
|
|
93
93
|
) : (
|
|
94
|
-
<p className="text-
|
|
94
|
+
<p className="text-[11px] text-secondary">{rev.label}</p>
|
|
95
95
|
)}
|
|
96
96
|
{rev.detail ? (
|
|
97
|
-
<p className="text-
|
|
97
|
+
<p className="mt-1 text-[10px] text-secondary">{rev.detail}</p>
|
|
98
98
|
) : null}
|
|
99
|
-
<div className="
|
|
99
|
+
<div className="mt-1.5 flex gap-1">
|
|
100
100
|
{rev.actionability === "actionable" ? (
|
|
101
101
|
<>
|
|
102
102
|
<button
|
|
103
103
|
type="button"
|
|
104
104
|
disabled={!rev.canAccept || rev.status === "accepted"}
|
|
105
|
-
className="inline-flex items-center gap-1 rounded-md px-
|
|
105
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-insert hover:bg-insert-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
106
106
|
onClick={(e) => {
|
|
107
107
|
e.stopPropagation();
|
|
108
108
|
props.onAcceptRevision?.(rev.revisionId);
|
|
@@ -113,7 +113,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
113
113
|
<button
|
|
114
114
|
type="button"
|
|
115
115
|
disabled={!rev.canReject || rev.status === "rejected"}
|
|
116
|
-
className="inline-flex items-center gap-1 rounded-md px-
|
|
116
|
+
className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] text-danger hover:bg-delete-soft transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
117
117
|
onClick={(e) => {
|
|
118
118
|
e.stopPropagation();
|
|
119
119
|
props.onRejectRevision?.(rev.revisionId);
|
|
@@ -123,7 +123,7 @@ export function TwRevisionSidebar(props: TwRevisionSidebarProps) {
|
|
|
123
123
|
</button>
|
|
124
124
|
</>
|
|
125
125
|
) : (
|
|
126
|
-
<span className="
|
|
126
|
+
<span className="px-1.5 py-0.5 text-[10px] text-tertiary">Preserve-only</span>
|
|
127
127
|
)}
|
|
128
128
|
</div>
|
|
129
129
|
</div>
|