@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.54",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -3058,12 +3058,16 @@ export interface WordReviewEditorRef {
|
|
|
3058
3058
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
3059
3059
|
clearWorkflowOverlay(): void;
|
|
3060
3060
|
/**
|
|
3061
|
-
*
|
|
3062
|
-
* overlay
|
|
3063
|
-
* `
|
|
3064
|
-
*
|
|
3065
|
-
*
|
|
3066
|
-
*
|
|
3061
|
+
* Return a structural clone of the currently-registered workflow
|
|
3062
|
+
* overlay, or `null` when no overlay has been set (or
|
|
3063
|
+
* `clearWorkflowOverlay` has been called). Intended for the canonical
|
|
3064
|
+
* host read-before-write pattern — read current, merge host-owned
|
|
3065
|
+
* fields (e.g. `candidates`), write back via `setWorkflowOverlay`
|
|
3066
|
+
* without clobbering engine-authored `scopes`, `workItems`, or the
|
|
3067
|
+
* overlay-level `metadataPersistence` default. `setWorkflowOverlay`
|
|
3068
|
+
* replaces the overlay wholesale; passing `scopes: []` will drop
|
|
3069
|
+
* every scope registered via `addScope` and cause subsequent
|
|
3070
|
+
* `replaceText` calls to block with `workflow_comment_only`.
|
|
3067
3071
|
*/
|
|
3068
3072
|
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
3069
3073
|
setSharedWorkflowState(state: SharedWorkflowState | null): void;
|
|
@@ -3304,7 +3308,31 @@ export interface WordReviewEditorProps {
|
|
|
3304
3308
|
suggestionsEnabled?: boolean;
|
|
3305
3309
|
chromePreset?: WordReviewEditorChromePreset;
|
|
3306
3310
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
3307
|
-
|
|
3311
|
+
/**
|
|
3312
|
+
* Controls how tracked changes and comments render. Accepts Word's
|
|
3313
|
+
* 4-mode grammar (`"all-markup" | "simple-markup" | "no-markup" | "original"`)
|
|
3314
|
+
* or the legacy triple (`"all" | "simple" | "clean"`), which maps 1:1
|
|
3315
|
+
* onto the first three canonical names. `"original"` is new in 6d.N2:
|
|
3316
|
+
* insertions are hidden, deletions render as plain body text so the
|
|
3317
|
+
* reviewer sees the pre-change state.
|
|
3318
|
+
*/
|
|
3319
|
+
markupDisplay?:
|
|
3320
|
+
| "all-markup"
|
|
3321
|
+
| "simple-markup"
|
|
3322
|
+
| "no-markup"
|
|
3323
|
+
| "original"
|
|
3324
|
+
| "clean"
|
|
3325
|
+
| "simple"
|
|
3326
|
+
| "all";
|
|
3327
|
+
/**
|
|
3328
|
+
* L6d.N2 — invoked when the user picks a different display mode from
|
|
3329
|
+
* the in-chrome selector. Values are always emitted in the canonical
|
|
3330
|
+
* Word grammar (`"all-markup" | "simple-markup" | "no-markup" | "original"`),
|
|
3331
|
+
* not the legacy aliases.
|
|
3332
|
+
*/
|
|
3333
|
+
onMarkupDisplayChange?: (
|
|
3334
|
+
value: "all-markup" | "simple-markup" | "no-markup" | "original",
|
|
3335
|
+
) => void;
|
|
3308
3336
|
/**
|
|
3309
3337
|
* @internal HARNESS-ONLY debug-ports token.
|
|
3310
3338
|
*
|
package/src/io/docx-session.ts
CHANGED
|
@@ -67,13 +67,12 @@ import {
|
|
|
67
67
|
getDocumentBackedWorkflowMetadata,
|
|
68
68
|
parseWorkflowPayloadEnvelopeFromPackage,
|
|
69
69
|
resolvePayloadPartPath,
|
|
70
|
+
resolveWorkflowPayloadPartPaths,
|
|
70
71
|
WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
71
72
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
|
|
72
73
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
73
74
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
|
|
74
75
|
WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
|
|
75
|
-
WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
|
|
76
|
-
WORKFLOW_PAYLOAD_PART_PATH,
|
|
77
76
|
WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
|
|
78
77
|
} from "./ooxml/workflow-payload.ts";
|
|
79
78
|
import {
|
|
@@ -2105,13 +2104,20 @@ function exportDocxEditorSession(
|
|
|
2105
2104
|
const hasSettingsSurface =
|
|
2106
2105
|
Boolean(state.sourceSettingsPartPath) ||
|
|
2107
2106
|
exportedSubParts?.settings !== undefined;
|
|
2107
|
+
const resolvedWorkflowPayloadPartPaths = resolveWorkflowPayloadPartPaths(
|
|
2108
|
+
state.sourcePackage,
|
|
2109
|
+
sessionState.documentId,
|
|
2110
|
+
);
|
|
2111
|
+
const internalEditorState = (
|
|
2112
|
+
options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined
|
|
2113
|
+
)?._editorState;
|
|
2108
2114
|
|
|
2109
2115
|
const exportSession = createExportSession(state.sourcePackage, [
|
|
2110
2116
|
state.sourceDocumentPartPath,
|
|
2111
2117
|
APP_PROPERTIES_PART_PATH,
|
|
2112
2118
|
CORE_PROPERTIES_PART_PATH,
|
|
2113
|
-
|
|
2114
|
-
|
|
2119
|
+
resolvedWorkflowPayloadPartPaths.payloadPartPath,
|
|
2120
|
+
resolvedWorkflowPayloadPartPaths.itemPropsPartPath,
|
|
2115
2121
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
2116
2122
|
numberingPartPath,
|
|
2117
2123
|
commentsPartPath,
|
|
@@ -2360,8 +2366,14 @@ function exportDocxEditorSession(
|
|
|
2360
2366
|
|
|
2361
2367
|
ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
|
|
2362
2368
|
// Schema 1.2: pass through editorState payload collected by the runtime channel.
|
|
2363
|
-
|
|
2364
|
-
|
|
2369
|
+
ensureWorkflowPayloadParts(
|
|
2370
|
+
exportSession,
|
|
2371
|
+
sessionState,
|
|
2372
|
+
currentDocument,
|
|
2373
|
+
state.sourcePackage,
|
|
2374
|
+
resolvedWorkflowPayloadPartPaths,
|
|
2375
|
+
internalEditorState,
|
|
2376
|
+
);
|
|
2365
2377
|
|
|
2366
2378
|
return {
|
|
2367
2379
|
bytes: exportSession.serialize(),
|
|
@@ -4087,6 +4099,10 @@ function ensureWorkflowPayloadParts(
|
|
|
4087
4099
|
sessionState: EditorSessionState,
|
|
4088
4100
|
document: CanonicalDocumentEnvelope,
|
|
4089
4101
|
sourcePackage: OpcPackage,
|
|
4102
|
+
resolvedPartPaths: {
|
|
4103
|
+
payloadPartPath: string;
|
|
4104
|
+
itemPropsPartPath: string;
|
|
4105
|
+
},
|
|
4090
4106
|
editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
|
|
4091
4107
|
): void {
|
|
4092
4108
|
const payloadParts = buildWorkflowPayloadParts({
|
|
@@ -4102,6 +4118,14 @@ function ensureWorkflowPayloadParts(
|
|
|
4102
4118
|
if (!payloadParts) {
|
|
4103
4119
|
return;
|
|
4104
4120
|
}
|
|
4121
|
+
if (
|
|
4122
|
+
payloadParts.payloadPartPath !== resolvedPartPaths.payloadPartPath ||
|
|
4123
|
+
payloadParts.itemPropsPartPath !== resolvedPartPaths.itemPropsPartPath
|
|
4124
|
+
) {
|
|
4125
|
+
throw new Error(
|
|
4126
|
+
"Workflow payload export resolved inconsistent customXml paths; export session ownership no longer matches payload serialization.",
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4105
4129
|
|
|
4106
4130
|
const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
|
|
4107
4131
|
const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
|
|
@@ -141,6 +141,10 @@ export const BROADCAST_COMMAND_TYPES: ReadonlySet<EditorCommand["type"]> = new S
|
|
|
141
141
|
"section.set-header-footer-link",
|
|
142
142
|
"content.insert-page-break",
|
|
143
143
|
"content.insert-table",
|
|
144
|
+
// C1: Shift+Tab list/paragraph de-indent — produces a document mutation, must broadcast
|
|
145
|
+
"text.outdent-tab",
|
|
146
|
+
// C2: host insertFragment() API — routes through executeEditorCommand same as other mutations
|
|
147
|
+
"fragment.insert",
|
|
144
148
|
]);
|
|
145
149
|
|
|
146
150
|
/**
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type CommandEvent,
|
|
23
23
|
} from "./event-types.ts";
|
|
24
24
|
import { computeBaseDocFingerprint } from "./base-doc-fingerprint.ts";
|
|
25
|
-
import type
|
|
25
|
+
import { CHECKPOINTS_KEY, type Checkpoint } from "./checkpoint-store.ts";
|
|
26
26
|
import { createWorkflowShared, type WorkflowSharedHandle } from "./workflow-shared.ts";
|
|
27
27
|
|
|
28
28
|
/** Shared Y.Map key — {@link SHARED_META_MAP_KEY}. */
|
|
@@ -30,7 +30,6 @@ const SHARED_META_MAP_KEY = "meta";
|
|
|
30
30
|
const META_BASE_DOC_HASH_KEY = "baseDocHash";
|
|
31
31
|
const META_SCHEMA_VERSION_KEY = "schemaVersion";
|
|
32
32
|
const META_CREATED_AT_KEY = "createdAt";
|
|
33
|
-
const CHECKPOINTS_KEY = "checkpoints";
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Lifecycle + correctness events surfaced by a
|
|
@@ -160,6 +160,7 @@ import {
|
|
|
160
160
|
createDocumentSectionSnapshots,
|
|
161
161
|
createSectionLocations,
|
|
162
162
|
createTocSnapshot,
|
|
163
|
+
findBookmarkNameForOffset,
|
|
163
164
|
findDocumentSectionSnapshot,
|
|
164
165
|
} from "./document-outline.ts";
|
|
165
166
|
import {
|
|
@@ -209,6 +210,7 @@ import type {
|
|
|
209
210
|
BlockNode,
|
|
210
211
|
FieldNode,
|
|
211
212
|
FieldRefreshStatus,
|
|
213
|
+
HyperlinkNode,
|
|
212
214
|
InlineNode,
|
|
213
215
|
PageMargins,
|
|
214
216
|
ParagraphNode,
|
|
@@ -5197,7 +5199,7 @@ function refreshDocumentTableOfContents(
|
|
|
5197
5199
|
} {
|
|
5198
5200
|
const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
|
|
5199
5201
|
let changed = false;
|
|
5200
|
-
let resultEntries: Array<{ level: number; text: string; pageIndex: number }> = [];
|
|
5202
|
+
let resultEntries: Array<{ level: number; text: string; pageIndex: number; bookmarkName?: string }> = [];
|
|
5201
5203
|
let changedFrom: number | undefined;
|
|
5202
5204
|
let changedTo: number | undefined;
|
|
5203
5205
|
const nextChildren = refreshBlocksWithCursor(document.content.children, (field, range) => {
|
|
@@ -5209,11 +5211,15 @@ function refreshDocumentTableOfContents(
|
|
|
5209
5211
|
: parseTocLevelRange(field.instruction);
|
|
5210
5212
|
const entries = navigation.headings
|
|
5211
5213
|
.filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
|
|
5212
|
-
.map((heading) =>
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5214
|
+
.map((heading) => {
|
|
5215
|
+
const bookmarkName = findBookmarkNameForOffset(document, heading.offset);
|
|
5216
|
+
return {
|
|
5217
|
+
level: heading.level,
|
|
5218
|
+
text: heading.text,
|
|
5219
|
+
pageIndex: heading.pageIndex,
|
|
5220
|
+
...(bookmarkName ? { bookmarkName } : {}),
|
|
5221
|
+
};
|
|
5222
|
+
});
|
|
5217
5223
|
if (resultEntries.length === 0) {
|
|
5218
5224
|
resultEntries = entries;
|
|
5219
5225
|
}
|
|
@@ -5410,12 +5416,20 @@ function buildInlineNodesFromDisplayText(text: string): InlineNode[] {
|
|
|
5410
5416
|
* resolver, falls back to the raw `pageIndex + 1` (pre-P5 behavior).
|
|
5411
5417
|
*/
|
|
5412
5418
|
function buildTocInlineNodes(
|
|
5413
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5419
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5414
5420
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5415
5421
|
): InlineNode[] {
|
|
5416
5422
|
const children: InlineNode[] = [];
|
|
5417
5423
|
entries.forEach((entry, index) => {
|
|
5418
|
-
|
|
5424
|
+
if (entry.bookmarkName) {
|
|
5425
|
+
children.push({
|
|
5426
|
+
type: "hyperlink",
|
|
5427
|
+
href: `#${entry.bookmarkName}`,
|
|
5428
|
+
children: [{ type: "text", text: entry.text }],
|
|
5429
|
+
} as HyperlinkNode);
|
|
5430
|
+
} else {
|
|
5431
|
+
children.push({ type: "text", text: entry.text });
|
|
5432
|
+
}
|
|
5419
5433
|
children.push({ type: "tab" });
|
|
5420
5434
|
const displayed = resolveDisplayPageNumber?.(entry.pageIndex);
|
|
5421
5435
|
children.push({
|
|
@@ -5431,7 +5445,7 @@ function buildTocInlineNodes(
|
|
|
5431
5445
|
|
|
5432
5446
|
/** Test-only export of `buildTocInlineNodes` (P5 unit tests). */
|
|
5433
5447
|
export function __buildTocInlineNodes(
|
|
5434
|
-
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number }>,
|
|
5448
|
+
entries: ReadonlyArray<{ level: number; text: string; pageIndex: number; bookmarkName?: string }>,
|
|
5435
5449
|
resolveDisplayPageNumber?: (pageIndex: number) => number | null,
|
|
5436
5450
|
): InlineNode[] {
|
|
5437
5451
|
return buildTocInlineNodes(entries, resolveDisplayPageNumber);
|
|
@@ -59,6 +59,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
59
59
|
swapMeasurementProvider: () => undefined,
|
|
60
60
|
invalidateMeasurementCache: () => undefined,
|
|
61
61
|
getTableRenderPlan: () => null,
|
|
62
|
+
getTableBodyYOffsetOnPage: () => null,
|
|
62
63
|
getDirtyFieldFamilies: () => [],
|
|
63
64
|
getFieldDirtinessReport: () => emptyReport,
|
|
64
65
|
setVisibleBlockRange: () => undefined,
|
|
@@ -113,6 +113,13 @@
|
|
|
113
113
|
* pages so chrome can prepend header rows visually. No
|
|
114
114
|
* pixel-geometry change; cache envelopes from v11 invalidate
|
|
115
115
|
* because the table-render-plan contract changed.
|
|
116
|
+
* 13 — Lane 6d.U2 canvas-seam pill polish: the canvas-posture page-break
|
|
117
|
+
* widget's "N / M" badge is promoted from transparent text over the
|
|
118
|
+
* dotted seam to a true pill with `--radius-pill` geometry, hairline
|
|
119
|
+
* `--color-border-default` border, and `--shadow-soft`. Widget DOM
|
|
120
|
+
* shape changed (new `data-variant="pill"` attribute; additional
|
|
121
|
+
* inline style declarations on the badge). Cache envelopes from v12
|
|
122
|
+
* invalidate because the decoration's cacheable DOM shape changed.
|
|
116
123
|
* 13 — Lane 3a P14.c: render-kernel gains a single-slot `DecorationIndex`
|
|
117
124
|
* cache keyed on (revision, activeStory.kind, zoom.pxPerTwip, and
|
|
118
125
|
* reference equality on each decoration source). When layout
|
|
@@ -122,8 +129,58 @@
|
|
|
122
129
|
* rebuild path (on every keystroke that triggers a layout event).
|
|
123
130
|
* No pixel-geometry change; cache envelopes from v12 invalidate
|
|
124
131
|
* because the render-kernel source changed.
|
|
132
|
+
* 14 — Lane 3a Slice 5: `RuntimeBlockFragment` gains `resolvedStyleChainRef`
|
|
133
|
+
* (block's styleId) and `numberingInstanceId` (block's list-instance id).
|
|
134
|
+
* `analyzeInvalidation` for `styles-change` (when `dirtyStyleIds` is
|
|
135
|
+
* supplied) and `numbering-change` (when `numberingInstanceId` is
|
|
136
|
+
* supplied) now return `scope: "bounded"` starting from the first page
|
|
137
|
+
* whose fragments reference the dirty style / instance. Fallback to
|
|
138
|
+
* `scope: "full"` when payload is absent or no match found. No
|
|
139
|
+
* pixel-geometry change; cache envelopes from v13 invalidate because
|
|
140
|
+
* the fragment shape and invalidation-scope contract changed.
|
|
141
|
+
* 15 — Bug fixes: `pageNodesStructurallyEqual` now compares
|
|
142
|
+
* `lineBoxes.length` and `noteAllocations.length` as structural
|
|
143
|
+
* proxies to prevent stale-node reuse when line geometry changes
|
|
144
|
+
* with stable fragment IDs (L1). `analyzeSectionChange` normalizes
|
|
145
|
+
* `dirtySectionRange` to guarantee from ≤ to for all graph states
|
|
146
|
+
* including empty-sections fallback (L2).
|
|
147
|
+
* 16 — Bug fixes: `diffRenderFrames` now flags pages whose physical frame
|
|
148
|
+
* changed (but block regions are stable) with `pageFrameChanged: true`
|
|
149
|
+
* in `changedPages` so consumers can re-project without a block-region
|
|
150
|
+
* signal (R1). Chrome reservation changes (`railLaneTwips`,
|
|
151
|
+
* `balloonLaneTwips`, `footnoteAreaTwips`, `pageFrameWidthPx`,
|
|
152
|
+
* `pageFrameHeightPx`) now trigger `changedPages` so overlay
|
|
153
|
+
* re-projection is not silently skipped (R2).
|
|
154
|
+
* 17 — Lane 3a Slice 2 + R4: `WordReviewEditorLayoutFacet` gains
|
|
155
|
+
* `getTableBodyYOffsetOnPage(blockId, pageIndex)` which returns the
|
|
156
|
+
* Y offset (in twips from body top) of the table's first fragment on
|
|
157
|
+
* a given page by summing prior body-fragment heights. Used by the
|
|
158
|
+
* new `TwTableContinuationHeader` chrome overlay to position repeated
|
|
159
|
+
* header rows on continuation pages of multi-page tables — no DOM
|
|
160
|
+
* measurement, layout-engine fragment heights only. No cached-geometry
|
|
161
|
+
* change; cache envelopes from v16 invalidate because the facet
|
|
162
|
+
* interface changed.
|
|
163
|
+
* 18 — Lane 3a Slice 6: `buildPageStackFromWithSplits` no longer discards
|
|
164
|
+
* `resumeAt.startOffset`. When `startOffset > 0` and no block
|
|
165
|
+
* straddles the dirty section boundary, only sections at and after
|
|
166
|
+
* the first dirty section are paginated; the resulting page indices
|
|
167
|
+
* are shifted by `startPageIndex` so they align with the global graph.
|
|
168
|
+
* Full-paginate + tail-slice fallback used when a block straddles the
|
|
169
|
+
* section boundary (safety guard). This eliminates re-paginating
|
|
170
|
+
* settled head sections on every bounded-invalidation relayout.
|
|
171
|
+
* No pixel-geometry change; cache envelopes from v17 invalidate
|
|
172
|
+
* because `buildPageStackFromWithSplits` output contract changed.
|
|
173
|
+
* 19 — Slice 5 bug-fix: `analyzeNumberingChange` now honors its own
|
|
174
|
+
* "Fallback to full rebuild when absent or no match" contract. When
|
|
175
|
+
* `numberingInstanceId` is supplied but no materialized fragment
|
|
176
|
+
* matches it, the analyzer returns `scope: "full"` +
|
|
177
|
+
* `requiresFullRecompute: true` instead of the prior "bounded over
|
|
178
|
+
* full range" shortcut, which bypassed the safety guard and could
|
|
179
|
+
* leak stale field-family projections. No pixel-geometry change;
|
|
180
|
+
* cache envelopes from v18 invalidate because the invalidation
|
|
181
|
+
* classifier's contract corrected.
|
|
125
182
|
*/
|
|
126
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
183
|
+
export const LAYOUT_ENGINE_VERSION = 19 as const;
|
|
127
184
|
|
|
128
185
|
/**
|
|
129
186
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
* edit offsets (§analyzeContentEdit).
|
|
14
14
|
* - `section-change`: bounded to the first page of the affected section
|
|
15
15
|
* onward — P10 Phase C (§analyzeSectionChange).
|
|
16
|
-
* - `
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
19
|
-
*
|
|
16
|
+
* - `styles-change` (Slice 5): when `dirtyStyleIds` is supplied, bounded
|
|
17
|
+
* to the first page whose fragments carry a matching `resolvedStyleChainRef`.
|
|
18
|
+
* Fallback to full rebuild when the payload is absent or no match found.
|
|
19
|
+
* - `numbering-change` (Slice 5): when `numberingInstanceId` is supplied,
|
|
20
|
+
* bounded to the first page whose fragments carry a matching
|
|
21
|
+
* `numberingInstanceId`. Fallback to full rebuild when absent or no match.
|
|
20
22
|
*
|
|
21
23
|
* Remaining full-rebuild triggers:
|
|
22
|
-
* - `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* - `theme-change`: theme token changes cascade through the entire
|
|
25
|
+
* style chain; no fragment-level tightening possible without knowing
|
|
26
|
+
* which blocks reference theme tokens.
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
29
|
import type { LayoutInvalidationReason } from "./paginated-layout-engine.ts";
|
|
28
|
-
import type {
|
|
30
|
+
import type { RuntimeBlockFragment, RuntimePageGraph } from "./page-graph.ts";
|
|
29
31
|
import type { ResolvedDocumentSection } from "../document-layout.ts";
|
|
30
32
|
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -92,15 +94,16 @@ export function analyzeInvalidation(
|
|
|
92
94
|
|
|
93
95
|
switch (reason.kind) {
|
|
94
96
|
case "full":
|
|
95
|
-
case "styles-change":
|
|
96
97
|
case "theme-change":
|
|
97
|
-
// These affect the entire document
|
|
98
98
|
return {
|
|
99
99
|
scope: "full",
|
|
100
100
|
requiresFullRecompute: true,
|
|
101
101
|
dirtyFieldFamilies: ["PAGE", "NUMPAGES", "SECTIONPAGES"],
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
+
case "styles-change":
|
|
105
|
+
return analyzeStylesChange(reason, graph);
|
|
106
|
+
|
|
104
107
|
case "content-edit":
|
|
105
108
|
return analyzeContentEdit(reason, graph);
|
|
106
109
|
|
|
@@ -108,26 +111,9 @@ export function analyzeInvalidation(
|
|
|
108
111
|
return analyzeSectionChange(reason, graph);
|
|
109
112
|
|
|
110
113
|
case "numbering-change":
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
scope: "full",
|
|
114
|
-
requiresFullRecompute: true,
|
|
115
|
-
dirtyFieldFamilies: [],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
scope: "bounded",
|
|
120
|
-
requiresFullRecompute: false,
|
|
121
|
-
dirtyPageRange: {
|
|
122
|
-
firstPageIndex: 0,
|
|
123
|
-
lastPageIndex: Math.max(0, graph.pages.length - 1),
|
|
124
|
-
},
|
|
125
|
-
dirtySectionRange: null,
|
|
126
|
-
dirtyFieldFamilies: [],
|
|
127
|
-
};
|
|
114
|
+
return analyzeNumberingChange(reason, graph);
|
|
128
115
|
|
|
129
116
|
case "field-refresh":
|
|
130
|
-
// Field refresh doesn't change layout, just field display values
|
|
131
117
|
return {
|
|
132
118
|
scope: "field-only",
|
|
133
119
|
requiresFullRecompute: false,
|
|
@@ -277,12 +263,18 @@ function analyzeSectionChange(
|
|
|
277
263
|
reason.sectionIndex,
|
|
278
264
|
);
|
|
279
265
|
if (firstPageOfSection < 0) {
|
|
266
|
+
// L2: Clamp `from` so the range is never backward when the affected
|
|
267
|
+
// section index exceeds the number of materialized sections (e.g.
|
|
268
|
+
// section-change on a section that hasn't rendered yet, or an empty
|
|
269
|
+
// graph). Without the clamp, {from:5, to:0} is a backward range that
|
|
270
|
+
// confuses consumers iterating [from..to].
|
|
271
|
+
const lastSectionIdx = Math.max(0, graph.sections.length - 1);
|
|
280
272
|
return {
|
|
281
273
|
scope: "full",
|
|
282
274
|
requiresFullRecompute: true,
|
|
283
275
|
dirtySectionRange: {
|
|
284
|
-
from: reason.sectionIndex,
|
|
285
|
-
to:
|
|
276
|
+
from: Math.min(reason.sectionIndex, lastSectionIdx),
|
|
277
|
+
to: lastSectionIdx,
|
|
286
278
|
},
|
|
287
279
|
dirtyFieldFamilies: ["PAGE", "NUMPAGES", "SECTIONPAGES"],
|
|
288
280
|
};
|
|
@@ -321,3 +313,131 @@ function findFirstPageIndexForSection(
|
|
|
321
313
|
// rendered tail. Caller treats this as a full-rebuild trigger.
|
|
322
314
|
return -1;
|
|
323
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Slice 5 — narrow `styles-change` invalidation.
|
|
319
|
+
*
|
|
320
|
+
* When `reason.dirtyStyleIds` is present, scan fragments for the first
|
|
321
|
+
* fragment whose `resolvedStyleChainRef` matches any dirty style id.
|
|
322
|
+
* The page containing that fragment is the first that must rebuild.
|
|
323
|
+
*
|
|
324
|
+
* Fallback to full rebuild when `dirtyStyleIds` is absent, empty, or no
|
|
325
|
+
* fragment matches (e.g. graphs built before Slice 5 shipped).
|
|
326
|
+
*/
|
|
327
|
+
function analyzeStylesChange(
|
|
328
|
+
reason: Extract<LayoutInvalidationReason, { kind: "styles-change" }>,
|
|
329
|
+
graph: RuntimePageGraph,
|
|
330
|
+
): InvalidationResult {
|
|
331
|
+
if (!reason.dirtyStyleIds || reason.dirtyStyleIds.length === 0) {
|
|
332
|
+
return {
|
|
333
|
+
scope: "full",
|
|
334
|
+
requiresFullRecompute: true,
|
|
335
|
+
dirtyFieldFamilies: ["PAGE", "NUMPAGES", "SECTIONPAGES"],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const dirtySet = new Set(reason.dirtyStyleIds);
|
|
340
|
+
const firstDirtyPageIndex = findFirstPageIndexByFragmentPredicate(
|
|
341
|
+
graph,
|
|
342
|
+
(f) =>
|
|
343
|
+
f.resolvedStyleChainRef !== undefined &&
|
|
344
|
+
dirtySet.has(f.resolvedStyleChainRef),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (firstDirtyPageIndex < 0) {
|
|
348
|
+
return {
|
|
349
|
+
scope: "full",
|
|
350
|
+
requiresFullRecompute: true,
|
|
351
|
+
dirtyFieldFamilies: ["PAGE", "NUMPAGES", "SECTIONPAGES"],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
scope: "bounded",
|
|
357
|
+
requiresFullRecompute: false,
|
|
358
|
+
dirtyPageRange: {
|
|
359
|
+
firstPageIndex: firstDirtyPageIndex,
|
|
360
|
+
lastPageIndex: Math.max(0, graph.pages.length - 1),
|
|
361
|
+
},
|
|
362
|
+
dirtySectionRange: null,
|
|
363
|
+
dirtyFieldFamilies: [],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Slice 5 — narrow `numbering-change` invalidation.
|
|
369
|
+
*
|
|
370
|
+
* When `reason.numberingInstanceId` is present, find the first page
|
|
371
|
+
* containing a fragment with matching `numberingInstanceId` and start
|
|
372
|
+
* the rebuild there. Fallback to full rebuild when absent or no match.
|
|
373
|
+
*/
|
|
374
|
+
function analyzeNumberingChange(
|
|
375
|
+
reason: Extract<LayoutInvalidationReason, { kind: "numbering-change" }>,
|
|
376
|
+
graph: RuntimePageGraph,
|
|
377
|
+
): InvalidationResult {
|
|
378
|
+
if (!reason.numberingInstanceId) {
|
|
379
|
+
return {
|
|
380
|
+
scope: "full",
|
|
381
|
+
requiresFullRecompute: true,
|
|
382
|
+
dirtyFieldFamilies: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const instanceId = reason.numberingInstanceId;
|
|
387
|
+
const firstDirtyPageIndex = findFirstPageIndexByFragmentPredicate(
|
|
388
|
+
graph,
|
|
389
|
+
(f) => f.numberingInstanceId === instanceId,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (firstDirtyPageIndex < 0) {
|
|
393
|
+
// No fragment matches (e.g. graph has no fragments yet, or the
|
|
394
|
+
// numberingInstanceId references an instance that hasn't materialized on
|
|
395
|
+
// any page). Per the function's contract — "Fallback to full rebuild
|
|
396
|
+
// when absent or no match" — return a full rebuild so the caller
|
|
397
|
+
// re-paginates from scratch rather than a bounded pass over stale
|
|
398
|
+
// geometry.
|
|
399
|
+
return {
|
|
400
|
+
scope: "full",
|
|
401
|
+
requiresFullRecompute: true,
|
|
402
|
+
dirtyFieldFamilies: [],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
scope: "bounded",
|
|
408
|
+
requiresFullRecompute: false,
|
|
409
|
+
dirtyPageRange: {
|
|
410
|
+
firstPageIndex: firstDirtyPageIndex,
|
|
411
|
+
lastPageIndex: Math.max(0, graph.pages.length - 1),
|
|
412
|
+
},
|
|
413
|
+
dirtySectionRange: null,
|
|
414
|
+
dirtyFieldFamilies: [],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Find the pageIndex of the first non-filler page whose fragments include
|
|
420
|
+
* at least one fragment satisfying `predicate`. Returns -1 if none found.
|
|
421
|
+
*/
|
|
422
|
+
function findFirstPageIndexByFragmentPredicate(
|
|
423
|
+
graph: RuntimePageGraph,
|
|
424
|
+
predicate: (f: RuntimeBlockFragment) => boolean,
|
|
425
|
+
): number {
|
|
426
|
+
const pageIdToIndex = new Map<string, number>();
|
|
427
|
+
for (const page of graph.pages) {
|
|
428
|
+
if (!page.isBlankFiller) {
|
|
429
|
+
pageIdToIndex.set(page.pageId, page.pageIndex);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let firstDirtyPageIndex = -1;
|
|
434
|
+
for (const fragment of graph.fragments) {
|
|
435
|
+
if (!predicate(fragment)) continue;
|
|
436
|
+
const pageIndex = pageIdToIndex.get(fragment.pageId);
|
|
437
|
+
if (pageIndex === undefined) continue;
|
|
438
|
+
if (firstDirtyPageIndex < 0 || pageIndex < firstDirtyPageIndex) {
|
|
439
|
+
firstDirtyPageIndex = pageIndex;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return firstDirtyPageIndex;
|
|
443
|
+
}
|
|
@@ -149,6 +149,18 @@ export interface RuntimeBlockFragment {
|
|
|
149
149
|
to: number;
|
|
150
150
|
totalRows: number;
|
|
151
151
|
};
|
|
152
|
+
/**
|
|
153
|
+
* Slice 5 — opaque style-chain ref derived from the block's `styleId`.
|
|
154
|
+
* Used by `analyzeStylesChange` to bound invalidation to the first page
|
|
155
|
+
* that carries a fragment referencing a dirty style.
|
|
156
|
+
*/
|
|
157
|
+
resolvedStyleChainRef?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Slice 5 — numbering instance id copied from the block's `numbering`
|
|
160
|
+
* field. Used by `analyzeNumberingChange` to bound invalidation to the
|
|
161
|
+
* first page that carries a fragment from the affected list instance.
|
|
162
|
+
*/
|
|
163
|
+
numberingInstanceId?: string;
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
export interface RuntimeLineBox {
|
|
@@ -629,6 +641,13 @@ function pageNodesStructurallyEqual(
|
|
|
629
641
|
for (let i = 0; i < aFoot.length; i += 1) {
|
|
630
642
|
if (!regionFragmentsEqual(aFoot[i]!, bFoot[i]!)) return false;
|
|
631
643
|
}
|
|
644
|
+
// L1: Include line-box and note-allocation counts as structural proxies.
|
|
645
|
+
// Fragment IDs staying stable while line geometry changes (font-metric
|
|
646
|
+
// change, float-wrap) would allow stale node reuse without these guards.
|
|
647
|
+
// Length-only is O(1) and catches the common case; deep baseline
|
|
648
|
+
// comparison is deferred.
|
|
649
|
+
if (a.lineBoxes.length !== b.lineBoxes.length) return false;
|
|
650
|
+
if (a.noteAllocations.length !== b.noteAllocations.length) return false;
|
|
632
651
|
return true;
|
|
633
652
|
}
|
|
634
653
|
|