@beyondwork/docx-react-component 1.0.53 → 1.0.54
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 +35 -7
- package/src/io/docx-session.ts +30 -6
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +1 -2
- package/src/runtime/document-runtime.ts +23 -9
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +58 -1
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/ui/WordReviewEditor.tsx +6 -3
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/editor-theme.css +249 -22
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
export type LayoutInvalidationReason =
|
|
82
82
|
| { kind: "content-edit"; from: number; to: number }
|
|
83
83
|
| { kind: "section-change"; sectionIndex: number }
|
|
84
|
-
| { kind: "styles-change" }
|
|
84
|
+
| { kind: "styles-change"; dirtyStyleIds?: readonly string[] }
|
|
85
85
|
| { kind: "theme-change" }
|
|
86
86
|
| { kind: "numbering-change"; numberingInstanceId?: string }
|
|
87
87
|
| { kind: "field-refresh"; family?: string }
|
|
@@ -416,28 +416,137 @@ export function buildPageStackFromWithSplits(
|
|
|
416
416
|
resumeAt: { startPageIndex: number; startOffset: number },
|
|
417
417
|
measurementProvider?: LayoutMeasurementProvider,
|
|
418
418
|
): PageStackResultWithSplits {
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
419
|
+
const startOffset = resumeAt.startOffset;
|
|
420
|
+
const dirtyPageNumberOffset = Math.max(0, resumeAt.startPageIndex);
|
|
421
|
+
|
|
422
|
+
// Fast path: offset at document start → full paginate is identical.
|
|
423
|
+
if (startOffset <= 0) {
|
|
424
|
+
return buildPageStackWithSplits(
|
|
425
|
+
document,
|
|
426
|
+
sections as ResolvedDocumentSection[],
|
|
427
|
+
mainSurface,
|
|
428
|
+
measurementProvider,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Find the first section that contains or starts after startOffset.
|
|
433
|
+
const firstDirtySectionIdx = (sections as ResolvedDocumentSection[]).findIndex(
|
|
434
|
+
(s) => s.end > startOffset,
|
|
425
435
|
);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
436
|
+
if (firstDirtySectionIdx < 0) {
|
|
437
|
+
// All sections end before the dirty offset — nothing to re-paginate.
|
|
438
|
+
return buildPageStackWithSplits(
|
|
439
|
+
document,
|
|
440
|
+
sections as ResolvedDocumentSection[],
|
|
441
|
+
mainSurface,
|
|
442
|
+
measurementProvider,
|
|
443
|
+
);
|
|
432
444
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
445
|
+
|
|
446
|
+
// Safety guard: if any block straddles the section boundary (to > startOffset
|
|
447
|
+
// but from < section.start), we cannot safely skip without reconstructing
|
|
448
|
+
// carry-over state — fall back to full paginate to avoid silent correctness loss.
|
|
449
|
+
const dirtySectionStart = (sections as ResolvedDocumentSection[])[firstDirtySectionIdx]!.start;
|
|
450
|
+
const straddling = mainSurface.blocks.some(
|
|
451
|
+
(b) => b.from < dirtySectionStart && b.to > startOffset,
|
|
452
|
+
);
|
|
453
|
+
// When the dirty section is section 0, there is nothing to skip — fall
|
|
454
|
+
// back to the full-build + tail-slice path so page startOffsets are
|
|
455
|
+
// correct (the section-skip path shifts only pageIndex, not startOffset,
|
|
456
|
+
// which would produce phantom pages when spliceGraph prepends page 0).
|
|
457
|
+
if (straddling || firstDirtySectionIdx === 0) {
|
|
458
|
+
const full = buildPageStackWithSplits(
|
|
459
|
+
document,
|
|
460
|
+
sections as ResolvedDocumentSection[],
|
|
461
|
+
mainSurface,
|
|
462
|
+
measurementProvider,
|
|
463
|
+
);
|
|
464
|
+
const tailPages = full.pages.slice(dirtyPageNumberOffset);
|
|
465
|
+
const tailSplits = new Map<string, ParagraphLineSlice[]>();
|
|
466
|
+
for (const [blockId, slices] of full.splits.byBlockId) {
|
|
467
|
+
const tail = slices.filter((s) => s.pageIndex >= dirtyPageNumberOffset);
|
|
468
|
+
if (tail.length > 0) tailSplits.set(blockId, tail);
|
|
469
|
+
}
|
|
470
|
+
const tailTableSplits = new Map<string, TableRowSlice[]>();
|
|
471
|
+
for (const [blockId, slices] of full.splits.tablesByBlockId) {
|
|
472
|
+
const tail = slices.filter((s) => s.pageIndex >= dirtyPageNumberOffset);
|
|
473
|
+
if (tail.length > 0) tailTableSplits.set(blockId, tail);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
pages: tailPages,
|
|
477
|
+
splits: { byBlockId: tailSplits, tablesByBlockId: tailTableSplits },
|
|
478
|
+
};
|
|
437
479
|
}
|
|
480
|
+
|
|
481
|
+
// Section-level skip: paginate only the dirty sections onward (only
|
|
482
|
+
// reached when firstDirtySectionIdx > 0).
|
|
483
|
+
const dirtySections = (sections as ResolvedDocumentSection[]).slice(firstDirtySectionIdx);
|
|
484
|
+
const dirtyBlockSet = new Set(
|
|
485
|
+
mainSurface.blocks
|
|
486
|
+
.filter((b) => b.from >= dirtySectionStart)
|
|
487
|
+
.map((b) => b.blockId),
|
|
488
|
+
);
|
|
489
|
+
const dirtySurface: EditorSurfaceSnapshot = {
|
|
490
|
+
...mainSurface,
|
|
491
|
+
blocks: mainSurface.blocks.filter((b) => dirtyBlockSet.has(b.blockId)),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const tailResult = buildPageStackWithSplits(
|
|
495
|
+
document,
|
|
496
|
+
dirtySections,
|
|
497
|
+
dirtySurface,
|
|
498
|
+
measurementProvider,
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Shift global page indices on all returned pages and splits so they align
|
|
502
|
+
// with the global page graph (the caller's spliceGraph prepends the head).
|
|
503
|
+
const shiftedPages = tailResult.pages.map((p) => ({
|
|
504
|
+
...p,
|
|
505
|
+
pageIndex: p.pageIndex + dirtyPageNumberOffset,
|
|
506
|
+
}));
|
|
507
|
+
const shiftedSplits: BlockSplits = {
|
|
508
|
+
byBlockId: new Map(
|
|
509
|
+
Array.from(tailResult.splits.byBlockId.entries()).map(([id, slices]) => [
|
|
510
|
+
id,
|
|
511
|
+
slices.map((s) => ({ ...s, pageIndex: s.pageIndex + dirtyPageNumberOffset })),
|
|
512
|
+
]),
|
|
513
|
+
),
|
|
514
|
+
tablesByBlockId: new Map(
|
|
515
|
+
Array.from(tailResult.splits.tablesByBlockId.entries()).map(([id, slices]) => [
|
|
516
|
+
id,
|
|
517
|
+
slices.map((s) => ({ ...s, pageIndex: s.pageIndex + dirtyPageNumberOffset })),
|
|
518
|
+
]),
|
|
519
|
+
),
|
|
520
|
+
};
|
|
521
|
+
// Shift note-allocation page indices if present.
|
|
522
|
+
const shiftedNoteAllocs =
|
|
523
|
+
tailResult.noteAllocationsByPageIndex && tailResult.noteAllocationsByPageIndex.size > 0
|
|
524
|
+
? new Map(
|
|
525
|
+
Array.from(tailResult.noteAllocationsByPageIndex.entries()).map(([pi, allocs]) => [
|
|
526
|
+
pi + dirtyPageNumberOffset,
|
|
527
|
+
allocs,
|
|
528
|
+
]),
|
|
529
|
+
)
|
|
530
|
+
: undefined;
|
|
531
|
+
const shiftedNoteFragments =
|
|
532
|
+
tailResult.noteFragmentsByPageIndex && tailResult.noteFragmentsByPageIndex.size > 0
|
|
533
|
+
? new Map(
|
|
534
|
+
Array.from(tailResult.noteFragmentsByPageIndex.entries()).map(([pi, frags]) => [
|
|
535
|
+
pi + dirtyPageNumberOffset,
|
|
536
|
+
frags,
|
|
537
|
+
]),
|
|
538
|
+
)
|
|
539
|
+
: undefined;
|
|
540
|
+
|
|
438
541
|
return {
|
|
439
|
-
pages:
|
|
440
|
-
splits:
|
|
542
|
+
pages: shiftedPages,
|
|
543
|
+
splits: shiftedSplits,
|
|
544
|
+
...(shiftedNoteAllocs !== undefined
|
|
545
|
+
? { noteAllocationsByPageIndex: shiftedNoteAllocs }
|
|
546
|
+
: {}),
|
|
547
|
+
...(shiftedNoteFragments !== undefined
|
|
548
|
+
? { noteFragmentsByPageIndex: shiftedNoteFragments }
|
|
549
|
+
: {}),
|
|
441
550
|
};
|
|
442
551
|
}
|
|
443
552
|
|
|
@@ -109,6 +109,7 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
109
109
|
from: block.from,
|
|
110
110
|
to: block.to,
|
|
111
111
|
heightTwips: estimateBlockHeightFromSpan(block),
|
|
112
|
+
...deriveStyleMetadata(block),
|
|
112
113
|
kind: "whole",
|
|
113
114
|
};
|
|
114
115
|
|
|
@@ -138,6 +139,7 @@ function emitSlicedParagraph(
|
|
|
138
139
|
from: block.from,
|
|
139
140
|
to: block.to,
|
|
140
141
|
heightTwips: estimateSliceHeightFromLines(slice.lineRange),
|
|
142
|
+
...deriveStyleMetadata(block),
|
|
141
143
|
kind: "paragraph-slice",
|
|
142
144
|
paragraphLineRange: slice.lineRange,
|
|
143
145
|
};
|
|
@@ -178,6 +180,7 @@ function emitSlicedTable(
|
|
|
178
180
|
from: block.from,
|
|
179
181
|
to: block.to,
|
|
180
182
|
heightTwips: estimateSliceHeightFromRows(slice.rowRange),
|
|
183
|
+
...deriveStyleMetadata(block),
|
|
181
184
|
kind: "table-slice",
|
|
182
185
|
tableRowRange: slice.rowRange,
|
|
183
186
|
};
|
|
@@ -223,3 +226,27 @@ function estimateBlockHeightFromSpan(block: SurfaceBlockSnapshot): number {
|
|
|
223
226
|
const lines = Math.max(1, Math.ceil(span / 80));
|
|
224
227
|
return lines * 240;
|
|
225
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Slice 5 — extract style metadata from a surface block for use in
|
|
232
|
+
* bounded invalidation. `resolvedStyleChainRef` is the block's styleId
|
|
233
|
+
* (paragraphs and tables only); `numberingInstanceId` comes from the
|
|
234
|
+
* block's list-instance (paragraphs only). Both fields are optional and
|
|
235
|
+
* absent when the block carries no style or numbering data.
|
|
236
|
+
*/
|
|
237
|
+
function deriveStyleMetadata(
|
|
238
|
+
block: SurfaceBlockSnapshot,
|
|
239
|
+
): Pick<FragmentWithoutPageId, "resolvedStyleChainRef" | "numberingInstanceId"> {
|
|
240
|
+
const styleId =
|
|
241
|
+
block.kind === "paragraph" || block.kind === "table"
|
|
242
|
+
? (block.styleId ?? undefined)
|
|
243
|
+
: undefined;
|
|
244
|
+
const numberingInstanceId =
|
|
245
|
+
block.kind === "paragraph"
|
|
246
|
+
? (block.numbering?.numberingInstanceId ?? undefined)
|
|
247
|
+
: undefined;
|
|
248
|
+
return {
|
|
249
|
+
...(styleId !== undefined ? { resolvedStyleChainRef: styleId } : {}),
|
|
250
|
+
...(numberingInstanceId !== undefined ? { numberingInstanceId } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -560,6 +560,15 @@ export interface WordReviewEditorLayoutFacet {
|
|
|
560
560
|
pageIndex: number,
|
|
561
561
|
): import("./table-render-plan.ts").TableRenderPlan | null;
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Returns the Y offset (in twips from the top of the page body) where this
|
|
565
|
+
* table fragment starts on the given page. Computed by summing
|
|
566
|
+
* `heightTwips` of all body fragments with `orderInRegion` less than the
|
|
567
|
+
* table fragment's order. Returns `null` when the block is not found on
|
|
568
|
+
* the specified page.
|
|
569
|
+
*/
|
|
570
|
+
getTableBodyYOffsetOnPage(blockId: string, pageIndex: number): number | null;
|
|
571
|
+
|
|
563
572
|
// Fields ---------------------------------------------------------------
|
|
564
573
|
getDirtyFieldFamilies(): readonly string[];
|
|
565
574
|
getFieldDirtinessReport(): PublicFieldDirtinessReport;
|
|
@@ -1255,6 +1264,24 @@ export function createLayoutFacet(
|
|
|
1255
1264
|
});
|
|
1256
1265
|
},
|
|
1257
1266
|
|
|
1267
|
+
getTableBodyYOffsetOnPage(blockId, pageIndex) {
|
|
1268
|
+
const graph = currentGraph();
|
|
1269
|
+
const pageNode = graph.pages.find(
|
|
1270
|
+
(p) => p.pageIndex === pageIndex && !p.isBlankFiller,
|
|
1271
|
+
);
|
|
1272
|
+
if (!pageNode) return null;
|
|
1273
|
+
const bodyFragmentIdSet = new Set(pageNode.regions.body.fragmentIds);
|
|
1274
|
+
const bodyFragments = graph.fragments
|
|
1275
|
+
.filter((f) => bodyFragmentIdSet.has(f.fragmentId))
|
|
1276
|
+
.sort((a, b) => a.orderInRegion - b.orderInRegion);
|
|
1277
|
+
let yOffset = 0;
|
|
1278
|
+
for (const frag of bodyFragments) {
|
|
1279
|
+
if (frag.blockId === blockId) return yOffset;
|
|
1280
|
+
yOffset += frag.heightTwips;
|
|
1281
|
+
}
|
|
1282
|
+
return null;
|
|
1283
|
+
},
|
|
1284
|
+
|
|
1258
1285
|
getDirtyFieldFamilies() {
|
|
1259
1286
|
return engine.getDirtyFieldFamilies();
|
|
1260
1287
|
},
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
RenderPage,
|
|
23
23
|
RenderStoryRegion,
|
|
24
24
|
DecorationIndex,
|
|
25
|
+
PageChromeReservations,
|
|
25
26
|
} from "./render-frame-types.ts";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
@@ -46,6 +47,14 @@ export interface ChangedPageEntry {
|
|
|
46
47
|
*/
|
|
47
48
|
changedBlockIds: readonly string[];
|
|
48
49
|
}[];
|
|
50
|
+
/**
|
|
51
|
+
* R1: Set when the page's physical frame rect changed but no individual
|
|
52
|
+
* block regions changed. Consumers that iterate `regions` for targeted
|
|
53
|
+
* re-render targets must also honour this flag to avoid silently skipping
|
|
54
|
+
* pages whose geometry shifted without block-level changes (e.g. zoom
|
|
55
|
+
* change or page margin edit that shifts every page frame uniformly).
|
|
56
|
+
*/
|
|
57
|
+
pageFrameChanged?: boolean;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
export interface RenderFrameDiff {
|
|
@@ -110,10 +119,21 @@ export function diffRenderFrames(
|
|
|
110
119
|
continue;
|
|
111
120
|
}
|
|
112
121
|
const regions = diffPage(prevPage, nextPage, prev.decorationIndex, next.decorationIndex);
|
|
113
|
-
|
|
122
|
+
// R1: track page-frame geometry changes independently of block regions.
|
|
123
|
+
const frameChanged = !rectEquals(prevPage.frame, nextPage.frame);
|
|
124
|
+
// R2: track chrome-reservation changes so chrome re-projection isn't skipped.
|
|
125
|
+
const reservationsChanged = !reservationsEqual(
|
|
126
|
+
prevPage.chromeReservations,
|
|
127
|
+
nextPage.chromeReservations,
|
|
128
|
+
);
|
|
129
|
+
if (regions.length === 0 && !frameChanged && !reservationsChanged) {
|
|
114
130
|
unchangedPages.push(pageIndex);
|
|
115
131
|
} else {
|
|
116
|
-
changedPages.push({
|
|
132
|
+
changedPages.push({
|
|
133
|
+
pageIndex,
|
|
134
|
+
regions,
|
|
135
|
+
...(frameChanged ? { pageFrameChanged: true } : {}),
|
|
136
|
+
});
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
|
|
@@ -268,6 +288,22 @@ function decorationHashForBlock(
|
|
|
268
288
|
return tokens.join("|");
|
|
269
289
|
}
|
|
270
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Chrome reservations compare
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
// R2: Compare all five reservation fields so changes to rail/balloon lanes
|
|
296
|
+
// or footnote area trigger changedPages, not unchangedPages.
|
|
297
|
+
function reservationsEqual(a: PageChromeReservations, b: PageChromeReservations): boolean {
|
|
298
|
+
return (
|
|
299
|
+
a.railLaneTwips === b.railLaneTwips &&
|
|
300
|
+
a.balloonLaneTwips === b.balloonLaneTwips &&
|
|
301
|
+
a.footnoteAreaTwips === b.footnoteAreaTwips &&
|
|
302
|
+
a.pageFrameWidthPx === b.pageFrameWidthPx &&
|
|
303
|
+
a.pageFrameHeightPx === b.pageFrameHeightPx
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
271
307
|
// ---------------------------------------------------------------------------
|
|
272
308
|
// Geometry compare
|
|
273
309
|
// ---------------------------------------------------------------------------
|
|
@@ -782,7 +782,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
782
782
|
clearWorkflowOverlay: () => {
|
|
783
783
|
runtime.clearWorkflowOverlay();
|
|
784
784
|
},
|
|
785
|
-
getWorkflowOverlay: () =>
|
|
785
|
+
getWorkflowOverlay: () => {
|
|
786
|
+
return clonePublicValue(runtime.getWorkflowOverlay());
|
|
787
|
+
},
|
|
786
788
|
setSharedWorkflowState: (state) => {
|
|
787
789
|
runtime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
788
790
|
},
|
|
@@ -1834,8 +1836,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1834
1836
|
clearWorkflowOverlay: () => {
|
|
1835
1837
|
activeRuntime.clearWorkflowOverlay();
|
|
1836
1838
|
},
|
|
1837
|
-
getWorkflowOverlay: () =>
|
|
1838
|
-
clonePublicValue(activeRuntime.getWorkflowOverlay())
|
|
1839
|
+
getWorkflowOverlay: () => {
|
|
1840
|
+
return clonePublicValue(activeRuntime.getWorkflowOverlay());
|
|
1841
|
+
},
|
|
1839
1842
|
setSharedWorkflowState: (state) => {
|
|
1840
1843
|
activeRuntime.setSharedWorkflowState(state === null ? null : clonePublicValue(state));
|
|
1841
1844
|
},
|
|
@@ -77,7 +77,61 @@ export function getCommentRangeState(
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Markup display mode — controls how tracked changes and comments render.
|
|
82
|
+
*
|
|
83
|
+
* L6d.N2 widens the union to Word's 4-mode grammar (`"all-markup"`,
|
|
84
|
+
* `"simple-markup"`, `"no-markup"`, `"original"`) while keeping the
|
|
85
|
+
* legacy aliases `"clean" | "simple" | "all"` for backward compat with
|
|
86
|
+
* hosts that already pass the old values. Callers that switch on this
|
|
87
|
+
* value should either:
|
|
88
|
+
* - handle both sets of names (the decoration models do), or
|
|
89
|
+
* - normalize via `normalizeMarkupDisplay()` before matching.
|
|
90
|
+
*
|
|
91
|
+
* New in 6d.N2:
|
|
92
|
+
* - `"original"` — hide insertions entirely, render deletions as
|
|
93
|
+
* plain body text. Shows what the document looked like before the
|
|
94
|
+
* current batch of tracked changes.
|
|
95
|
+
*/
|
|
96
|
+
export type MarkupDisplay =
|
|
97
|
+
| "all-markup"
|
|
98
|
+
| "simple-markup"
|
|
99
|
+
| "no-markup"
|
|
100
|
+
| "original"
|
|
101
|
+
// Legacy aliases — accepted on the public API; prefer the canonical names above.
|
|
102
|
+
| "clean"
|
|
103
|
+
| "simple"
|
|
104
|
+
| "all";
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Collapse legacy `MarkupDisplay` aliases to their canonical Word-grammar
|
|
108
|
+
* name:
|
|
109
|
+
* - `"all"` → `"all-markup"`
|
|
110
|
+
* - `"simple"` → `"simple-markup"`
|
|
111
|
+
* - `"clean"` → `"no-markup"`
|
|
112
|
+
* - `"original"` → `"original"` (identity)
|
|
113
|
+
*
|
|
114
|
+
* The two sets remain interchangeable inside the decoration models, but
|
|
115
|
+
* external surfaces (selector labels, telemetry, host events) should
|
|
116
|
+
* normalize first so Word's grammar is the single source of truth.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeMarkupDisplay(
|
|
119
|
+
value: MarkupDisplay,
|
|
120
|
+
): "all-markup" | "simple-markup" | "no-markup" | "original" {
|
|
121
|
+
switch (value) {
|
|
122
|
+
case "all":
|
|
123
|
+
case "all-markup":
|
|
124
|
+
return "all-markup";
|
|
125
|
+
case "simple":
|
|
126
|
+
case "simple-markup":
|
|
127
|
+
return "simple-markup";
|
|
128
|
+
case "clean":
|
|
129
|
+
case "no-markup":
|
|
130
|
+
return "no-markup";
|
|
131
|
+
case "original":
|
|
132
|
+
return "original";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
81
135
|
|
|
82
136
|
export function getCommentHighlightClass(
|
|
83
137
|
model: CommentDecorationModel | undefined,
|
|
@@ -90,10 +144,11 @@ export function getCommentHighlightClass(
|
|
|
90
144
|
return "";
|
|
91
145
|
}
|
|
92
146
|
|
|
93
|
-
switch (markupDisplay) {
|
|
94
|
-
case "
|
|
147
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
148
|
+
case "no-markup":
|
|
149
|
+
case "original":
|
|
95
150
|
return state.hasActive ? "bg-comment-soft" : "";
|
|
96
|
-
case "simple":
|
|
151
|
+
case "simple-markup":
|
|
97
152
|
if (state.hasActive) {
|
|
98
153
|
return "underline decoration-comment decoration-2 underline-offset-4";
|
|
99
154
|
}
|
|
@@ -101,7 +156,7 @@ export function getCommentHighlightClass(
|
|
|
101
156
|
return "underline decoration-comment/60 decoration-1 underline-offset-4";
|
|
102
157
|
}
|
|
103
158
|
return "underline decoration-comment/40 decoration-1 underline-offset-4";
|
|
104
|
-
case "all":
|
|
159
|
+
case "all-markup":
|
|
105
160
|
if (state.hasActive) {
|
|
106
161
|
return "bg-comment-strong";
|
|
107
162
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizeMarkupDisplay,
|
|
4
|
+
rangesOverlap,
|
|
5
|
+
type MarkupDisplay,
|
|
6
|
+
} from "./comment-decoration-model";
|
|
3
7
|
|
|
4
8
|
export interface RevisionDecorationModel {
|
|
5
9
|
revisions: RevisionDecorationEntry[];
|
|
@@ -13,6 +17,65 @@ export interface RevisionDecorationEntry {
|
|
|
13
17
|
status: TrackedChangeEntrySnapshot["status"];
|
|
14
18
|
actionability: TrackedChangeEntrySnapshot["actionability"];
|
|
15
19
|
isActive: boolean;
|
|
20
|
+
/** L6d.N3 — source author, used to pick the decoration color. */
|
|
21
|
+
authorId?: string;
|
|
22
|
+
/** L6d.N3 — palette slot for `authorId` (0..7, stable via FNV-1a hash). */
|
|
23
|
+
authorPaletteIndex?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* L6d.N3 — author-color assignment.
|
|
28
|
+
*
|
|
29
|
+
* Map `authorId` → one of the 8 `--color-chart-categorical-*` vars
|
|
30
|
+
* using FNV-1a 32-bit hashing. FNV-1a is deterministic, allocation-
|
|
31
|
+
* free, and has better avalanche on short ASCII keys than djb2, so
|
|
32
|
+
* typical author-id alphabets (emails, UUIDs, Slack IDs) distribute
|
|
33
|
+
* evenly across the 8 slots.
|
|
34
|
+
*
|
|
35
|
+
* The palette lives as CSS vars so theme swaps / reduced-contrast
|
|
36
|
+
* overrides in `tokens.css` cascade automatically — we emit `var(...)`
|
|
37
|
+
* strings, never hex literals.
|
|
38
|
+
*/
|
|
39
|
+
export const AUTHOR_PALETTE: readonly string[] = [
|
|
40
|
+
"var(--color-chart-categorical-1)",
|
|
41
|
+
"var(--color-chart-categorical-2)",
|
|
42
|
+
"var(--color-chart-categorical-3)",
|
|
43
|
+
"var(--color-chart-categorical-4)",
|
|
44
|
+
"var(--color-chart-categorical-5)",
|
|
45
|
+
"var(--color-chart-categorical-6)",
|
|
46
|
+
"var(--color-chart-categorical-7)",
|
|
47
|
+
"var(--color-chart-categorical-8)",
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* FNV-1a 32-bit hash. Constants from the Fowler–Noll–Vo reference:
|
|
52
|
+
* offset basis = 0x811c9dc5
|
|
53
|
+
* prime = 0x01000193
|
|
54
|
+
* The `>>> 0` keeps the value in unsigned 32-bit territory after each
|
|
55
|
+
* multiplication so two identical inputs always produce the same hash.
|
|
56
|
+
*/
|
|
57
|
+
export function hashAuthorId(authorId: string): number {
|
|
58
|
+
let hash = 0x811c9dc5;
|
|
59
|
+
for (let i = 0; i < authorId.length; i += 1) {
|
|
60
|
+
hash ^= authorId.charCodeAt(i);
|
|
61
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
62
|
+
}
|
|
63
|
+
return hash;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Reduce a hash to a palette slot in `[0, AUTHOR_PALETTE.length)`. */
|
|
67
|
+
export function authorPaletteIndex(authorId: string): number {
|
|
68
|
+
return hashAuthorId(authorId) % AUTHOR_PALETTE.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a CSS color string for an author. Returns `undefined` when
|
|
73
|
+
* `authorId` is missing so callers can fall back to the default
|
|
74
|
+
* per-kind class (e.g. `bg-insert-soft`).
|
|
75
|
+
*/
|
|
76
|
+
export function getAuthorColor(authorId: string | undefined): string | undefined {
|
|
77
|
+
if (!authorId) return undefined;
|
|
78
|
+
return AUTHOR_PALETTE[authorPaletteIndex(authorId)];
|
|
16
79
|
}
|
|
17
80
|
|
|
18
81
|
export interface RevisionRangeState {
|
|
@@ -47,6 +110,8 @@ export function createRevisionDecorationModel(
|
|
|
47
110
|
status: rev.status,
|
|
48
111
|
actionability: rev.actionability,
|
|
49
112
|
isActive: rev.revisionId === activeRevisionId,
|
|
113
|
+
authorId: rev.authorId,
|
|
114
|
+
authorPaletteIndex: rev.authorId ? authorPaletteIndex(rev.authorId) : undefined,
|
|
50
115
|
};
|
|
51
116
|
}),
|
|
52
117
|
};
|
|
@@ -94,12 +159,17 @@ export function getRevisionHighlightClass(
|
|
|
94
159
|
|
|
95
160
|
const activeRing = state.hasActive ? " ring-1 ring-accent/30" : "";
|
|
96
161
|
|
|
97
|
-
switch (markupDisplay) {
|
|
98
|
-
case "
|
|
99
|
-
// In
|
|
162
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
163
|
+
case "no-markup":
|
|
164
|
+
// In no-markup mode, deletions are hidden entirely (caller should not render).
|
|
100
165
|
// Insertions render as normal text with no decoration.
|
|
101
166
|
return "";
|
|
102
|
-
case "
|
|
167
|
+
case "original":
|
|
168
|
+
// L6d.N2 — "original" shows what the doc looked like BEFORE the
|
|
169
|
+
// pending tracked changes: insertions are hidden (caller checks
|
|
170
|
+
// `shouldHideInOriginalMode`), deletions render as plain body text.
|
|
171
|
+
return "";
|
|
172
|
+
case "simple-markup":
|
|
103
173
|
if (state.hasInsertions) {
|
|
104
174
|
return `underline decoration-insert/60 decoration-1 underline-offset-2 text-primary${activeRing}`;
|
|
105
175
|
}
|
|
@@ -107,7 +177,7 @@ export function getRevisionHighlightClass(
|
|
|
107
177
|
return `text-secondary line-through decoration-danger/70 decoration-1${activeRing}`;
|
|
108
178
|
}
|
|
109
179
|
return activeRing;
|
|
110
|
-
case "all":
|
|
180
|
+
case "all-markup":
|
|
111
181
|
if (state.hasInsertions) {
|
|
112
182
|
return `text-primary bg-insert-soft/80 ring-1 ring-insert/20${activeRing}`;
|
|
113
183
|
}
|
|
@@ -118,6 +188,10 @@ export function getRevisionHighlightClass(
|
|
|
118
188
|
}
|
|
119
189
|
}
|
|
120
190
|
|
|
191
|
+
/**
|
|
192
|
+
* `no-markup` hides deletions (pretending the doc already accepted all
|
|
193
|
+
* pending changes). The caller skips rendering affected spans entirely.
|
|
194
|
+
*/
|
|
121
195
|
export function shouldHideInCleanMode(
|
|
122
196
|
model: RevisionDecorationModel | undefined,
|
|
123
197
|
from: number,
|
|
@@ -126,3 +200,17 @@ export function shouldHideInCleanMode(
|
|
|
126
200
|
const state = getRevisionRangeState(model, from, to);
|
|
127
201
|
return state.hasDeletions;
|
|
128
202
|
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* L6d.N2 — `original` mode hides insertions (pretending the pending
|
|
206
|
+
* batch of tracked changes was rejected). Caller skips rendering
|
|
207
|
+
* affected spans entirely.
|
|
208
|
+
*/
|
|
209
|
+
export function shouldHideInOriginalMode(
|
|
210
|
+
model: RevisionDecorationModel | undefined,
|
|
211
|
+
from: number,
|
|
212
|
+
to: number,
|
|
213
|
+
): boolean {
|
|
214
|
+
const state = getRevisionRangeState(model, from, to);
|
|
215
|
+
return state.hasInsertions;
|
|
216
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import type { RuntimeRenderSnapshot } from "../../api/public-types";
|
|
2
|
+
import {
|
|
3
|
+
normalizeMarkupDisplay,
|
|
4
|
+
type MarkupDisplay,
|
|
5
|
+
} from "../headless/comment-decoration-model";
|
|
2
6
|
|
|
3
7
|
type Revision = RuntimeRenderSnapshot["trackedChanges"]["revisions"][number];
|
|
4
|
-
type MarkupDisplay = "clean" | "simple" | "all";
|
|
5
8
|
|
|
6
9
|
export function selectVisibleRevisions(
|
|
7
10
|
revisions: readonly Revision[],
|
|
8
11
|
markupDisplay: MarkupDisplay,
|
|
9
12
|
): Revision[] {
|
|
10
|
-
switch (markupDisplay) {
|
|
11
|
-
case "
|
|
12
|
-
case "simple":
|
|
13
|
+
switch (normalizeMarkupDisplay(markupDisplay)) {
|
|
14
|
+
case "no-markup":
|
|
15
|
+
case "simple-markup":
|
|
16
|
+
case "original":
|
|
13
17
|
return revisions.filter(
|
|
14
18
|
(revision) =>
|
|
15
19
|
revision.status === "active" && revision.actionability === "actionable",
|
|
16
20
|
);
|
|
17
|
-
case "all":
|
|
21
|
+
case "all-markup":
|
|
18
22
|
return [...revisions];
|
|
19
23
|
}
|
|
20
24
|
}
|
|
@@ -23,7 +27,13 @@ export function describeEmptyRevisionState(
|
|
|
23
27
|
markupDisplay: MarkupDisplay,
|
|
24
28
|
totalCount: number,
|
|
25
29
|
): string {
|
|
26
|
-
|
|
30
|
+
const canonical = normalizeMarkupDisplay(markupDisplay);
|
|
31
|
+
if (
|
|
32
|
+
(canonical === "no-markup" ||
|
|
33
|
+
canonical === "simple-markup" ||
|
|
34
|
+
canonical === "original") &&
|
|
35
|
+
totalCount > 0
|
|
36
|
+
) {
|
|
27
37
|
return "Simple markup keeps the rail focused on actionable live changes. Switch to All to inspect preserve-only or historical revision records.";
|
|
28
38
|
}
|
|
29
39
|
|