@coco-vexel/coco-office-editor 0.0.0-stage → 1.0.0

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 (57) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +510 -2
  3. package/THIRD_PARTY_NOTICES.md +212 -0
  4. package/dist/index.cjs +26401 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +12 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.js +26192 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/model-DB6aN6uJ.d.cts +321 -0
  11. package/dist/model-DB6aN6uJ.d.ts +321 -0
  12. package/dist/native-B6B649CC.d.ts +1111 -0
  13. package/dist/native-Ct9nN06K.d.cts +1111 -0
  14. package/dist/ppt-native.cjs +5334 -0
  15. package/dist/ppt-native.cjs.map +1 -0
  16. package/dist/ppt-native.d.cts +2 -0
  17. package/dist/ppt-native.d.ts +2 -0
  18. package/dist/ppt-native.js +5332 -0
  19. package/dist/ppt-native.js.map +1 -0
  20. package/dist/ppt.cjs +5426 -0
  21. package/dist/ppt.cjs.map +1 -0
  22. package/dist/ppt.d.cts +306 -0
  23. package/dist/ppt.d.ts +306 -0
  24. package/dist/ppt.js +5410 -0
  25. package/dist/ppt.js.map +1 -0
  26. package/dist/spreadsheet.cjs +4276 -0
  27. package/dist/spreadsheet.cjs.map +1 -0
  28. package/dist/spreadsheet.d.cts +676 -0
  29. package/dist/spreadsheet.d.ts +676 -0
  30. package/dist/spreadsheet.js +4213 -0
  31. package/dist/spreadsheet.js.map +1 -0
  32. package/dist/word-editing.cjs +812 -0
  33. package/dist/word-editing.cjs.map +1 -0
  34. package/dist/word-editing.d.cts +149 -0
  35. package/dist/word-editing.d.ts +149 -0
  36. package/dist/word-editing.js +803 -0
  37. package/dist/word-editing.js.map +1 -0
  38. package/dist/word-native.cjs +13400 -0
  39. package/dist/word-native.cjs.map +1 -0
  40. package/dist/word-native.d.cts +4 -0
  41. package/dist/word-native.d.ts +4 -0
  42. package/dist/word-native.js +13293 -0
  43. package/dist/word-native.js.map +1 -0
  44. package/dist/word-prosemirror.cjs +489 -0
  45. package/dist/word-prosemirror.cjs.map +1 -0
  46. package/dist/word-prosemirror.d.cts +21 -0
  47. package/dist/word-prosemirror.d.ts +21 -0
  48. package/dist/word-prosemirror.js +483 -0
  49. package/dist/word-prosemirror.js.map +1 -0
  50. package/dist/word.cjs +16764 -0
  51. package/dist/word.cjs.map +1 -0
  52. package/dist/word.d.cts +84 -0
  53. package/dist/word.d.ts +84 -0
  54. package/dist/word.js +16632 -0
  55. package/dist/word.js.map +1 -0
  56. package/package.json +106 -3
  57. package/src/styles/editor.css +1507 -0
