@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
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SearchOptions,
|
|
17
17
|
SearchResultSnapshot,
|
|
18
18
|
SelectionSnapshot,
|
|
19
|
+
WorkflowScope,
|
|
19
20
|
} from "../../api/public-types";
|
|
20
21
|
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
21
22
|
import { searchDocument } from "../../runtime/document-search.ts";
|
|
@@ -38,7 +39,12 @@ import {
|
|
|
38
39
|
} from "./pm-command-bridge";
|
|
39
40
|
import { buildDecorations } from "./pm-decorations";
|
|
40
41
|
import { createContextualInteractionPlugin } from "./pm-contextual-ui";
|
|
41
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
finishPerfProbe,
|
|
44
|
+
incrementInvalidationCounter,
|
|
45
|
+
recordPerfSample,
|
|
46
|
+
startPerfProbe,
|
|
47
|
+
} from "./perf-probe";
|
|
42
48
|
import type { PositionMap } from "./pm-position-map";
|
|
43
49
|
import {
|
|
44
50
|
clearSearch as clearSearchPlugin,
|
|
@@ -47,8 +53,13 @@ import {
|
|
|
47
53
|
performSearch,
|
|
48
54
|
searchPluginKey,
|
|
49
55
|
} from "./search-plugin";
|
|
56
|
+
import {
|
|
57
|
+
createSurfaceDecorationKey,
|
|
58
|
+
createSurfaceDocumentBuildKey,
|
|
59
|
+
} from "./surface-build-keys";
|
|
50
60
|
import { tableNodeViews } from "./tw-table-node-view";
|
|
51
61
|
import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
|
|
62
|
+
import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
|
|
52
63
|
|
|
53
64
|
/**
|
|
54
65
|
* Same props interface as the legacy TwEditorSurface — drop-in replacement.
|
|
@@ -77,6 +88,8 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
77
88
|
onCommentActivated?: (commentId: string) => void;
|
|
78
89
|
onRevisionActivated?: (revisionId: string) => void;
|
|
79
90
|
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
91
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
92
|
+
workflowScopes?: readonly WorkflowScope[];
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -97,6 +110,17 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
97
110
|
onBlur,
|
|
98
111
|
} = props;
|
|
99
112
|
const surface = snapshot.surface;
|
|
113
|
+
const mediaPreviewKey = useMemo(
|
|
114
|
+
() =>
|
|
115
|
+
Object.entries(props.mediaPreviews ?? {})
|
|
116
|
+
.sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
|
|
117
|
+
.map(
|
|
118
|
+
([mediaId, preview]) =>
|
|
119
|
+
`${mediaId}:${preview.widthEmu ?? ""}:${preview.heightEmu ?? ""}:${preview.src}`,
|
|
120
|
+
)
|
|
121
|
+
.join("|"),
|
|
122
|
+
[props.mediaPreviews],
|
|
123
|
+
);
|
|
100
124
|
|
|
101
125
|
const canEdit = Boolean(
|
|
102
126
|
surface && snapshot.isReady && !snapshot.readOnly && !snapshot.fatalError,
|
|
@@ -109,7 +133,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
109
133
|
const activeSearchRef = useRef<{ query: string; options: SearchOptions } | null>(null);
|
|
110
134
|
const pendingTypingProbeRef = useRef<string | null>(null);
|
|
111
135
|
const pendingSelectionProbeRef = useRef<string | null>(null);
|
|
112
|
-
const
|
|
136
|
+
const documentBuildKeyRef = useRef<string | null>(null);
|
|
137
|
+
const decorationBuildKeyRef = useRef<string | null>(null);
|
|
113
138
|
const suppressSelectionEchoRef = useRef(false);
|
|
114
139
|
const selectionToolbarFrameRef = useRef<number | null>(null);
|
|
115
140
|
const lastSelectionToolbarMeasurementRef = useRef<{
|
|
@@ -158,6 +183,34 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
158
183
|
() => createRevisionDecorationModel(snapshot.trackedChanges, props.activeRevisionId),
|
|
159
184
|
[snapshot.trackedChanges, props.activeRevisionId],
|
|
160
185
|
);
|
|
186
|
+
const documentBuildKey = useMemo(
|
|
187
|
+
() =>
|
|
188
|
+
createSurfaceDocumentBuildKey({
|
|
189
|
+
surface,
|
|
190
|
+
activeStory: snapshot.activeStory,
|
|
191
|
+
mediaPreviewKey,
|
|
192
|
+
}),
|
|
193
|
+
[mediaPreviewKey, snapshot.activeStory, surface],
|
|
194
|
+
);
|
|
195
|
+
const decorationBuildKey = useMemo(
|
|
196
|
+
() =>
|
|
197
|
+
createSurfaceDecorationKey({
|
|
198
|
+
markupDisplay,
|
|
199
|
+
showTrackedChanges,
|
|
200
|
+
canEdit,
|
|
201
|
+
activeCommentId: snapshot.comments.activeCommentId,
|
|
202
|
+
activeRevisionId: props.activeRevisionId,
|
|
203
|
+
workflowScopeSignature: JSON.stringify(props.workflowScopes ?? []),
|
|
204
|
+
}),
|
|
205
|
+
[
|
|
206
|
+
canEdit,
|
|
207
|
+
markupDisplay,
|
|
208
|
+
props.activeRevisionId,
|
|
209
|
+
props.workflowScopes,
|
|
210
|
+
showTrackedChanges,
|
|
211
|
+
snapshot.comments.activeCommentId,
|
|
212
|
+
],
|
|
213
|
+
);
|
|
161
214
|
|
|
162
215
|
// Create PM plugins (stable across renders — callbacks accessed via ref)
|
|
163
216
|
const plugins = useMemo(() => {
|
|
@@ -174,6 +227,8 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
174
227
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
175
228
|
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
176
229
|
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
230
|
+
isSelectionSyncSuppressed: () =>
|
|
231
|
+
callbacksRef.current?.isSelectionSyncSuppressed?.() ?? false,
|
|
177
232
|
}),
|
|
178
233
|
createContextualInteractionPlugin({
|
|
179
234
|
onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
|
|
@@ -183,37 +238,42 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
183
238
|
];
|
|
184
239
|
}, [props.onCommentActivated, props.onRevisionActivated]);
|
|
185
240
|
|
|
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
|
-
}
|
|
241
|
+
const applyDecorationProps = useCallback(
|
|
242
|
+
(view: EditorView, positionMap: PositionMap): void => {
|
|
205
243
|
const decorations = buildDecorations(
|
|
206
|
-
|
|
244
|
+
view.state.doc,
|
|
207
245
|
positionMap,
|
|
208
246
|
commentModel,
|
|
209
247
|
revisionModel,
|
|
210
248
|
markupDisplay,
|
|
211
249
|
showTrackedChanges,
|
|
250
|
+
props.workflowScopes,
|
|
251
|
+
snapshot.activeStory,
|
|
212
252
|
);
|
|
213
|
-
|
|
253
|
+
view.setProps({
|
|
214
254
|
editable: () => canEdit,
|
|
215
255
|
decorations: () => decorations,
|
|
216
256
|
});
|
|
257
|
+
decorationBuildKeyRef.current = decorationBuildKey;
|
|
258
|
+
recordPerfSample("pm.decorations");
|
|
259
|
+
incrementInvalidationCounter("pm.laneB.decorationUpdates");
|
|
260
|
+
},
|
|
261
|
+
[
|
|
262
|
+
canEdit,
|
|
263
|
+
commentModel,
|
|
264
|
+
decorationBuildKey,
|
|
265
|
+
markupDisplay,
|
|
266
|
+
revisionModel,
|
|
267
|
+
showTrackedChanges,
|
|
268
|
+
props.workflowScopes,
|
|
269
|
+
],
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Create or update the PM document only when the structural key changes.
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (!mountRef.current || !surface) return;
|
|
275
|
+
|
|
276
|
+
if (viewRef.current && documentBuildKeyRef.current === documentBuildKey) {
|
|
217
277
|
return;
|
|
218
278
|
}
|
|
219
279
|
|
|
@@ -221,9 +281,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
221
281
|
surface,
|
|
222
282
|
snapshot.selection,
|
|
223
283
|
plugins,
|
|
284
|
+
props.mediaPreviews,
|
|
224
285
|
);
|
|
225
286
|
positionMapRef.current = positionMap;
|
|
226
|
-
|
|
227
287
|
const decorations = buildDecorations(
|
|
228
288
|
state.doc,
|
|
229
289
|
positionMap,
|
|
@@ -231,7 +291,11 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
231
291
|
revisionModel,
|
|
232
292
|
markupDisplay,
|
|
233
293
|
showTrackedChanges,
|
|
294
|
+
props.workflowScopes,
|
|
295
|
+
snapshot.activeStory,
|
|
234
296
|
);
|
|
297
|
+
recordPerfSample("pm.rebuild");
|
|
298
|
+
incrementInvalidationCounter("pm.laneA.rebuilds");
|
|
235
299
|
|
|
236
300
|
if (!viewRef.current) {
|
|
237
301
|
// First time surface is available — create the EditorView
|
|
@@ -246,19 +310,16 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
246
310
|
},
|
|
247
311
|
});
|
|
248
312
|
viewRef.current = view;
|
|
313
|
+
recordPerfSample("pm.mount");
|
|
249
314
|
} else {
|
|
250
|
-
// View exists — update state and decorations
|
|
251
|
-
viewRef.current.setProps({
|
|
252
|
-
editable: () => canEdit,
|
|
253
|
-
decorations: () => decorations,
|
|
254
|
-
});
|
|
255
315
|
suppressSelectionEchoRef.current = true;
|
|
256
316
|
viewRef.current.updateState(state);
|
|
257
317
|
queueMicrotask(() => {
|
|
258
318
|
suppressSelectionEchoRef.current = false;
|
|
259
319
|
});
|
|
260
320
|
}
|
|
261
|
-
|
|
321
|
+
documentBuildKeyRef.current = documentBuildKey;
|
|
322
|
+
applyDecorationProps(viewRef.current, positionMap);
|
|
262
323
|
|
|
263
324
|
if (activeSearchRef.current) {
|
|
264
325
|
applySearch(
|
|
@@ -271,16 +332,29 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
271
332
|
pendingTypingProbeRef.current = null;
|
|
272
333
|
}
|
|
273
334
|
}, [
|
|
274
|
-
|
|
275
|
-
|
|
335
|
+
applyDecorationProps,
|
|
336
|
+
documentBuildKey,
|
|
276
337
|
surface,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
canEdit,
|
|
281
|
-
showTrackedChanges,
|
|
338
|
+
snapshot.selection,
|
|
339
|
+
plugins,
|
|
340
|
+
props.mediaPreviews,
|
|
282
341
|
]);
|
|
283
342
|
|
|
343
|
+
// Update decorations and editability without rebuilding the PM document.
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
const view = viewRef.current;
|
|
346
|
+
const positionMap = positionMapRef.current;
|
|
347
|
+
if (!view || !surface || !positionMap) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (decorationBuildKeyRef.current === decorationBuildKey) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
applyDecorationProps(view, positionMap);
|
|
356
|
+
}, [applyDecorationProps, decorationBuildKey, surface]);
|
|
357
|
+
|
|
284
358
|
useEffect(() => {
|
|
285
359
|
const view = viewRef.current;
|
|
286
360
|
const positionMap = positionMapRef.current;
|
|
@@ -299,6 +373,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
299
373
|
|
|
300
374
|
suppressSelectionEchoRef.current = true;
|
|
301
375
|
view.dispatch(view.state.tr.setSelection(nextSelection));
|
|
376
|
+
recordPerfSample("selection.sync");
|
|
302
377
|
queueMicrotask(() => {
|
|
303
378
|
suppressSelectionEchoRef.current = false;
|
|
304
379
|
});
|
|
@@ -356,11 +431,41 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
356
431
|
|
|
357
432
|
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
358
433
|
const view = viewRef.current;
|
|
434
|
+
const hiddenDeletionRanges =
|
|
435
|
+
markupDisplay === "clean"
|
|
436
|
+
? snapshot.trackedChanges.revisions
|
|
437
|
+
.filter(
|
|
438
|
+
(
|
|
439
|
+
revision,
|
|
440
|
+
): revision is typeof revision & {
|
|
441
|
+
anchor: Extract<typeof revision.anchor, { kind: "range" }>;
|
|
442
|
+
} =>
|
|
443
|
+
revision.kind === "deletion" &&
|
|
444
|
+
revision.status === "active" &&
|
|
445
|
+
revision.anchor.kind === "range",
|
|
446
|
+
)
|
|
447
|
+
.map((revision) => ({
|
|
448
|
+
from: revision.anchor.from,
|
|
449
|
+
to: revision.anchor.to,
|
|
450
|
+
}))
|
|
451
|
+
: [];
|
|
359
452
|
if (view) {
|
|
360
|
-
const rawResults = performSearch(view.state, query, options)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
453
|
+
const rawResults = performSearch(view.state, query, options)
|
|
454
|
+
.filter((result) => {
|
|
455
|
+
if (hiddenDeletionRanges.length === 0) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
const positionMap = positionMapRef.current;
|
|
459
|
+
if (!positionMap) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
const runtimeFrom = positionMap.pmToRuntime(result.from);
|
|
463
|
+
const runtimeTo = positionMap.pmToRuntime(result.to);
|
|
464
|
+
return !hiddenDeletionRanges.some(
|
|
465
|
+
(range) => runtimeFrom < range.to && runtimeTo > range.from,
|
|
466
|
+
);
|
|
467
|
+
})
|
|
468
|
+
.slice(0, options.limit ?? Number.POSITIVE_INFINITY);
|
|
364
469
|
view.dispatch(
|
|
365
470
|
view.state.tr.setMeta(searchPluginKey, {
|
|
366
471
|
results: rawResults,
|
|
@@ -369,16 +474,37 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
369
474
|
);
|
|
370
475
|
}
|
|
371
476
|
|
|
372
|
-
return
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
477
|
+
return filterHiddenDeletionSearchResults(
|
|
478
|
+
searchDocument(
|
|
479
|
+
props.canonicalDocument,
|
|
480
|
+
snapshot.selection,
|
|
481
|
+
snapshot.activeStory,
|
|
482
|
+
props.documentNavigation,
|
|
483
|
+
query,
|
|
484
|
+
options,
|
|
485
|
+
),
|
|
486
|
+
hiddenDeletionRanges,
|
|
379
487
|
);
|
|
380
488
|
}
|
|
381
489
|
|
|
490
|
+
function filterHiddenDeletionSearchResults(
|
|
491
|
+
results: SearchResultSnapshot[],
|
|
492
|
+
hiddenRanges: Array<{ from: number; to: number }>,
|
|
493
|
+
): SearchResultSnapshot[] {
|
|
494
|
+
if (hiddenRanges.length === 0) {
|
|
495
|
+
return results;
|
|
496
|
+
}
|
|
497
|
+
return results.filter((result) => {
|
|
498
|
+
const anchor = result.anchor;
|
|
499
|
+
if (anchor.kind !== "range") {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
return !hiddenRanges.some(
|
|
503
|
+
(range) => anchor.from < range.to && anchor.to > range.from,
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
382
508
|
function clearLiveSearch(): void {
|
|
383
509
|
const view = viewRef.current;
|
|
384
510
|
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>
|
|
@@ -266,6 +266,10 @@
|
|
|
266
266
|
margin: 0 0 0.5em 0;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
.prosemirror-surface .ProseMirror p[data-numbered="true"][data-list-continuation="true"] {
|
|
270
|
+
margin-bottom: 0.25em;
|
|
271
|
+
}
|
|
272
|
+
|
|
269
273
|
.prosemirror-surface .ProseMirror [data-node-type="opaque_block"] {
|
|
270
274
|
user-select: none;
|
|
271
275
|
cursor: default;
|