@beyondwork/docx-react-component 1.0.38 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -31
- package/src/api/public-types.ts +183 -6
- package/src/core/commands/table-structure-commands.ts +31 -2
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-numbering.ts +42 -8
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +83 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-runtime.ts +134 -18
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +69 -2
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/page-graph.ts +36 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +342 -28
- package/src/runtime/layout/project-block-fragments.ts +154 -20
- package/src/runtime/layout/public-facet.ts +40 -1
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +21 -1
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/render-kernel.ts +5 -1
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/ui/WordReviewEditor.tsx +285 -5
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +4 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +34 -5
- package/src/ui/headless/scoped-chrome-policy.ts +29 -0
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
- package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +1 -5
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
- package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.39",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
|
+
"packageManager": "pnpm@10.30.3",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"sideEffects": [
|
|
8
9
|
"**/*.css"
|
|
@@ -49,6 +50,10 @@
|
|
|
49
50
|
"types": "./src/runtime/document-runtime.ts",
|
|
50
51
|
"import": "./src/runtime/document-runtime.ts"
|
|
51
52
|
},
|
|
53
|
+
"./runtime/collab": {
|
|
54
|
+
"types": "./src/runtime/collab/index.ts",
|
|
55
|
+
"import": "./src/runtime/collab/index.ts"
|
|
56
|
+
},
|
|
52
57
|
"./core/commands/formatting-commands": {
|
|
53
58
|
"types": "./src/core/commands/formatting-commands.ts",
|
|
54
59
|
"import": "./src/core/commands/formatting-commands.ts"
|
|
@@ -88,6 +93,31 @@
|
|
|
88
93
|
"./ui-tailwind/theme/editor-theme.css": "./src/ui-tailwind/theme/editor-theme.css"
|
|
89
94
|
},
|
|
90
95
|
"types": "./src/index.ts",
|
|
96
|
+
"scripts": {
|
|
97
|
+
"build": "tsup",
|
|
98
|
+
"test": "bash scripts/run-workspace-tests.sh",
|
|
99
|
+
"test:repo": "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
|
+
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
106
|
+
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
107
|
+
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
108
|
+
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
109
|
+
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
110
|
+
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
111
|
+
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
112
|
+
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
113
|
+
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
114
|
+
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
115
|
+
"wave:status": "bash scripts/wave-status.sh",
|
|
116
|
+
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
117
|
+
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
118
|
+
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
119
|
+
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
120
|
+
},
|
|
91
121
|
"keywords": [
|
|
92
122
|
"docx",
|
|
93
123
|
"word",
|
|
@@ -132,16 +162,12 @@
|
|
|
132
162
|
"react-dom": "^19.2.0",
|
|
133
163
|
"tailwindcss": "^4.2.2",
|
|
134
164
|
"yjs": "^13.6.0",
|
|
135
|
-
"y-prosemirror": "^1.2.0",
|
|
136
165
|
"y-protocols": "^1.0.0"
|
|
137
166
|
},
|
|
138
167
|
"peerDependenciesMeta": {
|
|
139
168
|
"yjs": {
|
|
140
169
|
"optional": true
|
|
141
170
|
},
|
|
142
|
-
"y-prosemirror": {
|
|
143
|
-
"optional": true
|
|
144
|
-
},
|
|
145
171
|
"y-protocols": {
|
|
146
172
|
"optional": true
|
|
147
173
|
}
|
|
@@ -163,33 +189,17 @@
|
|
|
163
189
|
"react-dom": "19.2.4",
|
|
164
190
|
"tsup": "^8.3.0",
|
|
165
191
|
"tsx": "^4.21.0",
|
|
166
|
-
"y-prosemirror": "^1.3.7",
|
|
167
192
|
"y-protocols": "^1.0.7",
|
|
168
193
|
"yjs": "^13.6.30"
|
|
169
194
|
},
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"lint": "pnpm run lint:no-authored-js && pnpm run lint:docs-contracts && pnpm run lint:tsgo && pnpm run lint:tsgo:harness",
|
|
180
|
-
"lint:docs-contracts": "bash scripts/check-reference-load-contract.sh",
|
|
181
|
-
"lint:no-authored-js": "bash scripts/check-no-authored-js.sh",
|
|
182
|
-
"lint:tsgo": "tsgo --noEmit -p tsconfig.build.json",
|
|
183
|
-
"lint:tsgo:harness": "pnpm --filter @docx-react-component/react-word-editor-harness lint:tsgo",
|
|
184
|
-
"context7:api-check": "bash scripts/context7-export-env.sh run bash scripts/context7-api-check.sh",
|
|
185
|
-
"wave:doctor": "bash scripts/context7-export-env.sh run pnpm exec wave doctor --json",
|
|
186
|
-
"wave:dry-run": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main --dry-run --no-dashboard",
|
|
187
|
-
"wave:launch": "bash scripts/context7-export-env.sh run pnpm exec wave launch --lane main",
|
|
188
|
-
"wave:launch:managed": "bash scripts/wave-launch.sh",
|
|
189
|
-
"wave:status": "bash scripts/wave-status.sh",
|
|
190
|
-
"wave:watch": "bash scripts/wave-watch.sh --follow",
|
|
191
|
-
"wave:dashboard:current": "bash scripts/wave-dashboard-attach.sh current",
|
|
192
|
-
"wave:dashboard:global": "bash scripts/wave-dashboard-attach.sh global",
|
|
193
|
-
"harness:dev": "pnpm --filter @docx-react-component/react-word-editor-harness dev"
|
|
195
|
+
"pnpm": {
|
|
196
|
+
"onlyBuiltDependencies": [
|
|
197
|
+
"esbuild",
|
|
198
|
+
"sharp"
|
|
199
|
+
],
|
|
200
|
+
"overrides": {
|
|
201
|
+
"react": "19.2.4",
|
|
202
|
+
"react-dom": "19.2.4"
|
|
203
|
+
}
|
|
194
204
|
}
|
|
195
|
-
}
|
|
205
|
+
}
|
package/src/api/public-types.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { PersistedEditorSnapshot as RuntimePersistedEditorSnapshot } from "../core/state/editor-state.ts";
|
|
2
|
+
import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
|
|
2
3
|
import type { WordReviewEditorLayoutFacet } from "../runtime/layout/public-facet.ts";
|
|
3
4
|
|
|
5
|
+
export type { CanonicalParagraphFormatting, CanonicalRunFormatting };
|
|
6
|
+
|
|
4
7
|
export type {
|
|
5
8
|
WordReviewEditorLayoutFacet,
|
|
6
9
|
LayoutFacetEvent,
|
|
@@ -701,6 +704,8 @@ export type SurfaceInlineSegment =
|
|
|
701
704
|
textColor?: string;
|
|
702
705
|
};
|
|
703
706
|
hyperlinkHref?: string;
|
|
707
|
+
/** Cascaded run formatting for this text segment (docDefaults → paragraph style chain → character style → direct). Added in Task 11. TODO Task 15: wire up from resolveEffectiveRunFormatting in surface-projection. */
|
|
708
|
+
resolvedRunFormatting?: CanonicalRunFormatting;
|
|
704
709
|
}
|
|
705
710
|
| {
|
|
706
711
|
segmentId: string;
|
|
@@ -767,6 +772,13 @@ export interface SurfaceTableCellSnapshot {
|
|
|
767
772
|
borderRight?: string | null;
|
|
768
773
|
borderBottom?: string | null;
|
|
769
774
|
borderLeft?: string | null;
|
|
775
|
+
/**
|
|
776
|
+
* R2a: space-joined CSS class names from the resolved table-style conditional
|
|
777
|
+
* regions (e.g. "band-firstRow band-band1Horz"). Consumers apply these to the
|
|
778
|
+
* cell so theme toggles repaint via CSS vars instead of re-projecting the
|
|
779
|
+
* surface. Direct shading overrides still win over band defaults at render.
|
|
780
|
+
*/
|
|
781
|
+
bandClasses?: string | null;
|
|
770
782
|
content: SurfaceBlockSnapshot[];
|
|
771
783
|
}
|
|
772
784
|
|
|
@@ -777,6 +789,8 @@ export interface SurfaceTableRowSnapshot {
|
|
|
777
789
|
height?: number;
|
|
778
790
|
heightRule?: "auto" | "atLeast" | "exact";
|
|
779
791
|
isHeader?: boolean;
|
|
792
|
+
/** R1b: row carries `w:cantSplit` — pagination keeps the whole table on one page. */
|
|
793
|
+
cantSplit?: boolean;
|
|
780
794
|
}
|
|
781
795
|
|
|
782
796
|
export interface ResolvedNumberingGeometrySnapshot {
|
|
@@ -797,6 +811,8 @@ export interface ResolvedNumberingSnapshot {
|
|
|
797
811
|
isLegalNumbering?: boolean;
|
|
798
812
|
suffix?: "tab" | "space" | "nothing";
|
|
799
813
|
geometry: ResolvedNumberingGeometrySnapshot;
|
|
814
|
+
/** CASCADED marker run formatting: docDefaults → paragraph style chain rPr → paragraph-mark rPr → level rPr. Added in Task 11. The raw level rPr is still available at geometry.markerRunProperties. */
|
|
815
|
+
markerRunProperties?: CanonicalRunFormatting;
|
|
800
816
|
}
|
|
801
817
|
|
|
802
818
|
export type SurfaceBlockSnapshot =
|
|
@@ -813,6 +829,8 @@ export type SurfaceBlockSnapshot =
|
|
|
813
829
|
numberingPrefix?: string;
|
|
814
830
|
numberingSuffix?: "tab" | "space" | "nothing";
|
|
815
831
|
resolvedNumbering?: ResolvedNumberingSnapshot;
|
|
832
|
+
/** Cascaded paragraph formatting: docDefaults → style chain → direct paragraph properties. Added in Task 11. */
|
|
833
|
+
resolvedParagraphFormatting?: CanonicalParagraphFormatting;
|
|
816
834
|
alignment?: "left" | "center" | "right" | "both" | "distribute";
|
|
817
835
|
spacing?: { before?: number; after?: number; line?: number; lineRule?: string };
|
|
818
836
|
contextualSpacing?: boolean;
|
|
@@ -838,6 +856,8 @@ export type SurfaceBlockSnapshot =
|
|
|
838
856
|
gridColumns: number[];
|
|
839
857
|
alignment?: "left" | "center" | "right";
|
|
840
858
|
tblLook?: {
|
|
859
|
+
/** R2d: raw `w:tblLook/@w:val` hex so vendor-extended bits survive round-trip. */
|
|
860
|
+
val?: string;
|
|
841
861
|
firstRow?: boolean;
|
|
842
862
|
lastRow?: boolean;
|
|
843
863
|
firstColumn?: boolean;
|
|
@@ -1288,11 +1308,129 @@ export interface TableOpResult {
|
|
|
1288
1308
|
* read. Read-only geometry queries (columns, rows, render plan, ...)
|
|
1289
1309
|
* land with the layout engine + render kernel integration.
|
|
1290
1310
|
*/
|
|
1311
|
+
/**
|
|
1312
|
+
* Compact summary for one canonical table in the document.
|
|
1313
|
+
*/
|
|
1314
|
+
export interface PublicTableSummary {
|
|
1315
|
+
/** 0-based index across top-level canonical table blocks (walk order). */
|
|
1316
|
+
tableBlockIndex: number;
|
|
1317
|
+
/** The projected surface block id ("table-{index}"). */
|
|
1318
|
+
blockId: string;
|
|
1319
|
+
/** Style chain head, if declared. */
|
|
1320
|
+
styleId: string | null;
|
|
1321
|
+
/** Rows and logical columns (including gridBefore/after padding). */
|
|
1322
|
+
rowCount: number;
|
|
1323
|
+
columnCount: number;
|
|
1324
|
+
/** Raw canonical gridColumns in twips. */
|
|
1325
|
+
gridColumnsTwips: readonly number[];
|
|
1326
|
+
/** Table-level alignment if declared. */
|
|
1327
|
+
alignment: "left" | "center" | "right" | null;
|
|
1328
|
+
/** Whether any row has a vMerge chain crossing it. */
|
|
1329
|
+
hasVerticalMerges: boolean;
|
|
1330
|
+
/** Whether any cell in the table has colspan > 1. */
|
|
1331
|
+
hasHorizontalSpans: boolean;
|
|
1332
|
+
/** 0-based page index where the table's start offset lands, if known. */
|
|
1333
|
+
pageIndex: number | null;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Public shape of the per-page render plan. Sanitized copy of the
|
|
1338
|
+
* internal `TableRenderPlan`: chrome consumers read here, not directly
|
|
1339
|
+
* from the runtime layout module.
|
|
1340
|
+
*/
|
|
1341
|
+
export interface PublicTableRenderPlan {
|
|
1342
|
+
blockId: string;
|
|
1343
|
+
pageIndex: number;
|
|
1344
|
+
columnsTwips: readonly number[];
|
|
1345
|
+
bandClasses: {
|
|
1346
|
+
rows: readonly { rowIndex: number; regions: readonly string[] }[];
|
|
1347
|
+
cells: readonly {
|
|
1348
|
+
rowIndex: number;
|
|
1349
|
+
columnIndex: number;
|
|
1350
|
+
regions: readonly string[];
|
|
1351
|
+
}[];
|
|
1352
|
+
};
|
|
1353
|
+
verticalMerges: readonly {
|
|
1354
|
+
columnIndex: number;
|
|
1355
|
+
startRowIndex: number;
|
|
1356
|
+
endRowIndex: number;
|
|
1357
|
+
columnSpan: number;
|
|
1358
|
+
}[];
|
|
1359
|
+
repeatedHeaderRows: readonly {
|
|
1360
|
+
sourceRowIndex: number;
|
|
1361
|
+
virtualFragmentId: string;
|
|
1362
|
+
}[];
|
|
1363
|
+
columnResizeHandles: readonly {
|
|
1364
|
+
columnIndex: number;
|
|
1365
|
+
originTwips: number;
|
|
1366
|
+
heightTwips: number;
|
|
1367
|
+
}[];
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Per-row height read derived from pagination measurement + explicit
|
|
1372
|
+
* row height declarations.
|
|
1373
|
+
*/
|
|
1374
|
+
export interface PublicTableRowHeight {
|
|
1375
|
+
/** Measured height in twips from the pagination engine (fallback estimate
|
|
1376
|
+
* when no fragments exist for the row). */
|
|
1377
|
+
measured: number;
|
|
1378
|
+
/** Explicit w:trHeight value when declared. */
|
|
1379
|
+
explicit?: number;
|
|
1380
|
+
/** w:trHeight rule when declared. */
|
|
1381
|
+
rule?: "auto" | "atLeast" | "exact";
|
|
1382
|
+
/** Whether the row is marked as an OOXML header row (tblHeader). */
|
|
1383
|
+
isHeader: boolean;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Minimal public representation of a table style definition.
|
|
1388
|
+
*/
|
|
1389
|
+
export interface PublicTableStyle {
|
|
1390
|
+
styleId: string;
|
|
1391
|
+
displayName: string;
|
|
1392
|
+
basedOn?: string;
|
|
1393
|
+
isDefault: boolean;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Events emitted by `ref.tables.subscribe(listener)`. Sourced from the
|
|
1398
|
+
* runtime change stream + layout facet events so chrome surfaces can
|
|
1399
|
+
* refresh their derived reads without polling.
|
|
1400
|
+
*/
|
|
1401
|
+
export type PublicTableEvent =
|
|
1402
|
+
| { kind: "table_structure_changed"; revisionToken: string }
|
|
1403
|
+
| { kind: "table_style_changed"; revisionToken: string }
|
|
1404
|
+
| { kind: "table_render_plan_ready"; revision: number }
|
|
1405
|
+
| { kind: "table_capabilities_changed" };
|
|
1406
|
+
|
|
1291
1407
|
export interface WordReviewEditorTablesFacet {
|
|
1292
1408
|
/** Dispatch a typed table op through the runtime. */
|
|
1293
1409
|
apply(op: TableOp): TableOpResult;
|
|
1294
1410
|
/** Current capability snapshot for the active table selection. */
|
|
1295
1411
|
getCapabilities(): TableStructureContextSnapshot | null;
|
|
1412
|
+
/** List every top-level table in the main document. */
|
|
1413
|
+
getTables(options?: { sectionIndex?: number }): PublicTableSummary[];
|
|
1414
|
+
/** Summary for a single table by tableBlockIndex, or `null` when out of range. */
|
|
1415
|
+
getTable(tableBlockIndex: number): PublicTableSummary | null;
|
|
1416
|
+
/** The table the active selection currently sits in, or `null`. */
|
|
1417
|
+
getTableForSelection(): PublicTableSummary | null;
|
|
1418
|
+
/** Per-page render plan for a table, or `null` when no table / no kernel. */
|
|
1419
|
+
getRenderPlan(
|
|
1420
|
+
tableBlockIndex: number,
|
|
1421
|
+
pageIndex?: number,
|
|
1422
|
+
): PublicTableRenderPlan | null;
|
|
1423
|
+
/** Current gridColumns in twips for the table. */
|
|
1424
|
+
getColumnWidths(tableBlockIndex: number): readonly number[];
|
|
1425
|
+
/** Per-row height reads, combining pagination measurement + explicit declarations. */
|
|
1426
|
+
getRowHeights(tableBlockIndex: number): readonly PublicTableRowHeight[];
|
|
1427
|
+
/** Table-style subset of the document style catalog. */
|
|
1428
|
+
getStyleCatalog(): readonly PublicTableStyle[];
|
|
1429
|
+
/**
|
|
1430
|
+
* Subscribe to table-facet events. Returns an unsubscribe function.
|
|
1431
|
+
* Chrome consumers use this instead of polling `getTables()`.
|
|
1432
|
+
*/
|
|
1433
|
+
subscribe(listener: (event: PublicTableEvent) => void): () => void;
|
|
1296
1434
|
}
|
|
1297
1435
|
|
|
1298
1436
|
export interface PageRegionHitTest {
|
|
@@ -1343,12 +1481,19 @@ export interface EditorViewStateSnapshot {
|
|
|
1343
1481
|
/**
|
|
1344
1482
|
* Role-scoped chrome dimension (spec §6.4 of runtime-rendering-and-chrome-phase.md).
|
|
1345
1483
|
*
|
|
1346
|
-
* - `"editor"` — authoring posture.
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1350
|
-
* - `"
|
|
1351
|
-
*
|
|
1484
|
+
* - `"editor"` — authoring posture. Role action region surfaces the two
|
|
1485
|
+
* review-layer icons: add comment + inline tracked-changes toggle. Left
|
|
1486
|
+
* cluster carries formatting + insert/update actions as usual. Scope
|
|
1487
|
+
* posture is owned by the `"workflow"` role, not editor.
|
|
1488
|
+
* - `"review"` — reviewing posture. Role action region surfaces optional
|
|
1489
|
+
* sidebar-panel shortcuts (`onReviewSidebarTrackedChanges` /
|
|
1490
|
+
* `onReviewSidebarComments`, hidden unless the host provides callbacks),
|
|
1491
|
+
* add comment + inline tracked-changes toggle, review-queue prev/next +
|
|
1492
|
+
* counts + active label, per-item accept/reject, batch accept-all /
|
|
1493
|
+
* reject-all, and markup-mode selector.
|
|
1494
|
+
* - `"workflow"` — workflow-actor posture. Role action region surfaces
|
|
1495
|
+
* the scope posture menu + work-item prev/next, claim, skip, mark
|
|
1496
|
+
* complete, mark blocked, jump to scope.
|
|
1352
1497
|
*/
|
|
1353
1498
|
export type EditorRole = "editor" | "review" | "workflow";
|
|
1354
1499
|
|
|
@@ -2204,6 +2349,10 @@ export interface WordReviewEditorRef {
|
|
|
2204
2349
|
getProtectionSnapshot(): ProtectionSnapshot;
|
|
2205
2350
|
setWorkspaceMode(mode: WorkspaceMode): void;
|
|
2206
2351
|
setZoom(level: ZoomLevel): void;
|
|
2352
|
+
/** Switch the per-role top-chrome action set. Drives toolbar + review rail. */
|
|
2353
|
+
setEditorRole(role: EditorRole): void;
|
|
2354
|
+
/** Persist a detach/attach pin for a chrome surface across snapshot rebuilds. */
|
|
2355
|
+
setChromePin(surface: ChromePinSurface, pin: PinState | null): void;
|
|
2207
2356
|
insertSectionBreak(type: SectionBreakType, options?: { afterSectionIndex?: number }): void;
|
|
2208
2357
|
deleteSectionBreak(sectionIndex: number): void;
|
|
2209
2358
|
updateSectionLayout(sectionIndex: number, patch: SectionLayoutPatch): void;
|
|
@@ -2279,7 +2428,20 @@ export type WordReviewEditorChromePreset =
|
|
|
2279
2428
|
| "workflow";
|
|
2280
2429
|
|
|
2281
2430
|
export interface WordReviewEditorChromeOptions {
|
|
2431
|
+
/**
|
|
2432
|
+
* @deprecated The legacy second-strip review queue bar has been collapsed
|
|
2433
|
+
* into the role action region (spec §6.4). Set `role="review"` on the
|
|
2434
|
+
* editor to surface queue Prev/Next + accept/reject inline in the top
|
|
2435
|
+
* toolbar. This flag is retained for type back-compat and has no runtime
|
|
2436
|
+
* effect; it will be removed in a future release.
|
|
2437
|
+
*/
|
|
2282
2438
|
showReviewQueueBar: boolean;
|
|
2439
|
+
/**
|
|
2440
|
+
* @deprecated The "Mark section" button lived on the legacy
|
|
2441
|
+
* `TwReviewQueueBar`. Scope tagging is now the workflow role's posture
|
|
2442
|
+
* menu (`editor-scope-posture-menu`, surfaced via `role="workflow"`).
|
|
2443
|
+
* Retained for type back-compat; will be removed in a future release.
|
|
2444
|
+
*/
|
|
2283
2445
|
showSectionTagAction: boolean;
|
|
2284
2446
|
showReviewRail: boolean;
|
|
2285
2447
|
}
|
|
@@ -2312,6 +2474,21 @@ export interface WordReviewEditorProps {
|
|
|
2312
2474
|
onEvent?: (event: WordReviewEditorEvent) => void;
|
|
2313
2475
|
onWarning?: (warning: EditorWarning) => void;
|
|
2314
2476
|
onError?: (error: EditorError) => void;
|
|
2477
|
+
/**
|
|
2478
|
+
* Optional: opens the host's sidebar to the tracked-changes panel. When
|
|
2479
|
+
* supplied, the review role surfaces an inline "Tracked changes panel"
|
|
2480
|
+
* icon in the top toolbar's role action region (spec §6.4). The button
|
|
2481
|
+
* is hidden when this callback is not provided, so the base runtime
|
|
2482
|
+
* chrome is sidebar-agnostic by default and only the harness / host that
|
|
2483
|
+
* owns a sidebar opts in.
|
|
2484
|
+
*/
|
|
2485
|
+
onReviewSidebarTrackedChanges?: () => void;
|
|
2486
|
+
/**
|
|
2487
|
+
* Optional: opens the host's sidebar to the comments panel. Same
|
|
2488
|
+
* hidden-by-default gating as `onReviewSidebarTrackedChanges` — the
|
|
2489
|
+
* inline "Comments panel" icon appears only when a callback is wired.
|
|
2490
|
+
*/
|
|
2491
|
+
onReviewSidebarComments?: () => void;
|
|
2315
2492
|
}
|
|
2316
2493
|
|
|
2317
2494
|
export interface WordReviewEditorChromeVisibility {
|
|
@@ -358,7 +358,14 @@ export function getTableStructureContext(
|
|
|
358
358
|
? disabledCapability(
|
|
359
359
|
"Selection cuts through a merged cell. Extend the selection to fully enclose the span.",
|
|
360
360
|
)
|
|
361
|
-
:
|
|
361
|
+
: mergeRectCoverage &&
|
|
362
|
+
mergeRectCoverage.fullyCoveredOrigins.some(
|
|
363
|
+
(origin) => origin.columnSpan > 1 || origin.rowSpan > 1,
|
|
364
|
+
)
|
|
365
|
+
? disabledCapability(
|
|
366
|
+
"Selection encloses an already-merged cell. Split it first, then merge.",
|
|
367
|
+
)
|
|
368
|
+
: enabledCapability(),
|
|
362
369
|
splitCell:
|
|
363
370
|
splitWidth === 1 && splitHeight === 1
|
|
364
371
|
? disabledCapability("Select a merged or spanning cell to split it.")
|
|
@@ -803,14 +810,36 @@ function mergeSelectedCells(
|
|
|
803
810
|
selection: TableSelectionDescriptor,
|
|
804
811
|
fallbackSelection: SelectionSnapshot,
|
|
805
812
|
): StructuralMutationResult {
|
|
813
|
+
// R1a: the merge command was previously gated on `!isSimpleTable(table)`,
|
|
814
|
+
// which blocked merging on any table that already carried a single merged
|
|
815
|
+
// span anywhere in the table, even when the user's current selection was
|
|
816
|
+
// over a simple region. The correct gate is whether the rect itself is:
|
|
817
|
+
// (a) clean — every origin it touches is fully enclosed (analyzeRect);
|
|
818
|
+
// (b) internally simple — no pre-existing merged origin lives inside
|
|
819
|
+
// the rect. findCellAtColumn in the splice body below assumes cells
|
|
820
|
+
// at logical column C correspond 1:1 with row.cells entries; that
|
|
821
|
+
// assumption holds iff the rect's internal cells are all gridSpan=1
|
|
822
|
+
// and verticalMerge=undefined.
|
|
823
|
+
// A rect that satisfies both is safe to merge even on a table that has
|
|
824
|
+
// merged spans elsewhere (the common imported-agreement case).
|
|
806
825
|
if (
|
|
807
|
-
!isSimpleTable(table) ||
|
|
808
826
|
selection.selectionKind !== "cell" ||
|
|
809
827
|
selection.rect.bottom - selection.rect.top < 1 ||
|
|
810
828
|
selection.rect.right - selection.rect.left < 1
|
|
811
829
|
) {
|
|
812
830
|
return createNoopStructuralMutation(document, fallbackSelection);
|
|
813
831
|
}
|
|
832
|
+
const grid = buildLogicalGrid(table);
|
|
833
|
+
const coverage = analyzeRect(grid, selection.rect);
|
|
834
|
+
if (!coverage.clean) {
|
|
835
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
836
|
+
}
|
|
837
|
+
const rectInternallySimple = coverage.fullyCoveredOrigins.every(
|
|
838
|
+
(origin) => origin.columnSpan === 1 && origin.rowSpan === 1,
|
|
839
|
+
);
|
|
840
|
+
if (!rectInternallySimple) {
|
|
841
|
+
return createNoopStructuralMutation(document, fallbackSelection);
|
|
842
|
+
}
|
|
814
843
|
|
|
815
844
|
const height = selection.rect.bottom - selection.rect.top;
|
|
816
845
|
const width = selection.rect.right - selection.rect.left;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { InsertTableOptions } from "../../api/public-types";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DocumentRootNode,
|
|
4
|
+
ParagraphNode,
|
|
5
|
+
ParagraphStyleDefinition,
|
|
6
|
+
StylesCatalog,
|
|
7
|
+
} from "../../model/canonical-document.ts";
|
|
3
8
|
import {
|
|
4
9
|
createSelectionSnapshot,
|
|
5
10
|
type CanonicalDocumentEnvelope,
|
|
@@ -18,11 +23,102 @@ import {
|
|
|
18
23
|
resolveParagraphScope,
|
|
19
24
|
type StructuralMutationResult,
|
|
20
25
|
} from "./structural-helpers.ts";
|
|
26
|
+
import { createEditorSurfaceSnapshot } from "../../runtime/surface-projection.ts";
|
|
21
27
|
|
|
22
28
|
export interface TextCommandContext {
|
|
23
29
|
timestamp: string;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Walk the `basedOn` chain of paragraph styles looking for a `nextStyle`
|
|
34
|
+
* definition. Returns the resolved `nextStyle` id, or undefined if none is
|
|
35
|
+
* found. Caps the walk at 32 steps to guard against circular `basedOn` chains.
|
|
36
|
+
*/
|
|
37
|
+
function resolveNextStyle(
|
|
38
|
+
styleId: string | undefined,
|
|
39
|
+
catalog: StylesCatalog,
|
|
40
|
+
): string | undefined {
|
|
41
|
+
if (!styleId) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
let current: string | undefined = styleId;
|
|
45
|
+
let steps = 0;
|
|
46
|
+
while (current && steps < 32) {
|
|
47
|
+
steps += 1;
|
|
48
|
+
const def: ParagraphStyleDefinition | undefined = catalog.paragraphs[current];
|
|
49
|
+
if (!def) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (def.nextStyle && catalog.paragraphs[def.nextStyle]) {
|
|
53
|
+
return def.nextStyle;
|
|
54
|
+
}
|
|
55
|
+
current = def.basedOn;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Given the result document and the head of the new selection (which sits at
|
|
62
|
+
* the start of the newly-created paragraph), locate that paragraph in the
|
|
63
|
+
* top-level `doc` children and return a new document with its `styleId` set to
|
|
64
|
+
* `nextStyleId` and its `numbering` cleared.
|
|
65
|
+
*/
|
|
66
|
+
function applyNextStyleToNewParagraph(
|
|
67
|
+
result: TextTransactionResult,
|
|
68
|
+
nextStyleId: string,
|
|
69
|
+
): TextTransactionResult {
|
|
70
|
+
const root = result.document.content as DocumentRootNode;
|
|
71
|
+
if (!root || root.type !== "doc") {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const head = result.selection.head;
|
|
76
|
+
const surface = createEditorSurfaceSnapshot(result.document, result.selection);
|
|
77
|
+
|
|
78
|
+
let targetBlockIndex = -1;
|
|
79
|
+
for (let i = 0; i < surface.blocks.length; i += 1) {
|
|
80
|
+
const surfaceBlock = surface.blocks[i];
|
|
81
|
+
if (
|
|
82
|
+
surfaceBlock?.kind === "paragraph" &&
|
|
83
|
+
surfaceBlock.from === head
|
|
84
|
+
) {
|
|
85
|
+
targetBlockIndex = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (targetBlockIndex === -1) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const targetBlock = root.children[targetBlockIndex];
|
|
95
|
+
if (!targetBlock || targetBlock.type !== "paragraph") {
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { numbering: _numbering, ...restProps } = targetBlock;
|
|
100
|
+
const updatedParagraph: ParagraphNode = {
|
|
101
|
+
...restProps,
|
|
102
|
+
styleId: nextStyleId,
|
|
103
|
+
children: targetBlock.children,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...result,
|
|
108
|
+
document: {
|
|
109
|
+
...result.document,
|
|
110
|
+
content: {
|
|
111
|
+
...root,
|
|
112
|
+
children: [
|
|
113
|
+
...root.children.slice(0, targetBlockIndex),
|
|
114
|
+
updatedParagraph,
|
|
115
|
+
...root.children.slice(targetBlockIndex + 1),
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
26
122
|
export function insertText(
|
|
27
123
|
document: CanonicalDocumentEnvelope,
|
|
28
124
|
selection: SelectionSnapshot,
|
|
@@ -122,7 +218,11 @@ export function splitParagraph(
|
|
|
122
218
|
selection: SelectionSnapshot,
|
|
123
219
|
context: TextCommandContext,
|
|
124
220
|
): TextTransactionResult {
|
|
125
|
-
|
|
221
|
+
// Resolve the current paragraph's styleId before the split so we can look up
|
|
222
|
+
// `nextStyle` from the styles catalog.
|
|
223
|
+
const scope = resolveParagraphScope(document, selection);
|
|
224
|
+
|
|
225
|
+
const result = applyTextTransaction(
|
|
126
226
|
document,
|
|
127
227
|
selection,
|
|
128
228
|
{
|
|
@@ -131,6 +231,26 @@ export function splitParagraph(
|
|
|
131
231
|
},
|
|
132
232
|
context,
|
|
133
233
|
);
|
|
234
|
+
|
|
235
|
+
// Only apply nextStyle for top-level paragraphs; table-cell traversal
|
|
236
|
+
// would require walking into nested blocks which the surface snapshot doesn't expose.
|
|
237
|
+
if (scope?.kind !== "top-level") {
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const originalStyleId = scope.paragraph.styleId;
|
|
242
|
+
const nextStyleId =
|
|
243
|
+
originalStyleId !== undefined
|
|
244
|
+
? resolveNextStyle(originalStyleId, document.styles)
|
|
245
|
+
: undefined;
|
|
246
|
+
|
|
247
|
+
// If the original paragraph's style specifies a `nextStyle`, apply it to the
|
|
248
|
+
// newly-created paragraph (the one at result.selection.head).
|
|
249
|
+
if (nextStyleId !== undefined) {
|
|
250
|
+
return applyNextStyleToNewParagraph(result, nextStyleId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
134
254
|
}
|
|
135
255
|
|
|
136
256
|
export function insertPageBreak(
|
package/src/io/docx-session.ts
CHANGED
|
@@ -1560,6 +1560,7 @@ function filterValidStyleIds(
|
|
|
1560
1560
|
characters: filterRecord(catalog.characters),
|
|
1561
1561
|
tables: filterRecord(catalog.tables),
|
|
1562
1562
|
...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
|
|
1563
|
+
...(catalog.docDefaults ? { docDefaults: catalog.docDefaults } : {}),
|
|
1563
1564
|
...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
|
|
1564
1565
|
};
|
|
1565
1566
|
}
|