nuvra 0.5.0 → 0.7.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,399 +1,474 @@
1
- # nuvra
2
-
3
- Word-style document editor for Vue 3. A real page view with paper sizes and margins, a lighter web view for
4
- forms, tables, images, lists, find & replace, comments, HTML source mode, printing, export to HTML and Word files
5
- (`.docx`) you can open again.
6
-
7
- **[Documentation and live demo →](https://nuvra-docs.vercel.app)**
8
-
9
- - No editor framework underneath: its own small editing engine with a sanitizing schema.
10
- - No UI framework either: native HTML controls, built-in SVG icons and plain CSS variables. Vue is the only
11
- dependency.
12
- - Pagination in the page view (A4, A5, Letter, …, portrait or landscape).
13
- - Tables with merge / split, images with resize and alignment, task lists, links, colors, fonts.
14
- - Undo / redo, keyboard shortcuts that work with non-Latin keyboard layouts, Markdown-like input rules.
15
- - Office tools: format painter, letter case, paragraph spacing, formatting marks, a right-click menu and
16
- Ctrl + wheel zoom.
17
- - Headers and footers with page number, page count, date and title tokens; printing breaks the pages exactly where
18
- the editor shows them, and the Word export uses Word's own header, footer and page fields.
19
- - A ruler above the page for the margins and for the first line, left and right indents of a paragraph.
20
- - A page watermark such as DRAFT or COPY, drawn behind the text of every page and carried into print, HTML and Word.
21
- - Real `.docx` export and import, multilevel (1.1.1) numbering, a table of contents and a navigation pane.
22
- - Footnotes at the bottom of their page, carried into print and Word.
23
- - Comments with replies, tracked changes that round-trip with Word, a comparison of two versions, and templates
24
- filled in as a form.
25
- - A `/` command menu and a toolbar slot for your own commands and buttons.
26
-
27
- ## Installation
28
-
29
- ```sh
30
- pnpm add nuvra
31
- # or
32
- npm install nuvra
33
- ```
34
-
35
- `vue` (3.5+) is the only peer dependency.
36
-
37
- Import the styles once, for example in `main.ts`:
38
-
39
- ```ts
40
- import 'nuvra/style.css';
41
- ```
42
-
43
- ## Usage
44
-
45
- ### Document editor
46
-
47
- ```vue
48
- <script setup lang="ts">
49
- import { ref } from 'vue';
50
- import { DocumentEditor, type PageSettings, createPageSettings } from 'nuvra';
51
-
52
- const html = ref('');
53
- const page = ref<PageSettings>(createPageSettings());
54
- </script>
55
-
56
- <template>
57
- <DocumentEditor v-model="html" v-model:page="page" height="100%" title="Contract" />
58
- </template>
59
- ```
60
-
61
- ### Form field
62
-
63
- `Editor` is the same editor in the web view, growing with its content between `minHeight` and `maxHeight`:
64
-
65
- ```vue
66
- <script setup lang="ts">
67
- import { ref } from 'vue';
68
- import { Editor } from 'nuvra';
69
-
70
- const description = ref('');
71
- </script>
72
-
73
- <template>
74
- <Editor v-model="description" placeholder="Description" :max-length="2000" />
75
- </template>
76
- ```
77
-
78
- ### Image upload
79
-
80
- Without an upload handler images are embedded as data URLs. Pass `uploadImage` to store them on your server:
81
-
82
- ```ts
83
- import type { DocumentImageUploadHandler } from 'nuvra';
84
-
85
- const uploadImage: DocumentImageUploadHandler = async file => {
86
- const body = new FormData();
87
- body.append('file', file);
88
- const response = await fetch('/api/files', { method: 'POST', body });
89
- return (await response.json()).url;
90
- };
91
- ```
92
-
93
- ## API
94
-
95
- ### `DocumentEditor` props
96
-
97
- | Prop | Type | Default | Description |
98
- | ----------------- | --------------------------------- | -------- | -------------------------------------------------------------------------- |
99
- | `v-model` | `string` | `''` | Document HTML; an empty document is an empty string. |
100
- | `v-model:page` | `PageSettings` | A4 | Paper size, orientation, margins, headers, footers, watermark, numbering. |
101
- | `v-model:comments`| `DocumentComment[]` | — | Comments; binding it turns the comment tools on. |
102
- | `v-model:trackChanges` | `boolean` | `false` | Records edits as tracked changes. |
103
- | `author` | `string` | `''` | Name written on new comments, replies and tracked changes. |
104
- | `autofocus` | `boolean` | `false` | Places the caret at the end of the document once ready. |
105
- | `canvasPadding` | `number \| string` | `50` | Gray space around the page or web sheet. |
106
- | `defaultViewMode` | `'page' \| 'web'` | `'page'` | View shown first. |
107
- | `disabled` | `boolean` | `false` | Read-only document, disabled controls. |
108
- | `height` | `number \| string` | `760` | Height of the editor, or `'auto'` to grow between `minHeight`/`maxHeight`. |
109
- | `locale` | `EditorLocale \| 'uz' \| 'en' \| 'ru'` | — | Interface language; defaults to the app-wide language (Uzbek). |
110
- | `minHeight` | `number \| string` | `240` | Smallest height of an auto-height editor. |
111
- | `maxHeight` | `number \| string` | `600` | Largest height of an auto-height editor. |
112
- | `maxImageSizeMb` | `number` | `10` | Largest accepted image file. |
113
- | `maxLength` | `number` | `0` | Character limit; `0` means unlimited. |
114
- | `placeholder` | `string` | `''` | Text shown while the document is empty. |
115
- | `ruler` | `boolean` | `true` | Shows the ruler in the page view. |
116
- | `slashCommands` | `SlashCommand[]` | `[]` | Your own commands, listed first in the `/` menu. |
117
- | `title` | `string` | `''` | Print title and exported file name. |
118
- | `uploadImage` | `(file: File) => Promise<string>` | — | Uploads an image and resolves with its URL. |
119
- | `variables` | `TemplateVariable[]` | `[]` | Template variables the user can insert. |
120
-
121
- `Editor` accepts the same props except `v-model:page`, `v-model:comments`, `v-model:trackChanges`, `author`,
122
- `defaultViewMode`, `height`, `ruler`, `slashCommands` and `title`.
123
-
124
- Numbers are pixels; strings are used as CSS lengths (`'100%'`, `'50vh'`).
125
-
126
- ### Events
127
-
128
- | Event | Payload | Description |
129
- | ------------- | ---------------- | ------------------------------------------------- |
130
- | `focus` | — | The document received focus. |
131
- | `blur` | — | The document lost focus; the model is up to date. |
132
- | `uploadError` | `error: unknown` | An image was rejected or its upload failed. |
133
- | `importError` | `error: unknown` | A Word file could not be read. |
134
-
135
- ### Exposed methods (`DocumentEditor` ref)
136
-
137
- | Method | Description |
138
- | ------------------------- | ---------------------------------------------------------------- |
139
- | `focus()` | Moves keyboard focus into the document. |
140
- | `getHTML()` | Returns the document HTML, including edits not yet in the model. |
141
- | `insertVariable(name)` | Inserts a template variable at the selection. |
142
- | `updateTableOfContents()` | Inserts or refreshes the table of contents. |
143
- | `importWord(file)` | Replaces the document with the content of a `.docx` file. |
144
- | `print()` | Opens the browser print dialog. |
145
- | `exportHtml()` | Downloads the document as an HTML page. |
146
- | `exportWord()` | Downloads the document as a Word file (`.docx`). |
147
- | `engine` | The editing engine (`DocumentEngine`), for advanced integrations. |
148
-
149
- ### Keyboard shortcuts
150
-
151
- `Ctrl/⌘+B`, `I`, `U` — bold, italic, underline · `Ctrl/⌘+Shift+H` — highlight · `Ctrl/⌘+Z`, `Ctrl/⌘+Shift+Z` — undo,
152
- redo · `Ctrl/⌘+F` — find · `Ctrl/⌘+H` — replace · `Ctrl/⌘+K` — link · `Ctrl/⌘+Alt+M` — comment · `Ctrl/⌘+P` — print.
153
-
154
- The full list, with Markdown-like input rules, is in the
155
- [keyboard shortcuts guide](https://nuvra-docs.vercel.app/docs/keyboard-shortcuts).
156
-
157
- ## Templates and signatures
158
-
159
- Pass `variables` to let users insert template variables with the toolbar's **{ }** menu or by typing `{{name}}`.
160
- They are saved as `<span data-variable="name">{{name}}</span>`; `fillTemplate` replaces them with escaped values,
161
- in the browser or on a Node server:
162
-
163
- ```ts
164
- import { type TemplateVariable, fillTemplate } from 'nuvra';
165
-
166
- const variables: TemplateVariable[] = [
167
- { name: 'full_name', label: 'Full name' },
168
- { name: 'letter_date', label: 'Letter date' }
169
- ];
170
-
171
- const letter = fillTemplate(template, { full_name: 'Aziz Karimov', letter_date: '14.09.2026' });
172
- ```
173
-
174
- The pen button inserts a signature block in the editor's language: a signature line, an “Approved” or “Agreed”
175
- block, or the signatures of both parties of a contract. It is a borderless table, so it prints and exports to Word
176
- without lines.
177
-
178
- The document button inserts ready-made templates (official letter, order, application, certificate, act), and the
179
- toolbar writes amounts in words (`15 000 000 (o‘n besh million)`), inserts long dates (`2026-yil 14-sentabr`) and
180
- converts Uzbek text between the Latin and Cyrillic alphabets. The same helpers are exported: `getDocumentTemplate`,
181
- `numberToWords`, `formatAmountInWords`, `parseAmount`, `formatLongDate` and `transliterate`.
182
-
183
- ### Filling a template as a form
184
-
185
- `DocumentForm` shows a saved template as it will be printed, with an input in place of every variable. Fields of the
186
- same variable share one value:
187
-
188
- ```vue
189
- <script setup lang="ts">
190
- import { ref } from 'vue';
191
- import { DocumentForm } from 'nuvra';
192
-
193
- const values = ref<Record<string, string>>({});
194
- const form = ref<InstanceType<typeof DocumentForm>>();
195
-
196
- const submit = () => {
197
- if (form.value?.validate().length) return; // empty fields are marked and focused
198
- const html = form.value?.getHTML(); // filled with fillTemplate
199
- };
200
- </script>
201
-
202
- <template>
203
- <DocumentForm ref="form" v-model="values" :template="template" :variables="variables" />
204
- </template>
205
- ```
206
-
207
- ## Word files and long documents
208
-
209
- "Download as Word (.docx)" in the "More" menu writes a real Office Open XML file with the page setup, headers and
210
- footers with page fields, the watermark, lists, tables with merged cells, images, footnotes and tracked changes as
211
- Word revisions. "Open Word file (.docx)" (or `importWord(file)`) loads one into the editor, footnotes and revisions
212
- included; a file that cannot be read emits `importError`. `buildDocx` and `readDocx` do the same in your own code. A
213
- document with several sections gets the page setup of its last section, and the content of a different first page
214
- header is not imported.
215
-
216
- ### Footnotes
217
-
218
- "Footnote" in the insert menu or the `/` menu adds a numbered reference and opens a small form for the note; clicking
219
- a reference edits or deletes it. A footnote is saved in the reference, `<sup data-footnote="note text">1</sup>`, and
220
- renumbered in document order. The page view draws the notes at the bottom of the sheet the reference is on, the web
221
- view after the document; printing follows the sheets, and the Word export writes Word footnotes. Notes are plain text.
222
- From code: `engine.insertFootnote(text)`, `setFootnoteText(element, text)`, `removeFootnote(element)`,
223
- `getFootnotes()`.
224
-
225
- For long documents the toolbar offers multilevel numbering (1., 1.1., 1.1.1., saved as `<ol data-numbering="legal">`),
226
- a table of contents of the headings with page numbers (saved as `<table data-type="toc">`, refreshed with
227
- `updateTableOfContents()`), and a navigation pane. `PageSettings` has `differentFirstPage` to hide the header, footer
228
- and page number on the first page, and `firstPageNumber` for the number printed on it.
229
-
230
- ## Comments, tracked changes and comparison
231
-
232
- Bind `v-model:comments` to turn comments on. The HTML keeps only the anchors, `<span data-comment="id">`; the
233
- comments are plain data you store next to the document:
234
-
235
- ```vue
236
- <script setup lang="ts">
237
- import { ref } from 'vue';
238
- import { DocumentEditor, type DocumentComment } from 'nuvra';
239
-
240
- const html = ref('');
241
- const comments = ref<DocumentComment[]>([]);
242
-
243
- const save = () =>
244
- fetch('/api/documents/42', {
245
- method: 'PUT',
246
- headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify({ html: html.value, comments: comments.value })
248
- });
249
- </script>
250
-
251
- <template>
252
- <DocumentEditor v-model="html" v-model:comments="comments" author="Aziz Karimov" @blur="save" />
253
- </template>
254
- ```
255
-
256
- Users select text and press "Add comment" (`Ctrl/⌘+Alt+M`); the "Comments" panel replies, resolves, reopens and
257
- deletes them, and flags comments whose text was deleted.
258
-
259
- ### Tracked changes
260
-
261
- Bind `v-model:trackChanges` (or press "Track changes") to record typing, deleting, cut and paste as tracked changes
262
- by `author`. They are part of the HTML, `<ins data-change="id" data-author="…" data-time="…">` and `<del …>`, shown
263
- and printed green-underlined and red-struck. The "Changes" panel accepts or rejects them one by one or all at once;
264
- from code use `engine.getChanges()`, `engine.resolveChanges(accept, id?)` and `engine.selectChange(id)`. Formatting,
265
- block changes (headings, lists, tables), Enter and joining paragraphs are not tracked. The Word export writes them as
266
- revisions and the import reads Word revisions back.
267
-
268
- ```vue
269
- <DocumentEditor v-model="html" v-model:track-changes="tracking" author="Aziz Karimov" />
270
- ```
271
-
272
- `DocumentCompare` shows what changed between two versions: inserted words in green, deleted words struck through in
273
- red. Unchanged blocks stay as they are, changed paragraphs are compared word by word, and added or removed blocks are
274
- shown whole. `compareDocuments(before, after)` returns the same HTML and counts for your own view.
275
-
276
- ```vue
277
- <DocumentCompare :before="previousVersion" :after="html" :height="600" />
278
- ```
279
-
280
- ## Commands and toolbar buttons
281
-
282
- Typing `/` at the start of a line or after a space opens a command menu: headings, lists, table, page break, footnote,
283
- table of contents, dates, signature blocks and your variables. `slashCommands` adds your own commands at the top, and the
284
- `toolbar` slot adds your own buttons:
285
-
286
- ```vue
287
- <script setup lang="ts">
288
- import { DocumentEditor, type SlashCommand } from 'nuvra';
289
-
290
- const slashCommands: SlashCommand[] = [
291
- { id: 'director', label: 'Director’s name', icon: 'pencil', run: engine => engine.insertText('A. Karimov') }
292
- ];
293
- </script>
294
-
295
- <template>
296
- <DocumentEditor v-model="html" :slash-commands="slashCommands">
297
- <template #toolbar="{ engine, disabled }">
298
- <button type="button" class="doc-tb-button" :disabled="disabled" @mousedown.prevent @click="engine.insertText('✓')">
299
- ✓
300
- </button>
301
- </template>
302
- </DocumentEditor>
303
- </template>
304
- ```
305
-
306
- ## Languages
307
-
308
- The interface ships in Uzbek (`uz`, the default), Uzbek Cyrillic (`uzCyrl`), English (`en`) and Russian (`ru`). The translations are part of
309
- the package and cannot be changed from outside; an app only picks the language.
310
-
311
- For one editor, pass the locale (or just its code) to the `locale` prop:
312
-
313
- ```vue
314
- <script setup lang="ts">
315
- import { DocumentEditor, ru } from 'nuvra';
316
- </script>
317
-
318
- <template>
319
- <DocumentEditor v-model="html" :locale="ru" />
320
- </template>
321
- ```
322
-
323
- For the whole app, call `setEditorLocale` once, for example in `main.ts`. It is reactive, so calling it again from a
324
- language switcher updates editors already on the page:
325
-
326
- ```ts
327
- import { en, setEditorLocale } from 'nuvra';
328
-
329
- setEditorLocale(en);
330
- ```
331
-
332
- The `locale` prop wins over `setEditorLocale`; without either the editor is in Uzbek. `editorLocales` lists every
333
- built-in locale with its name, for language pickers.
334
-
335
- ## Theming
336
-
337
- Colors come from CSS variables declared on `.document-editor` with zero specificity, so any rule that targets
338
- `.document-editor` overrides them, whatever order the stylesheets load in. Set them on the editor element itself:
339
- values on an ancestor such as `body` do not apply, because the editor declares its own.
340
-
341
- ```css
342
- .document-editor {
343
- --nuvra-color-primary: #7c3aed;
344
- --nuvra-color-primary-hover: #8b5cf6;
345
- --nuvra-color-primary-border: #c4b5fd;
346
- --nuvra-color-primary-muted: #ddd6fe;
347
- --nuvra-color-primary-soft: #f5f3ff;
348
- }
349
- ```
350
-
351
- | Variable | Used for |
352
- | ------------------------------- | ---------------------------------------------------- |
353
- | `--nuvra-color-primary` | Active buttons, focus, primary buttons, selection |
354
- | `--nuvra-color-primary-hover` | Hovered primary buttons |
355
- | `--nuvra-color-primary-border` | Focused editor frame, disabled primary buttons |
356
- | `--nuvra-color-primary-muted` | Table size preview, hovered button borders |
357
- | `--nuvra-color-primary-soft` | Active button and menu item backgrounds |
358
- | `--nuvra-color-on-primary` | Text on primary buttons |
359
- | `--nuvra-color-danger` | Destructive actions, character limit reached |
360
- | `--nuvra-color-danger-soft` | Hovered destructive actions |
361
- | `--nuvra-text-strong` | Headings in forms |
362
- | `--nuvra-text` | Regular text and icons |
363
- | `--nuvra-text-muted` | Labels, captions, status bar |
364
- | `--nuvra-text-placeholder` | Placeholders, shortcut hints |
365
- | `--nuvra-text-disabled` | Disabled buttons |
366
- | `--nuvra-border` | Editor frame, inputs |
367
- | `--nuvra-border-hover` | Hovered inputs |
368
- | `--nuvra-border-light` | Popovers and floating panels |
369
- | `--nuvra-border-lighter` | Dividers |
370
- | `--nuvra-fill` | Hovered buttons, segmented controls |
371
- | `--nuvra-bg` | Toolbar, status bar, inputs |
372
- | `--nuvra-bg-overlay` | Popovers, menus, find bar |
373
- | `--nuvra-shadow` | Popovers and floating panels |
374
- | `--nuvra-fullscreen-z-index` | Stacking order of the fullscreen editor (`2000`) |
375
-
376
- A dark palette is applied when an ancestor (usually `<html>`) has the `dark` class or `data-theme="dark"`. To change
377
- dark values separately, target `.dark .document-editor`.
378
-
379
- To follow an Element Plus theme, map the variables to its own:
380
-
381
- ```css
382
- .document-editor {
383
- --nuvra-color-primary: var(--el-color-primary);
384
- --nuvra-color-primary-soft: var(--el-color-primary-light-9);
385
- --nuvra-text: var(--el-text-color-regular);
386
- --nuvra-border: var(--el-border-color);
387
- --nuvra-bg: var(--el-bg-color);
388
- }
389
- ```
390
-
391
- ## Browser support
392
-
393
- Popovers, menus and bubble toolbars use the [Popover API](https://developer.mozilla.org/docs/Web/API/Popover_API)
394
- (Chrome/Edge 114+, Safari 17+, Firefox 125+), which keeps them above dialogs and the fullscreen editor. Older browsers
395
- show them as fixed elements instead. Search highlighting uses the CSS Custom Highlight API.
396
-
397
- ## License
398
-
399
- [MIT](./LICENSE). Icon shapes come from [Lucide](https://lucide.dev) (ISC).
1
+ # nuvra
2
+
3
+ Word-style document editor for Vue 3. A real page view with paper sizes and margins, a lighter web view for
4
+ forms, tables, images, lists, find & replace, comments, HTML source mode, printing, PDF download, export to HTML and Word
5
+ files (`.docx`) you can open again.
6
+
7
+ **[Documentation and live demo →](https://nuvra-docs.vercel.app)**
8
+
9
+ - No editor framework underneath: its own small editing engine with a sanitizing schema.
10
+ - No UI framework either: native HTML controls, built-in SVG icons and plain CSS variables. Vue is the only
11
+ dependency.
12
+ - Pagination in the page view (A4, A5, Letter, …, portrait or landscape), with section breaks for landscape pages inside a
13
+ portrait document.
14
+ - Tables with merge / split, images with resize and alignment, task lists, links, colors, fonts.
15
+ - Undo / redo, keyboard shortcuts that work with non-Latin keyboard layouts, Markdown-like input rules.
16
+ - Office tools: format painter, letter case, paragraph spacing, formatting marks, a right-click menu and
17
+ Ctrl + wheel zoom.
18
+ - Headers and footers with page number, page count, date and title tokens; printing breaks the pages exactly where
19
+ the editor shows them, and the Word export uses Word's own header, footer and page fields.
20
+ - A ruler above the page for the margins and for the first line, left and right indents of a paragraph.
21
+ - A page watermark such as DRAFT or COPY, drawn behind the text of every page and carried into print, HTML and Word.
22
+ - Real `.docx` export and import, PDF download without the print dialog, multilevel (1.1.1) numbering, a table of
23
+ contents and a navigation pane with headings and page thumbnails.
24
+ - Footnotes at the bottom of their page, carried into print and Word.
25
+ - Comments with replies, tracked changes that round-trip with Word, a comparison of two versions, and templates
26
+ filled in as a form.
27
+ - Hooks for editing together: other people's carets and selections, your selection as character positions.
28
+ - A `/` command menu and a toolbar slot for your own commands and buttons.
29
+
30
+ ## Installation
31
+
32
+ ```sh
33
+ pnpm add nuvra
34
+ # or
35
+ npm install nuvra
36
+ ```
37
+
38
+ `vue` (3.5+) is the only peer dependency.
39
+
40
+ Import the styles once, for example in `main.ts`:
41
+
42
+ ```ts
43
+ import 'nuvra/style.css';
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Document editor
49
+
50
+ ```vue
51
+ <script setup lang="ts">
52
+ import { ref } from 'vue';
53
+ import { DocumentEditor, type PageSettings, createPageSettings } from 'nuvra';
54
+
55
+ const html = ref('');
56
+ const page = ref<PageSettings>(createPageSettings());
57
+ </script>
58
+
59
+ <template>
60
+ <DocumentEditor v-model="html" v-model:page="page" height="100%" title="Contract" />
61
+ </template>
62
+ ```
63
+
64
+ ### Form field
65
+
66
+ `Editor` is the same editor in the web view, growing with its content between `minHeight` and `maxHeight`:
67
+
68
+ ```vue
69
+ <script setup lang="ts">
70
+ import { ref } from 'vue';
71
+ import { Editor } from 'nuvra';
72
+
73
+ const description = ref('');
74
+ </script>
75
+
76
+ <template>
77
+ <Editor v-model="description" placeholder="Description" :max-length="2000" />
78
+ </template>
79
+ ```
80
+
81
+ ### Image upload
82
+
83
+ Without an upload handler images are embedded as data URLs. Pass `uploadImage` to store them on your server:
84
+
85
+ ```ts
86
+ import type { DocumentImageUploadHandler } from 'nuvra';
87
+
88
+ const uploadImage: DocumentImageUploadHandler = async file => {
89
+ const body = new FormData();
90
+ body.append('file', file);
91
+ const response = await fetch('/api/files', { method: 'POST', body });
92
+ return (await response.json()).url;
93
+ };
94
+ ```
95
+
96
+ ## API
97
+
98
+ ### `DocumentEditor` props
99
+
100
+ | Prop | Type | Default | Description |
101
+ | ----------------- | --------------------------------- | -------- | -------------------------------------------------------------------------- |
102
+ | `v-model` | `string` | `''` | Document HTML; an empty document is an empty string. |
103
+ | `v-model:page` | `PageSettings` | A4 | Paper size, orientation, margins, headers, footers, watermark, numbering. |
104
+ | `v-model:comments`| `DocumentComment[]` | — | Comments; binding it turns the comment tools on. |
105
+ | `v-model:trackChanges` | `boolean` | `false` | Records edits as tracked changes. |
106
+ | `author` | `string` | `''` | Name written on new comments, replies and tracked changes. |
107
+ | `autofocus` | `boolean` | `false` | Places the caret at the end of the document once ready. |
108
+ | `canvasPadding` | `number \| string` | `50` | Gray space around the page or web sheet. |
109
+ | `collaborators` | `Collaborator[]` | `[]` | Other people editing the document; their carets and selections are drawn. |
110
+ | `defaultViewMode` | `'page' \| 'web'` | `'page'` | View shown first. |
111
+ | `disabled` | `boolean` | `false` | Read-only document, disabled controls. |
112
+ | `height` | `number \| string` | `760` | Height of the editor, or `'auto'` to grow between `minHeight`/`maxHeight`. |
113
+ | `locale` | `EditorLocale \| 'uz' \| 'en' \| 'ru'` | — | Interface language; defaults to the app-wide language (Uzbek). |
114
+ | `minHeight` | `number \| string` | `240` | Smallest height of an auto-height editor. |
115
+ | `maxHeight` | `number \| string` | `600` | Largest height of an auto-height editor. |
116
+ | `maxImageSizeMb` | `number` | `10` | Largest accepted image file. |
117
+ | `maxLength` | `number` | `0` | Character limit; `0` means unlimited. |
118
+ | `placeholder` | `string` | `''` | Text shown while the document is empty. |
119
+ | `ruler` | `boolean` | `true` | Shows the ruler in the page view. |
120
+ | `slashCommands` | `SlashCommand[]` | `[]` | Your own commands, listed first in the `/` menu. |
121
+ | `title` | `string` | `''` | Print title and exported file name. |
122
+ | `tools` | `ToolbarTool[]` | all | Toolbar tools to show; see [Choosing the toolbar tools](#choosing-the-toolbar-tools). |
123
+ | `uploadImage` | `(file: File) => Promise<string>` | — | Uploads an image and resolves with its URL. |
124
+ | `variables` | `TemplateVariable[]` | `[]` | Template variables the user can insert. |
125
+
126
+ `Editor` accepts the same props except `v-model:page`, `v-model:comments`, `v-model:trackChanges`, `author`,
127
+ `collaborators`, `defaultViewMode`, `height`, `ruler`, `slashCommands` and `title`.
128
+
129
+ Numbers are pixels; strings are used as CSS lengths (`'100%'`, `'50vh'`).
130
+
131
+ ### Events
132
+
133
+ | Event | Payload | Description |
134
+ | ----------------- | ----------------------------------- | -------------------------------------------------------------- |
135
+ | `focus` | — | The document received focus. |
136
+ | `blur` | — | The document lost focus; the model is up to date. |
137
+ | `uploadError` | `error: unknown` | An image was rejected or its upload failed. |
138
+ | `importError` | `error: unknown` | A Word file could not be read. |
139
+ | `exportError` | `error: unknown` | The PDF could not be drawn; no file is downloaded. |
140
+ | `selectionChange` | `selection: SelectionOffsets \| null` | The caret or selection moved; `null` when it left the document. |
141
+
142
+ ### Exposed methods (`DocumentEditor` ref)
143
+
144
+ | Method | Description |
145
+ | ------------------------- | ---------------------------------------------------------------- |
146
+ | `focus()` | Moves keyboard focus into the document. |
147
+ | `getHTML()` | Returns the document HTML, including edits not yet in the model. |
148
+ | `insertVariable(name)` | Inserts a template variable at the selection. |
149
+ | `updateTableOfContents()` | Inserts or refreshes the table of contents. |
150
+ | `importWord(file)` | Replaces the document with the content of a `.docx` file. |
151
+ | `print()` | Opens the browser print dialog. |
152
+ | `exportHtml()` | Downloads the document as an HTML page. |
153
+ | `exportWord()` | Downloads the document as a Word file (`.docx`). |
154
+ | `exportPdf()` | Downloads the document as a PDF drawn from its pages. |
155
+ | `engine` | The editing engine (`DocumentEngine`), for advanced integrations. |
156
+
157
+ ### Keyboard shortcuts
158
+
159
+ `Ctrl/⌘+B`, `I`, `U` — bold, italic, underline · `Ctrl/⌘+Shift+H` — highlight · `Ctrl/⌘+Z`, `Ctrl/⌘+Shift+Z` — undo,
160
+ redo · `Ctrl/⌘+F` — find · `Ctrl/⌘+H` — replace · `Ctrl/⌘+K` — link · `Ctrl/⌘+Alt+M` — comment · `Ctrl/⌘+P` — print.
161
+
162
+ The full list, with Markdown-like input rules, is in the
163
+ [keyboard shortcuts guide](https://nuvra-docs.vercel.app/docs/keyboard-shortcuts).
164
+
165
+ ## Templates and signatures
166
+
167
+ Pass `variables` to let users insert template variables with the toolbar's **{ }** menu or by typing `{{name}}`.
168
+ They are saved as `<span data-variable="name">{{name}}</span>`; `fillTemplate` replaces them with escaped values,
169
+ in the browser or on a Node server:
170
+
171
+ ```ts
172
+ import { type TemplateVariable, fillTemplate } from 'nuvra';
173
+
174
+ const variables: TemplateVariable[] = [
175
+ { name: 'full_name', label: 'Full name' },
176
+ { name: 'letter_date', label: 'Letter date' }
177
+ ];
178
+
179
+ const letter = fillTemplate(template, { full_name: 'Aziz Karimov', letter_date: '14.09.2026' });
180
+ ```
181
+
182
+ The pen button inserts a signature block in the editor's language: a signature line, an “Approved” or “Agreed”
183
+ block, or the signatures of both parties of a contract. It is a borderless table, so it prints and exports to Word
184
+ without lines.
185
+
186
+ The document button inserts ready-made templates (official letter, order, application, certificate, act), and the
187
+ toolbar writes amounts in words (`15 000 000 (o‘n besh million)`), inserts long dates (`2026-yil 14-sentabr`) and
188
+ converts Uzbek text between the Latin and Cyrillic alphabets. The same helpers are exported: `getDocumentTemplate`,
189
+ `numberToWords`, `formatAmountInWords`, `parseAmount`, `formatLongDate` and `transliterate`.
190
+
191
+ ### Filling a template as a form
192
+
193
+ `DocumentForm` shows a saved template as it will be printed, with an input in place of every variable. Fields of the
194
+ same variable share one value:
195
+
196
+ ```vue
197
+ <script setup lang="ts">
198
+ import { ref } from 'vue';
199
+ import { DocumentForm } from 'nuvra';
200
+
201
+ const values = ref<Record<string, string>>({});
202
+ const form = ref<InstanceType<typeof DocumentForm>>();
203
+
204
+ const submit = () => {
205
+ if (form.value?.validate().length) return; // empty fields are marked and focused
206
+ const html = form.value?.getHTML(); // filled with fillTemplate
207
+ };
208
+ </script>
209
+
210
+ <template>
211
+ <DocumentForm ref="form" v-model="values" :template="template" :variables="variables" />
212
+ </template>
213
+ ```
214
+
215
+ ## Word files and long documents
216
+
217
+ "Download as Word (.docx)" in the "More" menu writes a real Office Open XML file with the page setup, headers and
218
+ footers with page fields, the watermark, lists, tables with merged cells, images, footnotes and tracked changes as
219
+ Word revisions. "Open Word file (.docx)" (or `importWord(file)`) loads one into the editor, footnotes and revisions
220
+ included; a file that cannot be read emits `importError`. `buildDocx` and `readDocx` do the same in your own code. A
221
+ document with several sections takes the page setup of its first section, and every further section starts with a
222
+ section break; the content of a different first page header is not imported.
223
+
224
+ Tables longer than a page break between their rows in the page view, in print and in the PDF, as in Word: the rows
225
+ that do not fit continue at the top of the next page, and rows joined by a merged cell stay together.
226
+
227
+ ### PDF download
228
+
229
+ "Download as PDF" in the "More" menu, or `exportPdf()`, saves a PDF without the print dialog. Every sheet is drawn into a
230
+ picture (SVG `foreignObject` → canvas → JPEG) and written into the PDF, so it looks like the printout, but its text
231
+ cannot be selected or searched. Images from servers without CORS are left out, and in the web view the editor switches to
232
+ the page view for the moment of the export. A PDF that cannot be drawn emits `exportError`; printing with "Save as PDF"
233
+ still gives a PDF with real text.
234
+
235
+ ### Pages in different orientations
236
+
237
+ "Section break: landscape pages" and "Section break: portrait pages" in the insert menu and the `/` menu (or
238
+ `engine.insertSectionBreak('landscape')`) turn the pages after the break; paper size and margins stay, and the break
239
+ starts a new page. It is saved as `<div data-type="section-break" data-orientation="landscape"></div>`. The page view
240
+ draws sheets of different sizes and gives the blocks of a turned section their sheet's text width; printing uses a named
241
+ `@page` for turned sheets, the PDF has turned pages, and the Word export writes every section as a Word section with its
242
+ own orientation.
243
+
244
+ ### Footnotes
245
+
246
+ "Footnote" in the insert menu or the `/` menu adds a numbered reference and opens a small form for the note; clicking
247
+ a reference edits or deletes it. A footnote is saved in the reference, `<sup data-footnote="note text">1</sup>`, and
248
+ renumbered in document order. The page view draws the notes at the bottom of the sheet the reference is on, the web
249
+ view after the document; printing follows the sheets, and the Word export writes Word footnotes. Notes are plain text.
250
+ From code: `engine.insertFootnote(text)`, `setFootnoteText(element, text)`, `removeFootnote(element)`,
251
+ `getFootnotes()`.
252
+
253
+ For long documents the toolbar offers multilevel numbering (1., 1.1., 1.1.1., saved as `<ol data-numbering="legal">`),
254
+ a table of contents of the headings with page numbers (saved as `<table data-type="toc">`, refreshed with
255
+ `updateTableOfContents()`), and a navigation pane. `PageSettings` has `differentFirstPage` to hide the header, footer
256
+ and page number on the first page, and `firstPageNumber` for the number printed on it. The navigation pane lists the
257
+ headings or shows page thumbnails; clicking one scrolls to it.
258
+
259
+ ## Comments, tracked changes and comparison
260
+
261
+ Bind `v-model:comments` to turn comments on. The HTML keeps only the anchors, `<span data-comment="id">`; the
262
+ comments are plain data you store next to the document:
263
+
264
+ ```vue
265
+ <script setup lang="ts">
266
+ import { ref } from 'vue';
267
+ import { DocumentEditor, type DocumentComment } from 'nuvra';
268
+
269
+ const html = ref('');
270
+ const comments = ref<DocumentComment[]>([]);
271
+
272
+ const save = () =>
273
+ fetch('/api/documents/42', {
274
+ method: 'PUT',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify({ html: html.value, comments: comments.value })
277
+ });
278
+ </script>
279
+
280
+ <template>
281
+ <DocumentEditor v-model="html" v-model:comments="comments" author="Aziz Karimov" @blur="save" />
282
+ </template>
283
+ ```
284
+
285
+ The comment and change tools are in the **Review** menu of the toolbar. Users select text and choose "Add comment"
286
+ (`Ctrl/⌘+Alt+M`); the "Comments" panel replies, resolves, reopens and deletes them, and flags comments whose text was
287
+ deleted.
288
+
289
+ ### Tracked changes
290
+
291
+ Bind `v-model:trackChanges` (or choose "Track changes" in the Review menu) to record typing, deleting, cut and paste as tracked changes
292
+ by `author`. They are part of the HTML, `<ins data-change="id" data-author="…" data-time="…">` and `<del …>`, shown
293
+ and printed green-underlined and red-struck. The "Changes" panel accepts or rejects them one by one or all at once;
294
+ from code use `engine.getChanges()`, `engine.resolveChanges(accept, id?)` and `engine.selectChange(id)`. Formatting,
295
+ block changes (headings, lists, tables), Enter and joining paragraphs are not tracked. The Word export writes them as
296
+ revisions and the import reads Word revisions back.
297
+
298
+ ```vue
299
+ <DocumentEditor v-model="html" v-model:track-changes="tracking" author="Aziz Karimov" />
300
+ ```
301
+
302
+ `DocumentCompare` shows what changed between two versions: inserted words in green, deleted words struck through in
303
+ red. Unchanged blocks stay as they are, changed paragraphs are compared word by word, and added or removed blocks are
304
+ shown whole. `compareDocuments(before, after)` returns the same HTML and counts for your own view.
305
+
306
+ ```vue
307
+ <DocumentCompare :before="previousVersion" :after="html" :height="600" />
308
+ ```
309
+
310
+ ## Editing together
311
+
312
+ The editor has hooks for several people on one document; the transport (WebSocket, WebRTC, …) is up to your app.
313
+ `selectionChange` reports your caret or selection as character positions, `collaborators` draws other people's carets
314
+ (with names) and selections, and a new `v-model` value from outside keeps your caret at the same character position.
315
+ The engine also offers `getSelectionOffsets()`, `getOffsetRects(offsets)` and `setContent(html, { keepSelection })`;
316
+ `collaboratorColor` gives the colour a collaborator is drawn in.
317
+
318
+ ```vue
319
+ <script setup lang="ts">
320
+ import { ref } from 'vue';
321
+ import { type Collaborator, DocumentEditor, type SelectionOffsets } from 'nuvra';
322
+
323
+ const html = ref('');
324
+ const collaborators = ref<Collaborator[]>([]); // filled from your socket messages
325
+
326
+ const sendSelection = (selection: SelectionOffsets | null) =>
327
+ socket.send(JSON.stringify({ type: 'selection', id: me.id, name: me.name, selection }));
328
+ </script>
329
+
330
+ <template>
331
+ <DocumentEditor v-model="html" :collaborators="collaborators" @selection-change="sendSelection" />
332
+ </template>
333
+ ```
334
+
335
+ This is not a CRDT: the document travels as a whole, so when two people type at the same time the document sent last
336
+ wins, and because positions are character offsets, a remote edit before your caret shifts it. A library such as Yjs
337
+ can carry the document and the selections (awareness), but edits are still not merged character by character. See the
338
+ [guide](https://nuvra-docs.vercel.app/docs/collaboration).
339
+
340
+ ## Commands and toolbar buttons
341
+
342
+ Typing `/` at the start of a line or after a space opens a command menu: headings, lists, table, page and section breaks, footnote,
343
+ table of contents, dates, signature blocks and your variables. `slashCommands` adds your own commands at the top, and the
344
+ `toolbar` slot adds your own buttons:
345
+
346
+ ```vue
347
+ <script setup lang="ts">
348
+ import { DocumentEditor, type SlashCommand } from 'nuvra';
349
+
350
+ const slashCommands: SlashCommand[] = [
351
+ { id: 'director', label: 'Director’s name', icon: 'pencil', run: engine => engine.insertText('A. Karimov') }
352
+ ];
353
+ </script>
354
+
355
+ <template>
356
+ <DocumentEditor v-model="html" :slash-commands="slashCommands">
357
+ <template #toolbar="{ engine, disabled }">
358
+ <button type="button" class="doc-tb-button" :disabled="disabled" @mousedown.prevent @click="engine.insertText('✓')">
359
+ ✓
360
+ </button>
361
+ </template>
362
+ </DocumentEditor>
363
+ </template>
364
+ ```
365
+
366
+ ### Choosing the toolbar tools
367
+
368
+ The `tools` prop shows only the toolbar tools you list, in their usual order; without it every tool is shown. Groups left
369
+ without a tool disappear together with their dividers, and the `toolbar` slot is always shown. Keyboard shortcuts, the
370
+ `/` menu and the right-click menu are not affected.
371
+
372
+ ```vue
373
+ <DocumentEditor v-model="html" :tools="['history', 'blockStyle', 'marks', 'lists', 'link', 'table']" />
374
+ ```
375
+
376
+ The tools are `history` (undo, redo), `formatPainter`, `blockStyle`, `fontFamily`, `fontSize`, `marks` (bold, italic,
377
+ underline and more), `textCase`, `color`, `highlight`, `align`, `lineHeight`, `direction`, `lists`, `indent`, `link`,
378
+ `image`, `table`, `specialCharacters`, `variables`, `signature`, `insert`, `review`, `search`, `templates`,
379
+ `headerFooter`, `pageSetup` and `more`. `TOOLBAR_TOOLS` lists them all, and `Editor` takes the same prop.
380
+
381
+ ## Languages
382
+
383
+ The interface ships in Uzbek (`uz`, the default), Uzbek Cyrillic (`uzCyrl`), English (`en`) and Russian (`ru`). The translations are part of
384
+ the package and cannot be changed from outside; an app only picks the language.
385
+
386
+ For one editor, pass the locale (or just its code) to the `locale` prop:
387
+
388
+ ```vue
389
+ <script setup lang="ts">
390
+ import { DocumentEditor, ru } from 'nuvra';
391
+ </script>
392
+
393
+ <template>
394
+ <DocumentEditor v-model="html" :locale="ru" />
395
+ </template>
396
+ ```
397
+
398
+ For the whole app, call `setEditorLocale` once, for example in `main.ts`. It is reactive, so calling it again from a
399
+ language switcher updates editors already on the page:
400
+
401
+ ```ts
402
+ import { en, setEditorLocale } from 'nuvra';
403
+
404
+ setEditorLocale(en);
405
+ ```
406
+
407
+ The `locale` prop wins over `setEditorLocale`; without either the editor is in Uzbek. `editorLocales` lists every
408
+ built-in locale with its name, for language pickers.
409
+
410
+ ## Theming
411
+
412
+ Colors come from CSS variables declared on `.document-editor` with zero specificity, so any rule that targets
413
+ `.document-editor` overrides them, whatever order the stylesheets load in. Set them on the editor element itself:
414
+ values on an ancestor such as `body` do not apply, because the editor declares its own.
415
+
416
+ ```css
417
+ .document-editor {
418
+ --nuvra-color-primary: #7c3aed;
419
+ --nuvra-color-primary-hover: #8b5cf6;
420
+ --nuvra-color-primary-border: #c4b5fd;
421
+ --nuvra-color-primary-muted: #ddd6fe;
422
+ --nuvra-color-primary-soft: #f5f3ff;
423
+ }
424
+ ```
425
+
426
+ | Variable | Used for |
427
+ | ------------------------------- | ---------------------------------------------------- |
428
+ | `--nuvra-color-primary` | Active buttons, focus, primary buttons, selection |
429
+ | `--nuvra-color-primary-hover` | Hovered primary buttons |
430
+ | `--nuvra-color-primary-border` | Focused editor frame, disabled primary buttons |
431
+ | `--nuvra-color-primary-muted` | Table size preview, hovered button borders |
432
+ | `--nuvra-color-primary-soft` | Active button and menu item backgrounds |
433
+ | `--nuvra-color-on-primary` | Text on primary buttons |
434
+ | `--nuvra-color-danger` | Destructive actions, character limit reached |
435
+ | `--nuvra-color-danger-soft` | Hovered destructive actions |
436
+ | `--nuvra-text-strong` | Headings in forms |
437
+ | `--nuvra-text` | Regular text and icons |
438
+ | `--nuvra-text-muted` | Labels, captions, status bar |
439
+ | `--nuvra-text-placeholder` | Placeholders, shortcut hints |
440
+ | `--nuvra-text-disabled` | Disabled buttons |
441
+ | `--nuvra-border` | Editor frame, inputs |
442
+ | `--nuvra-border-hover` | Hovered inputs |
443
+ | `--nuvra-border-light` | Popovers and floating panels |
444
+ | `--nuvra-border-lighter` | Dividers |
445
+ | `--nuvra-fill` | Hovered buttons, segmented controls |
446
+ | `--nuvra-bg` | Toolbar, status bar, inputs |
447
+ | `--nuvra-bg-overlay` | Popovers, menus, find bar |
448
+ | `--nuvra-shadow` | Popovers and floating panels |
449
+ | `--nuvra-fullscreen-z-index` | Stacking order of the fullscreen editor (`2000`) |
450
+
451
+ A dark palette is applied when an ancestor (usually `<html>`) has the `dark` class or `data-theme="dark"`. To change
452
+ dark values separately, target `.dark .document-editor`.
453
+
454
+ To follow an Element Plus theme, map the variables to its own:
455
+
456
+ ```css
457
+ .document-editor {
458
+ --nuvra-color-primary: var(--el-color-primary);
459
+ --nuvra-color-primary-soft: var(--el-color-primary-light-9);
460
+ --nuvra-text: var(--el-text-color-regular);
461
+ --nuvra-border: var(--el-border-color);
462
+ --nuvra-bg: var(--el-bg-color);
463
+ }
464
+ ```
465
+
466
+ ## Browser support
467
+
468
+ Popovers, menus and bubble toolbars use the [Popover API](https://developer.mozilla.org/docs/Web/API/Popover_API)
469
+ (Chrome/Edge 114+, Safari 17+, Firefox 125+), which keeps them above dialogs and the fullscreen editor. Older browsers
470
+ show them as fixed elements instead. Search highlighting uses the CSS Custom Highlight API.
471
+
472
+ ## License
473
+
474
+ [MIT](./LICENSE). Icon shapes come from [Lucide](https://lucide.dev) (ISC).