kindy-editor 1.0.1 → 2.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.
@@ -0,0 +1,510 @@
1
+ # API reference
2
+
3
+ Public API của `kindy-editor@2`. TypeScript declarations trong package là nguồn chính xác khi tài liệu và runtime khác nhau.
4
+
5
+ ## Package entrypoints
6
+
7
+ ```ts
8
+ import { KindyEditor, KindyDocumentLibrary } from 'kindy-editor'
9
+ import 'kindy-editor/style'
10
+ ```
11
+
12
+ `kindy-editor` export UI, core, codecs, engines và mock helpers.
13
+
14
+ ```ts
15
+ import {
16
+ createDocumentLibrary,
17
+ createRestDocumentAdapter,
18
+ } from 'kindy-editor/core'
19
+ ```
20
+
21
+ `kindy-editor/core` chỉ export types, client, state, errors và adapters; entrypoint này không tải Vue UI.
22
+
23
+ ## `KindyDocumentLibrary`
24
+
25
+ Workspace hoàn chỉnh kết nối UI với `DocumentLibraryClient`.
26
+
27
+ ### Props
28
+
29
+ | Prop | Type | Default | Ghi chú |
30
+ |---|---|---|---|
31
+ | `adapter` | `DocumentApiAdapter` | — | Bắt buộc nếu không truyền `client` |
32
+ | `client` | `DocumentLibraryClient` | — | Client do ứng dụng chủ quản lý |
33
+ | `autosave` | `{ enabled?: boolean; delay?: number }` | client default | Delay tối thiểu 250ms |
34
+ | `stateSyncDelay` | `number` | `300` | Gom chuỗi transaction trước khi tạo canonical JSON; tối thiểu 32ms |
35
+ | `docxProfile` | `'kindy-docx-v2.0' \| 'kindy-docx-v2.1' \| 'kindy-docx-v2.2'` | `kindy-docx-v2.0` | Profile strict dùng cho import/export; chỉ nâng khi integration đã chấp nhận capability matrix tương ứng |
36
+ | `collaboration` | `CollaborationAdapter` | — | Adapter realtime của host; workspace connect/disconnect theo vòng đời document/editor |
37
+ | `user` | `{ id?, name?, color? }` | — | Identity dùng chung cho Track Changes và collaboration |
38
+ | `locale` | `string` | `vi-VN` | Library UI và editor locale |
39
+ | `theme` | `Record<string,string>` | `{}` | CSS variable overrides |
40
+ | `messages` | `Partial<KindyLibraryMessages>` | `{}` | Override Library UI labels |
41
+ | `ui` | `Partial<KindyLibraryUiOptions>` | defaults | Layout/density/panels |
42
+ | `editorOptions` | `Record<string,unknown>` | preset Contract | Override cấu hình `KindyEditor`; mặc định compact và page-only |
43
+ | `showVersions` | `boolean` | `true` | Backward-compatible versions switch |
44
+ | `confirmCompatibility` | `(report) => boolean \| Promise<boolean>` | `window.confirm` | Xác nhận best-effort import/export |
45
+
46
+ Phải truyền đúng một trong `adapter` hoặc `client`. Khi tự truyền client, component không destroy client lúc unmount.
47
+
48
+ ### Events
49
+
50
+ | Event | Payload |
51
+ |---|---|
52
+ | `ready` | `{ document, editor }` |
53
+ | `opened` | `DocumentSnapshot` |
54
+ | `changed` | `DocumentSnapshot` |
55
+ | `save-started` | `{ documentId, reason }` |
56
+ | `saved` | `SaveResult & { documentId }` |
57
+ | `save-failed` | `DocumentLibraryError` |
58
+ | `created` | `DocumentSummary` |
59
+ | `imported` | `DocumentSummary` |
60
+ | `compatibility-warning` | `CompatibilityReport` |
61
+ | `version-restored` | `DocumentSnapshot` |
62
+ | `printed` | `DocumentRecord` |
63
+ | `locale-changed` | `string` |
64
+ | `error` | `unknown` |
65
+
66
+ ### Exposed handle
67
+
68
+ ```ts
69
+ workspace.client
70
+ workspace.openDocument(document)
71
+ workspace.closeDocument()
72
+ workspace.save()
73
+ workspace.exportDocx({ mode, store, fileName, preferOriginal })
74
+ workspace.downloadDocx()
75
+ workspace.importDocument() // mở file picker và import qua client/adapter
76
+ workspace.preparePrint()
77
+ workspace.print()
78
+ workspace.getState()
79
+ workspace.flushState()
80
+ workspace.getEditor()
81
+ workspace.refresh()
82
+ workspace.toggleExplorer(open?)
83
+ workspace.toggleVersions(open?)
84
+ ```
85
+
86
+ `exportDocx()` trả `{ blob, report, source }` nhưng không tự download; `source` là `original` hoặc `serialized`. Mặc định hàm ưu tiên blob gốc nếu `DocumentRecord.originalSource.revisionId` vẫn là revision đang mở; đặt `preferOriginal: false` để buộc serialize. `downloadDocx()` cũng ưu tiên artifact gốc (Blob hoặc URL), sau đó mới chạy flow strict → confirm best-effort → download.
87
+ `save()` luôn flush transaction đang chờ. `flushState()` chỉ đồng bộ editor vào client và đánh dấu dirty, không tự tạo version.
88
+
89
+ ## `KindyDocumentLibraryShell`
90
+
91
+ Layout component không có IO hoặc document state.
92
+
93
+ ### Props
94
+
95
+ ```ts
96
+ {
97
+ density?: 'compact' | 'comfortable'
98
+ explorerWidth?: string
99
+ versionsWidth?: string
100
+ explorerOpen?: boolean
101
+ versionsOpen?: boolean
102
+ showExplorer?: boolean
103
+ showVersions?: boolean
104
+ showTopbar?: boolean
105
+ theme?: Record<string, string>
106
+ }
107
+ ```
108
+
109
+ Slots: `explorer`, `topbar`, default workspace content, `versions`.
110
+
111
+ Event: `close-panels` khi người dùng click scrim ở viewport nhỏ.
112
+
113
+ ## `KindyDocumentExplorer`
114
+
115
+ Component Explorer độc lập. Component gọi trực tiếp `client.listDocuments`, `adapter.listTemplates`, `adapter.listFolders`, import codec và `client.create/import`.
116
+
117
+ ### Props
118
+
119
+ ```ts
120
+ {
121
+ client: DocumentLibraryClient
122
+ selectedId?: string
123
+ locale?: string
124
+ messages?: Partial<KindyLibraryMessages>
125
+ confirmCompatibility?: (report: CompatibilityReport) => boolean | Promise<boolean>
126
+ closable?: boolean
127
+ pageSize?: number
128
+ }
129
+ ```
130
+
131
+ Events: `open`, `created`, `imported`, `compatibility-warning`, `error`, `close`.
132
+
133
+ Slots: `title`, `actions`, `document`.
134
+
135
+ Handle: `refresh()`, `clearSearch()`.
136
+
137
+ Refresh mới abort request cũ để tránh kết quả search đến sai thứ tự.
138
+
139
+ ## `KindyVersionPanel`
140
+
141
+ ### Props
142
+
143
+ ```ts
144
+ {
145
+ client: DocumentLibraryClient
146
+ documentId?: string
147
+ currentVersionId?: string
148
+ previewVersionId?: string
149
+ canRestore?: boolean
150
+ locale?: string
151
+ messages?: Partial<KindyLibraryMessages>
152
+ closable?: boolean
153
+ }
154
+ ```
155
+
156
+ Events: `preview`, `restore`, `error`, `close`.
157
+
158
+ Handle: `refresh()`.
159
+
160
+ Panel không tự load snapshot hoặc restore; component cha quyết định hành động khi nhận event.
161
+
162
+ ## `KindyEditor`
163
+
164
+ Editor Vue độc lập. Props đầy đủ vẫn tương thích options hiện có; các prop chính:
165
+
166
+ ```ts
167
+ interface KindyEditorOptions {
168
+ locale?: 'vi-VN' | 'en-US' | 'zh-CN' | 'it-IT' | 'ru-RU'
169
+ theme?: 'light' | 'dark' | 'auto'
170
+ skin?: 'default' | 'modern'
171
+ height?: string
172
+ toolbar?: Record<string, unknown>
173
+ statusbar?: Record<string, boolean>
174
+ page?: Record<string, unknown>
175
+ document?: {
176
+ title?: string
177
+ content?: string | JSONContent
178
+ assets?: AssetReference[]
179
+ readOnly?: boolean
180
+ autoSave?: { enabled: boolean; interval?: number }
181
+ }
182
+ translations?: Record<string, Record<string, string>>
183
+ }
184
+ ```
185
+
186
+ ### Contract editor preset
187
+
188
+ ```ts
189
+ CONTRACT_EDITOR_OPTIONS
190
+ createContractEditorOptions(overrides?)
191
+ ```
192
+
193
+ Preset mặc định của `KindyDocumentLibrary` dùng `toolbar.defaultMode = 'classic'`, `toolbar.allowModeSwitch = false`, các nhóm `base/insert/table/page/export`, `page.layouts = ['page']` và vô hiệu hóa các extension ngoài phạm vi hợp đồng. Trong workspace, Import/Export trên toolbar được chuyển sang pipeline của `DocumentLibraryClient` để giữ đúng revision và artifact. Các field status bar:
194
+
195
+ ```ts
196
+ {
197
+ showOutline, showSpellcheck, showShortcuts, showReset,
198
+ showLayout, showPageStatus, showWordCount, showBranding,
199
+ showFullscreen, showPreview, showZoom, showLocale
200
+ }
201
+ ```
202
+
203
+ Các method public thường dùng:
204
+
205
+ ```ts
206
+ editor.setContent(content, options?)
207
+ editor.insertContent(content)
208
+ editor.getContent(type?)
209
+ editor.getJSON()
210
+ editor.getState()
211
+ editor.getHTML()
212
+ editor.getText()
213
+ editor.getPage()
214
+ editor.setPage(page)
215
+ editor.setReadOnly(readOnly)
216
+ editor.focus()
217
+ editor.preparePrint()
218
+ editor.print()
219
+ editor.saveContent()
220
+ editor.useEditor() // raw Tiptap Editor
221
+ ```
222
+
223
+ Khi editor nằm trong `KindyDocumentLibrary`, không bật `document.autoSave`; workspace dùng `DocumentLibraryClient` làm pipeline lưu duy nhất.
224
+
225
+ ## `mountKindyEditor`
226
+
227
+ ```ts
228
+ const mounted = mountKindyEditor(container, props)
229
+
230
+ mounted.app // Vue App
231
+ mounted.instance // KindyEditor handle
232
+ mounted.unmount()
233
+ ```
234
+
235
+ `container` là HTMLElement hoặc CSS selector.
236
+
237
+ ## `KindyDocumentState`
238
+
239
+ ```ts
240
+ interface KindyDocumentState {
241
+ schemaVersion: '2.0'
242
+ content: JSONContent
243
+ page: KindyPageState
244
+ assets: AssetReference[]
245
+ }
246
+ ```
247
+
248
+ State phải là JSON serializable. Không lưu Vue proxy, DOM node, File object hoặc callback trong state.
249
+
250
+ `DocumentRecord` của một tài liệu import có thể chứa:
251
+
252
+ ```ts
253
+ originalSource?: {
254
+ artifactId: string
255
+ revisionId: string
256
+ format: 'original-docx'
257
+ fileName: string
258
+ compatibilityReport?: CompatibilityReport
259
+ }
260
+ ```
261
+
262
+ Backend REST phải trả binding này từ `/documents/import` nếu muốn workspace hỗ trợ tải nguyên bản byte-for-byte.
263
+
264
+ ```ts
265
+ const state = createEmptyDocumentState({ content, page, assets })
266
+ const migrated = migrateDocumentState(input)
267
+ ```
268
+
269
+ `migrateDocumentState` chấp nhận raw ProseMirror document v1 và bọc thành state v2.
270
+
271
+ ## `DocumentLibraryClient`
272
+
273
+ ```ts
274
+ const client = createDocumentLibrary({
275
+ adapter,
276
+ autosave: { enabled: true, delay: 1500 },
277
+ codecs,
278
+ engine,
279
+ locale: 'vi-VN',
280
+ theme,
281
+ })
282
+ ```
283
+
284
+ ### Properties
285
+
286
+ ```ts
287
+ client.adapter
288
+ client.current // clone của snapshot hiện tại hoặc null
289
+ client.hasUnsavedChanges
290
+ client.hasConflict
291
+ ```
292
+
293
+ ### Methods
294
+
295
+ ```ts
296
+ client.listDocuments(query?, signal?)
297
+ client.open(documentId, versionId?, signal?)
298
+ client.create(input, signal?)
299
+ client.import(input, signal?)
300
+ const snapshot = client.updateState(state)
301
+ client.save('autosave' | 'manual')
302
+ client.restore(versionId, signal?)
303
+ client.storeArtifact(input, signal?)
304
+ client.resolveConflict(snapshot)
305
+ client.destroy()
306
+ ```
307
+
308
+ `updateState` trả snapshot clone, đánh dấu dirty và lên lịch autosave. Nếu có save đang chạy, `save()` chờ save đó hoàn tất rồi mới chạy lượt kế tiếp. Backend phải dùng `baseRevisionId` để compare-and-swap.
309
+
310
+ ### Client events
311
+
312
+ ```ts
313
+ const off = client.on('saved', listener)
314
+ off()
315
+ ```
316
+
317
+ Events: `opened`, `imported`, `changed`, `save-started`, `saved`, `save-failed`, `version-restored`, `error`.
318
+
319
+ ## `DocumentApiAdapter`
320
+
321
+ ```ts
322
+ interface DocumentApiAdapter {
323
+ listDocuments(query?, signal?): Promise<Page<DocumentSummary>>
324
+ getDocument(documentId, signal?): Promise<DocumentRecord>
325
+ createDocument(input, signal?): Promise<DocumentRecord>
326
+ importDocument(input, signal?): Promise<DocumentRecord>
327
+ updateDocument(documentId, patch, signal?): Promise<DocumentRecord>
328
+ loadState(documentId, versionId?, signal?): Promise<DocumentSnapshot>
329
+ saveState(documentId, input, signal?): Promise<SaveResult>
330
+ listVersions(documentId, query?, signal?): Promise<Page<DocumentVersion>>
331
+ restoreVersion(documentId, versionId, signal?): Promise<DocumentSnapshot>
332
+ listFolders(query?, signal?): Promise<Folder[]>
333
+ listTemplates(query?, signal?): Promise<Page<DocumentSummary>>
334
+ storeArtifact(documentId, input, signal?): Promise<DocumentArtifact>
335
+ getArtifact(documentId, artifactId, signal?): Promise<DocumentArtifact>
336
+ }
337
+ ```
338
+
339
+ Mọi method phải tôn trọng `AbortSignal` nếu transport hỗ trợ cancellation.
340
+
341
+ ### Save input
342
+
343
+ ```ts
344
+ interface SaveStateInput {
345
+ state: KindyDocumentState
346
+ baseRevisionId: string
347
+ reason: 'autosave' | 'manual'
348
+ clientMutationId: string
349
+ }
350
+ ```
351
+
352
+ Backend trả HTTP 409 hoặc lỗi `VERSION_CONFLICT` khi `baseRevisionId` không phải revision hiện hành.
353
+
354
+ ## REST adapter
355
+
356
+ ```ts
357
+ const adapter = createRestDocumentAdapter({
358
+ baseUrl: '/document-api',
359
+ transport: customFetch,
360
+ })
361
+ ```
362
+
363
+ `transport` có cùng contract với `fetch`. REST adapter map:
364
+
365
+ - HTTP 404 → `DOCUMENT_NOT_FOUND`.
366
+ - HTTP 409 → `VERSION_CONFLICT`.
367
+ - HTTP lỗi khác → `ADAPTER_ERROR`.
368
+ - Fetch/transport failure → `NETWORK_ERROR`.
369
+
370
+ ## Memory adapter
371
+
372
+ ```ts
373
+ const adapter = createMemoryDocumentAdapter({
374
+ documents: snapshots,
375
+ folders,
376
+ })
377
+ ```
378
+
379
+ Manual save tạo version; autosave chỉ tạo revision. Restore tạo revision và version mới. Adapter giữ Blob trong memory và không bền vững qua reload.
380
+
381
+ ## DOCX codecs
382
+
383
+ ```ts
384
+ inspectDocx(file)
385
+ extractDocxPackage(file, limits?)
386
+ ooxmlToDocumentState(parts)
387
+ importDocx(file, options?)
388
+ importDocxInWorker(file, options?)
389
+ exportDocx(state, options?)
390
+ createDocxCodec()
391
+ ```
392
+
393
+ ### Import options
394
+
395
+ ```ts
396
+ {
397
+ mode?: 'strict' | 'best-effort'
398
+ signal?: AbortSignal // worker variant
399
+ limits?: Partial<DocxImportLimits>
400
+ }
401
+ ```
402
+
403
+ Limits mặc định kiểm tra compressed bytes, uncompressed bytes, số ZIP entry và compression ratio.
404
+
405
+ ### Results
406
+
407
+ ```ts
408
+ interface DocxImportResult {
409
+ state: KindyDocumentState
410
+ report: CompatibilityReport
411
+ messages: Array<{ type: string; message: string }>
412
+ }
413
+
414
+ interface DocxExportResult {
415
+ blob: Blob
416
+ report: CompatibilityReport
417
+ }
418
+ ```
419
+
420
+ ## Errors
421
+
422
+ ```ts
423
+ class DocumentLibraryError extends Error {
424
+ readonly code: DocumentErrorCode
425
+ readonly status?: number
426
+ readonly details?: unknown
427
+ }
428
+ ```
429
+
430
+ Codes:
431
+
432
+ ```text
433
+ DOCX_INVALID
434
+ DOCX_UNSUPPORTED
435
+ IMPORT_FAILED
436
+ EXPORT_FAILED
437
+ ADAPTER_ERROR
438
+ NETWORK_ERROR
439
+ VERSION_CONFLICT
440
+ DOCUMENT_NOT_FOUND
441
+ OPERATION_CANCELLED
442
+ ```
443
+
444
+ Không kiểm tra message string; luôn branch theo `error.code`.
445
+
446
+ ## UI theme/messages helpers
447
+
448
+ ```ts
449
+ DEFAULT_LIBRARY_UI
450
+ DEFAULT_LIBRARY_THEME
451
+ VI_LIBRARY_MESSAGES
452
+ EN_LIBRARY_MESSAGES
453
+ resolveLibraryUi(options)
454
+ resolveLibraryMessages(locale, overrides)
455
+ createLibraryTheme(overrides)
456
+ ```
457
+
458
+ Xem [UI engine](./ui-engine.md).
459
+
460
+ ## Engine adapter
461
+
462
+ ```ts
463
+ interface EditorEngineAdapter {
464
+ readonly id: string
465
+ mount(container, options?): EditorEngineHandle | Promise<EditorEngineHandle>
466
+ }
467
+ ```
468
+
469
+ V2 ship `TiptapEngineAdapter`:
470
+
471
+ ```ts
472
+ const engine = createTiptapEngineAdapter()
473
+ const handle = engine.mount(container, options)
474
+ await handle.load(state)
475
+ handle.onChange(nextState => {})
476
+ handle.setReadOnly(true)
477
+ handle.destroy()
478
+ ```
479
+
480
+ `EditorEngineAdapter` là extension boundary. Workspace Vue hiện dùng `KindyEditor` trực tiếp; adapter phù hợp khi ứng dụng muốn mount engine theo kiểu headless controller.
481
+
482
+ ## Collaboration adapter
483
+
484
+ ```ts
485
+ const collaboration = createYjsCollaborationAdapter({
486
+ providerFactory: context => createCompanyProvider(context),
487
+ })
488
+
489
+ // Workspace sở hữu vòng đời connect/disconnect; Yjs editor extensions/provider
490
+ // vẫn do ứng dụng chủ cấu hình vì SDK không ship collaboration server.
491
+ <KindyDocumentLibrary
492
+ :adapter="documentAdapter"
493
+ :collaboration="collaboration"
494
+ :user="{ id: currentUser.id, name: currentUser.name, color: currentUser.color }"
495
+ />
496
+ ```
497
+
498
+ SDK không ship WebSocket server. Ứng dụng chủ chịu trách nhiệm auth, document isolation, provider lifecycle và awareness.
499
+
500
+ ## Mock REST transport
501
+
502
+ ```ts
503
+ const mock = createMockDocumentTransport({ latency: 20 })
504
+ const adapter = createRestDocumentAdapter({
505
+ baseUrl: mock.baseUrl,
506
+ transport: mock.transport,
507
+ })
508
+ ```
509
+
510
+ Mock dùng Memory adapter phía sau và phù hợp cho contract test không cần HTTP server.
@@ -0,0 +1,62 @@
1
+ # Completion roadmap của Kindy Editor v2
2
+
3
+ Tài liệu này là bảng kiểm phát hành, không phải danh sách ý tưởng. Một capability
4
+ chỉ chuyển sang **Done** khi runtime, automated test, compatibility fixture và
5
+ tài liệu public cùng khớp nhau.
6
+
7
+ ## Trạng thái hiện tại
8
+
9
+ | Hạng mục | Trạng thái | Bằng chứng / gate còn thiếu |
10
+ |---|---|---|
11
+ | Document state, migration, client, optimistic concurrency | Done v2.0 | Contract/unit tests trong `src/core/__tests__` |
12
+ | Memory/REST/legacy adapters và OpenAPI | Done v2.0 | Adapter contract + OpenAPI route test |
13
+ | Explorer, workspace, version preview/restore | Done v2.0 | Playwright E2E production và CI job |
14
+ | DOCX body text/style/list/table/image/page break/page setup | Done trong profile v2.0 | Golden/unit test; DOCX v2.2 đã mở và convert PDF bằng LibreOffice headless không yêu cầu repair |
15
+ | Browser print | Supported browser flow | E2E kiểm tra iframe/@page/content; vẫn không phải deterministic PDF Blob |
16
+ | Tài liệu dài | 100 trang text/mixed pass local production gate | Text 490,3ms/21,5ms p95; mixed 849,7ms/36,5ms p95, 40 mẫu. Pagination 2,8–3,7ms. CI upload JSON; 200 mixed vẫn là stress limit |
17
+ | Multiple section DOCX round-trip | Done v2.1 codec/pagination/print | Editing canvas liên tục vẫn có thể khác Word; cần Chromium visual regression |
18
+ | Header/footer DOCX import + variants | Done v2.1 codec | Golden strict gồm default/first/even và logo header |
19
+ | Page-number start/first/odd/even | Done v2.1 codec | Golden strict gồm page-number start và section landscape |
20
+ | Comment DOCX round-trip | Done v2.2 codec/editor | Range, reply, resolved và strict round-trip pass |
21
+ | Track Changes DOCX round-trip | Done v2.2 codec/editor | Insert/delete/replace transaction, author/time, accept/reject, undo/redo, table text pass |
22
+ | Realtime Yjs | SDK lifecycle + local convergence done | Hai Tiptap editor dùng chung host Y.Doc hội tụ/recreate không nhân đôi; host vẫn phải test provider/server, auth và network reconnect thật |
23
+
24
+ ## Thứ tự hoàn thiện bắt buộc
25
+
26
+ ### Gate A — v2.0 release candidate
27
+
28
+ - Regression, typecheck, build và package audit pass.
29
+ - Golden DOCX v2.2 mở được bằng LibreOffice headless không yêu cầu repair. **Done** trên Debian/LibreOffice 7.4.7 ARM64 và chạy lại trong CI.
30
+ - Browser E2E: Explorer → import → edit → autosave → version → restore → export → print. **Done** bằng Playwright production build.
31
+ - Corpus text 100/200 trang ghi JSON benchmark artifact trong CI (`benchmark:ci` + GitHub Actions đã cấu hình).
32
+ - Corpus text/mixed 100 trang chạy trong Playwright; không công bố SLA nếu p95 fail trên thiết bị tích hợp.
33
+ - Xóa hoặc giữ ngoài public entrypoint toàn bộ prototype AI/IO/storage cũ.
34
+
35
+ ### Gate B — v2.1 sections/header/footer
36
+
37
+ - Mọi section có stable ID và semantic `sectionBreak` node.
38
+ - Import/export `sectPr`, page size/orientation/margin và page-number start theo từng section.
39
+ - Header/footer là ProseMirror sub-document; hỗ trợ default/first/odd/even trong profile đã công bố.
40
+ - Golden DOCX cho section landscape giữa tài liệu, header logo, footer số trang và different-first-page.
41
+ - Pagination browser đọc section boundary; vẫn ghi rõ không phải Word layout engine.
42
+
43
+ ### Gate C — v2.2 review/realtime
44
+
45
+ - Comment range/thread import/export bằng OOXML comments parts.
46
+ - Track insert/delete có author, timestamp, stable ID; accept/reject từng change và toàn bộ.
47
+ - Track Changes golden test gồm text, undo/redo và text trong table.
48
+ - Hai editor dùng host-provided Yjs provider hội tụ cùng document state; reconnect không nhân đôi transaction.
49
+ - Comment, review và collaboration dùng identity do ứng dụng chủ truyền vào.
50
+
51
+ ### Gate D — release nhưng chưa publish
52
+
53
+ - `npm pack --dry-run` chỉ chứa file public đã khai báo. **Done** — 34 file, khoảng 1,7MB nén; `test:package` build lại tarball trong một app Vite consumer và kiểm tra DOCX Worker/CSS.
54
+ - README/API/types/capability matrix khớp runtime. **Done** và được kiểm tra cùng build/typecheck.
55
+ - Changelog và migration guide hoàn chỉnh. **Done**.
56
+ - Tạo release candidate local. **Done** với `kindy-editor-2.0.0.tgz`; chỉ publish npm khi chủ dự án yêu cầu lại.
57
+
58
+ ## Definition of done cho hiệu năng
59
+
60
+ Mỗi báo cáo phải ghi browser, máy, build mode, corpus và số mẫu. Ngân sách mặc
61
+ định: open 100 trang text ≤ 3 giây, typing p95 ≤ 50 ms, pagination sau khi dừng
62
+ gõ ≤ 500 ms. Kết quả text không được suy rộng sang bảng, ảnh hoặc review.