package/README.md CHANGED
@@ -1,3 +1,511 @@
1
- # Temporary Holding Version
1
+ # @coco-vexel/coco-office-editor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Coco Office editor runtime for browser and Electron hosts.
4
+
5
+ The package is designed as an embeddable editor layer rather than an app-specific UI. Hosts provide file loading, toolbar UI, persistence, and desktop/browser integration. The editor package owns document projection, rendering, selection mapping, editing commands, and export-oriented change records.
6
+
7
+ After the initial npm release: `npm install @coco-vexel/coco-office-editor`. Core is installed as a dependency.
8
+ For a runnable integration, build the repository then run `corepack pnpm demo`.
9
+ See [runtime and demo requirements](https://github.com/coco-vexel/coco-office/blob/main/docs/runtime-and-demos.md).
10
+
11
+ ## Quick start: a runnable PowerPoint editor
12
+
13
+ Use Node.js 22 or 24 for tooling and a modern browser with DOM/Canvas2D support. The runtime works in a browser or Electron renderer, not Node SSR. No Office installation is required. ESM/CJS entrypoints and TypeScript declarations are included; browser apps need a bundler. This quick start uses Vite.
14
+
15
+ In an empty directory, run:
16
+
17
+ ```bash
18
+ npm init -y
19
+ npm install @coco-vexel/coco-office-editor
20
+ npm install --save-dev vite
21
+ ```
22
+
23
+ Create `index.html`:
24
+
25
+ ```html
26
+ <!doctype html>
27
+ <html lang="en">
28
+ <meta charset="UTF-8" />
29
+ <title>Coco PowerPoint editor</title>
30
+ <input id="file" type="file" accept=".pptx" />
31
+ <button id="save" disabled>Export copy</button>
32
+ <p id="status">Open a PPTX file, edit its text, then export a copy.</p>
33
+ <div id="editor" style="height: 75vh; min-height: 500px"></div>
34
+ <script type="module" src="/main.js"></script>
35
+ </html>
36
+ ```
37
+
38
+ Create `main.js`, then run `npx vite` and open the printed HTTP URL:
39
+
40
+ ```js
41
+ import { buildPptEditorViewFromPptx, renderNativePptEditor } from '@coco-vexel/coco-office-editor/ppt';
42
+
43
+ const input = document.querySelector('#file');
44
+ const save = document.querySelector('#save');
45
+ const host = document.querySelector('#editor');
46
+ const status = document.querySelector('#status');
47
+ let editor;
48
+ let filename;
49
+ async function run(action) {
50
+ input.disabled = save.disabled = true;
51
+ try {
52
+ await action();
53
+ } catch (error) {
54
+ status.textContent = error.message;
55
+ } finally {
56
+ input.disabled = false;
57
+ save.disabled = !editor;
58
+ }
59
+ }
60
+ input.addEventListener('change', () =>
61
+ run(async () => {
62
+ const file = input.files?.[0];
63
+ if (!file) return;
64
+ const view = await buildPptEditorViewFromPptx(new Uint8Array(await file.arrayBuffer()));
65
+ editor?.destroy();
66
+ editor = undefined;
67
+ // A new mount avoids reattaching a ShadowRoot when switching files.
68
+ const mount = document.createElement('div');
69
+ mount.style.height = '100%';
70
+ host.replaceChildren(mount);
71
+ editor = renderNativePptEditor(mount, view);
72
+ filename = file.name.replace(/\.pptx$/i, '.edited.pptx');
73
+ input.value = '';
74
+ status.textContent =
75
+ 'Edit supported text/shapes, then export. Export before opening another file.';
76
+ }),
77
+ );
78
+ save.addEventListener('click', () =>
79
+ run(async () => {
80
+ const bytes = await editor.toPptx();
81
+ const url = URL.createObjectURL(new Blob([bytes]));
82
+ const link = document.createElement('a');
83
+ link.href = url;
84
+ link.download = filename;
85
+ link.click();
86
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
87
+ status.textContent = 'Exported a copy. Reopen it to check the result.';
88
+ }),
89
+ );
90
+ window.addEventListener('pagehide', () => editor?.destroy());
91
+ ```
92
+
93
+ This is an unstyled integration shell; the native PPT renderer supplies its editor surface. The host provides file access, save UI and session-switch policy. For React/Vue, create the editor after the mount exists and call `destroy()` on unmount. Do not share a live editor instance between DOM mounts or create it during server rendering.
94
+
95
+ ## Choose an editor and export path
96
+
97
+ | Document | Public entry | Load/render | Export |
98
+ | -------- | ----------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
99
+ | Word | `/word`, `/word/native` | `buildWordEditorViewFromDocx` + `renderNativeWordEditor` | Apply `getChanges()` to a fresh input package, then Core `toZip`; inspect unsupported/skipped/unresolved results |
100
+ | Excel | `/spreadsheet` | `await renderSpreadsheetEditor(host, bytes, options)` | `await editor.toZip()` |
101
+ | PPT | `/ppt` or `/ppt/native` | `buildPptEditorViewFromPptx` + `renderNativePptEditor` | `await editor.toPptx()` |
102
+
103
+ All import paths above start with `@coco-vexel/coco-office-editor`. If your app directly imports Core (for example to apply Word changes), declare it explicitly with `npm install @coco-vexel/coco-office`. Examples below named `host`, `xlsxBytes` or `persistWorkbook` are integration fragments: supply your own mount, file bytes and persistence callback.
104
+
105
+ Word custom Shadow DOM integration needs stylesheet text via `styleText`; Vite supports `@coco-vexel/coco-office-editor/styles.css?inline`. Other bundlers must provide an equivalent CSS-string loader. The `?inline` suffix is a Vite feature, not a portable JavaScript import. Spreadsheet's default isolated renderer and the default PPT renderer manage their surfaces. Use a mount with an explicit nonzero height.
106
+
107
+ The complete three-format file/open/edit/export flow, including Word export diagnostics, is in the [public demo source](https://github.com/coco-vexel/coco-office/blob/main/packages/editor/demo/main.js). No private product dependency is needed. These editors are experimental; file round-trip success does not certify visual parity or support for every advanced object.
108
+
109
+ ## Scope
110
+
111
+ Word, spreadsheet and fixed-canvas PowerPoint editors share Core document APIs. All three browser surfaces are experimental; advanced objects and layout have feature-specific limits.
112
+
113
+ ```text
114
+ @coco-vexel/coco-office-editor
115
+ word/
116
+ XML package or EditorDocumentView
117
+ -> NativeWordDocument
118
+ -> custom element DOM renderer
119
+ -> selection bridge
120
+ -> native command layer
121
+ -> change records for export
122
+ spreadsheet/ Canvas2D spreadsheet runtime
123
+ ppt/ fixed-canvas slide runtime
124
+ ```
125
+
126
+ ## Word Architecture
127
+
128
+ The Word editor uses a native Coco model and custom elements as the core editing surface. The legacy ProseMirror spike remains isolated for compatibility and is not part of the active implementation path.
129
+
130
+ The runtime has five layers:
131
+
132
+ 1. `extract.ts` reads a `.docx` package into `EditorDocumentView`.
133
+ 2. `native-model.ts` projects `EditorDocumentView` into `NativeWordDocument`.
134
+ 3. `native-renderer.ts` renders the native model into Word-specific custom elements.
135
+ 4. `native-selection.ts` maps DOM selection and selected objects back to native model positions.
136
+ 5. `native-commands.ts` mutates the native model through explicit commands and emits change records.
137
+
138
+ `native.ts` wires those layers into `renderNativeWordEditor(host, view, options)` for browser and Electron usage.
139
+
140
+ Export back to Word follows a two-step path:
141
+
142
+ 1. `ooxml-patch.ts` maps native editor change records into typed OOXML patch intents.
143
+ 2. A package executor applies those patch intents to the original `.docx` XML parts.
144
+
145
+ The browser demo applies changes to a fresh copy of the input package on each export and reports unsupported, skipped or unresolved changes before offering a download.
146
+
147
+ ## Public Entrypoints
148
+
149
+ Use the native Word entrypoint for the self-developed editor path:
150
+
151
+ ```ts
152
+ import { renderNativeWordEditor } from '@coco-vexel/coco-office-editor/word/native';
153
+ import {
154
+ buildWordEditorViewFromDocx,
155
+ buildWordEditorViewFromFlatOpc,
156
+ } from '@coco-vexel/coco-office-editor/word';
157
+ import editorStyles from '@coco-vexel/coco-office-editor/styles.css?inline';
158
+ import '@coco-vexel/coco-office-editor/styles.css';
159
+
160
+ const documentView = await buildWordEditorViewFromDocx(docxBytes);
161
+ const editor = renderNativeWordEditor(host, documentView, {
162
+ contextMenu: true,
163
+ // Pass the bundled editor CSS text when the host uses Shadow DOM isolation.
164
+ // Vite hosts can load it with: import editorStyles from '../styles/editor.css?inline';
165
+ styleIsolation: 'shadow',
166
+ styleText: editorStyles,
167
+ onChange(event) {
168
+ persistIncrementalChanges(event.changes, event.revertedChanges);
169
+ },
170
+ });
171
+
172
+ // If an agent returns Flat OPC instead of a DOCX zip, reuse the same view boundary.
173
+ editor.loadDocument(buildWordEditorViewFromFlatOpc(flatOpcXml));
174
+
175
+ editor.setSelection({ kind: 'caret', paraId: 'paragraph-id', offset: 0 });
176
+ editor.dispatch({ kind: 'insert-text', text: 'Hello' });
177
+ ```
178
+
179
+ `buildWordEditorViewFromDocx(bytes)` is the browser/Electron host helper for the normal load path. It accepts `Uint8Array` or `ArrayBuffer`, so hosts do not need to assemble `fromZip`, `buildIndices`, and `buildWordEditorViewFromPackage` themselves. `buildWordEditorViewFromFlatOpc(xml)` provides the same boundary when an agent returns Flat OPC XML.
180
+
181
+ The same runtime is used in a normal browser page and an Electron renderer. File access and IPC stay in the host application; the editor itself only requires a DOM and an `EditorDocumentView`.
182
+
183
+ The native Word runtime uses Shadow DOM isolation by default. The editor surface,
184
+ search highlights, comment dialogs, and context menus are mounted in the same
185
+ ShadowRoot, so host-page rules do not leak into the editing surface. Hosts should
186
+ provide the editor stylesheet through `styleText` (for example with Vite's
187
+ `?inline` CSS import). Legacy hosts that intentionally manage styles in light DOM
188
+ can opt out with `styleIsolation: 'none'`.
189
+
190
+ ### Native runtime contract
191
+
192
+ `renderNativeWordEditor` returns a UI-independent runtime. A host toolbar should read `getCommandState()`, check `canDispatch(command)`, and call `dispatch(command)` rather than editing rendered custom elements directly.
193
+
194
+ ```ts
195
+ const unsubscribeSelection = editor.subscribe('selectionChange', ({ selection, commandState }) => {
196
+ toolbar.update(selection, commandState);
197
+ });
198
+
199
+ const unsubscribeChange = editor.subscribe('change', (event) => {
200
+ // Incremental since the previous change notification. Undo moves records to
201
+ // revertedChanges; redo returns them to changes.
202
+ console.log(event.revision, event.changes, event.revertedChanges, event.totalChangeCount);
203
+ });
204
+
205
+ editor.setSelection({
206
+ kind: 'text-range',
207
+ anchor: { paraId: 'p1', offset: 0 },
208
+ focus: { paraId: 'p2', offset: 4 },
209
+ });
210
+ editor.dispatch({ kind: 'toggle-bold' });
211
+ editor.dispatch({
212
+ kind: 'set-hyperlink',
213
+ hyperlink: { target: 'https://coco.example/docs', tooltip: 'Coco docs' },
214
+ });
215
+ editor.dispatch({ kind: 'remove-hyperlink' });
216
+
217
+ if (editor.canUndo()) editor.undo();
218
+ if (editor.canRedo()) editor.redo();
219
+
220
+ // Structural table commands are enabled for regular, unmerged grids.
221
+ editor.dispatch({ kind: 'insert-table', rows: 2, columns: 3 });
222
+ const table = editor.getCommandState().table;
223
+ if (table?.structuralEditingSupported) {
224
+ editor.dispatch({ kind: 'insert-table-row', position: 'after' });
225
+ editor.dispatch({ kind: 'insert-table-column', position: 'before' });
226
+ }
227
+ if (table) {
228
+ editor.dispatch({ kind: 'set-table-cell-background', color: '#FFF2CC' });
229
+ editor.dispatch({ kind: 'set-table-cell-vertical-alignment', alignment: 'center' });
230
+ editor.dispatch({ kind: 'set-table-cell-borders', color: '#808080' });
231
+ editor.dispatch({ kind: 'delete-table' });
232
+ }
233
+
234
+ // Insert from a browser File/Blob or a data URL, then edit the selected image.
235
+ await editor.insertImage(file, { width: 320, height: 180 });
236
+ const image = editor.getCommandState().image;
237
+ if (image) {
238
+ editor.dispatch({
239
+ kind: 'set-image-size',
240
+ objectId: image.objectId,
241
+ width: 400,
242
+ height: 225,
243
+ });
244
+ editor.dispatch({
245
+ kind: 'set-image-alignment',
246
+ objectId: image.objectId,
247
+ alignment: 'center',
248
+ });
249
+ }
250
+
251
+ // Search body paragraphs and nested table content without changing the editor DOM.
252
+ const searchState = editor.search({
253
+ query: 'OfficeAI',
254
+ matchCase: false,
255
+ wholeWord: false,
256
+ });
257
+ console.log(searchState.activeIndex, searchState.matches.length);
258
+ editor.findNext('forward');
259
+ editor.replaceCurrent('Coco');
260
+ const replacementCount = editor.replaceAll('Coco');
261
+ console.log(replacementCount);
262
+ editor.clearSearch();
263
+
264
+ // Reuse the host for another file without retaining selection or undo history.
265
+ editor.loadDocument(nextDocumentView);
266
+
267
+ unsubscribeChange();
268
+ unsubscribeSelection();
269
+ editor.destroy();
270
+ ```
271
+
272
+ The lifecycle and integration rules are:
273
+
274
+ - `getChanges()` returns a snapshot of currently active export-oriented changes.
275
+ - `loadDocument()` clears selection, pending changes, and undo/redo history before rendering the new view.
276
+ - `destroy()` is idempotent, removes DOM/document listeners and observers, clears the host, and makes commands return `false`.
277
+ - `destroyed` exposes lifecycle state. `loadDocument()` after destruction throws because the instance cannot be reused.
278
+ - Multiple runtimes may coexist in one document. Document-level shortcuts are routed only to the most recently active editor.
279
+ - The built-in HTML context menu is enabled by default in browsers and Electron renderers. Set `contextMenu: false` when the host supplies its own menu UI.
280
+ - `getDocument()` exposes the live native projection for inspection and export integration; hosts must not mutate it directly.
281
+ - Clicking an inline image creates an object selection exposed as `getCommandState().image`. The native resize handle preserves the aspect ratio by default; hold Shift while dragging for independent width and height changes.
282
+ - Image insertion, resize, alignment, deletion, undo, and redo are supported in the live editor model. Ordinary inline image insertion, resize, deletion, and tracked image insertion resolution are supported by the current OOXML patch executor and covered by DOCX round-trip tests. Complex floating drawings, unsupported drawing types, and layout-specific wrap semantics remain conservative and must not be treated as fully editable.
283
+
284
+ The broader Word barrel is still available:
285
+
286
+ ```ts
287
+ import { extractWordDocumentView } from '@coco-vexel/coco-office-editor/word';
288
+ ```
289
+
290
+ The self-developed spreadsheet editor entrypoint is backed by Coco Excel Core and is usable from browser and Electron hosts:
291
+
292
+ See [SPREADSHEET_EDITOR_API.md](https://github.com/coco-vexel/coco-office/blob/main/packages/editor/SPREADSHEET_EDITOR_API.md) for the full
293
+ custom-host integration contract and examples.
294
+
295
+ ```ts
296
+ import { renderSpreadsheetEditor } from '@coco-vexel/coco-office-editor/spreadsheet';
297
+
298
+ const editor = await renderSpreadsheetEditor(host, xlsxBytes, {
299
+ workbookId: 'browser-document.xlsx',
300
+ showSheetTabs: true,
301
+ // Shadow DOM isolation is enabled by default; use 'none' only for legacy hosts.
302
+ styleIsolation: 'shadow',
303
+ // Optional; column menus remain available when this is omitted.
304
+ enableHeaderFilters: false,
305
+ onChange({ bytes }) {
306
+ persistWorkbook(bytes);
307
+ },
308
+ });
309
+
310
+ await editor.setActiveSheet('Sheet2');
311
+ const nextBytes = await editor.toZip();
312
+ editor.destroy();
313
+ ```
314
+
315
+ `renderSpreadsheetEditor()` owns the canvas stack, viewport lifecycle, DPI-aware
316
+ resizing, scrolling, and worksheet switching. The host owns toolbar UI,
317
+ Agent panels, persistence policy, and browser/Electron integration. The lower
318
+ level `SpreadsheetEditorRuntime`, `SpreadsheetLayout`, and renderer exports
319
+ remain public for custom hosts and tests, but normal web and desktop hosts
320
+ should use the entrypoint so those responsibilities are not reimplemented in
321
+ each application.
322
+
323
+ The public entrypoint uses Shadow DOM style isolation by default. This keeps
324
+ editor controls, cell editing, menus, scrollbars, and sheet tabs from inheriting
325
+ host-page CSS. Hosts that require the legacy light-DOM behavior can opt out with
326
+ `styleIsolation: 'none'`.
327
+
328
+ The public surface also owns selection, cell editing, copy/paste, formula
329
+ reference highlighting, merged-cell selection, auto fill, row/column resizing,
330
+ filtering, sorting, freeze panes, and worksheet structure commands. Header
331
+ filter icons are opt-in with `enableHeaderFilters`; when disabled, filtering and
332
+ sorting remain available from a column-header context menu.
333
+
334
+ Formatting commands include font/fill colors, bold/italic/underline,
335
+ horizontal/vertical alignment, number formats, borders, and format clearing.
336
+
337
+ Custom hosts can subscribe to `subscribeState()` for selection, active-sheet,
338
+ and editing changes. Multi-step UI or Agent actions should use
339
+ `editor.runTransaction({ label }, async (editor) => { ... })`; the commands in
340
+ the callback share one Core history group and produce one render/export change
341
+ event after the transaction completes.
342
+
343
+ ### Spreadsheet worksheets
344
+
345
+ `SpreadsheetEditorRuntime` exposes the active worksheet through `activeSheet()` and switches it with `await setActiveSheet(name)`. The runtime rebuilds the render model and formula results for the selected worksheet; cell edits always apply only to that active sheet.
346
+
347
+ Web and desktop hosts should own view-only state by sheet name. Before a tab switch, store the current selection and scroll position; after `setActiveSheet()`, restore the target sheet's saved state, defaulting to `A1` and `{ x: 0, y: 0 }`. Do not store this host state in the workbook model or undo history. The dev host follows this rule and also serializes tab switches so quick consecutive clicks cannot apply out of order.
348
+
349
+ Freeze panes are workbook-backed view settings, not host-only scroll state. Use `await runtime.setFreezePanes(frozenRows, frozenColumns)` to persist them through undo/redo and XLSX export. The dev toolbar derives the split from the active cell: selecting `C4` and choosing `Freeze panes` freezes rows `1:3` and columns `A:B`; `Freeze rows above`, `Freeze columns left`, and `Unfreeze panes` cover the common variants. Hosts should continue to keep their transient scroll offsets separate from these persisted pane settings.
350
+
351
+ Worksheet structure changes are workbook-backed as well. Use `addSheet()`, `renameSheet()`, `copySheet()`, `moveSheet()`, and `deleteSheet()` on `SpreadsheetEditor` or `SpreadsheetEditorRuntime`. Direct user sheet-tab operations deliberately do not enter the Ctrl/Cmd+Z history, matching desktop spreadsheet behavior; deletion must be protected by host UI confirmation. When the same methods run inside `runAgentTransaction()`, they create a `workbook.sheets` structural patch so the complete Agent action can roll back atomically. The dev host exposes add from the `+` sheet-tab button and rename, duplicate, move, and delete from a sheet-tab context menu. A renamed sheet retains its host-side selection and scroll state; a copied sheet opens at `A1`; deleting the active sheet selects its nearest neighbor.
352
+
353
+ ### Spreadsheet auto fill
354
+
355
+ `autoFillRange(sourceRange, targetRange)` extends a cell selection down or right as one user undo group. The first version repeats ordinary values and styles, continues a consistent numeric sequence when at least two numeric source cells establish one, and rebases relative formula references through Core's `shiftFormulaReferences()` utility. `$`-anchored row/column references and formula string literals remain unchanged. The dev canvas presents the fill handle on the selection's lower-right corner and previews the target range while dragging. Merged selections intentionally do not expose a handle yet, because merged-block expansion requires structural copy semantics rather than a cell fill.
356
+
357
+ ### Spreadsheet merged cells
358
+
359
+ Spreadsheet hosts should treat merged ranges as one visual editing unit:
360
+
361
+ - Use `expandSelectionForMergedRanges()` before rendering drag/paste selections. If a range touches any merged block, the selection expands to include the full block.
362
+ - Internal copy keeps merged range metadata and exports text only from the merge master. Covered cells are copied as blank even if an imported workbook still contains stale hidden values.
363
+ - Internal copy also carries cell style ids for same-workbook paste, so bold/italic/fill/border/number-format state can paste with values. Plain external TSV paste remains value-only.
364
+ - Paste first unmerges intersecting target merges, writes the rectangular payload, then reapplies copied merges. Undo records that paste as one user transaction.
365
+ - Runtime paste supports `mode: 'all' | 'values' | 'formats'`. `all` writes values, styles, and copied merges; `values` writes only values; `formats` writes only style ids.
366
+ - The dev host context menu exposes `Paste`, `Paste values`, and `Paste formats`. `Paste formats` is shown only when the current clipboard matches an internal Coco copy payload.
367
+ - The dev host supports lightweight find and jump: `Ctrl/Cmd+F` opens a sheet search bar, Enter/Shift+Enter moves through matches, and the replace field can replace all matching values/formula text as one undo group. The name box accepts both cells such as `C5` and ranges such as `B2:D8`.
368
+ - Spreadsheet filtering is disabled by default. After enabling the toolbar `Filter` toggle, the first spreadsheet filter is opened by clicking the small funnel icon inside a first-row header cell; clicking the `A/B/C` coordinate header still selects the whole column. The menu uses a scroll-limited checkbox list, supports selecting multiple values, blank values, applying the selection, and clearing the filter. It also provides A-Z and Z-A sorting, which moves complete data rows through one Core snapshot. The `Headers` toggle controls whether row 1 is excluded from sorting/filtering; disable it for data-only sheets without a header row. The legacy toolbar filter remains available for explicit multi-row selections. Non-matching data rows are hidden through one Core snapshot transaction. `filterRows()` accepts one value or a value array, and `sortRows()` accepts a direction; both operations remain exportable and undoable.
369
+ - Editor merge/unmerge normalizes hidden follower styles to the merge master's style before changing structure. This prevents stale covered-cell formatting from reappearing after unmerge.
370
+ - Row/column deletion trims or shifts merged ranges through core structural patches. Undo/redo restores the deleted structure and merged ranges without requiring a full-sheet snapshot when formulas do not need reference recovery.
371
+
372
+ Merged-cell smoke checklist for web/desktop hosts:
373
+
374
+ - Click a covered cell in a merged block; selection should move to the merge master and cover the full merged range.
375
+ - Drag a range that partially touches a merged block; selection should expand until every touched merged block is fully included.
376
+ - Right-click inside an existing range selection; the range selection should not collapse to the clicked cell.
377
+ - Insert rows/columns from the context menu; the inserted rows/columns should remain selected as a range.
378
+ - Delete rows/columns from the context menu; selection should move to the nearest valid cell/row/column and expand if it lands inside a merged block.
379
+ - Copy/paste a merged range, then unmerge the pasted block; only the merge master should keep copied content.
380
+ - Copy/paste a formatted range; values and visible formatting should land together and undo/redo as one paste.
381
+ - Paste special values and paste special formats through runtime API; values-only should not overwrite formatting, and formats-only should not overwrite values.
382
+ - Use the context menu paste commands after an internal copy; `Paste values` should ignore styles/merges, while `Paste formats` should keep existing values.
383
+ - Use `Ctrl/Cmd+F` to find visible values, raw values, or formula text; results should wrap and scroll the active match into view.
384
+ - Type an address or range into the name box and press Enter; selection should jump there and scroll into view.
385
+ - Toggle merge/unmerge and undo; history should use `sheet.mergedRanges` patches without a full sheet snapshot.
386
+ - Delete rows/columns through a merged block and undo/redo; the visible merge shape and values outside the merge should round-trip.
387
+ - Drag column and row header boundaries; the resize guide should track the pointer, clamp to minimum sizes, commit through core row/column size patches, and undo/redo cleanly.
388
+ - Select a mixed-format range; toolbar and cell settings should show dashed mixed states for B/I/U, colors, number format, and alignment instead of using only the active cell.
389
+
390
+ ### Spreadsheet Agent actions
391
+
392
+ Spreadsheet agent integrations should use action objects instead of separate preview and apply implementations. An action is a reusable runner plus metadata. The same `run()` function is executed against a cloned workbook for preview and against the live workbook for apply, so web and desktop hosts do not accidentally show one result and commit another.
393
+
394
+ ```ts
395
+ import {
396
+ applySpreadsheetAgentAction,
397
+ previewSpreadsheetAgentAction,
398
+ type SpreadsheetAgentAction,
399
+ } from '@coco-vexel/coco-office-editor/spreadsheet';
400
+
401
+ const action: SpreadsheetAgentAction = {
402
+ id: 'fill-summary',
403
+ label: 'Fill summary',
404
+ actor: 'spreadsheet-agent',
405
+ agentRunId: runId,
406
+ agentStepId: stepId,
407
+ async run(runtime, context) {
408
+ const range = context.selection ?? { startRow: 1, startColumn: 1, endRow: 1, endColumn: 1 };
409
+ await runtime.setRangeValues(range.startRow, range.startColumn, [['Summary', 42]]);
410
+ await runtime.setRangeBold(range.startRow, range.startColumn, 1, 2, true);
411
+ },
412
+ };
413
+
414
+ const context = { selection: currentSelection };
415
+ const preview = await previewSpreadsheetAgentAction(runtime, action, context);
416
+ renderConfirmation(preview.transaction.summary, preview.transaction.diffSummary);
417
+
418
+ // Call this only after the user or host policy confirms the preview.
419
+ const applied = await applySpreadsheetAgentAction(runtime, action, context);
420
+ ```
421
+
422
+ Host integration rules:
423
+
424
+ - Build one `SpreadsheetAgentAction` per agent-visible operation.
425
+ - Use `previewSpreadsheetAgentAction()` for confirmation UI. It runs on a cloned workbook and does not mutate the live editor or live undo stack.
426
+ - Use `applySpreadsheetAgentAction()` to commit. It records one agent transaction group, so `undo()` reverts the whole action in one step.
427
+ - Keep preview and apply on the same action and context. Do not duplicate the runner logic in UI code.
428
+ - Store `summary` and `diffSummary` for agent explanations, audit trails, and desktop/web confirmation dialogs.
429
+
430
+ The ProseMirror entrypoint remains isolated:
431
+
432
+ ```ts
433
+ import { createWordEditor } from '@coco-vexel/coco-office-editor/word/prosemirror';
434
+ ```
435
+
436
+ ## Current Word Coverage
437
+
438
+ The detailed live-editing versus DOCX-writeback boundary is maintained in [docs/word-editor-capability-matrix.md](../../docs/word-editor-capability-matrix.md).
439
+
440
+ Implemented in the native path:
441
+
442
+ - Paragraph projection and rendering.
443
+ - Inline text and image projection.
444
+ - Table rows, cells, and nested paragraphs.
445
+ - Paragraph style rendering for alignment, spacing, indentation, line height, font size, bold, color, and borders.
446
+ - Run style rendering for bold, italic, underline, font size, and color.
447
+ - Table cell style rendering for width, shading, and vertical alignment.
448
+ - Basic editing commands for text insertion, backward delete, paragraph split/merge, image insertion/delete/resize/alignment, and browser text sync.
449
+ - Row and column insertion/deletion for regular tables, with local table rendering and undo/redo snapshots.
450
+ - Top-level table insertion at a body caret and whole-table deletion, with paragraph splitting, local block rendering, and one-step undo/redo.
451
+ - Current-cell background, vertical alignment, and four-side border formatting, including merged cells and undo/redo.
452
+ - Rectangular table-cell selection by cross-cell drag or Shift-click, with arrow-key extension, row/column selection, bulk formatting, clear, and one-step undo/redo.
453
+ - Table-cell range copy/cut/paste uses a private two-dimensional rich-content payload inside Coco, preserving paragraph/run formatting, cell presentation, and images with fresh object IDs. Other applications receive tab-separated plain text.
454
+ - Body and nested-table find/replace with case and whole-word options, CSS Highlight rendering, cyclic navigation, and one-step undo/redo for replace-all.
455
+ - External and document-anchor hyperlink parsing, native rendering, selection state, set/remove commands, rich clipboard preservation, and one-step undo/redo. External links open only through Ctrl/Cmd-click.
456
+ - A shared browser/Electron HTML context menu for history, clipboard, text, image, and table commands.
457
+ - OOXML patch planning for text, paragraph, image, comment, revision, table, header, and footer changes.
458
+ - OOXML patch application for ordinary text, formatting, inline images, comments, revisions, regular tables, headers, and footers, with conservative handling for unsupported structures.
459
+ - Coverage summaries for comparing source `EditorDocumentView` content against rendered native content.
460
+
461
+ Known gaps:
462
+
463
+ - Complex text ranges spanning unsupported Word run structures are detected and skipped by the executor.
464
+ - Complex Word layout remains partial: floating drawings, footnotes, section layout, advanced numbering, and special wrapping semantics need explicit model support.
465
+ - Complex merged-grid structural edits remain disabled when the original table structure cannot be represented safely.
466
+ - Collaborative editing and remote-operation reconciliation are not implemented.
467
+ - Find/replace does not currently include headers, footers, comments, hidden revision content, or phrases spanning paragraph boundaries.
468
+ - External http/https hyperlink changes are written back to DOCX relationships; document-anchor navigation works in the editor, while bookmark-aware anchor writeback waits for first-class bookmark projection.
469
+ - Cross-table mixed text selection is not yet an editable range.
470
+ - Structural editing of complex merged table grids remains disabled to prevent content loss; regular row/column changes and horizontal merge/split are applied to DOCX XML during export.
471
+
472
+ ## Development
473
+
474
+ Run the Word editor dev page:
475
+
476
+ ```bash
477
+ corepack pnpm --filter @coco-vexel/coco-office-editor dev
478
+ ```
479
+
480
+ Open the native engine:
481
+
482
+ ```text
483
+ http://127.0.0.1:5177/?engine=native
484
+ ```
485
+
486
+ Validate the package:
487
+
488
+ ```bash
489
+ corepack pnpm --filter @coco-vexel/coco-office-editor test
490
+ corepack pnpm --filter @coco-vexel/coco-office-editor build
491
+ ```
492
+
493
+ ## Extension Direction
494
+
495
+ The native Word runtime should stay model-first. New document features should usually be added in this order:
496
+
497
+ 1. Preserve the information in `EditorDocumentView` extraction.
498
+ 2. Add a typed native model representation.
499
+ 3. Render the native node or decoration through custom elements.
500
+ 4. Map selection or object interaction back to model positions.
501
+ 5. Add command records that can later be exported to OOXML.
502
+
503
+ This keeps display fidelity, editing operations, and export semantics tied to the original Word structure instead of a generic HTML document shape.
504
+
505
+ ## Standalone browser demo
506
+
507
+ From the repository root, run `pnpm build` then `pnpm demo`. The demo accepts local DOCX/XLSX/PPTX files and generates synthetic samples without external documents. `pnpm build:demo` produces static files without publishing them. Export changes before switching documents.
508
+
509
+ ## License
510
+
511
+ Apache-2.0 for original code; see LICENSE and THIRD_PARTY_NOTICES.md. The Core dependency carries its own notices.