@beyondwork/docx-react-component 1.0.52 → 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.
Files changed (103) hide show
  1. package/package.json +31 -40
  2. package/src/api/public-types.ts +67 -7
  3. package/src/io/chart-preview-resolver.ts +41 -0
  4. package/src/io/docx-session.ts +217 -23
  5. package/src/runtime/collab/checkpoint-store.ts +1 -1
  6. package/src/runtime/collab/event-types.ts +4 -0
  7. package/src/runtime/collab/runtime-collab-sync.ts +88 -8
  8. package/src/runtime/document-runtime.ts +182 -9
  9. package/src/runtime/layout/inert-layout-facet.ts +1 -0
  10. package/src/runtime/layout/layout-engine-version.ts +97 -2
  11. package/src/runtime/layout/layout-invalidation.ts +150 -30
  12. package/src/runtime/layout/page-graph.ts +19 -0
  13. package/src/runtime/layout/paginated-layout-engine.ts +128 -19
  14. package/src/runtime/layout/project-block-fragments.ts +27 -0
  15. package/src/runtime/layout/public-facet.ts +70 -1
  16. package/src/runtime/prerender/cache-envelope.ts +30 -0
  17. package/src/runtime/prerender/customxml-cache.ts +17 -3
  18. package/src/runtime/prerender/prerender-document.ts +17 -1
  19. package/src/runtime/render/render-frame-diff.ts +38 -2
  20. package/src/runtime/render/render-kernel.ts +67 -19
  21. package/src/runtime/surface-projection.ts +28 -0
  22. package/src/runtime/table-schema.ts +27 -0
  23. package/src/runtime/table-style-resolver.ts +51 -0
  24. package/src/ui/WordReviewEditor.tsx +6 -3
  25. package/src/ui/editor-runtime-boundary.ts +39 -2
  26. package/src/ui/headless/comment-decoration-model.ts +60 -5
  27. package/src/ui/headless/revision-decoration-model.ts +94 -6
  28. package/src/ui/shared/revision-filters.ts +16 -6
  29. package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
  30. package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
  31. package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
  32. package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
  33. package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
  34. package/src/ui-tailwind/chart/render/area.tsx +277 -0
  35. package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
  36. package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
  37. package/src/ui-tailwind/chart/render/combo.tsx +85 -0
  38. package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
  39. package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
  40. package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
  41. package/src/ui-tailwind/chart/render/line.tsx +363 -0
  42. package/src/ui-tailwind/chart/render/number-format.ts +120 -16
  43. package/src/ui-tailwind/chart/render/pie.tsx +275 -0
  44. package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
  45. package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
  46. package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
  47. package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
  48. package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
  49. package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
  50. package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
  51. package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
  52. package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
  53. package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
  54. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
  55. package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
  56. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
  57. package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
  58. package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
  59. package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
  60. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
  61. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
  62. package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
  63. package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
  64. package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
  65. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
  66. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
  67. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
  68. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
  69. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
  70. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
  71. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
  72. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
  73. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
  74. package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
  75. package/src/ui-tailwind/editor-surface/perf-probe.ts +1 -0
  76. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
  77. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +54 -0
  78. package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
  79. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +107 -3
  80. package/src/ui-tailwind/index.ts +11 -0
  81. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +2 -2
  82. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +274 -0
  83. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +15 -2
  84. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +15 -2
  85. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +19 -147
  86. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
  87. package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
  88. package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
  89. package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
  90. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
  91. package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
  92. package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
  93. package/src/ui-tailwind/theme/chart-palette-adapter.ts +57 -0
  94. package/src/ui-tailwind/theme/editor-theme.css +275 -46
  95. package/src/ui-tailwind/theme/tokens.css +345 -0
  96. package/src/ui-tailwind/theme/tokens.ts +313 -0
  97. package/src/ui-tailwind/theme/use-density.ts +60 -0
  98. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
  99. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
  100. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
  101. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
  102. package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
  103. package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@beyondwork/docx-react-component",
