@beyondwork/docx-react-component 1.0.38 → 1.0.40
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 +41 -31
- package/src/api/public-types.ts +305 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/index.ts +9 -0
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +141 -18
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +3 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +81 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +302 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +22 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +353 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +82 -18
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +170 -63
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +6 -5
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +207 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -15,14 +15,21 @@
|
|
|
15
15
|
import type {
|
|
16
16
|
EditorAnchorProjection,
|
|
17
17
|
EditorStoryTarget,
|
|
18
|
+
IssueMetadataValue,
|
|
19
|
+
ScopeCardModel,
|
|
18
20
|
WorkflowBlockedCommandReason,
|
|
19
21
|
WorkflowCandidateRange,
|
|
20
22
|
WorkflowLockedZone,
|
|
23
|
+
WorkflowMetadataMarkup,
|
|
21
24
|
WorkflowScope,
|
|
22
25
|
} from "../api/public-types";
|
|
26
|
+
import { ISSUE_METADATA_ID } from "../api/public-types";
|
|
23
27
|
import { MAIN_STORY_TARGET, storyTargetsEqual } from "../core/selection/mapping.ts";
|
|
24
28
|
import type { RuntimePageGraph } from "./layout/page-graph.ts";
|
|
25
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
RenderAnchorIndex,
|
|
31
|
+
RenderFrameRect,
|
|
32
|
+
} from "./render/render-frame-types.ts";
|
|
26
33
|
|
|
27
34
|
// ---------------------------------------------------------------------------
|
|
28
35
|
// Public shape
|
|
@@ -278,3 +285,144 @@ function resolveScopePosture(scope: WorkflowScope): ScopeRailPosture {
|
|
|
278
285
|
return "view";
|
|
279
286
|
}
|
|
280
287
|
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Scope card projection (P1b — consumed by ref.layout.getAllScopeCardModels)
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
export interface AttachScopeCardModelInput {
|
|
294
|
+
/**
|
|
295
|
+
* Segments produced by `collectScopeRailSegments`. The card model
|
|
296
|
+
* keys off segments so per-scope card state stays aligned with the
|
|
297
|
+
* rail stripe rendering.
|
|
298
|
+
*/
|
|
299
|
+
segments: readonly ScopeRailSegment[];
|
|
300
|
+
/**
|
|
301
|
+
* Original workflow scopes — used to resolve `workItemId` for each
|
|
302
|
+
* segment (the segment shape predates workItemId passthrough).
|
|
303
|
+
* Optional; when absent, cards drop `workItemId` and issue matching
|
|
304
|
+
* falls back to scope-id only.
|
|
305
|
+
*/
|
|
306
|
+
scopes?: readonly WorkflowScope[];
|
|
307
|
+
/**
|
|
308
|
+
* Workflow metadata markup from `WorkflowMarkupSnapshot.metadata`.
|
|
309
|
+
* Entries with `metadataId === ISSUE_METADATA_ID` that match a
|
|
310
|
+
* segment's `workItemId` or `scopeId` attach as the card's issue.
|
|
311
|
+
*/
|
|
312
|
+
metadata?: readonly WorkflowMetadataMarkup[];
|
|
313
|
+
/**
|
|
314
|
+
* Optional anchor index used to resolve each card's
|
|
315
|
+
* `primaryAnchorRect`. When omitted, cards are produced without
|
|
316
|
+
* rects — chrome consumers fall back to on-render positioning.
|
|
317
|
+
*/
|
|
318
|
+
anchorIndex?: RenderAnchorIndex | null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Project rail segments into per-scope `ScopeCardModel` values.
|
|
323
|
+
*
|
|
324
|
+
* For each unique `scopeId` in the segment list:
|
|
325
|
+
* - use the first segment (document order) as the card's anchor
|
|
326
|
+
* - look up an `ISSUE_METADATA_ID` metadata entry whose
|
|
327
|
+
* `workItemId` (preferred) or `scopeId` matches the segment
|
|
328
|
+
* - coerce that entry's `value` into `IssueMetadataValue` when the
|
|
329
|
+
* required shape is present; drop the issue silently otherwise
|
|
330
|
+
* so malformed host input never breaks chrome rendering
|
|
331
|
+
*
|
|
332
|
+
* P2 fields (`suggestionGroupIds`, `reviewActionCount`,
|
|
333
|
+
* `agentPending`) are populated as empty defaults and wired in a
|
|
334
|
+
* later phase.
|
|
335
|
+
*/
|
|
336
|
+
export function attachScopeCardModel(
|
|
337
|
+
input: AttachScopeCardModelInput,
|
|
338
|
+
): ScopeCardModel[] {
|
|
339
|
+
if (input.segments.length === 0) return [];
|
|
340
|
+
|
|
341
|
+
// Take the first segment per scopeId (document order preserved by
|
|
342
|
+
// the segment collector).
|
|
343
|
+
const firstByScope = new Map<string, ScopeRailSegment>();
|
|
344
|
+
for (const segment of input.segments) {
|
|
345
|
+
if (!firstByScope.has(segment.scopeId)) {
|
|
346
|
+
firstByScope.set(segment.scopeId, segment);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const workItemByScope = new Map<string, string>();
|
|
351
|
+
for (const scope of input.scopes ?? []) {
|
|
352
|
+
if (scope.workItemId) {
|
|
353
|
+
workItemByScope.set(scope.scopeId, scope.workItemId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const models: ScopeCardModel[] = [];
|
|
358
|
+
for (const segment of firstByScope.values()) {
|
|
359
|
+
const workItemId = workItemByScope.get(segment.scopeId);
|
|
360
|
+
const issue = resolveIssueForScope(
|
|
361
|
+
segment.scopeId,
|
|
362
|
+
workItemId,
|
|
363
|
+
input.metadata,
|
|
364
|
+
);
|
|
365
|
+
const primaryAnchorRect = input.anchorIndex
|
|
366
|
+
? input.anchorIndex.bySelection(segment.fromOffset, segment.toOffset)
|
|
367
|
+
: null;
|
|
368
|
+
|
|
369
|
+
models.push({
|
|
370
|
+
scopeId: segment.scopeId,
|
|
371
|
+
...(workItemId ? { workItemId } : {}),
|
|
372
|
+
label: segment.label ?? "",
|
|
373
|
+
posture: segment.posture,
|
|
374
|
+
primaryAnchorRect,
|
|
375
|
+
...(issue ? { issue } : {}),
|
|
376
|
+
suggestionGroupIds: [],
|
|
377
|
+
reviewActionCount: 0,
|
|
378
|
+
agentPending: false,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return models;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function resolveIssueForScope(
|
|
386
|
+
scopeId: string,
|
|
387
|
+
workItemId: string | undefined,
|
|
388
|
+
metadata: readonly WorkflowMetadataMarkup[] | undefined,
|
|
389
|
+
): IssueMetadataValue | undefined {
|
|
390
|
+
if (!metadata || metadata.length === 0) return undefined;
|
|
391
|
+
for (const entry of metadata) {
|
|
392
|
+
if (entry.metadataId !== ISSUE_METADATA_ID) continue;
|
|
393
|
+
const matchesWorkItem =
|
|
394
|
+
workItemId !== undefined &&
|
|
395
|
+
entry.workItemId !== undefined &&
|
|
396
|
+
entry.workItemId === workItemId;
|
|
397
|
+
const matchesScope =
|
|
398
|
+
entry.scopeId !== undefined && entry.scopeId === scopeId;
|
|
399
|
+
if (!matchesWorkItem && !matchesScope) continue;
|
|
400
|
+
const coerced = coerceIssueValue(entry.value);
|
|
401
|
+
if (coerced) return coerced;
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Validate that a host-supplied metadata value looks like an
|
|
408
|
+
* `IssueMetadataValue`. Guards against malformed input — the card
|
|
409
|
+
* reads strongly-typed fields, so a missing severity or title must
|
|
410
|
+
* drop the issue rather than render `undefined` in the UI.
|
|
411
|
+
*/
|
|
412
|
+
function coerceIssueValue(
|
|
413
|
+
value: unknown,
|
|
414
|
+
): IssueMetadataValue | undefined {
|
|
415
|
+
if (!value || typeof value !== "object") return undefined;
|
|
416
|
+
const candidate = value as Partial<IssueMetadataValue>;
|
|
417
|
+
if (
|
|
418
|
+
typeof candidate.issueId !== "string" ||
|
|
419
|
+
typeof candidate.topic !== "string" ||
|
|
420
|
+
typeof candidate.severity !== "string" ||
|
|
421
|
+
typeof candidate.mode !== "string" ||
|
|
422
|
+
typeof candidate.checklistState !== "string" ||
|
|
423
|
+
typeof candidate.title !== "string"
|
|
424
|
+
) {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
return candidate as IssueMetadataValue;
|
|
428
|
+
}
|
|
@@ -50,6 +50,11 @@ import type {
|
|
|
50
50
|
SurfaceInlineSegment,
|
|
51
51
|
TableOp,
|
|
52
52
|
TableOpResult,
|
|
53
|
+
PublicTableEvent,
|
|
54
|
+
PublicTableRenderPlan,
|
|
55
|
+
PublicTableRowHeight,
|
|
56
|
+
PublicTableStyle,
|
|
57
|
+
PublicTableSummary,
|
|
53
58
|
TrackedChangeEntrySnapshot,
|
|
54
59
|
TocRefreshResult,
|
|
55
60
|
UpdateFieldsResult,
|
|
@@ -201,7 +206,12 @@ import {
|
|
|
201
206
|
resolveChromePreset,
|
|
202
207
|
resolveChromeVisibilityForPreset,
|
|
203
208
|
} from "../ui-tailwind/chrome/chrome-preset-model.ts";
|
|
204
|
-
import {
|
|
209
|
+
import { createRuntimeCollabSync } from "../runtime/collab/runtime-collab-sync.ts";
|
|
210
|
+
import {
|
|
211
|
+
clearLocalCursorState,
|
|
212
|
+
getCursorColorForUser,
|
|
213
|
+
setLocalCursorState,
|
|
214
|
+
} from "../runtime/collab/remote-cursor-awareness.ts";
|
|
205
215
|
|
|
206
216
|
export {
|
|
207
217
|
__createFallbackRuntime,
|
|
@@ -508,6 +518,12 @@ export function __createWordReviewEditorRefBridge(
|
|
|
508
518
|
setZoom: (level) => {
|
|
509
519
|
runtime.setZoom(level);
|
|
510
520
|
},
|
|
521
|
+
setEditorRole: (role) => {
|
|
522
|
+
runtime.setEditorRole(role);
|
|
523
|
+
},
|
|
524
|
+
setChromePin: (surface, pin) => {
|
|
525
|
+
runtime.setChromePin(surface, pin);
|
|
526
|
+
},
|
|
511
527
|
insertSectionBreak: (type, options) => {
|
|
512
528
|
applyRuntimeInsertSectionBreak(runtime, type, options);
|
|
513
529
|
},
|
|
@@ -642,6 +658,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
642
658
|
onError,
|
|
643
659
|
onEvent,
|
|
644
660
|
onWarning,
|
|
661
|
+
onReviewSidebarTrackedChanges,
|
|
662
|
+
onReviewSidebarComments,
|
|
645
663
|
readOnly = false,
|
|
646
664
|
reviewMode = "review",
|
|
647
665
|
suggestionsEnabled = false,
|
|
@@ -673,6 +691,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
673
691
|
runtime,
|
|
674
692
|
loadError,
|
|
675
693
|
activeRuntime,
|
|
694
|
+
commandAppliedBridge,
|
|
676
695
|
fallbackSnapshot,
|
|
677
696
|
loadingSessionState,
|
|
678
697
|
loadingViewState,
|
|
@@ -993,9 +1012,48 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
993
1012
|
|
|
994
1013
|
useEffect(() => {
|
|
995
1014
|
if (!ydoc || !runtime) return;
|
|
996
|
-
const handle =
|
|
1015
|
+
const handle = createRuntimeCollabSync({
|
|
1016
|
+
ydoc,
|
|
1017
|
+
runtime,
|
|
1018
|
+
authorId: currentUser.userId,
|
|
1019
|
+
commandAppliedBridge,
|
|
1020
|
+
});
|
|
997
1021
|
return () => handle.destroy();
|
|
998
|
-
}, [
|
|
1022
|
+
}, [commandAppliedBridge, currentUser.userId, runtime, ydoc]);
|
|
1023
|
+
|
|
1024
|
+
useEffect(() => {
|
|
1025
|
+
if (!awareness) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
return () => clearLocalCursorState(awareness);
|
|
1029
|
+
}, [awareness]);
|
|
1030
|
+
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (!awareness) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (!runtime) {
|
|
1036
|
+
clearLocalCursorState(awareness);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
setLocalCursorState(awareness, {
|
|
1041
|
+
userId: currentUser.userId,
|
|
1042
|
+
displayName: currentUser.displayName,
|
|
1043
|
+
color: getCursorColorForUser(currentUser.userId),
|
|
1044
|
+
anchor: snapshot.selection.anchor,
|
|
1045
|
+
head: snapshot.selection.head,
|
|
1046
|
+
storyTarget: snapshot.activeStory,
|
|
1047
|
+
});
|
|
1048
|
+
}, [
|
|
1049
|
+
awareness,
|
|
1050
|
+
currentUser.displayName,
|
|
1051
|
+
currentUser.userId,
|
|
1052
|
+
runtime,
|
|
1053
|
+
snapshot.activeStory,
|
|
1054
|
+
snapshot.selection.anchor,
|
|
1055
|
+
snapshot.selection.head,
|
|
1056
|
+
]);
|
|
999
1057
|
|
|
1000
1058
|
useEffect(() => {
|
|
1001
1059
|
runtimeViewStateSeedRef.current = {
|
|
@@ -1331,6 +1389,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1331
1389
|
setZoom: (level) => {
|
|
1332
1390
|
activeRuntime.setZoom(level);
|
|
1333
1391
|
},
|
|
1392
|
+
setEditorRole: (role) => {
|
|
1393
|
+
activeRuntime.setEditorRole(role);
|
|
1394
|
+
},
|
|
1395
|
+
setChromePin: (surface, pin) => {
|
|
1396
|
+
activeRuntime.setChromePin(surface, pin);
|
|
1397
|
+
},
|
|
1334
1398
|
insertSectionBreak: (type, options) => {
|
|
1335
1399
|
applyRuntimeInsertSectionBreak(activeRuntime, type, options);
|
|
1336
1400
|
},
|
|
@@ -2188,6 +2252,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2188
2252
|
openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "header"),
|
|
2189
2253
|
onOpenFooterStory: () =>
|
|
2190
2254
|
openDefaultStoryVariant(activeRuntime, snapshot.pageLayout, documentNavigation, "footer"),
|
|
2255
|
+
onOpenHeaderStoryForPage: (pageIndex: number) =>
|
|
2256
|
+
openStoryForPage(activeRuntime, pageIndex, "header"),
|
|
2257
|
+
onOpenFooterStoryForPage: (pageIndex: number) =>
|
|
2258
|
+
openStoryForPage(activeRuntime, pageIndex, "footer"),
|
|
2191
2259
|
onDeleteSectionBreak: (sectionIndex) =>
|
|
2192
2260
|
applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
|
|
2193
2261
|
onUpdateSectionLayout: (sectionIndex, patch) =>
|
|
@@ -2235,7 +2303,6 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2235
2303
|
<EditorSurfaceController
|
|
2236
2304
|
ref={surfaceRef}
|
|
2237
2305
|
currentUser={currentUser}
|
|
2238
|
-
ydoc={ydoc}
|
|
2239
2306
|
awareness={awareness}
|
|
2240
2307
|
snapshot={snapshot}
|
|
2241
2308
|
canonicalDocument={canonicalDocument}
|
|
@@ -2261,6 +2328,16 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2261
2328
|
dispatchRuntimeCommand={(command) =>
|
|
2262
2329
|
activeRuntime.applyActiveStoryTextCommand(command as never)
|
|
2263
2330
|
}
|
|
2331
|
+
layoutFacet={activeRuntime.layout}
|
|
2332
|
+
pageChromeHeaderBandPx={isPageWorkspace ? 32 : 0}
|
|
2333
|
+
pageChromeFooterBandPx={isPageWorkspace ? 32 : 0}
|
|
2334
|
+
pageChromeInterGapPx={isPageWorkspace ? 24 : 16}
|
|
2335
|
+
onOpenHeaderStoryForPage={(pageIndex: number) =>
|
|
2336
|
+
openStoryForPage(activeRuntime, pageIndex, "header")
|
|
2337
|
+
}
|
|
2338
|
+
onOpenFooterStoryForPage={(pageIndex: number) =>
|
|
2339
|
+
openStoryForPage(activeRuntime, pageIndex, "footer")
|
|
2340
|
+
}
|
|
2264
2341
|
onCommentActivated={(commentId) => {
|
|
2265
2342
|
activeRuntime.openComment(commentId);
|
|
2266
2343
|
setActiveRailTab("comments");
|
|
@@ -2354,6 +2431,25 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2354
2431
|
selectionToolbarRef={selectionToolbarElementRef}
|
|
2355
2432
|
commands={commands}
|
|
2356
2433
|
document={documentElement}
|
|
2434
|
+
onReviewSidebarTrackedChanges={onReviewSidebarTrackedChanges}
|
|
2435
|
+
onReviewSidebarComments={onReviewSidebarComments}
|
|
2436
|
+
onScopeModeChangeRequested={(payload) => {
|
|
2437
|
+
onEventRef.current?.({
|
|
2438
|
+
type: "scope-mode-change-requested",
|
|
2439
|
+
documentId,
|
|
2440
|
+
scopeId: payload.scopeId,
|
|
2441
|
+
mode: payload.mode,
|
|
2442
|
+
});
|
|
2443
|
+
}}
|
|
2444
|
+
onScopeIssueActionRequested={(payload) => {
|
|
2445
|
+
onEventRef.current?.({
|
|
2446
|
+
type: "scope-issue-action-requested",
|
|
2447
|
+
documentId,
|
|
2448
|
+
scopeId: payload.scopeId,
|
|
2449
|
+
issueId: payload.issueId,
|
|
2450
|
+
action: payload.action,
|
|
2451
|
+
});
|
|
2452
|
+
}}
|
|
2357
2453
|
/>
|
|
2358
2454
|
);
|
|
2359
2455
|
},
|
|
@@ -3453,7 +3549,7 @@ function buildTablesFacet(
|
|
|
3453
3549
|
) {
|
|
3454
3550
|
const getCapabilities = () => {
|
|
3455
3551
|
const snapshot = runtime.getRenderSnapshot();
|
|
3456
|
-
const document = runtime.
|
|
3552
|
+
const document = runtime.getCanonicalDocument();
|
|
3457
3553
|
return (
|
|
3458
3554
|
clonePublicValue(
|
|
3459
3555
|
getTableStructureContext(
|
|
@@ -3464,6 +3560,178 @@ function buildTablesFacet(
|
|
|
3464
3560
|
) ?? null
|
|
3465
3561
|
);
|
|
3466
3562
|
};
|
|
3563
|
+
|
|
3564
|
+
const buildSummary = (
|
|
3565
|
+
table: Extract<ReturnType<typeof runtime.getCanonicalDocument>["content"]["children"][number], { type: "table" }>,
|
|
3566
|
+
tableBlockIndex: number,
|
|
3567
|
+
): PublicTableSummary => {
|
|
3568
|
+
const blockId = `table-${tableBlockIndex}`;
|
|
3569
|
+
const hasVerticalMerges = table.rows.some((row) =>
|
|
3570
|
+
row.cells.some((cell) => cell.verticalMerge !== undefined),
|
|
3571
|
+
);
|
|
3572
|
+
const hasHorizontalSpans = table.rows.some((row) =>
|
|
3573
|
+
row.cells.some((cell) => (cell.gridSpan ?? 1) > 1),
|
|
3574
|
+
);
|
|
3575
|
+
return {
|
|
3576
|
+
tableBlockIndex,
|
|
3577
|
+
blockId,
|
|
3578
|
+
styleId: table.styleId ?? null,
|
|
3579
|
+
rowCount: table.rows.length,
|
|
3580
|
+
columnCount: table.gridColumns.length,
|
|
3581
|
+
gridColumnsTwips: table.gridColumns,
|
|
3582
|
+
alignment: table.alignment ?? null,
|
|
3583
|
+
hasVerticalMerges,
|
|
3584
|
+
hasHorizontalSpans,
|
|
3585
|
+
pageIndex: runtime.layout.getFirstPageIndexForBlock(blockId) ?? null,
|
|
3586
|
+
};
|
|
3587
|
+
};
|
|
3588
|
+
|
|
3589
|
+
const getTables = (options?: { sectionIndex?: number }): PublicTableSummary[] => {
|
|
3590
|
+
const document = runtime.getCanonicalDocument();
|
|
3591
|
+
const summaries: PublicTableSummary[] = [];
|
|
3592
|
+
let idx = 0;
|
|
3593
|
+
for (const block of document.content.children) {
|
|
3594
|
+
if (block.type === "table") {
|
|
3595
|
+
summaries.push(clonePublicValue(buildSummary(block, idx)));
|
|
3596
|
+
idx++;
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
if (options?.sectionIndex != null) {
|
|
3600
|
+
const section = runtime.layout.getSection(options.sectionIndex);
|
|
3601
|
+
if (!section) return [];
|
|
3602
|
+
return summaries.filter(
|
|
3603
|
+
(s) =>
|
|
3604
|
+
s.pageIndex != null &&
|
|
3605
|
+
s.pageIndex >= section.firstPageIndex &&
|
|
3606
|
+
s.pageIndex <= section.lastPageIndex,
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
return summaries;
|
|
3610
|
+
};
|
|
3611
|
+
|
|
3612
|
+
const getTable = (tableBlockIndex: number): PublicTableSummary | null => {
|
|
3613
|
+
const document = runtime.getCanonicalDocument();
|
|
3614
|
+
let idx = 0;
|
|
3615
|
+
for (const block of document.content.children) {
|
|
3616
|
+
if (block.type === "table") {
|
|
3617
|
+
if (idx === tableBlockIndex) {
|
|
3618
|
+
return clonePublicValue(buildSummary(block, idx));
|
|
3619
|
+
}
|
|
3620
|
+
idx++;
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
return null;
|
|
3624
|
+
};
|
|
3625
|
+
|
|
3626
|
+
const getTableForSelection = (): PublicTableSummary | null => {
|
|
3627
|
+
const sel = mountedSurface?.getTableSelection() ?? null;
|
|
3628
|
+
if (!sel) return null;
|
|
3629
|
+
return getTable(sel.tableBlockIndex);
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3632
|
+
const getRenderPlan = (
|
|
3633
|
+
tableBlockIndex: number,
|
|
3634
|
+
pageIndex?: number,
|
|
3635
|
+
): PublicTableRenderPlan | null => {
|
|
3636
|
+
const blockId = `table-${tableBlockIndex}`;
|
|
3637
|
+
const effectivePage =
|
|
3638
|
+
pageIndex ??
|
|
3639
|
+
runtime.layout.getFirstPageIndexForBlock(blockId) ??
|
|
3640
|
+
0;
|
|
3641
|
+
const plan = runtime.layout.getTableRenderPlan(blockId, effectivePage);
|
|
3642
|
+
if (!plan) return null;
|
|
3643
|
+
return clonePublicValue({
|
|
3644
|
+
blockId: plan.blockId,
|
|
3645
|
+
pageIndex: plan.pageIndex,
|
|
3646
|
+
columnsTwips: plan.columnsTwips,
|
|
3647
|
+
bandClasses: plan.bandClasses,
|
|
3648
|
+
verticalMerges: plan.verticalMerges,
|
|
3649
|
+
repeatedHeaderRows: plan.repeatedHeaderRows,
|
|
3650
|
+
columnResizeHandles: plan.columnResizeHandles,
|
|
3651
|
+
} satisfies PublicTableRenderPlan);
|
|
3652
|
+
};
|
|
3653
|
+
|
|
3654
|
+
const getColumnWidths = (tableBlockIndex: number): readonly number[] => {
|
|
3655
|
+
const document = runtime.getCanonicalDocument();
|
|
3656
|
+
let idx = 0;
|
|
3657
|
+
for (const block of document.content.children) {
|
|
3658
|
+
if (block.type === "table") {
|
|
3659
|
+
if (idx === tableBlockIndex) {
|
|
3660
|
+
return block.gridColumns;
|
|
3661
|
+
}
|
|
3662
|
+
idx++;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return [];
|
|
3666
|
+
};
|
|
3667
|
+
|
|
3668
|
+
const getRowHeights = (tableBlockIndex: number): readonly PublicTableRowHeight[] => {
|
|
3669
|
+
const document = runtime.getCanonicalDocument();
|
|
3670
|
+
let idx = 0;
|
|
3671
|
+
for (const block of document.content.children) {
|
|
3672
|
+
if (block.type === "table") {
|
|
3673
|
+
if (idx === tableBlockIndex) {
|
|
3674
|
+
const blockId = `table-${tableBlockIndex}`;
|
|
3675
|
+
const measurement = runtime.layout.getMeasurement(blockId);
|
|
3676
|
+
const totalMeasured = measurement?.heightTwips ?? 0;
|
|
3677
|
+
const rows = block.rows;
|
|
3678
|
+
const perRowFallback = rows.length > 0 ? totalMeasured / rows.length : 0;
|
|
3679
|
+
return rows.map((row): PublicTableRowHeight => ({
|
|
3680
|
+
measured: perRowFallback,
|
|
3681
|
+
...(row.height != null ? { explicit: row.height } : {}),
|
|
3682
|
+
...(row.heightRule != null ? { rule: row.heightRule } : {}),
|
|
3683
|
+
isHeader: row.isHeader ?? false,
|
|
3684
|
+
}));
|
|
3685
|
+
}
|
|
3686
|
+
idx++;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return [];
|
|
3690
|
+
};
|
|
3691
|
+
|
|
3692
|
+
const getStyleCatalog = (): readonly PublicTableStyle[] => {
|
|
3693
|
+
const document = runtime.getCanonicalDocument();
|
|
3694
|
+
return Object.values(document.styles.tables).map(
|
|
3695
|
+
(style): PublicTableStyle =>
|
|
3696
|
+
clonePublicValue({
|
|
3697
|
+
styleId: style.styleId,
|
|
3698
|
+
displayName: style.displayName,
|
|
3699
|
+
...(style.basedOn != null ? { basedOn: style.basedOn } : {}),
|
|
3700
|
+
isDefault: style.isDefault,
|
|
3701
|
+
}),
|
|
3702
|
+
);
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
const subscribe = (
|
|
3706
|
+
listener: (event: PublicTableEvent) => void,
|
|
3707
|
+
): (() => void) => {
|
|
3708
|
+
const unsubRuntime = runtime.subscribeToEvents((event) => {
|
|
3709
|
+
if (event.type === "dirty_changed" && event.isDirty) {
|
|
3710
|
+
listener({
|
|
3711
|
+
kind: "table_structure_changed",
|
|
3712
|
+
revisionToken: runtime.getRenderSnapshot().revisionToken,
|
|
3713
|
+
});
|
|
3714
|
+
} else if (event.type === "selection_changed") {
|
|
3715
|
+
listener({ kind: "table_capabilities_changed" });
|
|
3716
|
+
}
|
|
3717
|
+
});
|
|
3718
|
+
const unsubLayout = runtime.layout.subscribe((event) => {
|
|
3719
|
+
if (
|
|
3720
|
+
event.kind === "layout_recomputed" ||
|
|
3721
|
+
event.kind === "incremental_relayout"
|
|
3722
|
+
) {
|
|
3723
|
+
listener({
|
|
3724
|
+
kind: "table_render_plan_ready",
|
|
3725
|
+
revision: event.revision,
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
});
|
|
3729
|
+
return () => {
|
|
3730
|
+
unsubRuntime();
|
|
3731
|
+
unsubLayout();
|
|
3732
|
+
};
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3467
3735
|
return {
|
|
3468
3736
|
apply(op: TableOp): TableOpResult {
|
|
3469
3737
|
if (op.kind === "insert") {
|
|
@@ -3487,9 +3755,19 @@ function buildTablesFacet(
|
|
|
3487
3755
|
};
|
|
3488
3756
|
},
|
|
3489
3757
|
getCapabilities,
|
|
3758
|
+
getTables,
|
|
3759
|
+
getTable,
|
|
3760
|
+
getTableForSelection,
|
|
3761
|
+
getRenderPlan,
|
|
3762
|
+
getColumnWidths,
|
|
3763
|
+
getRowHeights,
|
|
3764
|
+
getStyleCatalog,
|
|
3765
|
+
subscribe,
|
|
3490
3766
|
};
|
|
3491
3767
|
}
|
|
3492
3768
|
|
|
3769
|
+
export { buildTablesFacet as __buildTablesFacet };
|
|
3770
|
+
|
|
3493
3771
|
function applyRuntimeTextCommand(
|
|
3494
3772
|
runtime: WordReviewEditorRuntime,
|
|
3495
3773
|
command:
|
|
@@ -3982,6 +4260,25 @@ function clonePublicValue<T>(value: T): T {
|
|
|
3982
4260
|
return structuredClone(value);
|
|
3983
4261
|
}
|
|
3984
4262
|
|
|
4263
|
+
/**
|
|
4264
|
+
* Open the correct header/footer story for a specific page. The page's
|
|
4265
|
+
* resolved `stories.header` / `stories.footer` already carries the
|
|
4266
|
+
* right variant (default / first / even) for that page's section, so we
|
|
4267
|
+
* can hand it directly to `runtime.openStory()` without re-running the
|
|
4268
|
+
* variant-pick logic.
|
|
4269
|
+
*/
|
|
4270
|
+
function openStoryForPage(
|
|
4271
|
+
runtime: WordReviewEditorRuntime,
|
|
4272
|
+
pageIndex: number,
|
|
4273
|
+
kind: "header" | "footer",
|
|
4274
|
+
): void {
|
|
4275
|
+
const page = runtime.layout?.getPage(pageIndex);
|
|
4276
|
+
if (!page) return;
|
|
4277
|
+
const target = kind === "header" ? page.stories.header : page.stories.footer;
|
|
4278
|
+
if (!target) return;
|
|
4279
|
+
runtime.openStory(target);
|
|
4280
|
+
}
|
|
4281
|
+
|
|
3985
4282
|
function openDefaultStoryVariant(
|
|
3986
4283
|
runtime: WordReviewEditorRuntime,
|
|
3987
4284
|
pageLayout: PageLayoutSnapshot | undefined,
|
|
@@ -89,6 +89,10 @@ export interface EditorCommandBag {
|
|
|
89
89
|
onCloseStory?(): void;
|
|
90
90
|
onOpenHeaderStory?(): void;
|
|
91
91
|
onOpenFooterStory?(): void;
|
|
92
|
+
/** Open the header story for a specific page (double-click on its band). */
|
|
93
|
+
onOpenHeaderStoryForPage?(pageIndex: number): void;
|
|
94
|
+
/** Open the footer story for a specific page (double-click on its band). */
|
|
95
|
+
onOpenFooterStoryForPage?(pageIndex: number): void;
|
|
92
96
|
onSetParagraphIndentation?(indentation: {
|
|
93
97
|
left?: number;
|
|
94
98
|
right?: number;
|
|
@@ -34,6 +34,10 @@ import {
|
|
|
34
34
|
type DocumentRuntimeEvent,
|
|
35
35
|
type DocumentRuntime,
|
|
36
36
|
} from "../runtime/document-runtime.ts";
|
|
37
|
+
import {
|
|
38
|
+
createRuntimeCommandAppliedBridge,
|
|
39
|
+
type RuntimeCommandAppliedBridge,
|
|
40
|
+
} from "../runtime/collab/runtime-collab-sync.ts";
|
|
37
41
|
import { createInertLayoutFacet } from "../runtime/layout/index.ts";
|
|
38
42
|
import { loadDocxEditorSession } from "../io/docx-session.ts";
|
|
39
43
|
import {
|
|
@@ -71,6 +75,7 @@ export interface CreateRuntimeArgs {
|
|
|
71
75
|
hostAdapter?: EditorHostAdapter;
|
|
72
76
|
datastore?: EditorDatastoreAdapter;
|
|
73
77
|
currentUserId?: string;
|
|
78
|
+
commandAppliedBridge?: RuntimeCommandAppliedBridge;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
interface RuntimeLifecycleHandlers {
|
|
@@ -103,6 +108,7 @@ export interface EditorRuntimeBoundaryState {
|
|
|
103
108
|
runtime: WordReviewEditorRuntime | null;
|
|
104
109
|
loadError: EditorError | null;
|
|
105
110
|
activeRuntime: WordReviewEditorRuntime;
|
|
111
|
+
commandAppliedBridge: RuntimeCommandAppliedBridge;
|
|
106
112
|
fallbackSnapshot: RuntimeRenderSnapshot;
|
|
107
113
|
loadingSessionState: EditorSessionState;
|
|
108
114
|
loadingViewState: EditorViewStateSnapshot;
|
|
@@ -280,6 +286,10 @@ export function useEditorRuntimeBoundary(
|
|
|
280
286
|
const onWarningRef = useRef(onWarning);
|
|
281
287
|
const onErrorRef = useRef(onError);
|
|
282
288
|
const currentUserIdRef = useRef(currentUser.userId);
|
|
289
|
+
const commandAppliedBridge = useMemo(
|
|
290
|
+
() => createRuntimeCommandAppliedBridge(),
|
|
291
|
+
[documentId],
|
|
292
|
+
);
|
|
283
293
|
const runtimeViewStateSeedRef = useRef<{
|
|
284
294
|
workspaceMode: WorkspaceMode;
|
|
285
295
|
zoomLevel: ZoomLevel;
|
|
@@ -374,6 +384,7 @@ export function useEditorRuntimeBoundary(
|
|
|
374
384
|
hostAdapter: hostAdapterRef.current,
|
|
375
385
|
datastore: datastoreRef.current,
|
|
376
386
|
currentUserId: currentUserIdRef.current,
|
|
387
|
+
commandAppliedBridge,
|
|
377
388
|
},
|
|
378
389
|
{
|
|
379
390
|
onWarning: onWarningRef.current,
|
|
@@ -536,6 +547,7 @@ export function useEditorRuntimeBoundary(
|
|
|
536
547
|
runtime,
|
|
537
548
|
loadError,
|
|
538
549
|
activeRuntime: runtime ?? loadingRuntimeBridge,
|
|
550
|
+
commandAppliedBridge,
|
|
539
551
|
fallbackSnapshot,
|
|
540
552
|
loadingSessionState,
|
|
541
553
|
loadingViewState,
|
|
@@ -616,6 +628,7 @@ function createRuntime(
|
|
|
616
628
|
bootstrapEvents.push(event);
|
|
617
629
|
},
|
|
618
630
|
defaultAuthorId: args.currentUserId,
|
|
631
|
+
onCommandApplied: args.commandAppliedBridge?.onCommandApplied,
|
|
619
632
|
}), {
|
|
620
633
|
drainBootstrapEvents: () => bootstrapEvents.splice(0, bootstrapEvents.length),
|
|
621
634
|
emitBlockedCommand: (
|
|
@@ -782,6 +795,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
782
795
|
],
|
|
783
796
|
}),
|
|
784
797
|
dispatch: () => undefined,
|
|
798
|
+
applyRemoteCommand: () => undefined,
|
|
785
799
|
undo: () => undefined,
|
|
786
800
|
redo: () => undefined,
|
|
787
801
|
focus: () => undefined,
|
|
@@ -807,6 +821,8 @@ function createLoadingRuntimeBridge(input: {
|
|
|
807
821
|
getProtectionSnapshot: () => input.snapshot.protectionSnapshot,
|
|
808
822
|
setWorkspaceMode: () => undefined,
|
|
809
823
|
setZoom: () => undefined,
|
|
824
|
+
setEditorRole: () => undefined,
|
|
825
|
+
setChromePin: () => undefined,
|
|
810
826
|
getPageLayoutSnapshot: () => null,
|
|
811
827
|
getDocumentNavigationSnapshot: () => input.navigation,
|
|
812
828
|
layout: inertLayoutFacet,
|
|
@@ -83,6 +83,28 @@ export interface EditorShellViewProps {
|
|
|
83
83
|
onSelectionToolbarBlurCapture?: React.FocusEventHandler<HTMLDivElement>;
|
|
84
84
|
selectionToolbarRef?: React.Ref<HTMLDivElement>;
|
|
85
85
|
chromeVisibility?: Partial<WordReviewEditorChromeVisibility>;
|
|
86
|
+
/** Review-role sidebar panel: open sidebar to tracked-changes panel. */
|
|
87
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
88
|
+
/** Review-role sidebar panel: open sidebar to comments panel. */
|
|
89
|
+
onReviewSidebarComments?: () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Scope card mode selector fired a mode change (forwarded from the
|
|
92
|
+
* workspace). The editor turns this into a
|
|
93
|
+
* `scope-mode-change-requested` event.
|
|
94
|
+
*/
|
|
95
|
+
onScopeModeChangeRequested?: (payload: {
|
|
96
|
+
scopeId: string;
|
|
97
|
+
mode: import("../api/public-types.ts").WorkflowScopeMode;
|
|
98
|
+
}) => void;
|
|
99
|
+
/**
|
|
100
|
+
* Scope card issue action fired (forwarded from the workspace).
|
|
101
|
+
* The editor turns this into a `scope-issue-action-requested` event.
|
|
102
|
+
*/
|
|
103
|
+
onScopeIssueActionRequested?: (payload: {
|
|
104
|
+
scopeId: string;
|
|
105
|
+
issueId: string;
|
|
106
|
+
action: import("../api/public-types.ts").ScopeIssueAction;
|
|
107
|
+
}) => void;
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
export function EditorShellView(props: EditorShellViewProps) {
|