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.
package/README.md CHANGED
@@ -1,333 +1,278 @@
1
- # kindy-editor (Kindy Editor)
2
-
3
- Trình biên tập tài liệu chuẩn Office (Word-like Document Editor) hiện đại dựa trên **Vue 3** và **Tiptap 3**. Được tùy biến, tối ưu hóa và hỗ trợ 100% giao diện **Tiếng Việt**.
4
-
5
- [![npm version](https://img.shields.io/npm/v/kindy-editor.svg)](https://www.npmjs.com/package/kindy-editor)
6
- [![license](https://img.shields.io/github/license/hotienky/editor.svg)](./LICENSE)
7
-
8
- ---
9
-
10
- ## ✨ Tính năng nổi bật
11
-
12
- - 📄 **Phân trang dạng Word (Page-based Pagination)**: Hỗ trợ ngắt trang, căn lề, khổ giấy (A4, A3, Letter, Legal...) chân trang & đầu trang (Header/Footer).
13
- - 💬 **Hệ thống Bình luận (Word-style Comments)**: Bôi đen văn bản để gắn bình luận, phản hồi (Reply), hoàn thành (Resolve), cuộn mượt và highlight từng người dùng theo tọa độ Y.
14
- - 🇻🇳 **Tiếng Việt 100%**: Chuẩn hóa toàn bộ nhãn giao diện, bộ cỡ chữ tiêu chuẩn (`pt`), từ điển ký hiệu và phông chữ mượt mà.
15
- - 🎨 **Giao diện hiện đại (Ribbon & Classic)**: Hỗ trợ 2 chế độ thanh công cụ dạng Ribbon (như MS Word) hoặc Classic, chế độ tối (Dark mode).
16
- - 🌐 **Đa nền tảng**: Tích hợp dễ dàng vào **Vue 3**, **React.js**, **Next.js**, **Nuxt 3**, **Angular**, **Svelte**, hoặc **Vanilla JS (CDN)**.
17
- - 🗄️ **Lưu trữ linh hoạt**: Tự động mã hóa nhúng bình luận vào HTML/JSON hoặc tách riêng lưu vào cơ sở dữ liệu (Database).
1
+ # Kindy Editor v2
2
+
3
+ Kindy Editor là SDK Document Library chuyên cho DOCX, xây dựng bằng Vue 3 và Tiptap/ProseMirror. Thư viện cung cấp giao diện quản lý tài liệu, editor trên web, lịch sử phiên bản, DOCX codec và hợp đồng API để ứng dụng chủ kết nối backend của mình.
4
+
5
+ Kindy Editor **không** cung cấp backend, database, authentication, object storage hoặc nghiệp vụ hợp đồng. JSON `KindyDocumentState` là trạng thái chỉnh sửa chuẩn; DOCX gốc và DOCX export là artifact do server ứng dụng chủ lưu.
6
+
7
+ ## Phạm vi v2.0
8
+
9
+ - Import DOCX trong Web Worker, kiểm tra ZIP/OOXML và trả `CompatibilityReport`.
10
+ - Chỉnh sửa nội dung bằng Tiptap/ProseMirror với preset `Contract`: toolbar compact, page-only và chỉ giữ công cụ cần cho hợp đồng.
11
+ - Explorer theo thư mục, tìm kiếm, tạo tài liệu trống và tạo từ template.
12
+ - Autosave có debounce, cancellation và optimistic concurrency.
13
+ - Lưu thủ công tạo version, xem read-only và khôi phục version.
14
+ - Export DOCX OOXML thật bằng package `docx`.
15
+ - In hoặc Save as PDF qua print dialog của trình duyệt.
16
+ - UI engine có shell responsive, theme tokens, locale messages, slots và typed hooks.
17
+
18
+ Khả năng round-trip DOCX chỉ được cam kết trong [Kindy DOCX Compatibility Profile](./CAPABILITIES.md). Kindy không phải Microsoft Word layout engine và không cam kết giữ nguyên mọi tính năng OOXML.
19
+
20
+ Ngoại lệ duy nhất có thể bảo đảm byte-for-byte là tài liệu vừa import và **chưa có revision chỉnh sửa**: workspace tải lại artifact `original-docx` thay vì serialize JSON. Sau lần chỉnh sửa/save đầu tiên, DOCX mới được dựng từ `KindyDocumentState` và chỉ có cam kết theo compatibility profile.
21
+
22
+ ## Kiến trúc
23
+
24
+ ```text
25
+ Ứng dụng chủ
26
+ ├─ KindyDocumentLibrary
27
+ │ ├─ DocumentLibraryShell layout/responsive/theme
28
+ │ ├─ KindyDocumentExplorer list/search/folder/import/template
29
+ │ ├─ KindyEditor Tiptap/ProseMirror editor
30
+ │ └─ KindyVersionPanel preview/restore
31
+ ├─ DocumentLibraryClient autosave/conflict/events
32
+ ├─ DOCX codecs / browser print
33
+ └─ DocumentApiAdapter
34
+ ├─ RestDocumentAdapter
35
+ ├─ MemoryDocumentAdapter
36
+ └─ adapter riêng của ứng dụng
37
+ ```
18
38
 
19
- ---
39
+ UI không gọi URL cố định và không biết token. Mọi IO đi qua `DocumentApiAdapter`.
20
40
 
21
- ## 📦 Cài đặt
41
+ ## Cài đặt
22
42
 
23
43
  ```bash
24
44
  npm install kindy-editor
25
- # hoặc
26
- pnpm add kindy-editor
27
- # hoặc
28
- yarn add kindy-editor
29
45
  ```
30
46
 
31
- ---
32
-
33
- ## 🚀 Hướng dẫn tích hợp (Integration Guides)
47
+ ```ts
48
+ import 'kindy-editor/style'
49
+ ```
34
50
 
35
- Thư viện `kindy-editor` hỗ trợ đa dạng môi trường dự án:
51
+ Vue phải được cài trong ứng dụng chủ và đáp ứng peer dependency `vue ^3.5.0`.
36
52
 
37
- ### 1. Vue 3 (Vite / Webpack)
53
+ ## Quick start với REST API
38
54
 
39
55
  ```vue
40
56
  <template>
41
- <div style="height: 100vh;">
42
- <KindyEditor ref="editorRef" v-bind="editorOptions" />
43
- </div>
57
+ <KindyDocumentLibrary
58
+ ref="workspace"
59
+ :adapter="adapter"
60
+ :autosave="{ enabled: true, delay: 5000 }"
61
+ :ui="{ explorerWidth: '320px', versionsWidth: '300px' }"
62
+ :theme="{ '--kindy-library-primary': '#0b74de' }"
63
+ locale="vi-VN"
64
+ style="height: 100vh"
65
+ @saved="onSaved"
66
+ @error="onError"
67
+ />
44
68
  </template>
45
69
 
46
- <script setup>
70
+ <script setup lang="ts">
47
71
  import { ref } from 'vue'
48
- import { KindyEditor } from 'kindy-editor'
72
+ import {
73
+ KindyDocumentLibrary,
74
+ createRestDocumentAdapter,
75
+ } from 'kindy-editor'
49
76
  import 'kindy-editor/style'
50
77
 
51
- const editorRef = ref(null)
52
- const editorOptions = ref({
53
- locale: 'vi-VN',
54
- document: {
55
- title: 'Tài liệu mới',
56
- content: '<h1>Nội dung ban đầu</h1>',
57
- },
58
- async onSave(content) {
59
- console.log('Đã lưu:', content)
60
- },
78
+ const workspace = ref()
79
+ const adapter = createRestDocumentAdapter({
80
+ baseUrl: '/document-api',
81
+ transport: (url, init) => fetch(url, {
82
+ ...init,
83
+ credentials: 'include',
84
+ headers: { ...init?.headers, 'x-tenant-id': 'acme' },
85
+ }),
61
86
  })
87
+
88
+ function onSaved(result: unknown) {
89
+ console.info('Saved', result)
90
+ }
91
+
92
+ function onError(error: unknown) {
93
+ console.error(error)
94
+ }
62
95
  </script>
63
96
  ```
64
97
 
65
- ### 2. Vue 3 Global Plugin (`main.js`)
98
+ Server phải triển khai contract trong [`openapi/document-api.yaml`](./openapi/document-api.yaml). OpenAPI không bắt buộc cơ chế auth; cookie, bearer token hoặc API gateway do ứng dụng chủ quyết định.
66
99
 
67
- ```javascript
68
- import { createApp } from 'vue'
69
- import App from './App.vue'
70
- import { useKindyEditor } from 'kindy-editor'
71
- import 'kindy-editor/style'
100
+ ### UI hợp đồng mặc định
72
101
 
73
- const app = createApp(App)
102
+ `KindyDocumentLibrary` tự dùng `CONTRACT_EDITOR_OPTIONS`: toolbar một hàng theo hướng Google Docs, chỉ có Định dạng/Chèn/Bảng/Trang/Xuất, không có Web view, Mermaid, biểu đồ, media web hoặc chuyển đổi số tiếng Trung. Nhóm Xuất chỉ gọi pipeline an toàn của Document Library cho Import DOCX, Export DOCX và In/PDF. Thanh trạng thái chỉ còn `Trang X / Y`, số ký tự, zoom và ngôn ngữ.
74
103
 
75
- // Đăng ký toàn cục component <KindyEditor />
76
- app.use(useKindyEditor, {
77
- locale: 'vi-VN',
78
- theme: 'light',
79
- })
104
+ Ứng dụng chủ vẫn có thể override rõ ràng qua `editorOptions`:
80
105
 
81
- app.mount('#app')
106
+ ```vue
107
+ <KindyDocumentLibrary
108
+ :adapter="adapter"
109
+ :editor-options="{
110
+ toolbar: { menus: ['base', 'insert', 'table', 'page', 'export'] },
111
+ statusbar: { showLocale: false },
112
+ }"
113
+ />
82
114
  ```
83
115
 
84
- ### 3. React.js (SPA)
116
+ Để dùng preset với `KindyEditor` độc lập:
85
117
 
86
- Dùng hàm helper `mountKindyEditor`:
118
+ ```ts
119
+ import { createContractEditorOptions } from 'kindy-editor'
87
120
 
88
- ```tsx
89
- import React, { useEffect, useRef } from 'react'
90
- import { mountKindyEditor } from 'kindy-editor'
91
- import 'kindy-editor/style'
121
+ const editorOptions = createContractEditorOptions({ locale: 'vi-VN' })
122
+ ```
92
123
 
93
- export function KindyEditorReact(props) {
94
- const containerRef = useRef(null)
124
+ ## Chạy demo không cần backend
95
125
 
96
- useEffect(() => {
97
- if (!containerRef.current) return
98
- const instance = mountKindyEditor(containerRef.current, props)
99
- return () => instance.unmount()
100
- }, [])
126
+ ```ts
127
+ import { createMemoryDocumentAdapter } from 'kindy-editor'
101
128
 
102
- return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />
103
- }
129
+ const adapter = createMemoryDocumentAdapter()
104
130
  ```
105
131
 
106
- ### 4. Next.js (App Router / Pages Router)
132
+ `MemoryDocumentAdapter` chỉ dành cho demo và test; dữ liệu mất khi reload trang.
107
133
 
108
- Vì Rich Text Editor thao tác với DOM trình duyệt, hãy load Client Component bằng `next/dynamic` (`ssr: false`):
134
+ Trong repository:
109
135
 
110
- ```tsx
111
- 'use client'
136
+ ```bash
137
+ npm install
138
+ npm run dev
139
+ ```
112
140
 
113
- import dynamic from 'next/dynamic'
141
+ Mở `http://localhost:9000/kindy-editor`.
114
142
 
115
- const KindyEditor = dynamic(
116
- () => import('./KindyEditorReact').then((mod) => mod.KindyEditorReact),
117
- { ssr: false }
118
- )
143
+ ## Dùng editor độc lập
119
144
 
120
- export default function DocumentPage() {
121
- return (
122
- <main style={{ height: '100vh' }}>
123
- <KindyEditor locale="vi-VN" />
124
- </main>
125
- )
126
- }
145
+ ```vue
146
+ <KindyEditor
147
+ ref="editor"
148
+ :document="{ content: initialState.content, assets: initialState.assets }"
149
+ :page="initialState.page"
150
+ />
127
151
  ```
128
152
 
129
- ### 5. Angular Component
153
+ ```ts
154
+ const state = editor.value.getState()
155
+ editor.value.setContent(nextContent)
156
+ editor.value.setReadOnly(true)
157
+ const printable = await editor.value.preparePrint()
158
+ editor.value.print()
159
+ ```
160
+
161
+ Vanilla/React/Angular/Svelte có thể dùng mount helper:
130
162
 
131
- ```typescript
132
- import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy, Input } from '@angular/core';
133
- import { mountKindyEditor } from 'kindy-editor';
134
- import 'kindy-editor/style';
163
+ ```ts
164
+ import { mountKindyEditor } from 'kindy-editor'
135
165
 
136
- @Component({
137
- selector: 'app-kindy-editor',
138
- template: `<div #editorContainer style="height: 100vh; width: 100%;"></div>`
166
+ const mounted = mountKindyEditor('#editor', {
167
+ locale: 'vi-VN',
168
+ document: { content: initialState.content },
139
169
  })
140
- export class KindyEditorComponent implements AfterViewInit, OnDestroy {
141
- @ViewChild('editorContainer') editorContainer!: ElementRef;
142
- @Input() locale: string = 'vi-VN';
143
- private instance: any;
144
-
145
- ngAfterViewInit() {
146
- this.instance = mountKindyEditor(this.editorContainer.nativeElement, {
147
- locale: this.locale,
148
- });
149
- }
150
170
 
151
- ngOnDestroy() {
152
- if (this.instance) {
153
- this.instance.unmount();
154
- }
155
- }
156
- }
171
+ mounted.instance.setContent(nextState.content)
172
+ mounted.instance.print()
173
+ mounted.unmount()
157
174
  ```
158
175
 
159
- ### 6. Svelte / SvelteKit Component
176
+ V2.0 chưa có native React component; wrapper dùng Vue mount helper.
160
177
 
161
- ```svelte
162
- <script>
163
- import { onMount, onDestroy } from 'svelte';
164
- import { mountKindyEditor } from 'kindy-editor';
165
- import 'kindy-editor/style';
178
+ ## Dùng headless client
166
179
 
167
- export let locale = 'vi-VN';
168
- let container;
169
- let instance;
180
+ Subpath `kindy-editor/core` không tải Vue UI:
170
181
 
171
- onMount(() => {
172
- instance = mountKindyEditor(container, { locale });
173
- });
182
+ ```ts
183
+ import {
184
+ createDocumentLibrary,
185
+ createRestDocumentAdapter,
186
+ } from 'kindy-editor/core'
174
187
 
175
- onDestroy(() => {
176
- if (instance) instance.unmount();
177
- });
178
- </script>
188
+ const client = createDocumentLibrary({
189
+ adapter: createRestDocumentAdapter({ baseUrl: '/document-api' }),
190
+ autosave: { enabled: true, delay: 5000 },
191
+ })
179
192
 
180
- <div bind:this={container} style="height: 100vh; width: 100%;"></div>
193
+ const snapshot = await client.open('document-123')
194
+ client.updateState(snapshot.state)
195
+ await client.save('manual')
181
196
  ```
182
197
 
183
- ### 7. SolidJS Component
198
+ `VERSION_CONFLICT` dừng autosave. SDK không tự merge hoặc ghi đè revision mới hơn:
184
199
 
185
- ```tsx
186
- import { onMount, onCleanup } from 'solid-js';
187
- import { mountKindyEditor } from 'kindy-editor';
188
- import 'kindy-editor/style';
200
+ ```ts
201
+ client.on('save-failed', (error) => {
202
+ if (error.code === 'VERSION_CONFLICT') {
203
+ // Hiển thị lựa chọn reload hoặc lưu thành bản sao.
204
+ }
205
+ })
206
+ ```
189
207
 
190
- export function KindyEditorSolid(props) {
191
- let containerRef;
192
- let instance;
208
+ `kindy-editor/core` dùng được trong Node/SSR. Entry `kindy-editor` chứa Vue UI và
209
+ các công cụ browser như cropper/print, vì vậy chỉ import nó trong bundle phía client.
193
210
 
194
- onMount(() => {
195
- instance = mountKindyEditor(containerRef, {
196
- locale: 'vi-VN',
197
- ...props,
198
- });
199
- });
211
+ ## Import và export DOCX
200
212
 
201
- onCleanup(() => {
202
- if (instance) instance.unmount();
203
- });
213
+ ```ts
214
+ import { importDocxInWorker, exportDocx } from 'kindy-editor'
204
215
 
205
- return <div ref={containerRef} style={{ height: '100vh', width: '100%' }} />;
216
+ const imported = await importDocxInWorker(file, {
217
+ mode: 'best-effort',
218
+ profile: 'kindy-docx-v2.0',
219
+ })
220
+ if (imported.report.issues.length) {
221
+ // Hiển thị warning trước khi tiếp tục.
206
222
  }
207
- ```
208
223
 
209
- ### 8. Vanilla JS / CDN (Direct Script Tag)
210
-
211
- ```html
212
- <!DOCTYPE html>
213
- <html>
214
- <head>
215
- <link rel="stylesheet" href="https://unpkg.com/kindy-editor/dist/kindy-editor.css">
216
- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
217
- <script src="https://unpkg.com/kindy-editor/dist/kindy-editor.iife.js"></script>
218
- </head>
219
- <body>
220
- <div id="editor-container" style="height: 100vh;"></div>
221
-
222
- <script>
223
- const { mountKindyEditor } = KindyEditor;
224
- mountKindyEditor('#editor-container', {
225
- locale: 'vi-VN',
226
- });
227
- </script>
228
- </body>
229
- </html>
224
+ const output = await exportDocx(imported.state) // strict mặc định
230
225
  ```
231
226
 
232
- ---
233
-
234
- ## ⚙️ Cấu hình (Props & Options API)
227
+ UI workspace tự xử lý luồng xác nhận best-effort khi import hoặc download. API programmatic mặc định strict và ném `DOCX_UNSUPPORTED` nếu state có feature ngoài profile.
228
+ Mặc định là v2.0. Chọn `kindy-docx-v2.1` cho sections/header/footer hoặc
229
+ `kindy-docx-v2.2` cho comments/Track Changes sau khi integration đã chạy corpus tương ứng.
235
230
 
236
- | Thuộc tính | Kiểu dữ liệu | Mặc định | Mô tả |
237
- |---|---|---|---|
238
- | `locale` | `string` | `'vi-VN'` | Ngôn ngữ giao diện (`vi-VN`, `en-US`, `zh-CN`) |
239
- | `theme` | `string` | `'light'` | Chế độ giao diện (`light`, `dark`, `auto`) |
240
- | `skin` | `string` | `'default'` | Phong cách thanh công cụ (`default`, `modern`) |
241
- | `height` | `string` | `'100%'` | Chiều cao khung biên tập |
242
- | `toolbar.mode` | `string` | `'ribbon'` | Chế độ thanh công cụ (`ribbon` hoặc `classic`) |
243
- | `document.title` | `string` | `''` | Tiêu đề tài liệu mặc định |
244
- | `document.content` | `string` \| `object` | `''` | Nội dung HTML hoặc JSON ban đầu |
245
- | `document.readOnly` | `boolean` | `false` | Chế độ chỉ đọc (Chỉ xem) |
246
- | `document.autoSave` | `object` | `{ enabled: false }` | Tự động lưu (`enabled: true, interval: 30000`) |
231
+ `POST /documents/import` phải lưu blob gốc và trả `DocumentRecord.originalSource` gồm `artifactId`, `revisionId`, `format: 'original-docx'` và `fileName`. Nhờ đó `KindyDocumentLibrary.downloadDocx()` trả đúng file gốc khi revision chưa đổi. URL artifact của REST backend phải là URL tải được (thường là signed URL); Memory adapter trả trực tiếp `Blob`.
247
232
 
248
- ---
233
+ ## Public components
249
234
 
250
- ## 🛠️ Danh sách Phương thức (Methods API)
235
+ | Export | Mục đích |
236
+ |---|---|
237
+ | `KindyDocumentLibrary` | Workspace hoàn chỉnh, kết nối adapter và điều phối state |
238
+ | `KindyDocumentLibraryShell` | Layout ba vùng độc lập cho UI tùy biến |
239
+ | `KindyDocumentExplorer` | List/search/folder/import/template độc lập |
240
+ | `KindyEditor` | Editor độc lập |
241
+ | `KindyVersionPanel` | Lịch sử version độc lập |
251
242
 
252
- Thông qua `ref` của component (hoặc `mountKindyEditor` instance):
243
+ ## Events chuẩn
253
244
 
254
- ```javascript
255
- // 1. Lấy nội dung HTML
256
- const html = editorRef.value.getContent('html')
245
+ `ready`, `opened`, `changed`, `save-started`, `saved`, `save-failed`, `created`, `imported`, `compatibility-warning`, `version-restored`, `printed`, `error`.
257
246
 
258
- // 2. Lấy nội dung JSON
259
- const json = editorRef.value.getContent('json')
247
+ ## Error codes
260
248
 
261
- // 3. Đặt nội dung mới
262
- editorRef.value.setContent('<h1>Nội dung mới</h1>')
249
+ `DOCX_INVALID`, `DOCX_UNSUPPORTED`, `IMPORT_FAILED`, `EXPORT_FAILED`, `ADAPTER_ERROR`, `NETWORK_ERROR`, `VERSION_CONFLICT`, `DOCUMENT_NOT_FOUND`, `OPERATION_CANCELLED`.
263
250
 
264
- // 4. Chèn nội dung tại vị trí con trỏ
265
- editorRef.value.insertContent('<p>Đoạn văn mới</p>')
251
+ ## Hiệu năng tài liệu dài
266
252
 
267
- // 5. Xuất tài liệu PDF
268
- await editorRef.value.exportPdf('tai-lieu.pdf')
269
-
270
- // 6. Xuất trang thành hình ảnh
271
- await editorRef.value.exportImage('png', 'tai-lieu.png')
272
-
273
- // 7. In tài liệu
274
- editorRef.value.print()
275
-
276
- // 8. Đổi giao diện tối / sáng
277
- editorRef.value.setTheme('dark')
278
- ```
253
+ Import parsing đã chạy ngoài main thread. Editor vẫn dùng một ProseMirror instance liên tục; pagination cache phép đo block và workspace gom state sync. Trên production preview cục bộ (Chrome 151, Apple M2/24GB, 40 mẫu), corpus 100 trang text mở khoảng 0,49 giây với typing p95 21,5ms; mixed mở khoảng 0,85 giây với typing p95 36,5ms. Pagination cached của hai corpus lần lượt khoảng 2,8ms và 3,7ms. Đây là regression baseline, không phải SLA cho mọi thiết bị. Corpus mixed 200 trang vẫn chỉ được xem là stress limit. Xem [Performance guide](./docs/performance.md) để biết phương pháp đo và giới hạn.
279
254
 
280
- ---
255
+ ## Tài liệu
281
256
 
282
- ## 💬 Hệ thống Bình luận (Word-style Comments)
257
+ - [Bắt đầu tích hợp](./docs/getting-started.md)
258
+ - [UI engine, theme, responsive và slots](./docs/ui-engine.md)
259
+ - [API reference](./docs/api-reference.md)
260
+ - [Backend, security và adapter](./GUIDE.md)
261
+ - [DOCX capability matrix](./CAPABILITIES.md)
262
+ - [Performance và tài liệu 100 trang](./docs/performance.md)
263
+ - [Migration v1 → v2](./MIGRATION.md)
264
+ - [REST OpenAPI](./openapi/document-api.yaml)
283
265
 
284
- ### Cơ chế lưu trữ:
285
- Bình luận được lưu trực tiếp dưới dạng thẻ `span` chứa thuộc tính `data-comment` và `data-thread` trong tài liệu HTML/JSON:
286
-
287
- ```html
288
- <span
289
- data-comment="c1a2b3"
290
- data-color="rgba(255, 213, 79, 0.4)"
291
- data-thread='{"id":"c1a2b3","text":"Cần chỉnh sửa","replies":[]}'
292
- class="kindy-comment"
293
- >
294
- Đoạn văn bản được bình luận
295
- </span>
296
- ```
297
-
298
- ### Lưu trữ Cơ sở dữ liệu (Database Integration):
299
- - **Cách 1 (Khuyên dùng)**: Lưu toàn bộ chuỗi HTML chứa thẻ `<span data-comment>` vào 1 cột `content TEXT` trong Database. Khi mở lại, bình luận tự động khôi phục 100%.
300
- - **Cách 2**: Lưu riêng danh sách mảng object comment qua API `editorRef.value.getComments()`.
301
-
302
- ---
303
-
304
- ## 💻 Dự án Mẫu (Examples)
305
-
306
- Thư mục `examples/` chứa các ứng dụng mẫu đầy đủ:
307
- - ⚛️ **[examples/react-demo](./examples/react-demo)**: React.js + IndexedDB Database Persistence.
308
-
309
- ---
310
-
311
- ## 🛠️ Lệnh phát triển (Development Commands)
266
+ ## Phát triển và kiểm thử
312
267
 
313
268
  ```bash
314
- # Cài đặt phụ thuộc
315
- npm install
316
-
317
- # Chạy server phát triển (Dev server tại http://localhost:9000/kindy-editor)
318
- npm run dev
319
-
320
- # Đóng gói sản phẩm (Production build ra thư mục dist/)
269
+ npm test
270
+ npm run test:e2e
271
+ npm run test:package
272
+ npm run test:libreoffice # cần có soffice; CI tự cài LibreOffice
273
+ npm run typecheck
274
+ npm run lint:check
321
275
  npm run build
322
-
323
- # Định dạng code
324
- npm run format
325
276
  ```
326
277
 
327
- ---
328
-
329
- ## 📄 License & Credits
330
-
331
- - Phát hành theo giấy phép [MIT License](./LICENSE).
332
- - Dựa trên mã nguồn gốc [Umo Editor](https://github.com/umodoc/editor) bởi đội ngũ umodoc.
333
- - Bản quyền thuộc về **Kindy** ([GitHub](https://github.com/hotienky)).
278
+ License: MIT.