3
3
  "publisher": "beyondwork",
4
- "version": "1.0.52",
4
+ "version": "1.0.54",
5
5
  "description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
6
- "packageManager": "pnpm@10.30.3",
7
6
  "type": "module",
8
7
  "sideEffects": [
9
8
  "**/*.css"
@@ -93,35 +92,6 @@
93
92
  "./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
94
93
  },
95
94
  "types": "./src/index.ts",
96
- "scripts": {
97
- "build": "tsup",
98
- "test": "bash scripts/run-workspace-tests.sh",
99
- "test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
100
- "test:repo:all": "node scripts/run-repo-tests.mjs all",
101
- "test:repo:optional": "node scripts/run-repo-tests.mjs optional",
102
- "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
103
- "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
104
- "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
105
- "test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
106
- "test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
107
- "visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
108
- "mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
109
- "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
110
- "lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
111
- "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
112
- "lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
113
- "lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
114
- "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
115
- "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
116
- "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
117
- "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
118
- "wave:launch:managed": "bash scripts/wave-launch.sh",
119
- "wave:status": "bash scripts/wave-status.sh",
120
- "wave:watch": "bash scripts/wave-watch.sh --follow",
121
- "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
122
- "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
123
- "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
124
- },
125
95
  "keywords": [
126
96
  "docx",
127
97
  "word",
@@ -205,14 +175,35 @@
205
175
  "y-protocols": "^1.0.7",
206
176
  "yjs": "^13.6.30"
207
177
  },
208
- "pnpm": {
209
- "onlyBuiltDependencies": [
210
- "esbuild",
211
- "sharp"
212
- ],
213
- "overrides": {
214
- "react": "19.2.4",
215
- "react-dom": "19.2.4"
216
- }
178
+ "scripts": {
179
+ "build": "tsup",
180
+ "test": "bash scripts/run-workspace-tests.sh",
181
+ "test:repo": "node scripts/ci-check-layout-engine-version.mjs && node scripts/run-repo-tests.mjs core",
182
+ "test:repo:all": "node scripts/run-repo-tests.mjs all",
183
+ "test:repo:optional": "node scripts/run-repo-tests.mjs optional",
184
+ "test:repo:browser-ui": "node scripts/run-repo-tests.mjs browser-ui",
185
+ "test:wcag-audit": "node scripts/run-repo-tests.mjs wcag-audit",
186
+ "test:harness": "pnpm --filter @docx-react-component/react-word-editor-harness test",
187
+ "test:visual": "VISUAL_SMOKE_PROFILE=bare pnpm exec playwright test --project=chromium",
188
+ "test:visual:chrome": "VISUAL_SMOKE_PROFILE=chrome-cycle pnpm exec playwright test --project=chromium",
189
+ "visual:list-runs": "node scripts/visual-smoke-list-runs.mjs",
190
+ "mcp:visual-smoke": "node scripts/visual-smoke-mcp.mjs",
191
+ "lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
192
+ "lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
193
+ "lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
194
+ "lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
195
+ "lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
196
+ "generate:token-reference": "node scripts/generate-token-reference.mjs",
197
+ "check:token-reference": "node scripts/generate-token-reference.mjs --check",
198
+ "context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
199
+ "wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
200
+ "wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
201
+ "wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
202
+ "wave:launch:managed": "bash scripts/wave-launch.sh",
203
+ "wave:status": "bash scripts/wave-status.sh",
204
+ "wave:watch": "bash scripts/wave-watch.sh --follow",
205
+ "wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
206
+ "wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
207
+ "harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
217
208
  }
218
209
  }
