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,180 @@
1
+ # Bắt đầu với Kindy Document Library SDK v2
2
+
3
+ Tài liệu này tạo một workspace DOCX chạy bằng Memory adapter, sau đó chuyển sang REST adapter.
4
+
5
+ ## 1. Yêu cầu môi trường
6
+
7
+ - Node.js `^20.19.0` hoặc `>=22.12.0`.
8
+ - Vue `^3.5.0` khi dùng UI.
9
+ - Trình duyệt hiện đại có `fetch`, `AbortController`, Web Worker và `structuredClone` hoặc JSON fallback.
10
+
11
+ ## 2. Cài package
12
+
13
+ ```bash
14
+ npm install kindy-editor vue
15
+ ```
16
+
17
+ Import stylesheet một lần tại entry của ứng dụng:
18
+
19
+ ```ts
20
+ import 'kindy-editor/style'
21
+ ```
22
+
23
+ Container của workspace phải có chiều cao xác định:
24
+
25
+ ```css
26
+ html, body, #app { height: 100%; margin: 0; }
27
+ .document-screen { height: 100vh; }
28
+ ```
29
+
30
+ ## 3. Demo bằng Memory adapter
31
+
32
+ ```vue
33
+ <template>
34
+ <div class="document-screen">
35
+ <KindyDocumentLibrary
36
+ :adapter="adapter"
37
+ :autosave="{ enabled: true, delay: 1500 }"
38
+ locale="vi-VN"
39
+ @error="console.error"
40
+ />
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import {
46
+ KindyDocumentLibrary,
47
+ createMemoryDocumentAdapter,
48
+ } from 'kindy-editor'
49
+ import 'kindy-editor/style'
50
+
51
+ const adapter = createMemoryDocumentAdapter({
52
+ folders: [
53
+ { id: 'contracts', name: 'Hợp đồng', parentId: null },
54
+ { id: 'forms', name: 'Biểu mẫu', parentId: null },
55
+ ],
56
+ })
57
+ </script>
58
+ ```
59
+
60
+ Từ giao diện, chọn **Tạo mới**, nhập nội dung, chờ autosave hoặc nhấn **Lưu** để tạo version bền vững trong adapter.
61
+
62
+ Memory adapter không dùng `localStorage`; reload trang sẽ mất dữ liệu.
63
+
64
+ ## 4. Kết nối REST backend
65
+
66
+ Đổi adapter mà không thay UI:
67
+
68
+ ```ts
69
+ import { createRestDocumentAdapter } from 'kindy-editor'
70
+
71
+ const adapter = createRestDocumentAdapter({
72
+ baseUrl: '/api/document-library',
73
+ transport: async (url, init) => {
74
+ const token = await getAccessToken()
75
+ return fetch(url, {
76
+ ...init,
77
+ credentials: 'include',
78
+ headers: {
79
+ ...init?.headers,
80
+ Authorization: `Bearer ${token}`,
81
+ },
82
+ })
83
+ },
84
+ })
85
+ ```
86
+
87
+ SDK không giữ token. Backend phải kiểm tra quyền trên từng document, version và artifact; `capabilities` chỉ điều khiển UI.
88
+
89
+ Các endpoint và schema nằm trong [`../openapi/document-api.yaml`](../openapi/document-api.yaml).
90
+
91
+ ## 5. Seed document và template
92
+
93
+ `MemoryDocumentAdapter` nhận `DocumentSnapshot[]` để test tích hợp:
94
+
95
+ ```ts
96
+ import {
97
+ createEmptyDocumentState,
98
+ createMemoryDocumentAdapter,
99
+ } from 'kindy-editor'
100
+
101
+ const state = createEmptyDocumentState({
102
+ content: {
103
+ type: 'doc',
104
+ content: [{
105
+ type: 'paragraph',
106
+ content: [{ type: 'text', text: 'Nội dung mẫu' }],
107
+ }],
108
+ },
109
+ })
110
+
111
+ const adapter = createMemoryDocumentAdapter({
112
+ documents: [{
113
+ document: {
114
+ id: 'template-1',
115
+ title: 'Mẫu hợp đồng',
116
+ fileName: 'mau-hop-dong.docx',
117
+ isTemplate: true,
118
+ currentRevisionId: 'rev-1',
119
+ currentVersionId: 'version-1',
120
+ createdAt: new Date().toISOString(),
121
+ updatedAt: new Date().toISOString(),
122
+ capabilities: { view: true, edit: false },
123
+ },
124
+ state,
125
+ revisionId: 'rev-1',
126
+ version: {
127
+ id: 'version-1',
128
+ documentId: 'template-1',
129
+ number: 1,
130
+ revisionId: 'rev-1',
131
+ reason: 'template',
132
+ createdAt: new Date().toISOString(),
133
+ },
134
+ }],
135
+ })
136
+ ```
137
+
138
+ ## 6. Lấy workspace handle
139
+
140
+ ```vue
141
+ <KindyDocumentLibrary ref="workspace" :adapter="adapter" />
142
+ ```
143
+
144
+ ```ts
145
+ const state = workspace.value.getState()
146
+ await workspace.value.save()
147
+
148
+ const result = await workspace.value.exportDocx({
149
+ mode: 'strict',
150
+ store: true,
151
+ })
152
+
153
+ workspace.value.print()
154
+ workspace.value.toggleExplorer(false)
155
+ workspace.value.toggleVersions(true)
156
+ ```
157
+
158
+ Các method công khai đầy đủ nằm trong [API reference](./api-reference.md).
159
+
160
+ ## 7. Kiểm tra integration tối thiểu
161
+
162
+ Trước khi nối backend thật, kiểm tra tuần tự:
163
+
164
+ 1. Explorer tải được document, folder và template.
165
+ 2. Create/import trả document rồi `loadState` mở được snapshot.
166
+ 3. Gõ nội dung làm phát `changed` và autosave gọi `saveState`.
167
+ 4. Manual save tạo version và VersionPanel làm mới.
168
+ 5. Mở version phải read-only; quay lại bản hiện hành phải editable.
169
+ 6. Restore tạo revision/version mới, không sửa bytes của version cũ.
170
+ 7. Export tạo ZIP/OOXML hợp lệ; print mở được print dialog.
171
+ 8. Hai client lưu cùng `baseRevisionId` phải khiến client thứ hai nhận `VERSION_CONFLICT`.
172
+
173
+ Với DOCX thực tế, kiểm tra thêm `CompatibilityReport`: floating image sẽ được làm phẳng thành inline; EMF/WMF/TIFF không thuộc profile render ổn định. Sections/header/footer dùng `docxProfile="kindy-docx-v2.1"`; comments/Track Changes dùng v2.2. Xem ma trận hỗ trợ trước khi bật strict profile cao hơn.
174
+
175
+ ## 8. Bước tiếp theo
176
+
177
+ - [UI engine](./ui-engine.md)
178
+ - [Backend và security](../GUIDE.md)
179
+ - [DOCX compatibility](../CAPABILITIES.md)
180
+ - [Performance](./performance.md)
@@ -0,0 +1,149 @@
1
+ # Hiệu năng và tài liệu dài
2
+
3
+ ## Trạng thái v2.0
4
+
5
+ Kindy v2.0 import DOCX trong Web Worker, nhưng editing surface vẫn là một ProseMirror instance liên tục. Toàn bộ node của document được gắn vào DOM. Automatic pagination chạy sau debounce, cache chiều cao theo DOM node và chỉ invalid block thuộc vùng transaction; `ResizeObserver` và font loading sẽ invalid cache khi layout thật sự đổi.
6
+
7
+ Vì vậy thư viện chưa cam kết mọi tài liệu 100–200 trang luôn mượt. Độ phức tạp phụ thuộc số node, bảng, ảnh, comment và tracked change nhiều hơn số trang hiển thị.
8
+
9
+ ## Chi phí chính
10
+
11
+ 1. **Mount editor:** ProseMirror tạo DOM cho toàn bộ document.
12
+ 2. **Pagination:** lần đầu phải đo tất cả top-level block. Các lần sau vẫn duyệt block metadata O(N), nhưng chỉ đọc layout DOM cho block mới/thay đổi. Engine chừa đúng khoảng trắng còn lại, lề dưới, page gap và lề trên trước block của trang kế tiếp.
13
+ 3. **Review UI:** suggestions cần thu thập tracked change trong document.
14
+ 4. **Autosave:** workspace gom update trong `stateSyncDelay` (mặc định 300ms), sau đó canonical state được clone và serialize để gửi adapter. Autosave mặc định của client là 30 giây; ứng dụng không nên đặt chu kỳ 1–2 giây cho tài liệu dài.
15
+ 5. **Ảnh/bảng:** ảnh editor dùng `loading="lazy"`; import giới hạn mặc định 20MB mỗi media và 100MB tổng media trước khi chuyển thành data URL. Decoding ảnh và layout bảng vẫn có thể tạo long task trên main thread. Bảng cao hơn một trang được giữ liên tục trong editing surface; engine đếm page span nhưng không tách DOM table thành nhiều ProseMirror node.
16
+
17
+ Module `VirtualScroller` trong codebase là performance primitive, chưa được nối trực tiếp vào editable ProseMirror DOM. Không nên gỡ các node ngoài viewport một cách cơ học vì sẽ phá selection, IME, history, decoration và mapping transaction.
18
+
19
+ ## Ngân sách khuyến nghị
20
+
21
+ Đây là mục tiêu nghiệm thu cho hệ thống tích hợp, không phải số liệu Kindy hiện đã bảo đảm:
22
+
23
+ | Chỉ số | Mục tiêu đề xuất |
24
+ |---|---:|
25
+ | Mở JSON 100 trang text cơ bản | ≤ 3 giây trên máy chuẩn dự án |
26
+ | Import DOCX 100 trang | ≤ 8 giây, UI không treo liên tục |
27
+ | Typing latency p95 | ≤ 50 ms |
28
+ | Pagination sau khi dừng gõ | ≤ 500 ms |
29
+ | Autosave không chặn input | Không có long task > 100 ms do save |
30
+ | Scroll | Không có blank page/nhảy layout nghiêm trọng |
31
+ | Peak memory | Đặt theo corpus và thiết bị mục tiêu |
32
+
33
+ Máy chuẩn, browser version, font và dataset phải được ghi trong báo cáo benchmark.
34
+
35
+ ## Corpus test
36
+
37
+ Tối thiểu cần năm nhóm fixture:
38
+
39
+ - 100 trang text/heading/list.
40
+ - 100 trang có bảng hợp đồng và merge cell.
41
+ - 100 trang có ảnh/logo/con dấu.
42
+ - 100 trang có comment/Track Changes.
43
+ - 200 trang mixed content để stress test.
44
+
45
+ Mỗi fixture cần đo:
46
+
47
+ ```text
48
+ importStart → importComplete
49
+ openStart → editorReady
50
+ firstInput → nextPaint
51
+ transaction → paginationComplete
52
+ changed → saveStarted → saved
53
+ heapBefore → heapAfterOpen → heapAfterClose
54
+ ```
55
+
56
+ Không dùng một paragraph cực dài thay cho tài liệu thật; pagination hiện tính theo top-level block nên fixture phải có cấu trúc tương tự hợp đồng thực tế.
57
+
58
+ ## Cách chạy kiểm thử
59
+
60
+ Repository hiện có regression/unit test:
61
+
62
+ ```bash
63
+ npm test
64
+ npm run test:e2e
65
+ npm run typecheck
66
+ npm run build
67
+ npm run benchmark:long-doc
68
+ npm run benchmark:ci
69
+ ```
70
+
71
+ `benchmark:ci` áp synthetic budget và ghi `.artifacts/long-document.json`.
72
+ `test:e2e` build demo production, chạy Chromium cho luồng workspace và ghi
73
+ `.artifacts/browser-performance-text.json` cùng
74
+ `.artifacts/browser-performance-mixed.json`. Workflow CI upload các file này
75
+ làm regression artifact; Node benchmark không được dùng thay DOM/paint benchmark.
76
+
77
+ Mở benchmark UI bằng:
78
+
79
+ ```text
80
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100
81
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=200
82
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100&benchmarkVariant=table
83
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100&benchmarkVariant=image
84
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100&benchmarkVariant=review
85
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100&benchmarkVariant=mixed
86
+ http://127.0.0.1:9000/kindy-editor?benchmarkPages=100&benchmarkVariant=section
87
+ ```
88
+
89
+ ### Baseline cục bộ ngày 22/08/2026
90
+
91
+ Production preview, Google Chrome 151, MacBook Pro Apple M2 8-core/24GB,
92
+ viewport 1600×1000, 40 input samples, autosave tắt trong benchmark:
93
+
94
+ | Corpus | Top-level block | Bảng | Ảnh | Editor ready | Typing median | Typing p95 | Typing max | Pagination cached | Kết quả budget |
95
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
96
+ | 100 trang text | 999 | 0 | 0 | 490,3 ms | 12,2 ms | 21,5 ms | 26,7 ms | 2,8 ms | Pass ≤ 3s / 50ms |
97
+ | 100 trang mixed | 1.101 | 50 | 100 | 849,7 ms | 29,2 ms | 36,5 ms | 89,9 ms | 3,7 ms | Pass guardrail 75ms |
98
+
99
+ Mixed có một spike 89,9ms; p95 vẫn 36,5ms và 39 mẫu còn lại chủ yếu dưới 37ms.
100
+ Số liệu JSON được sinh bởi Playwright thay vì chép tay. Baseline development
101
+ bên dưới được giữ để thấy ảnh hưởng của build mode, không dùng làm gate phát hành.
102
+
103
+ Corpus text semantic, Chromium trong Codex desktop, Vite development mode:
104
+
105
+ | Corpus | Top-level block | Editor ready | Typing median | Typing p95 | Pagination cached |
106
+ |---|---:|---:|---:|---:|---:|
107
+ | 100 trang | 999 | 0,59–0,67 s | 18,5 ms | 33,3 ms | khoảng 2,5 ms |
108
+ | 200 trang | 1.999 | 0,90 s | 26,0 ms | 38,9 ms | khoảng 3,4 ms |
109
+
110
+ Corpus mixed semantic (bảng 4x4 xen kẽ, ảnh inline, comment/Track Changes),
111
+ autosave tắt để phép đo không lẫn network/storage và vẫn chạy bằng Vite development:
112
+
113
+ | Corpus | Top-level block | Bảng | Ảnh | Editor ready | Typing median | Typing p95 | Pagination cached |
114
+ |---|---:|---:|---:|---:|---:|---:|---:|
115
+ | 100 trang mixed | 1.101 | 50 | 100 | 1,03–1,29 s | 33,0–41,3 ms | 36,1–56,6 ms | khoảng 3,3–5,1 ms |
116
+ | 200 trang mixed | 2.201 | 100 | 200 | 1,39 s | 69,5 ms | 85,1 ms | khoảng 11 ms |
117
+
118
+ Kết luận hiện tại: production harness xác nhận target 100 trang text và guardrail
119
+ 100 trang mixed trên máy chuẩn cục bộ. Kết quả development từng dao động quá 50ms,
120
+ vì vậy không suy rộng thành SLA cho mọi thiết bị. Corpus 200 trang mixed không đạt
121
+ và vẫn là stress limit, không được quảng bá là SLA.
122
+
123
+ Có spike p99 khoảng 184–391ms trong lần đo này. Kết quả chỉ là baseline text,
124
+ không phải SLA cho DOCX có bảng lớn, ảnh, comment hoặc Track Changes. Benchmark
125
+ Node riêng với fixture hiện tại đo state khoảng 328KB/657KB: clone p95 lần
126
+ lượt khoảng 2,23/4,88ms và stringify p95 khoảng 0,82/2,79ms; đây không bao gồm DOM/paint.
127
+
128
+ Benchmark trình duyệt nên chạy riêng bằng Chromium cố định trong CI và ghi kết quả JSON artifact. Không đặt threshold quá chặt trên shared CI runner; dùng dedicated runner hoặc so sánh regression theo baseline.
129
+
130
+ ## Hướng tối ưu tiếp theo
131
+
132
+ Thứ tự ưu tiên:
133
+
134
+ 1. Chuyển bước tính page assignment O(N) sang incremental index sau khi measurement cache đã ổn định.
135
+ 2. Cache/index tracked changes và chỉ cập nhật range bị thay đổi.
136
+ 3. Chuyển clone/hash/serialization autosave nặng sang Worker khi browser hỗ trợ.
137
+ 4. Lazy decode ảnh và giới hạn kích thước ảnh import.
138
+ 5. Giảm decoration/node view không cần thiết ngoài viewport.
139
+ 6. Chỉ sau khi có benchmark, thiết kế viewport rendering tương thích ProseMirror thay vì dùng virtual scroller prototype trực tiếp.
140
+
141
+ ## Checklist trước production
142
+
143
+ - Chạy corpus của chính hệ thống tích hợp, không chỉ fixture nhỏ của SDK.
144
+ - Đo cả laptop cấu hình thấp nếu đó là thiết bị người dùng.
145
+ - Cài đúng font tiếng Việt trước khi so layout.
146
+ - Test với DevTools đóng và production build.
147
+ - Test autosave qua mạng có latency/loss thực tế.
148
+ - Test mở/đóng nhiều document để phát hiện listener hoặc DOM leak.
149
+ - Không công bố “hỗ trợ 200 trang” nếu chưa có threshold pass trong CI.
@@ -0,0 +1,100 @@
1
+ # Mở rộng Kindy Editor v2
2
+
3
+ V2 không công bố một plugin manager tổng quát. Có ba extension boundary được hỗ trợ: UI composition, editor options/extensions và engine adapter.
4
+
5
+ ## 1. UI composition
6
+
7
+ Ưu tiên slots và component độc lập:
8
+
9
+ ```vue
10
+ <KindyDocumentLibrary :adapter="adapter">
11
+ <template #explorer-actions="{ refresh }">
12
+ <button @click="syncThen(refresh)">Đồng bộ DMS</button>
13
+ </template>
14
+
15
+ <template #topbar="scope">
16
+ <CompanyDocumentBar v-bind="scope" />
17
+ </template>
18
+ </KindyDocumentLibrary>
19
+ ```
20
+
21
+ Nếu cần layout hoàn toàn riêng, dùng `KindyDocumentLibraryShell`, `KindyDocumentExplorer`, `KindyEditor` và `KindyVersionPanel` độc lập.
22
+
23
+ ## 2. Tùy biến editor
24
+
25
+ Truyền option hiện có qua `editorOptions`:
26
+
27
+ ```vue
28
+ <KindyDocumentLibrary
29
+ :adapter="adapter"
30
+ :editor-options="{
31
+ toolbar: companyToolbar,
32
+ disableExtensions: ['ai'],
33
+ translations: companyTranslations,
34
+ }"
35
+ />
36
+ ```
37
+
38
+ Workspace luôn quản lý các giá trị sau và override option tương ứng:
39
+
40
+ - `document.title`
41
+ - `document.content`
42
+ - `document.readOnly`
43
+ - `document.autoSave`
44
+ - page state thuộc snapshot
45
+
46
+ Không tạo storage side effect trong Tiptap extension. Persistence phải đi qua `DocumentApiAdapter` để giữ conflict/version semantics.
47
+
48
+ ## 3. Engine adapter
49
+
50
+ ```ts
51
+ interface EditorEngineAdapter {
52
+ readonly id: string
53
+ mount(
54
+ container: HTMLElement,
55
+ options?: Record<string, unknown>,
56
+ ): EditorEngineHandle | Promise<EditorEngineHandle>
57
+ }
58
+
59
+ interface EditorEngineHandle {
60
+ load(state: KindyDocumentState): void | Promise<void>
61
+ getState(): KindyDocumentState
62
+ setReadOnly(readOnly: boolean): void
63
+ focus(): void
64
+ destroy(): void
65
+ onChange(listener: (state: KindyDocumentState) => void): () => void
66
+ }
67
+ ```
68
+
69
+ V2 chỉ ship `TiptapEngineAdapter`. Adapter riêng phải trả đúng canonical `KindyDocumentState`, không trả HTML làm state dài hạn.
70
+
71
+ ## 4. Adapter nghiệp vụ riêng
72
+
73
+ Đây là extension boundary chính để nối DMS/CRM/ERP:
74
+
75
+ ```ts
76
+ class CompanyDocumentAdapter implements DocumentApiAdapter {
77
+ async loadState(documentId, versionId, signal) {
78
+ const response = await companyApi.load({ documentId, versionId, signal })
79
+ return mapCompanySnapshot(response)
80
+ }
81
+
82
+ async saveState(documentId, input, signal) {
83
+ return companyApi.compareAndSave({ documentId, ...input, signal })
84
+ }
85
+
86
+ // Các method còn lại theo contract.
87
+ }
88
+ ```
89
+
90
+ Chạy cùng contract tests dùng cho Memory/REST adapter. Không nuốt `VERSION_CONFLICT`; SDK cần code này để dừng autosave.
91
+
92
+ ## 5. Checklist extension
93
+
94
+ - Không thêm token hoặc secret vào document state.
95
+ - Không gọi backend trực tiếp từ toolbar action nếu thao tác thuộc document lifecycle; gọi workspace/client/adapter.
96
+ - Extension state phải JSON serializable nếu cần lưu trong document.
97
+ - Mọi event listener phải được tháo khi component/engine destroy.
98
+ - Tác vụ DOCX nặng phải ở Worker khi có thể.
99
+ - Feature DOCX mới chỉ đánh dấu supported sau khi có golden round-trip test.
100
+ - Test read-only, undo/redo, IME tiếng Việt, table selection và 100-page corpus.
@@ -0,0 +1,242 @@
1
+ # UI engine
2
+
3
+ UI v2 được chia thành hai lớp: component trình bày và workspace orchestration. Cách chia này giúp ứng dụng chủ có thể dùng trọn bộ UI hoặc lấy từng component mà không làm thay đổi storage contract.
4
+
5
+ ## Thành phần
6
+
7
+ ```text
8
+ KindyDocumentLibrary orchestration
9
+ └─ KindyDocumentLibraryShell layout engine
10
+ ├─ KindyDocumentExplorer document navigation
11
+ ├─ KindyEditor editing surface
12
+ └─ KindyVersionPanel version navigation
13
+ ```
14
+
15
+ `KindyDocumentLibrary` chịu trách nhiệm mở snapshot, đồng bộ editor state với `DocumentLibraryClient`, autosave, preview/restore và export. `KindyDocumentLibraryShell` chỉ chịu trách nhiệm layout, responsive và slots; shell không gọi API.
16
+
17
+ ## Preset Contract mặc định
18
+
19
+ Workspace dùng `CONTRACT_EDITOR_OPTIONS` để giữ UI đúng phạm vi soạn hợp đồng:
20
+
21
+ - Toolbar `classic` compact, khóa một hàng và không hiển thị nút đổi sang ribbon.
22
+ - Chỉ có các nhóm `base`, `insert`, `table`, `page`, `export`.
23
+ - Import DOCX, tải DOCX và In/PDF luôn có ở topbar; nhóm `export` lặp lại ba action này để người dùng tìm thấy ngay trong toolbar.
24
+ - Với workspace, các action file gọi `DocumentLibraryClient`/adapter; không dùng IO trực tiếp của editor để tránh bỏ qua revision, artifact và compatibility report.
25
+ - Chỉ có Page view; Web view bị loại khỏi `page.layouts`.
26
+ - Không load menu Mermaid, diagram, chart, web page, media web, Chinese date hoặc Chinese case.
27
+ - Status bar chỉ giữ bộ đếm `Trang X / Y`, số ký tự, zoom và locale.
28
+
29
+ Preset là public API và dùng được cho editor độc lập:
30
+
31
+ ```ts
32
+ import {
33
+ CONTRACT_EDITOR_OPTIONS,
34
+ createContractEditorOptions,
35
+ } from 'kindy-editor'
36
+
37
+ const editorOptions = createContractEditorOptions({
38
+ statusbar: { showLocale: false },
39
+ })
40
+ ```
41
+
42
+ Override được merge theo từng nhóm `toolbar`, `statusbar`, `page`. Nếu truyền `disableExtensions`, mảng của ứng dụng chủ thay thế mảng preset; nhờ đó việc mở rộng UI luôn là quyết định tường minh của host.
43
+
44
+ ## Cấu hình layout
45
+
46
+ ```vue
47
+ <KindyDocumentLibrary
48
+ :adapter="adapter"
49
+ :ui="{
50
+ density: 'comfortable',
51
+ explorerWidth: '320px',
52
+ versionsWidth: '300px',
53
+ showTopbar: true,
54
+ showExplorer: true,
55
+ showVersions: true,
56
+ }"
57
+ />
58
+ ```
59
+
60
+ ```ts
61
+ interface KindyLibraryUiOptions {
62
+ density: 'compact' | 'comfortable'
63
+ explorerWidth: string
64
+ versionsWidth: string
65
+ showTopbar: boolean
66
+ showExplorer: boolean
67
+ showVersions: boolean
68
+ }
69
+ ```
70
+
71
+ Từ `1440px`, workspace mặc định mở đủ ba vùng. Trong khoảng `1025–1439px`, Explorer vẫn mở nhưng Versions mặc định thu gọn để editor/ribbon không bị ép chiều ngang; người dùng mở lại bằng nút lịch sử. Từ `1024px` trở xuống, Explorer và Versions trở thành drawer có scrim; workspace handle hoặc các nút topbar có thể mở/đóng panel.
72
+
73
+ ## Theme tokens
74
+
75
+ Truyền CSS variables qua prop `theme`:
76
+
77
+ ```ts
78
+ const theme = {
79
+ '--kindy-library-bg': '#f5f7fb',
80
+ '--kindy-library-surface': '#ffffff',
81
+ '--kindy-library-sidebar-bg': '#ffffff',
82
+ '--kindy-library-text': '#172033',
83
+ '--kindy-library-muted': '#667085',
84
+ '--kindy-library-border': '#d0d5dd',
85
+ '--kindy-library-selection': '#e8f3ff',
86
+ '--kindy-library-selection-text': '#075985',
87
+ '--kindy-library-primary': '#0b74de',
88
+ '--kindy-library-primary-hover': '#095fae',
89
+ '--kindy-library-danger': '#b42318',
90
+ '--kindy-library-radius': '10px',
91
+ '--kindy-library-shadow': '0 18px 48px rgb(15 23 42 / 14%)',
92
+ }
93
+ ```
94
+
95
+ `createLibraryTheme(overrides)` hợp nhất override với token mặc định:
96
+
97
+ ```ts
98
+ import { createLibraryTheme } from 'kindy-editor'
99
+
100
+ const theme = createLibraryTheme({
101
+ '--kindy-library-primary': '#7c3aed',
102
+ })
103
+ ```
104
+
105
+ Theme của Library shell độc lập với `theme`/`skin` nội bộ của `KindyEditor`. Truyền cấu hình editor qua `editorOptions`:
106
+
107
+ ```vue
108
+ <KindyDocumentLibrary
109
+ :adapter="adapter"
110
+ :editor-options="{
111
+ theme: 'light',
112
+ toolbar: { defaultMode: 'classic' },
113
+ statusbar: { showLocale: true },
114
+ }"
115
+ />
116
+ ```
117
+
118
+ Workspace luôn ghi đè `document.content`, `document.readOnly` và tắt autosave legacy của editor để tránh hai autosave pipeline chạy đồng thời.
119
+
120
+ ## Page canvas và điều hướng trang
121
+
122
+ Ở layout `page`, editor dùng một ProseMirror surface liên tục và chèn page gap bằng decoration. Page gap không hiển thị nhãn nổi trong nội dung; số trang nằm ở status bar theo dạng `Trang hiện tại / tổng trang`. Người dùng có thể bấm bộ đếm, nhập số trang và nhảy tới block đầu của trang đó. Trang hiện tại được cập nhật theo viewport khi cuộn, kể cả khi trang cuối ngắn hơn chiều cao viewport.
123
+
124
+ Ảnh DrawingML/VML nằm trong DOCX header được render từ canonical header state. Ảnh banner rộng giữ nguyên tỷ lệ và được lặp lại trong header band của các trang tự động tiếp theo. Pagination sử dụng cùng effective header/footer band với canvas để nội dung trang sau không chồng lên logo hoặc footer. Đây vẫn là browser preview; vị trí line-break tự động có thể khác Word do font metrics.
125
+
126
+ ## Locale và message override
127
+
128
+ Editor ship locale `vi-VN`, `en-US`, `zh-CN`, `it-IT` và `ru-RU`. `locale` truyền từ ứng dụng chủ được ưu tiên hơn giá trị localStorage cũ; đổi ngôn ngữ trong editor có hiệu lực ngay, không reload trang. Workspace đồng bộ Explorer/topbar/Versions trong cùng lần đổi và phát event `locale-changed`. Library shell dùng message tiếng Việt và fallback English:
129
+
130
+ ```vue
131
+ <KindyDocumentLibrary
132
+ locale="vi-VN"
133
+ :messages="{
134
+ documents: 'Kho văn bản',
135
+ newDocument: 'Soạn văn bản',
136
+ downloadDocx: 'Tải bản Word',
137
+ }"
138
+ />
139
+ ```
140
+
141
+ Các export liên quan:
142
+
143
+ ```ts
144
+ VI_LIBRARY_MESSAGES
145
+ EN_LIBRARY_MESSAGES
146
+ resolveLibraryMessages(locale, overrides)
147
+ ```
148
+
149
+ `messages` của Library UI không thay toàn bộ locale message của editor toolbar. Toolbar vẫn dùng hệ thống i18n của `KindyEditor`.
150
+
151
+ ## Slots
152
+
153
+ ### Workspace slots
154
+
155
+ | Slot | Slot props | Mục đích |
156
+ |---|---|---|
157
+ | `topbar` | `document`, `status`, `save` | Thay toàn bộ topbar mặc định |
158
+ | `empty` | — | Empty state khi chưa mở document |
159
+ | `explorer-actions` | `refresh`, `busy` | Thêm action ở Explorer |
160
+ | `document` | `document` | Bổ sung UI cho mỗi document item |
161
+
162
+ Các slot khác được chuyển tiếp vào `KindyEditor`, ví dụ bubble menu hoặc toolbar slot mà editor hỗ trợ.
163
+
164
+ ```vue
165
+ <KindyDocumentLibrary :adapter="adapter">
166
+ <template #topbar="{ document, status, save }">
167
+ <CompanyToolbar
168
+ :title="document?.title"
169
+ :status="status"
170
+ @save="save"
171
+ />
172
+ </template>
173
+
174
+ <template #explorer-actions="{ refresh }">
175
+ <button @click="refresh">Đồng bộ</button>
176
+ </template>
177
+ </KindyDocumentLibrary>
178
+ ```
179
+
180
+ ### Dùng shell độc lập
181
+
182
+ ```vue
183
+ <KindyDocumentLibraryShell
184
+ :explorer-open="explorerOpen"
185
+ :versions-open="versionsOpen"
186
+ @close-panels="closeMobileDrawers"
187
+ >
188
+ <template #explorer><CompanyExplorer /></template>
189
+ <template #topbar><CompanyToolbar /></template>
190
+ <CompanyEditor />
191
+ <template #versions><CompanyVersions /></template>
192
+ </KindyDocumentLibraryShell>
193
+ ```
194
+
195
+ Shell không quản lý state mở panel; component cha là source of truth.
196
+
197
+ ## Workspace handle
198
+
199
+ ```ts
200
+ interface KindyDocumentLibraryHandle {
201
+ client: DocumentLibraryClient
202
+ openDocument(document: DocumentSummary): Promise<void>
203
+ closeDocument(): void
204
+ save(): Promise<unknown>
205
+ exportDocx(options?: { mode?, store?, fileName?, preferOriginal? }): Promise<DocxExportResult & { source: 'original' | 'serialized' }>
206
+ downloadDocx(): Promise<void>
207
+ importDocument(): void
208
+ preparePrint(): Promise<unknown>
209
+ print(): void
210
+ getState(): KindyDocumentState
211
+ getEditor(): KindyEditorHandle | null
212
+ refresh(): Promise<void>
213
+ toggleExplorer(open?: boolean): void
214
+ toggleVersions(open?: boolean): void
215
+ }
216
+ ```
217
+
218
+ ## Trạng thái UI
219
+
220
+ Topbar chuẩn hóa các trạng thái:
221
+
222
+ - `idle`: chưa mở document.
223
+ - `ready`/`saved`: state hiện tại đã đồng bộ.
224
+ - `dirty`: đã thay đổi và đang chờ autosave/manual save.
225
+ - `saving`: adapter đang lưu.
226
+ - `readonly`: document không có quyền edit.
227
+ - `preview`: đang xem một version cũ.
228
+ - `conflict`: backend trả `VERSION_CONFLICT`.
229
+ - `error`: lỗi import/load/save/export khác.
230
+
231
+ Preview version dùng editor read-only và không thay `DocumentLibraryClient.current`. Nút “Quay lại bản hiện hành” phục hồi live snapshot; restore mới thay live snapshot và tạo version mới qua adapter.
232
+
233
+ ## Accessibility
234
+
235
+ - Explorer, workspace và VersionPanel có landmark/ARIA label riêng.
236
+ - Loading dùng `aria-busy` và `role=status`.
237
+ - Error dùng `role=alert` và có retry.
238
+ - Active document dùng `aria-current=page`.
239
+ - Nút icon có accessible name.
240
+ - Animation spinner tuân theo `prefers-reduced-motion`.
241
+
242
+ Ứng dụng chủ vẫn phải kiểm tra keyboard navigation và contrast sau khi override theme.