@@ -892,6 +892,13 @@ export interface SurfaceTableCellSnapshot {
892
892
  borderRight?: string | null;
893
893
  borderBottom?: string | null;
894
894
  borderLeft?: string | null;
895
+ /**
896
+ * R3.a Phase 2: per-cell text-flow direction copied from
897
+ * `TableCellNode.textDirection`. The node-view maps these to CSS
898
+ * `writing-mode` (`tbRl` → `vertical-rl`, `btLr` → `vertical-lr`, `lrTb` →
899
+ * unset / horizontal default). Cells with no direction stay horizontal.
900
+ */
901
+ textDirection?: "lrTb" | "tbRl" | "btLr" | null;
895
902
  /**
896
903
  * R2a: space-joined CSS class names from the resolved table-style conditional
897
904
  * regions (e.g. "band-firstRow band-band1Horz"). Consumers apply these to the
@@ -985,6 +992,31 @@ export type SurfaceBlockSnapshot =
985
992
  noHBand?: boolean;
986
993
  noVBand?: boolean;
987
994
  };
995
+ /**
996
+ * R3.a Phase 2: resolved table-level properties (width / layoutMode /
997
+ * cellSpacing / borders) projected from the canonical TableNode + its
998
+ * resolved table-style cascade. Borders carry the typed `BorderSpec`
999
+ * per side; the node-view converts top/right/bottom/left to CSS
1000
+ * shorthand. insideH / insideV are stamped here for round-trip and
1001
+ * future renderer use, but at render time their visual effect is
1002
+ * realized through per-cell borders on the cell snapshot (CSS has no
1003
+ * direct "table-inside-border" property).
1004
+ */
1005
+ tableResolved?: {
1006
+ width?: number | null;
1007
+ widthType?: "auto" | "dxa" | "pct" | "nil" | null;
1008
+ layoutMode?: "fixed" | "autofit" | null;
1009
+ cellSpacing?: number | null;
1010
+ cellSpacingType?: "auto" | "dxa" | "pct" | "nil" | null;
1011
+ borders?: {
1012
+ top?: { value?: string; size?: number; space?: number; color?: string } | null;
1013
+ right?: { value?: string; size?: number; space?: number; color?: string } | null;
1014
+ bottom?: { value?: string; size?: number; space?: number; color?: string } | null;
1015
+ left?: { value?: string; size?: number; space?: number; color?: string } | null;
1016
+ insideH?: { value?: string; size?: number; space?: number; color?: string } | null;
1017
+ insideV?: { value?: string; size?: number; space?: number; color?: string } | null;
1018
+ } | null;
1019
+ };
988
1020
  rows: SurfaceTableRowSnapshot[];
989
1021
  }
990
1022
  | {
@@ -3026,12 +3058,16 @@ export interface WordReviewEditorRef {
3026
3058
  setWorkflowOverlay(overlay: WorkflowOverlay): void;
3027
3059
  clearWorkflowOverlay(): void;
3028
3060
  /**
3029
- * Read the current runtime-backed `WorkflowOverlay`, or `null` when no
3030
- * overlay is active. Hosts use this for read-modify-write flows against
3031
- * `setWorkflowOverlay(...)` notably to preserve engine-authored scopes
3032
- * minted via `addScope(...)` when updating candidates / work items.
3033
- * Returns a defensive clone; mutating the returned object does not affect
3034
- * runtime state.
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`.
3035
3071
  */
3036
3072
  getWorkflowOverlay(): WorkflowOverlay | null;
3037
3073
  setSharedWorkflowState(state: SharedWorkflowState | null): void;
@@ -3272,7 +3308,31 @@ export interface WordReviewEditorProps {
3272
3308
  suggestionsEnabled?: boolean;
3273
3309
  chromePreset?: WordReviewEditorChromePreset;
3274
3310
  chromeOptions?: Partial<WordReviewEditorChromeOptions>;
3275
- markupDisplay?: "clean" | "simple" | "all";
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;
3276
3336
  /**
3277
3337
  * @internal HARNESS-ONLY debug-ports token.
3278
3338
  *
@@ -138,6 +138,47 @@ export async function resolveChartPreviewsForDocument(
138
138
  return applyResolutions(doc, successful);
139
139
  }
140
140
 
141
+ /**
142
+ * C3b — Phase A/B deferred chart preview resolution.
143
+ *
144
+ * Phase A (cheap, synchronous): check whether there are any unresolved
145
+ * chart_preview nodes. If not, return immediately without scheduling
146
+ * anything. The collect walk is O(paragraphs) and takes ~0 ms.
147
+ *
148
+ * Phase B (deferred): schedule the actual `renderChartPreview` calls
149
+ * outside the critical load path via requestIdleCallback (browser) or
150
+ * setTimeout(0) (Node / SSR). Fires `onReady` with the resolved
151
+ * CanonicalDocument when all charts complete.
152
+ *
153
+ * The caller (loadDocxEditorSessionAsync) returns the session without
154
+ * chart previews. When `onReady` fires, the consumer can update the
155
+ * runtime's media catalog (e.g. via a follow-up `hydrateChartPreviews`
156
+ * call, or by accepting the updated CanonicalDocument into a new
157
+ * progressive render). Expected win: 30–80 ms removed from the warm
158
+ * cold-open path on extra-large docs with ~12 charts.
159
+ */
160
+ export function scheduleChartPreviewResolution(
161
+ doc: CanonicalDocument,
162
+ pkg: OpcPackage,
163
+ adapter: EditorHostAdapter | undefined,
164
+ onReady: (resolved: CanonicalDocument) => void,
165
+ ): void {
166
+ if (!adapter?.renderChartPreview) return;
167
+ // Phase A: fast collect — avoids scheduling idle work when there are no
168
+ // unresolved charts (the common case for most non-chart documents).
169
+ const pending = collectUnresolvedChartPreviews(doc, pkg);
170
+ if (pending.length === 0) return;
171
+
172
+ const schedule =
173
+ typeof requestIdleCallback !== "undefined"
174
+ ? (fn: () => void) => { requestIdleCallback(fn); }
175
+ : (fn: () => void) => { setTimeout(fn, 0); };
176
+
177
+ schedule(() => {
178
+ void resolveChartPreviewsForDocument(doc, pkg, adapter).then(onReady);
179
+ });
180
+ }
181
+
141
182
  /**
142
183
  * Walk the whole content tree and yield one PendingResolution per
143
184
  * chart_preview node that (a) has no previewMediaId yet and (b) can
@@ -3,6 +3,7 @@ import type {
3
3
  EditorError,
4
4
  EditorHostAdapter,
5
5
  EditorSessionState,
6
+ EditorSurfaceSnapshot,
6
7
  EditorWarning as PublicEditorWarning,
7
8
  EditorAnchorProjection as PublicEditorAnchorProjection,
8
9
  ExportDocxOptions,
@@ -26,7 +27,13 @@ import type {
26
27
  RevisionRecord as RuntimeRevisionRecord,
27
28
  EditorWarning as InternalEditorWarning,
28
29
  } from "../core/state/editor-state.ts";
29
- import { createCanonicalDocumentId } from "../core/state/editor-state.ts";
30
+ import {
31
+ createCanonicalDocumentId,
32
+ createDefaultCanonicalDocument,
33
+ createSelectionSnapshot,
34
+ } from "../core/state/editor-state.ts";
35
+ import { createEditorSurfaceSnapshot } from "../runtime/surface-projection.ts";
36
+ import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
30
37
  import {
31
38
  createDetachedAnchor,
32
39
  storyTargetsEqual,
@@ -44,7 +51,7 @@ import {
44
51
  normalizeParsedTextDocument,
45
52
  normalizeParsedTextDocumentAsync,
46
53
  } from "./normalize/normalize-text.ts";
47
- import { createChartPartLookup, resolveChartPreviewsForDocument } from "./chart-preview-resolver.ts";
54
+ import { createChartPartLookup, resolveChartPreviewsForDocument, scheduleChartPreviewResolution } from "./chart-preview-resolver.ts";
48
55
  import { type LoadScheduler } from "./load-scheduler.ts";
49
56
  import type { CacheEnvelope } from "../runtime/prerender/cache-envelope.ts";
50
57
  import {
@@ -60,13 +67,12 @@ import {
60
67
  getDocumentBackedWorkflowMetadata,
61
68
  parseWorkflowPayloadEnvelopeFromPackage,
62
69
  resolvePayloadPartPath,
70
+ resolveWorkflowPayloadPartPaths,
63
71
  WORKFLOW_PAYLOAD_CONTENT_TYPE,
64
72
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
65
73
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
66
74
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
67
75
  WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
68
- WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
69
- WORKFLOW_PAYLOAD_PART_PATH,
70
76
  WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
71
77
  } from "./ooxml/workflow-payload.ts";
72
78
  import {
@@ -122,6 +128,7 @@ import type {
122
128
  SubPartsCatalog,
123
129
  } from "../model/canonical-document.ts";
124
130
  import { createCanonicalDocumentSignature } from "../model/canonical-document.ts";
131
+ import type { CanonicalDocument } from "../model/canonical-document.ts";
125
132
  import type {
126
133
  CommentImportDiagnostic,
127
134
  ImportedCommentDefinition,
@@ -959,6 +966,53 @@ export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSession
959
966
  * path because their outputs are required by downstream consumers.
960
967
  */
961
968
  laycacheEnvelope?: CacheEnvelope;
969
+ /**
970
+ * L7 Phase 2 Finale C2 — progressive initial mount. When supplied, the
971
+ * async loader fires this callback exactly once, after the body stage
972
+ * completes but before styles / sub-parts / compatibility / snapshot
973
+ * assembly, with a viewport-windowed `EditorSurfaceSnapshot`. The
974
+ * callback lets UI consumers show the first page's text before the
975
+ * rest of the load finishes, measurably shrinking perceived cold-open.
976
+ *
977
+ * The progressive surface is synthesized from a provisional
978
+ * `CanonicalDocumentEnvelope`: `content` is the normalized body; all
979
+ * other catalogs (styles, numbering, media, review, preservation) are
980
+ * empty. Viewport blocks render live; blocks beyond the viewport
981
+ * render as placeholders via the existing `cullBuild` flag. Consumers
982
+ * must treat the progressive surface as provisional — the final
983
+ * `LoadedDocxEditorSession` (returned from the same `await`) carries
984
+ * the real styled envelope.
985
+ *
986
+ * The callback receives `blocksRealized` (the viewport window size)
987
+ * and `blocksTotal` (total block count at body-stage time) so the
988
+ * consumer can size its viewport-commit telemetry.
989
+ *
990
+ * Optional. When absent, the async loader does not perform the
991
+ * provisional-envelope synthesis — no cold-path regression for
992
+ * consumers that do not opt in.
993
+ *
994
+ * Omitted on the Plan B short-circuit path (laycacheEnvelope !== undefined):
995
+ * the short-circuit is already fast enough that a progressive pre-commit
996
+ * adds more overhead than it saves.
997
+ */
998
+ onProgressiveSnapshot?: (partial: {
999
+ surface: EditorSurfaceSnapshot;
1000
+ phase: "viewport";
1001
+ blocksRealized: number;
1002
+ blocksTotal: number;
1003
+ }) => void;
1004
+ /**
1005
+ * C3b — deferred chart preview resolution. When supplied, chart preview
1006
+ * rendering (`renderChartPreview()` calls) is removed from the critical
1007
+ * load path: the session returns without chart previews, and this
1008
+ * callback fires later (via requestIdleCallback / setTimeout) with the
1009
+ * CanonicalDocument that has all chart previews resolved. Expected win:
1010
+ * 30–80 ms on extra-large warm-path docs with ~12 charts.
1011
+ *
1012
+ * Consumers that need chart previews on first render should not supply
1013
+ * this callback — the blocking path remains available by omitting it.
1014
+ */
1015
+ onChartPreviewsReady?: (resolvedDoc: CanonicalDocument) => void;
962
1016
  }
963
1017
 
964
1018
  /**
@@ -979,6 +1033,23 @@ export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSession
979
1033
  * and SSR. The DOM boundary in `editor-runtime-boundary.ts` calls this
980
1034
  * async path so the browser can paint the skeleton mid-parse.
981
1035
  */
1036
+
1037
+ /**
1038
+ * L7 Phase 2 Finale C2 — progressive initial mount viewport window size.
1039
+ *
1040
+ * Sized to cover the first page of a typical paginated document (~20
1041
+ * body blocks = ~1 page on CCEP-scale templates at default margins). The
1042
+ * window is intentionally small so the provisional-envelope synthesis +
1043
+ * surface projection stays under ~30 ms — making `firstViewportCommit`
1044
+ * significantly faster than the full load.
1045
+ *
1046
+ * Blocks beyond this window render as `placeholder-culled` entries via
1047
+ * Phase 2.9's cullBuild flag (auto-derived from `!isInViewport`) — they
1048
+ * preserve `from`/`to` offsets for selection stability while costing ~0
1049
+ * style-cascade work.
1050
+ */
1051
+ const PROGRESSIVE_VIEWPORT_BLOCKS = 20;
1052
+
982
1053
  export async function loadDocxEditorSessionAsync(
983
1054
  options: LoadDocxEditorSessionAsyncOptions,
984
1055
  ): Promise<LoadedDocxEditorSession> {
@@ -1090,18 +1161,45 @@ export async function loadDocxEditorSessionAsync(
1090
1161
  const protectionSnapshot = buildProtectionSnapshot(documentProtection, []);
1091
1162
 
1092
1163
  // Chart previews (`previewMediaId` is host-dependent) aren't cached
1093
- // in the envelope, so we still resolve them on the short-circuit.
1094
- const documentWithChartPreviews = (await resolveChartPreviewsForDocument(
1095
- canonicalDocument,
1096
- sourcePackage,
1097
- options.hostAdapter,
1098
- )) as CanonicalDocumentEnvelope;
1164
+ // in the envelope. C3b: when onChartPreviewsReady is provided, defer
1165
+ // resolution out of the critical path. Otherwise block (legacy behavior).
1166
+ let documentWithChartPreviews: CanonicalDocumentEnvelope;
1167
+ if (options.onChartPreviewsReady) {
1168
+ scheduleChartPreviewResolution(
1169
+ canonicalDocument,
1170
+ sourcePackage,
1171
+ options.hostAdapter,
1172
+ options.onChartPreviewsReady,
1173
+ );
1174
+ documentWithChartPreviews = canonicalDocument as CanonicalDocumentEnvelope;
1175
+ } else {
1176
+ documentWithChartPreviews = (await resolveChartPreviewsForDocument(
1177
+ canonicalDocument,
1178
+ sourcePackage,
1179
+ options.hostAdapter,
1180
+ )) as CanonicalDocumentEnvelope;
1181
+ }
1099
1182
 
1100
1183
  const timestamp = new Date().toISOString();
1101
- const compatibility = buildCompatibilityReport({
1102
- document: documentWithChartPreviews,
1103
- generatedAt: timestamp,
1104
- });
1184
+ // Phase 2 Finale C3: skip `buildCompatibilityReport` (60–100 ms on
1185
+ // extra-large) when the envelope carries a pre-computed report.
1186
+ // Pure-function determinism of the report is enforced by
1187
+ // `canonicalDocumentHash` (5th input to `deriveCacheKey`): any
1188
+ // change to the canonical doc flips the hash and rejects the
1189
+ // envelope on load.
1190
+ //
1191
+ // The cached report's `generatedAt` is a fixed sentinel
1192
+ // (`CACHE_NORMALIZED_GENERATED_AT`) for envelope byte-identity.
1193
+ // Swap it for the live ISO8601 timestamp here because downstream
1194
+ // `validatePersistedEditorSnapshot` requires
1195
+ // `$.compatibility.generatedAt` to be ISO 8601.
1196
+ const cachedReport = options.laycacheEnvelope?.compatibilityReport;
1197
+ const compatibility = cachedReport
1198
+ ? { ...cachedReport, generatedAt: timestamp }
1199
+ : buildCompatibilityReport({
1200
+ document: documentWithChartPreviews,
1201
+ generatedAt: timestamp,
1202
+ });
1105
1203
  await scheduler.yield();
1106
1204
 
1107
1205
  const snapshot = createImportedSnapshot({
@@ -1225,6 +1323,64 @@ export async function loadDocxEditorSessionAsync(
1225
1323
  );
1226
1324
  stages.emit("body");
1227
1325
  await scheduler.yield();
1326
+
1327
+ // L7 Phase 2 Finale C2 — progressive initial mount.
1328
+ //
1329
+ // Fire `onProgressiveSnapshot` exactly once, after the body stage and
1330
+ // its post-yield. At this point `normalizedDocument.content` carries
1331
+ // the full block tree with per-block runProperties already resolved
1332
+ // during `normalizeParsedTextDocumentAsync`. We synthesize a
1333
+ // throw-away `CanonicalDocumentEnvelope` using the normalized content
1334
+ // + empty style/review/preservation catalogs, then project a
1335
+ // viewport-windowed `EditorSurfaceSnapshot` (first `PROGRESSIVE_VIEWPORT_BLOCKS`
1336
+ // blocks real, rest as culled placeholders via the Phase 2.9 flag).
1337
+ //
1338
+ // The bench's measured signal: time from `loadDocxEditorSessionAsync`
1339
+ // entry to this callback's fire is `firstViewportCommitMs` — the
1340
+ // metric C2 gates on.
1341
+ //
1342
+ // Skipped on the Plan B short-circuit: `laycacheEnvelope !== undefined`
1343
+ // already completes ~376 ms faster than cold — adding a progressive
1344
+ // synthesis on top costs more than it saves. The short-circuit path
1345
+ // returns the real snapshot fast enough.
1346
+ if (
1347
+ options.onProgressiveSnapshot !== undefined &&
1348
+ options.laycacheEnvelope === undefined
1349
+ ) {
1350
+ const provisionalDoc: CanonicalDocumentEnvelope = {
1351
+ ...createDefaultCanonicalDocument(
1352
+ options.documentId,
1353
+ new Date().toISOString(),
1354
+ ),
1355
+ content: normalizedDocument.content,
1356
+ };
1357
+ const blocksTotal = normalizedDocument.content.children.length;
1358
+ const blocksRealized = Math.min(
1359
+ PROGRESSIVE_VIEWPORT_BLOCKS,
1360
+ blocksTotal,
1361
+ );
1362
+ const progressiveSurface = createEditorSurfaceSnapshot(
1363
+ provisionalDoc,
1364
+ createSelectionSnapshot(0, 0),
1365
+ MAIN_STORY_TARGET,
1366
+ blocksRealized < blocksTotal
1367
+ ? { viewportBlockRange: { start: 0, end: blocksRealized } }
1368
+ : undefined,
1369
+ );
1370
+ try {
1371
+ options.onProgressiveSnapshot({
1372
+ surface: progressiveSurface,
1373
+ phase: "viewport",
1374
+ blocksRealized,
1375
+ blocksTotal,
1376
+ });
1377
+ } catch {
1378
+ // A throwing consumer must not abort the load. Progressive is
1379
+ // a best-effort optimization; errors on the callback side
1380
+ // silently fall through to the normal full-commit path.
1381
+ }
1382
+ }
1383
+
1228
1384
  const commentsPartPath = resolveCommentsPartPath(
1229
1385
  sourcePackage,
1230
1386
  mainDocumentPath,
@@ -1579,11 +1735,24 @@ export async function loadDocxEditorSessionAsync(
1579
1735
  // chart_preview nodes inline so the first snapshot already carries the
1580
1736
  // synthesized `previewMediaId`. Fallback-safe: returning null or throwing
1581
1737
  // is per-chart — the typed badge renders as if the adapter weren't set.
1582
- const document = (await resolveChartPreviewsForDocument(
1583
- importedDocument,
1584
- sourcePackage,
1585
- options.hostAdapter,
1586
- )) as CanonicalDocumentEnvelope;
1738
+ // C3b: when onChartPreviewsReady is provided, defer resolution out of
1739
+ // the critical path (same pattern as the short-circuit branch above).
1740
+ let document: CanonicalDocumentEnvelope;
1741
+ if (options.onChartPreviewsReady) {
1742
+ scheduleChartPreviewResolution(
1743
+ importedDocument,
1744
+ sourcePackage,
1745
+ options.hostAdapter,
1746
+ options.onChartPreviewsReady,
1747
+ );
1748
+ document = importedDocument as CanonicalDocumentEnvelope;
1749
+ } else {
1750
+ document = (await resolveChartPreviewsForDocument(
1751
+ importedDocument,
1752
+ sourcePackage,
1753
+ options.hostAdapter,
1754
+ )) as CanonicalDocumentEnvelope;
1755
+ }
1587
1756
  const compatibility = buildCompatibilityReport({
1588
1757
  document,
1589
1758
  generatedAt: timestamp,
@@ -1935,13 +2104,20 @@ function exportDocxEditorSession(
1935
2104
  const hasSettingsSurface =
1936
2105
  Boolean(state.sourceSettingsPartPath) ||
1937
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;
1938
2114
 
1939
2115
  const exportSession = createExportSession(state.sourcePackage, [
1940
2116
  state.sourceDocumentPartPath,
1941
2117
  APP_PROPERTIES_PART_PATH,
1942
2118
  CORE_PROPERTIES_PART_PATH,
1943
- WORKFLOW_PAYLOAD_PART_PATH,
1944
- WORKFLOW_PAYLOAD_ITEM_PROPS_PART_PATH,
2119
+ resolvedWorkflowPayloadPartPaths.payloadPartPath,
2120
+ resolvedWorkflowPayloadPartPaths.itemPropsPartPath,
1945
2121
  WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
1946
2122
  numberingPartPath,
1947
2123
  commentsPartPath,
@@ -2190,8 +2366,14 @@ function exportDocxEditorSession(
2190
2366
 
2191
2367
  ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
2192
2368
  // Schema 1.2: pass through editorState payload collected by the runtime channel.
2193
- const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
2194
- ensureWorkflowPayloadParts(exportSession, sessionState, currentDocument, state.sourcePackage, internalEditorState);
2369
+ ensureWorkflowPayloadParts(
2370
+ exportSession,
2371
+ sessionState,
2372
+ currentDocument,
2373
+ state.sourcePackage,
2374
+ resolvedWorkflowPayloadPartPaths,
2375
+ internalEditorState,
2376
+ );
2195
2377
 
2196
2378
  return {
2197
2379
  bytes: exportSession.serialize(),
@@ -3917,6 +4099,10 @@ function ensureWorkflowPayloadParts(
3917
4099
  sessionState: EditorSessionState,
3918
4100
  document: CanonicalDocumentEnvelope,
3919
4101
  sourcePackage: OpcPackage,
4102
+ resolvedPartPaths: {
4103
+ payloadPartPath: string;
4104
+ itemPropsPartPath: string;
4105
+ },
3920
4106
  editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
3921
4107
  ): void {
3922
4108
  const payloadParts = buildWorkflowPayloadParts({
@@ -3932,6 +4118,14 @@ function ensureWorkflowPayloadParts(
3932
4118
  if (!payloadParts) {
3933
4119
  return;
3934
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
+ }
3935
4129
 
3936
4130
  const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
3937
4131
  const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
@@ -30,7 +30,7 @@ export interface Checkpoint {
30
30
  authorClientId: number;
31
31
  }
32
32
 
33
- const CHECKPOINTS_KEY = "checkpoints";
33
+ export const CHECKPOINTS_KEY = "checkpoints";
34
34
 
35
35
  export interface CreateCheckpointStoreOptions {
36
36
  ydoc: Y.Doc;
@@ -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
  /**