@makhmudov.kay/az-editor 0.1.0-beta.2 → 0.1.0-beta.3

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.
@@ -1,875 +1,896 @@
1
- # Интеграция Az Editor в React-приложение
2
-
3
- Руководство описывает публичный API `@makhmudov.kay/az-editor` версии `0.1.0-beta.2`.
4
- Примеры написаны на TypeScript. Для JavaScript удалите аннотации типов и `import type`.
5
-
6
- ## Содержание
7
-
8
- 1. [Выбор компонента](#1-выбор-компонента)
9
- 2. [Установка и сборщик](#2-установка-и-сборщик)
10
- 3. [Открытие и сохранение DOCX](#3-открытие-и-сохранение-docx)
11
- 4. [HTML: черновик, применение и отмена](#4-html-черновик-применение-и-отмена)
12
- 5. [Диагностика преобразования](#5-диагностика-преобразования)
13
- 6. [Переменные](#6-переменные)
14
- 7. [JSON, языки и блоки](#7-json-языки-и-блоки)
15
- 8. [PDF и печать](#8-pdf-и-печать)
16
- 9. [Интерфейс и локализация](#9-интерфейс-и-локализация)
17
- 10. [Шрифты](#10-шрифты)
18
- 11. [Next.js и SSR](#11-nextjs-и-ssr)
19
- 12. [Справочник API](#12-справочник-api)
20
- 13. [Ограничения](#13-ограничения)
21
- 14. [Ошибки и проверка интеграции](#14-ошибки-и-проверка-интеграции)
22
-
23
- ## 1. Выбор компонента
24
-
25
- | Задача | Компонент | Результат |
26
- | --- | --- | --- |
27
- | DOCX → DOCX | `AzDocumentEditor` | `await ref.current.save()` |
28
- | HTML-фрагмент → HTML-фрагмент | `AzHtmlEditor` | `await ref.current.exportHtml()` |
29
- | DOCX → ограниченный HTML/JSON | `AzDocumentEditor` | `await ref.current.exportHujjat(options)` |
30
-
31
- `AzDocumentEditor` редактирует документ Word в памяти браузера. Для нового документа
32
- передайте `document="blank"` или не задавайте `document`.
33
-
34
- `AzHtmlEditor` принимает один HTML-фрагмент, создаёт внутренний документ для
35
- редактирования и возвращает HTML по запросу приложения. Это отдельный черновик:
36
- редактор не заменяет значение формы при каждом нажатии клавиши.
37
-
38
- Библиотека предоставляет поверхность документа, форматирование, импорт, экспорт
39
- и диагностику. Приложение отвечает за выбор файлов, хранение, авторизацию, версии,
40
- маршрутизацию, модальные окна и сообщения пользователю. PDF-сервис подключается
41
- отдельно. HTTP-помощники не вызываются автоматически при монтировании компонента;
42
- шрифты и ресурсы движка могут загружаться браузером.
43
-
44
- Если уже используется другой редактор, храните HTML в общем состоянии формы.
45
- Открывайте Az Editor с копией значения. После «Применить» заменяйте HTML формы;
46
- после «Отмена» оставляйте прежнее значение. Пока открыт черновик, блокируйте правки
47
- той же версии в другом редакторе или разрешайте конфликты явно.
48
-
49
- ## 2. Установка и сборщик
50
-
51
- ### Требования
52
-
53
- - React `^19.0.0` и React DOM `^19.0.0`. React 18 не входит в peer-контракт этой версии.
54
- - Node.js `^24.15.0 || >=26.0.0` для установки и сборки. Node 25 не входит в этот диапазон.
55
- - Современный браузер с DOM, WebAssembly, File/Blob, `fetch`, `AbortSignal.any` и `AbortSignal.timeout`.
56
- - Сборщик ESM, обрабатывающий CSS, шрифты и WASM. Проверенная конфигурация — Vite.
57
- - Контейнер с явно заданной высотой.
58
-
59
- В существующем приложении:
60
-
61
- ```sh
62
- npm install @makhmudov.kay/az-editor@beta
63
- ```
64
-
65
- Для фиксации опубликованной версии:
66
-
67
- ```sh
68
- npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.2
69
- ```
70
-
71
- Тег `beta` может указывать на более новую версию. Сохраняйте lockfile и проверяйте
72
- обновления на своих документах. Вход в npm для установки публичного пакета не нужен.
73
- React должен быть одним общим экземпляром приложения и библиотеки.
74
-
75
- ### Новое приложение React + Vite
76
-
77
- ```sh
78
- npm create vite@latest editor-example -- --template react-ts
79
- cd editor-example
80
- npm install
81
- npm install @makhmudov.kay/az-editor@beta
82
- ```
83
-
84
- Проверьте, что созданное приложение использует React 19. В `vite.config.ts`:
85
-
86
- ```ts
87
- import { defineConfig } from 'vite';
88
- import react from '@vitejs/plugin-react';
89
-
90
- export default defineConfig({
91
- plugins: [react()],
92
- optimizeDeps: { exclude: ['@docx-editor.dev/fonts'] },
93
- });
94
- ```
95
-
96
- Исключение пакета шрифтов из предварительной сборки используется в конфигурации
97
- Az Editor. Если `optimizeDeps` уже задан, объедините списки, сохранив настройки
98
- приложения. [Описание `optimizeDeps.exclude`](https://vite.dev/config/dep-optimization-options#optimizedeps-exclude).
99
-
100
- Один раз подключите CSS, например в клиентском `src/main.tsx`:
101
-
102
- ```ts
103
- import '@makhmudov.kay/az-editor/styles.css';
104
- ```
105
-
106
- Собственные переопределения импортируйте после CSS библиотеки. Стили демонстрационного
107
- приложения не нужны. Проверьте загрузку файлов шрифтов и WASM в production, включая
108
- размещение приложения в подкаталоге.
109
-
110
- ### Первое подключение
111
-
112
- ```tsx
113
- import { useState } from 'react';
114
- import { AzDocumentEditor, type EditorStatus } from '@makhmudov.kay/az-editor';
115
- import '@makhmudov.kay/az-editor/styles.css';
116
-
117
- export function DocumentView() {
118
- const [status, setStatus] = useState<EditorStatus>({ loading: true, pages: 0, error: null });
119
- return <section>
120
- <p role="status">{status.loading ? 'Загрузка…' : `Страниц: ${status.pages}`}</p>
121
- {status.error && <p role="alert">{status.error}</p>}
122
- <AzDocumentEditor document="blank" title="Новый документ.docx"
123
- style={{ height: 700 }} onStatusChange={setStatus} />
124
- </section>;
125
- }
126
- ```
127
-
128
- ## 3. Открытие и сохранение DOCX
129
-
130
- ### Полный пример
131
-
132
- Скопируйте [DocxEditorExample.tsx](../examples/integration/DocxEditorExample.tsx).
133
- Он содержит выбор файла, валидацию, скачивание, ошибки, блокировку повторных
134
- операций и предупреждение о несохранённых правках:
135
-
136
- ```tsx
137
- import { DocxEditorExample } from './DocxEditorExample';
138
-
139
- export default function App() {
140
- return <DocxEditorExample />;
141
- }
142
- ```
143
-
144
- Примеры входят в npm-архив как исходники для копирования. Путь `examples/...` не
145
- является экспортом пакета. После копирования используйте относительные импорты.
146
-
147
- ### Открытие файла
148
-
149
- Для локального файла используйте `<input type="file" accept=".docx">`, затем
150
- `const opened = await readDocxFile(file)`. Передайте `opened.original` в `document`.
151
- Помощник проверяет имя, размер и структуру DOCX до замены текущего документа.
152
-
153
- Храните байты в state или стабильном объекте. Не создавайте `new Uint8Array(...)`
154
- на каждом рендере: смена идентичности `document` может пересоздать внутренний редактор.
155
- При открытии другого документа меняйте React `key` сессии, предварительно получив
156
- решение пользователя о несохранённых правках. Не меняйте `key` на каждом `onChange`.
157
-
158
- Для серверного маршрута, который возвращает бинарный DOCX:
159
-
160
- ```ts
161
- import { DOCX_MIME, readDocxFile } from '@makhmudov.kay/az-editor';
162
-
163
- export async function loadDocx(url: string, filename: string, signal?: AbortSignal) {
164
- const response = await fetch(url, { credentials: 'same-origin', signal });
165
- if (!response.ok) throw new Error(`Не удалось открыть документ: HTTP ${response.status}`);
166
- const bytes = await response.arrayBuffer();
167
- return readDocxFile(new File([bytes], filename, { type: DOCX_MIME }));
168
- }
169
- ```
170
-
171
- `filename` должен оканчиваться на `.docx`. Авторизацию и CSRF подключите через свой
172
- HTTP-клиент. Отменяйте загрузку при уходе со страницы; не применяйте старый ответ
173
- поверх новой сессии. Ограничьте размер ответа на сервере/шлюзе: этот пример загружает
174
- байты до проверки размера через `readDocxFile`.
175
-
176
- `opened.metadata` содержит число абзацев/таблиц/секций, список шрифтов и признаки
177
- колонтитулов/рецензирования. `savedPageCount` — число страниц из метаданных исходного
178
- файла; оно может быть устаревшим. Текущее число страниц берите из `onStatusChange`.
179
-
180
- ### Скачивание и сохранение на сервере
181
-
182
- Создайте `useRef<AzDocumentEditorHandle>(null)` и передайте его в компонент.
183
- `save()` возвращает текущий DOCX как `Promise<ArrayBuffer>`:
184
-
185
- ```ts
186
- import { downloadDocx, type AzDocumentEditorHandle } from '@makhmudov.kay/az-editor';
187
-
188
- export async function saveToDisk(editor: AzDocumentEditorHandle, filename: string) {
189
- const bytes = await editor.save();
190
- downloadDocx(bytes, filename);
191
- }
192
- ```
193
-
194
- `onSave` вызывается через меню и Ctrl/Cmd+S. Передавайте обработчик, который
195
- перехватывает ошибки асинхронной операции. До готовности и на время сохранения
196
- используйте `actionsDisabled`; чтобы блокировать и сам текст, задайте `mode="view"`.
197
- `actionsDisabled` сам по себе не делает документ read-only.
198
-
199
- Для своего сервера скопируйте [server-api.ts](../examples/integration/server-api.ts):
200
-
201
- ```ts
202
- import type { AzDocumentEditorHandle } from '@makhmudov.kay/az-editor';
203
- import { saveDocxToApi } from './server-api';
204
-
205
- export async function saveToServer(editor: AzDocumentEditorHandle, id: string, signal?: AbortSignal) {
206
- const bytes = await editor.save();
207
- await saveDocxToApi(`/api/documents/${encodeURIComponent(id)}/file`, bytes, 'document.docx', signal);
208
- }
209
- ```
210
-
211
- Маршрут в этом примере реализует приложение: `POST multipart/form-data`, поле `file`,
212
- успешный ответ `2xx`. Библиотека не создаёт endpoint. Не задавайте `Content-Type`
213
- для `FormData` вручную: браузер формирует multipart boundary.
214
-
215
- Сбрасывайте признак несохранённых правок после успешного ответа API. Если правки
216
- разрешены во время запроса, сравните локальную ревизию до запроса и после ответа,
217
- чтобы не пометить новые изменения сохранёнными. В DOCX-примере показан такой `revision`.
218
- `downloadDocx` лишь инициирует скачивание: браузер не подтверждает запись файла на диск.
219
- Для автосохранения приложение реализует очередь и контроль серверных версий.
220
-
221
- ## 4. HTML: черновик, применение и отмена
222
-
223
- ```text
224
- HTML формы → initialHtml → сессия AzHtmlEditor → exportHtml()
225
- ↑ ↓
226
- └──── применение после проверки ← HTML + предупреждения
227
- ```
228
-
229
- `initialHtml` читается один раз при монтировании. Новый prop в существующей сессии
230
- не загружает другой документ. Меняйте React `key` после решения о несохранённых
231
- правках. Нет props `value`/`onChange(html)` и синхронного `getContent()`.
232
-
233
- `onChange()` только сообщает о правке. Получайте HTML при применении, сохранении
234
- или подготовке PDF через `await ref.current.exportHtml()`, не на каждую клавишу.
235
-
236
- ### Полный пример формы
237
-
238
- Скопируйте [HtmlFormExample.tsx](../examples/integration/HtmlFormExample.tsx) и
239
- [HtmlDraft.tsx](../examples/integration/HtmlDraft.tsx) в одну папку:
240
-
241
- ```tsx
242
- import { HtmlFormExample } from './HtmlFormExample';
243
-
244
- export default function App() {
245
- return <HtmlFormExample />;
246
- }
247
- ```
248
-
249
- `textarea` в примере представляет HTML формы; его можно заменить другим редактором.
250
- `HtmlDraft` получает `initialHtml`, `variables`, `onApply(html)` и `onCancel()`.
251
- Родитель размонтирует сессию после применения или отмены.
252
-
253
- При предупреждениях результат хранится в `pending` до подтверждения, редактирование
254
- временно блокируется. Так пользователь подтверждает именно тот снимок, который
255
- затем попадёт в форму. Применение без правок закрывает черновик без замены HTML.
256
-
257
- ### Результат
258
-
259
- ```ts
260
- type HtmlResult = {
261
- html: string;
262
- variables: string[];
263
- warnings: { code: string; message: string; severity: 'warning' | 'loss' }[];
264
- changed: boolean;
265
- };
266
- ```
267
-
268
- - Без правок возвращается исходный `initialHtml`, `changed: false`. Повторной очистки строки нет.
269
- - После правок возвращается преобразованный HTML, `changed: true`.
270
- - `dirty`/`changed` отражают факт правок, а не сравнение строк; undo до исходного вида не гарантирует `dirty: false`.
271
- - Экспорт не сбрасывает `dirty` и не подтверждает запись на сервере.
272
- - Повторный одновременный экспорт, экспорт до готовности, после закрытия или из изменившегося во время операции черновика отклоняется.
273
-
274
- ### Сохранение и закрытие
275
-
276
- `saveHtmlToApi` из [server-api.ts](../examples/integration/server-api.ts) задаёт пример
277
- контракта `PUT { html }` → `200 { html }`. Передавайте уже применённое значение формы.
278
- При ошибке сохранения оставляйте его для повторной попытки. `onApply` сам по себе
279
- не означает, что данные записаны на сервер.
280
-
281
- После успешного ответа используйте возвращённый сервером HTML как новую исходную
282
- версию: сервер мог его нормализовать. Закройте черновик или перемонтируйте с новым `key`.
283
-
284
- Закрытие модального окна по Esc/подложке, переключение языка и переход по маршруту
285
- должны проходить через решение о применении/отмене. Размонтирование не запускает
286
- автоматический экспорт. `beforeunload` защищает уход из вкладки только в пределах
287
- возможностей браузера; для SPA-переходов нужен механизм вашего роутера.
288
-
289
- ## 5. Диагностика преобразования
290
-
291
- | Severity | Значение | Действие |
292
- | --- | --- | --- |
293
- | `warning` | Нормализация/приближение оформления, например замена шрифта | Показать диагностику |
294
- | `loss` | Неподдерживаемое содержимое или существенная потеря | По умолчанию операция отклоняется |
295
-
296
- Импорт с потерями переводит HTML-редактор в `phase: 'blocked'`. Покажите `warnings`;
297
- только по решению пользователя вызовите `acceptImportLoss()`. Это разрешает открыть
298
- черновик, но не разрешает все будущие сохранения с потерями.
299
-
300
- При экспорте `HujjatCompatibilityError` содержит `warnings`. Простой помощник:
301
-
302
- ```ts
303
- import { HujjatCompatibilityError, type AzHtmlEditorHandle } from '@makhmudov.kay/az-editor';
304
-
305
- export async function exportWithDecision(editor: AzHtmlEditorHandle) {
306
- try {
307
- return await editor.exportHtml();
308
- } catch (error) {
309
- if (!(error instanceof HujjatCompatibilityError) ||
310
- !error.warnings.some(warning => warning.severity === 'loss')) throw error;
311
- const accepted = window.confirm(error.warnings.map(warning => warning.message).join('\n'));
312
- if (!accepted) return null;
313
- return editor.exportHtml({ allowLossy: true });
314
- }
315
- }
316
- ```
317
-
318
- Для асинхронного диалога блокируйте редактирование на время подтверждения: решение
319
- не должно относиться к уже устаревшему результату. Полный пример `HtmlDraft`
320
- подготавливает снимок с `allowLossy: true`, но не применяет его до подтверждения.
321
- Не включайте этот флаг безусловно в сохранении формы.
322
-
323
- `sanitizeHujjatHtml` и `withHujjatLocale` возвращают диагностику, но сами не блокируют
324
- потери. Проверяйте `warnings`, включая сообщения о некорректных переменных.
325
-
326
- HTML в ветке «без правок» остаётся исходным и может содержать неподдерживаемые
327
- конструкции. Редактор и `changed: false` не заменяют серверную очистку/валидацию.
328
- Произвольный HTML в примерах показан в `textarea`, не через `dangerouslySetInnerHTML`.
329
-
330
- ## 6. Переменные
331
-
332
- Оба редактора поддерживают `ref.current.insertVariable('client.name')`. Передавайте
333
- ключ без фигурных скобок: вставится `{client.name}`. Это обычный редактируемый текст,
334
- не защищённый элемент. Подстановка значений выполняется вашим сервером.
335
-
336
- Для внешней кнопки предотвращайте смену фокуса при `mousedown`, чтобы сохранить
337
- позицию вставки. Готовый селектор есть в `HtmlDraft`.
338
-
339
- `validateHujjatVariable(key)` проверяет ASCII-буквы, цифры, `_` и точки между
340
- непустыми сегментами; длина до 100 символов. Сегменты `__proto__`, `prototype`,
341
- `constructor` запрещены. Ключ `client.name` допустим, `{client.name}` — нет.
342
-
343
- `exportHtml().variables` содержит найденные ключи без скобок; сверяйте их со своим
344
- каталогом и валидатором. Для JSON PDF-помощника данные вложенные:
345
- `{ client: { name: 'Иван' } }`. Сервер экранирует подставляемые значения.
346
-
347
- ## 7. JSON, языки и блоки
348
-
349
- `AzHtmlEditor` не требует определённой структуры JSON. Передайте нужную строку,
350
- после применения замените только её. Остальные языки, блоки и метаданные сохраняйте
351
- в состоянии приложения независимо.
352
-
353
- ### Встроенный формат
354
-
355
- Помощники с префиксом `Hujjat` — публичные имена API для ограниченного контракта:
356
-
357
- ```json
358
- {
359
- "uz": { "blocks": [{ "type": "html", "props": { "html": "<p>Shartnoma</p>" } }] },
360
- "ru": { "blocks": [{ "type": "html", "props": { "html": "<p>Договор</p>" } }] }
361
- }
362
- ```
363
-
364
- Поддерживаются `uz`, `ru`, `uz_cyrl`, `en` (`HUJJAT_LOCALES`). Это ключи JSON,
365
- не язык интерфейса. Для других ключей управляйте JSON самостоятельно и используйте
366
- `AzHtmlEditor` для отдельной строки.
367
-
368
- Пример импорта/экспорта HTML-only локали через DOCX-компонент:
369
-
370
- ```ts
371
- import {
372
- importHujjatTemplate, type AzDocumentEditorHandle, type HujjatLocales,
373
- } from '@makhmudov.kay/az-editor';
374
-
375
- export function openRussianTemplate(locales: HujjatLocales) {
376
- // Передайте result.document в AzDocumentEditor и покажите result.warnings.
377
- return importHujjatTemplate(locales, 'ru');
378
- }
379
-
380
- export function exportRussianTemplate(editor: AzDocumentEditorHandle, locales: HujjatLocales) {
381
- return editor.exportHujjat({ locale: 'ru', locales });
382
- }
383
- ```
384
-
385
- `readHujjatHtml` объединяет HTML-блоки выбранной локали. Смешанные блоки, блоки с
386
- непустым внешним `style` и отсутствующая локаль отклоняются. Подстановки другого
387
- языка не происходит. `importHujjatTemplate` возвращает `{ document, html, variables, warnings }`.
388
-
389
- `withHujjatLocale` и `exportHujjat({ locales, locale })` заменяют **всю выбранную
390
- локаль одним HTML-блоком**, сохраняя копии остальных локалей. Для одного блока
391
- среди изображений и других блоков обновляйте только его:
392
-
393
- ```ts
394
- import type { HujjatLocale, HujjatLocales } from '@makhmudov.kay/az-editor';
395
-
396
- export function replaceHtmlBlock(
397
- locales: HujjatLocales, locale: HujjatLocale, index: number, html: string,
398
- ): HujjatLocales {
399
- const next = structuredClone(locales);
400
- const block = next[locale]?.blocks[index];
401
- if (!block || block.type !== 'html') throw new Error('Выбранный блок не является HTML.');
402
- block.props = { ...block.props, html };
403
- return next;
404
- }
405
- ```
406
-
407
- Передавайте уже принятый пользователем HTML. Внешние стили блока не воспроизводятся
408
- автоматически, если в редактор передан только `props.html`.
409
-
410
- `createHujjatTemplatePayload(metadata, locales)` необязателен: он формирует
411
- `{ product_id, name, require_myid, locales }`. Используйте его только для совместимого
412
- API. `productId` и непустое `name` до 255 символов обязательны; в JSON обязательна
413
- локаль `uz`, `requireMyid` по умолчанию `false`. Для своего контракта формируйте payload
414
- самостоятельно. Помощник ничего не отправляет на сервер.
415
-
416
- ## 8. PDF и печать
417
-
418
- ### Локальная печать
419
-
420
- `await documentRef.current.print()` открывает предпросмотр всех страниц, затем
421
- диалог печати браузера. Пользователь выбирает принтер или сохранение в PDF. Это
422
- интерактивная печать, а не автоматическое получение PDF Blob. Метод доступен у
423
- `AzDocumentEditor`; ошибки подготовки компонент показывает в собственной строке ошибки.
424
-
425
- `ui.print: false` скрывает пункт и отключает перехват Ctrl/Cmd+P адаптером. Браузер
426
- может обработать сочетание сам. Прямой вызов `ref.print()` настройкой UI не запрещается.
427
-
428
- ### DOCX → серверный PDF
429
-
430
- Для PDF текущего состояния сначала вызовите `save()`:
431
-
432
- ```ts
433
- import {
434
- requestPdf, downloadPdf, pdfRenderProfile, type AzDocumentEditorHandle,
435
- } from '@makhmudov.kay/az-editor';
436
-
437
- export async function downloadCurrentPdf(editor: AzDocumentEditorHandle, signal?: AbortSignal) {
438
- const bytes = await editor.save();
439
- const result = await requestPdf(bytes, 'document.docx', pdfRenderProfile.id, {
440
- url: '/api/documents/export/pdf', signal,
441
- });
442
- downloadPdf(result);
443
- return result.warnings;
444
- }
445
- ```
446
-
447
- В React свяжите эту функцию с `onExportPdf`, обработайте ошибки и блокировку повторных
448
- вызовов так же, как при сохранении. Для неподвижного снимка можно временно установить
449
- `mode="view"` на время операции.
450
-
451
- Контракт сервиса для `requestPdf`:
452
-
453
- | Часть | Значение |
454
- | --- | --- |
455
- | Метод | `POST` |
456
- | Тело | `multipart/form-data`: `file` (DOCX), `renderProfile` (строка) |
457
- | Успех | `2xx`, `Content-Type: application/pdf`, непустой бинарный PDF |
458
- | Имя файла | Необязательный `Content-Disposition` с `filename*=UTF-8''...` |
459
- | Предупреждения | Необязательный `X-PDF-Warnings`, коды через запятую |
460
- | Ошибка | `4xx/5xx`, JSON `{ "error": { "message": "...", "requestId": "..." } }` |
461
-
462
- URL по умолчанию `/api/documents/export/pdf`. `pdfRenderProfile.id` — идентификатор
463
- согласованного с сервером профиля рендеринга; передача строки не устанавливает
464
- PDF-сервис автоматически. Для другого контракта используйте собственный HTTP-клиент.
465
-
466
- `PdfExportOptions`: `url`, `headers`, `signal`, `fetcher`. Timeout — 95 секунд;
467
- переданный сигнал может отменить запрос раньше. По умолчанию используются
468
- `credentials: 'same-origin'`. Для другого origin с cookie:
469
-
470
- ```ts
471
- import { requestPdf } from '@makhmudov.kay/az-editor';
472
-
473
- export function requestCrossOriginPdf(bytes: ArrayBuffer, profile: string) {
474
- return requestPdf(bytes, 'document.docx', profile, {
475
- url: 'https://api.example.com/documents/pdf',
476
- fetcher: (input, init) => fetch(input, { ...init, credentials: 'include' }),
477
- });
478
- }
479
- ```
480
-
481
- Настройте CORS, credentials и CSRF на своём сервере. Для чтения заголовков имени/
482
- предупреждений из браузера включите их в `Access-Control-Expose-Headers`.
483
- Секрет внутреннего PDF-сервиса храните на сервере приложения, не в браузерном коде.
484
-
485
- ### HTML/JSON → PDF через mPDF или другой движок
486
-
487
- `requestHujjatPreview` использует формат JSON из раздела 7. Подключаемый маршрут
488
- может работать на mPDF, если принимает это тело и возвращает PDF:
489
-
490
- ```ts
491
- import {
492
- requestHujjatPreview, downloadPdf, type HujjatLocales,
493
- } from '@makhmudov.kay/az-editor';
494
-
495
- export async function previewTemplate(locales: HujjatLocales, signal?: AbortSignal) {
496
- const result = await requestHujjatPreview({
497
- url: '/api/templates/preview/pdf',
498
- locales, locale: 'ru', data: { client: { name: 'Иван' } }, signal,
499
- });
500
- downloadPdf(result);
501
- }
502
- ```
503
-
504
- Запрос: `POST application/json` с `{ locales, locale, data }`. Успех: `2xx`,
505
- `Content-Type: application/pdf`, байты начинаются с `%PDF-`. Ошибки сервера могут
506
- использовать JSON-поля `message` и `errors`. Результат имеет имя `preview.pdf` и
507
- пустой массив `warnings`. Помощник не сохраняет шаблон или его версию.
508
-
509
- Для PDF текущих правок экспортируйте и примите HTML, обновите нужный блок в копии
510
- JSON, затем передайте эту копию в preview. Иначе PDF будет построен из старой формы.
511
-
512
- `HujjatPreviewOptions` также поддерживает `headers`, `credentials`, `signal`, `fetcher`.
513
- Timeout — 95 секунд; `credentials` по умолчанию `same-origin`, `locale` — `uz`,
514
- `data` — `{}`. Оба PDF-помощника только вызывают сервис; его реализация не входит
515
- в библиотеку. Серверный PDF остаётся итоговым предпросмотром при использовании mPDF.
516
-
517
- ## 9. Интерфейс и локализация
518
-
519
- ### Размеры контейнера
520
-
521
- Используйте `style={{ height: 700 }}` либо родителя с определённой высотой.
522
- Для flex-размещения задайте `minHeight: 0` и `minWidth: 0`:
523
-
524
- ```tsx
525
- import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
526
-
527
- export function EmbeddedEditor() {
528
- return <div style={{ height: '80vh', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
529
- <div>Панель приложения</div>
530
- <AzDocumentEditor style={{ flex: 1, minHeight: 0, minWidth: 0 }} />
531
- </div>;
532
- }
533
- ```
534
-
535
- ### Параметр `ui`
536
-
537
- | Опция | Назначение |
538
- | --- | --- |
539
- | `chrome` | Заголовок, меню и панель форматирования вместе |
540
- | `menu` | Меню редактора |
541
- | `rulers` | Линейки |
542
- | `navigation` | Панель навигации и поиска |
543
- | `contextMenu` | Контекстное меню |
544
- | `print` | Печать в меню и перехват Ctrl/Cmd+P |
545
-
546
- У `AzDocumentEditor` все опции по умолчанию включены. У `AzHtmlEditor` по умолчанию
547
- выключены `menu` и `print`, остальные включены. `ui={false}` скрывает все опциональные
548
- элементы, но не отключает редактирование. Для просмотра задайте `mode="view"`.
549
-
550
- ```tsx
551
- import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
552
-
553
- export function MinimalEditor() {
554
- return <AzDocumentEditor ui={false} style={{ height: 600 }} />;
555
- }
556
-
557
- export function Viewer() {
558
- return <AzDocumentEditor mode="view" ui={{ menu: false, rulers: false }} style={{ height: 600 }} />;
559
- }
560
- ```
561
-
562
- `chrome: false` оставляет поверхность движка без его рамки, в том числе встроенных
563
- линеек и навигации. Для отдельных переключателей сохраняйте `chrome: true`.
564
- UI и режим можно менять без замены документа.
565
-
566
- ### Действия приложения
567
-
568
- `onOpen`, `onSave`, `onExportPdf` добавляют пункты меню «Файл». Без обработчика пункт
569
- отсутствует. `labels` меняет подписи, `fileActions` добавляет дополнительные действия:
570
-
571
- ```tsx
572
- import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
573
-
574
- export function EditorWithActions({ onHistory }: { onHistory: () => void }) {
575
- return <AzDocumentEditor style={{ height: 700 }}
576
- fileActions={[{ id: 'history', label: 'История версий', onSelect: onHistory }]}
577
- labels={{ open: 'Загрузить файл', save: 'Сохранить', exportPdf: 'Получить PDF', print: 'Печать' }} />;
578
- }
579
- ```
580
-
581
- Ctrl/Cmd+S/O/P обрабатываются адаптером внутри активного редактора, не во всём
582
- приложении. Скрытое меню само по себе не отключает файловые сочетания клавиш.
583
- Кнопкам своего UI внутри `<form>` задавайте `type="button"`.
584
-
585
- ### Масштаб, тема и язык
586
-
587
- - `zoom={1}` — 100%; `zoom={1.25}` — 125%. `zoomMode={{ type: 'fixed' }}` закрепляет режим.
588
- - `zoomMode="auto"` — автоматическая подгонка. Расширенный объект описан типом `AzDocumentEditorProps['zoomMode']`.
589
- - `colorMode`: `light` (по умолчанию), `dark`, `system`; меняется UI, не лист документа.
590
- - `locale`, по умолчанию `ru-RU`, задаёт региональные правила дат и подписей, не язык меню.
591
- - `i18n` принимает каталог переводов движка. По умолчанию — частичный русский каталог
592
- с запасными английскими строками. Диалог локальной печати использует русский.
593
-
594
- Собственный каталог проверяйте типом `AzDocumentEditorProps['i18n']`, например
595
- для полей `toolbar.open` и `toolbar.save`. `renderTitleBarLeft` и
596
- `renderTitleBarRight` вставляют элементы приложения в заголовок; при скрытом `chrome`
597
- заголовок не отображается.
598
-
599
- Передайте `className="my-editor"` и ограничивайте свои стили этим классом:
600
-
601
- ```css
602
- .my-editor .docx-editor { --doc-primary: #2563eb; }
603
- ```
604
-
605
- Глобальные правила для `table`, `p`, `button`, `canvas` могут изменить документ и UI.
606
- CSS-переменные движка проверяйте при обновлении версии.
607
-
608
- ## 10. Шрифты
609
-
610
- По умолчанию подключены поставляемые с движком замены распространённых шрифтов Word.
611
- Для собственного набора используйте `fonts`. Один CSS `@font-face` не передаёт
612
- движку байты для расчёта строк: требуется конфигурация источников.
613
-
614
- Скопируйте [loadFonts.ts](../examples/integration/loadFonts.ts). Помощник загружает
615
- TTF/OTF и вычисляет SHA-256. Пример набора:
616
-
617
- ```ts
618
- import { loadEditorFonts } from './loadFonts';
619
-
620
- export function loadTemplateFonts(signal?: AbortSignal) {
621
- return loadEditorFonts([
622
- { url: '/fonts/FreeSerif.ttf', family: 'FreeSerif', weight: 400, style: 'normal' },
623
- { url: '/fonts/FreeSerifBold.ttf', family: 'FreeSerif', weight: 700, style: 'normal' },
624
- { url: '/fonts/FreeSerifItalic.ttf', family: 'FreeSerif', weight: 400, style: 'italic' },
625
- { url: '/fonts/FreeSerifBoldItalic.ttf', family: 'FreeSerif', weight: 700, style: 'italic' },
626
- ], signal);
627
- }
628
- ```
629
-
630
- URL — пример: разместите реальные разрешённые к распространению файлы. Имена и
631
- начертания должны соответствовать их метаданным. Для TTC пример с `faceIndex: 0`
632
- нужно адаптировать. `crypto.subtle` требует HTTPS или поддерживаемый браузером localhost.
633
-
634
- Дождитесь загрузки, сохраните результат в state и смонтируйте редактор с
635
- `fonts={loadedFonts}`. Сохраняйте объект стабильным всю сессию. Замена конфигурации
636
- может пересоздать редактор; предварительно сохраните или отмените правки.
637
- Пользовательский набор заменяет переданную конфигурацию: предоставьте все нужные
638
- семейства/начертания, не только regular. Показывайте ошибки `onFontError` пользователю.
639
-
640
- HTML-экспорт нормализует семейства в `freeserif`, `freesans`, `freemono`. Имена в HTML
641
- сами не загружают файлы. Для близкого оформления используйте одинаковые файлы,
642
- начертания, размер страницы и поля на клиенте и сервере. Даже так Word, браузерный
643
- движок и mPDF могут по-разному разбивать строки и страницы. Проверяйте итоговый PDF.
644
-
645
- ## 11. Next.js и SSR
646
-
647
- Редактор использует DOM. В App Router одной директивы `'use client'` недостаточно
648
- для отключения предварительного серверного рендера. Используйте `next/dynamic`
649
- с `ssr: false` из клиентского компонента.
650
- [Документация Next.js](https://nextjs.org/docs/app/guides/lazy-loading#skipping-ssr).
651
-
652
- После копирования `DocxEditorExample.tsx` создайте `EditorClient.tsx`:
653
-
654
- ```tsx
655
- 'use client';
656
-
657
- import dynamic from 'next/dynamic';
658
- import '@makhmudov.kay/az-editor/styles.css';
659
-
660
- const Editor = dynamic(
661
- () => import('./DocxEditorExample').then(module => module.DocxEditorExample),
662
- { ssr: false, loading: () => <p role="status">Загрузка редактора…</p> },
663
- );
664
-
665
- export default function EditorClient() {
666
- return <Editor />;
667
- }
668
- ```
669
-
670
- В файле страницы:
671
-
672
- ```tsx
673
- import EditorClient from './EditorClient';
674
-
675
- export default function Page() {
676
- return <EditorClient />;
677
- }
678
- ```
679
-
680
- Импорт CSS можно оставить в одном общем клиентском модуле. Next.js поддерживает
681
- [CSS сторонних компонентов](https://nextjs.org/docs/app/getting-started/css#external-stylesheets).
682
- Не выполняйте библиотечную HTML/DOCX-конвертацию в Server Component: она также
683
- использует DOM. DOCX удобнее загружать после монтирования клиентского компонента.
684
-
685
- Раздел описывает схему клиентского подключения. Сборку конкретного Next.js-приложения,
686
- его Turbopack/Webpack и загрузку WASM/шрифтов проверяйте отдельно. В поставляемом
687
- примере потребителя проверяется Vite.
688
-
689
- ## 12. Справочник API
690
-
691
- ### `AzDocumentEditorProps`
692
-
693
- | Поле | Тип / значение по умолчанию | Назначение |
694
- | --- | --- | --- |
695
- | `document` | `Uint8Array \| ArrayBuffer \| 'blank'`, `'blank'` | Исходный DOCX |
696
- | `title` | `string`, `'Документ.docx'` | Заголовок |
697
- | `className`, `style` | `string`, `CSSProperties` | Контейнер и размеры |
698
- | `ui` | `false \| EditorUI` | Видимость интерфейса |
699
- | `mode` | `'edit' \| 'view'`, `'edit'` | Редактирование/просмотр |
700
- | `locale` | `string`, `'ru-RU'` | Региональные правила |
701
- | `i18n` | `AzDocumentEditorProps['i18n']` | Каталог переводов |
702
- | `colorMode` | `'light' \| 'dark' \| 'system'`, `'light'` | Тема |
703
- | `zoom`, `zoomMode` | Типы движка | Масштаб и подгонка |
704
- | `fonts` | `AzDocumentEditorProps['fonts']` | Источники шрифтов |
705
- | `fileActions` | `readonly EditorFileAction[]` | `{ id, label, onSelect, disabled? }` |
706
- | `actionsDisabled` | `boolean` | Блокировка файловых действий |
707
- | `labels` | `{ open?, save?, exportPdf?, print? }` | Подписи действий |
708
- | `renderTitleBarLeft`, `renderTitleBarRight` | Функции рендера движка | Элементы заголовка |
709
- | `onOpen`, `onSave`, `onExportPdf` | `() => void` | Действия приложения |
710
- | `onChange` | `() => void` | Правка, без исходной загрузки |
711
- | `onStatusChange` | `(status: EditorStatus) => void` | `{ loading, pages, error }` |
712
- | `onFontError` | `(message: string) => void` | Ошибка шрифтов |
713
-
714
- `EditorStatus.error` — `string | null`. Ошибки собственных асинхронных действий
715
- приложение обрабатывает отдельно, не полагаясь на `onStatusChange`.
716
-
717
- ### `AzDocumentEditorHandle`
718
-
719
- | Метод | Результат |
720
- | --- | --- |
721
- | `save()` | `Promise<ArrayBuffer>` с текущим DOCX |
722
- | `exportHujjat({ locale, locales?, allowLossy? })` | `Promise<{ html, variables, warnings, locales }>` |
723
- | `insertVariable(key)` | Вставляет `{key}`, может выбросить ошибку |
724
- | `focus()` | Фокус в редакторе |
725
- | `print()` | `Promise<void>`, локальный предпросмотр/печать |
726
-
727
- ### `AzHtmlEditorProps` и ref
728
-
729
- Наследует общие props DOCX-компонента, кроме `document`, `onChange`, `onStatusChange`.
730
-
731
- | Поле | Назначение |
732
- | --- | --- |
733
- | `initialHtml: string` | Один фрагмент на сессию |
734
- | `onChange?: () => void` | Сигнал о правке, без HTML |
735
- | `onStatusChange?: (status: AzHtmlEditorStatus) => void` | Состояние сессии |
736
- | `renderFallback?: (status) => ReactNode` | UI загрузки/ошибки/блокировки до открытия документа |
737
-
738
- `AzHtmlEditorHandle` предоставляет `exportHtml({ allowLossy? })`, `acceptImportLoss()`,
739
- `insertVariable(key)`, `focus()`. Методов `save()`/`print()` на этом ref нет.
740
- Наследуемые `onSave`/`onExportPdf` нужно связать с HTML-экспортом и своим серверным процессом.
741
-
742
- `AzHtmlEditorStatus`: `phase`, `dirty`, `pages`, `error: string | null`, `warnings`.
743
-
744
- | Phase | Действие |
745
- | --- | --- |
746
- | `loading` | Дождаться импорта/открытия |
747
- | `blocked` | Показать потери, отменить или вызвать `acceptImportLoss()` |
748
- | `ready` | Редактирование, вставка переменных, экспорт |
749
- | `exporting` | Дождаться результата; документ временно доступен для просмотра |
750
- | `error` | Показать ошибку, открыть исправленный источник новой сессией |
751
-
752
- `acceptImportLoss()` допустим только в `blocked`. `exportHtml()` возвращает
753
- `Promise<AzHtmlExportResult>` и может отклониться, не переводя весь редактор в `error`.
754
- Перехватывайте исключения через `try/catch`.
755
-
756
- ### Файловые и PDF-помощники
757
-
758
- | Экспорт | Контракт |
759
- | --- | --- |
760
- | `readDocxFile(file)` | `Promise<OpenedDocument>`; принимает `name`, `size`, `arrayBuffer()` |
761
- | `OpenedDocument` | `{ name, original: Uint8Array, metadata }` |
762
- | `DocumentMetadata` | `paragraphs`, `tables`, `sections`, `savedPageCount`, `hasHeaders`, `hasFooters`, `hasReviewContent`, `fonts` |
763
- | `downloadDocx(bytes, filename)` | `void`; `ArrayBuffer` или `Uint8Array<ArrayBuffer>` |
764
- | `exportFilename(name)` | Очищает имя, добавляет суффикс ` — редакция.docx` |
765
- | `DOCX_MIME`, `MAX_DOCX_BYTES` | MIME DOCX и лимит 30 МиБ |
766
- | `requestPdf(bytes, filename, renderProfile, options?)` | `Promise<PdfExportResult>` |
767
- | `PdfExportResult` | `{ blob: Blob, filename: string, warnings: string[] }` |
768
- | `downloadPdf(result)` | `void`, скачивает `PdfExportResult` |
769
- | `pdfRenderProfile` | Readonly-профиль с `id` |
770
-
771
- ### Ограниченный HTML/JSON
772
-
773
- | Экспорт | Контракт |
774
- | --- | --- |
775
- | `sanitizeHujjatHtml(html)` | `{ html, variables, warnings }`; требуется DOM |
776
- | `readHujjatHtml(locales, locale)` | Чтение HTML-only локали |
777
- | `withHujjatLocale(locales, locale, html)` | Очистка HTML, замена всей выбранной локали |
778
- | `importHujjatTemplate(locales, locale, options?)` | `Promise<{ document, html, variables, warnings }>`; `options: { allowLossy? }` |
779
- | `exportHujjatDocx(bytes, options)` | DOCX → HTML/JSON без React-компонента; требуется браузерный DOM |
780
- | `requestHujjatPreview(options)` | JSON → серверный PDF, раздел 8 |
781
- | `createHujjatTemplatePayload(metadata, locales)` | Формирует и валидирует payload, без HTTP |
782
- | `validateHujjatVariable(key)` | `boolean` |
783
- | `HujjatCompatibilityError` | `Error` с `warnings` |
784
- | `HUJJAT_LOCALES`, `HUJJAT_MAX_HTML_BYTES`, `HUJJAT_PAGE` | Локали, лимит HTML, размер/поля страницы |
785
-
786
- Типы из корня пакета: `HujjatLocale`, `HujjatLocales`, `HujjatBlock`, `HujjatWarning`,
787
- `HujjatHtmlResult`, `HujjatExportOptions`, `HujjatExportResult`, `HujjatImportResult`,
788
- `HujjatPreviewOptions`, `HujjatTemplateMetadata`, `PdfExportOptions`, `PdfExportResult`,
789
- `AzHtmlExportResult` и типы компонентов из таблиц. Используйте `import type`, не
790
- импорты через внутренние пути `dist/...`.
791
-
792
- ## 13. Ограничения
793
-
794
- ### Поддерживаемый HTML
795
-
796
- ```text
797
- div p span br hr b strong i em u s sub sup
798
- h1 h2 h3 h4 h5 h6 ul ol li
799
- table thead tbody tfoot tr td th
800
- ```
801
-
802
- Сохраняются разрешённые inline-стили и `colspan`/`rowspan` ячеек в пределах 1–100.
803
- Произвольные `class`, `id`, обработчики событий и `data-*` в контракт не входят.
804
- `a` превращается в текст: ссылка как ссылка не сохраняется. `img`, `svg`, `script`,
805
- `style`, `iframe`, формы и медиа удаляются с диагностикой потери.
806
-
807
- CSS ограничен шрифтом/цветом, базовым оформлением, выравниванием, интервалами,
808
- размерами, полями, отступами, рамками, таблицами и `page-break-*`. `url(...)`,
809
- `expression`, `!important`, CSS-переменные, `position`, flex/grid и внешние стили
810
- не поддерживаются. Даже прошедший очистку CSS может преобразоваться приближённо
811
- при переходе HTML → документ → HTML.
812
-
813
- | Содержимое | В HTML-режиме / при DOCX → HTML |
814
- | --- | --- |
815
- | Текст, абзацы, базовое оформление | Поддерживаются с нормализацией |
816
- | Таблицы, обычные границы/заливки/объединения | Поддерживаются; сложные случаи проверяйте отдельно |
817
- | Простой bullet/decimal список | Поддерживается |
818
- | Многоуровневая нумерация, нестандартный старт/продолжение | Возможны потери; атрибут `start` удаляется |
819
- | Явный разрыв страницы | Передаётся, автоматическая пагинация рассчитывается заново |
820
- | Картинки, фигуры, текстовые блоки | Не входят в HTML-контракт |
821
- | Колонтитулы, сноски, комментарии, исправления, поля | Не сохраняются как интерактивные HTML-объекты |
822
- | Скрытый текст | Не раскрывается при экспорте; удаление требует принятия потерь |
823
-
824
- Это ограничения HTML-адаптера, а не перечень возможностей DOCX-режима. Возможности
825
- конкретного DOCX проверяются отдельно. Наличие кнопки форматирования не гарантирует
826
- сохранение её результата в ограниченном HTML.
827
-
828
- ### Лимиты и оформление
829
-
830
- - DOCX до 30 МиБ; DOC, DOCM, PDF и защищённые паролем файлы не являются входом.
831
- - DOCX-проверка/преобразование ограничивают число частей 2000 и читаемые XML-части 8 МиБ.
832
- - HTML до 1 048 576 байт UTF-8 на фрагмент; это байты, не `string.length`.
833
- - Очистка HTML ограничивает глубину 100 и число обходящихся узлов 50 000.
834
- - При преобразовании дополнительно проверяются сложность таблиц и размер создаваемого XML.
835
- - Документ из HTML: A4 210×297 мм; поля сверху 15, справа/слева 20, снизу 35 мм.
836
- - Исходный DOCX сохраняет собственные параметры до преобразования.
837
- - Лимиты вашего HTTP-сервера задаются отдельно и могут быть меньше клиентских.
838
-
839
- Совпадение с Microsoft Word один в один не гарантируется. Для документов с критичными
840
- переносами, подписями, таблицами и нумерацией сравнивайте исходник и конечный PDF
841
- на реальном корпусе документов с production-шрифтами и серверным рендерером.
842
-
843
- ## 14. Ошибки и проверка интеграции
844
-
845
- | Симптом | Что проверить |
846
- | --- | --- |
847
- | Пустой редактор / нулевая высота | CSS импортирован, высота контейнера задана, у flex-родителей есть `min-height: 0` |
848
- | `window/document is not defined` | SSR отключён; конвертация выполняется в браузере |
849
- | Шрифты/WASM не загружаются | Network, URL ресурсов, MIME, CORS, CSP, `base` сборщика, `onFontError` |
850
- | Правки пропадают при рендере | `key`, объект `document` или `fonts` не меняются без необходимости |
851
- | Новый `initialHtml` не появился | Открыта новая сессия с новым `key` после решения о правках |
852
- | `onChange` не возвращает HTML | Используется `await exportHtml()` |
853
- | Экспорт недоступен | Достигнут `ready`, нет повторного запроса/размонтирования, ошибка обработана |
854
- | `phase: blocked` | Показаны `warnings`, принято решение об импорте |
855
- | PDF-кнопки нет | Передан `onExportPdf`, меню включено |
856
- | Сервер возвращает HTML/JSON вместо PDF | Маршрут, авторизация, прокси, формат ответа |
857
- | HTML после сохранения отличается | Диагностика преобразования и серверная нормализация |
858
- | Исчезли соседние блоки | Для одного блока не используется замена всей локали |
859
- | Переносы отличаются | Шрифты, начертания, поля, размер страницы, возможности рендерера |
860
- | Ошибка React hooks | React 19, один экземпляр React |
861
-
862
- Перед выпуском приложения проверьте:
863
-
864
- 1. Открытие и правки DOCX, скачивание и повторное открытие результата.
865
- 2. Сохранность текущего документа при открытии повреждённого файла.
866
- 3. HTML без правок, с правками, с отменой и с подтверждением потерь.
867
- 4. Ошибку API: правки должны остаться доступными для повторной попытки.
868
- 5. Смену документа/языка, закрытие окна и переход по маршруту с правками.
869
- 6. Сохранность других языков, блоков и метаданных JSON.
870
- 7. PDF текущих правок на реальном сервере с реальными шрифтами.
871
- 8. Production-сборку и загрузку всех ресурсов после деплоя.
872
-
873
- В исходниках библиотеки доступны `npm run typecheck`, `npm test`, `npm run build`,
874
- `npm run test:e2e`. `npm pack` включает это руководство и примеры. Проверяйте
875
- подключение из архива или npm, чтобы не зависеть от локальных aliases репозитория.
1
+ # Интеграция Az Editor в React-приложение
2
+
3
+ Руководство описывает публичный API `@makhmudov.kay/az-editor` версии `0.1.0-beta.3`.
4
+ Примеры написаны на TypeScript. Для JavaScript удалите аннотации типов и `import type`.
5
+
6
+ ## Содержание
7
+
8
+ 1. [Выбор компонента](#1-выбор-компонента)
9
+ 2. [Установка и сборщик](#2-установка-и-сборщик)
10
+ 3. [Открытие и сохранение DOCX](#3-открытие-и-сохранение-docx)
11
+ 4. [HTML: черновик, применение и отмена](#4-html-черновик-применение-и-отмена)
12
+ 5. [Диагностика преобразования](#5-диагностика-преобразования)
13
+ 6. [Переменные](#6-переменные)
14
+ 7. [JSON, языки и блоки](#7-json-языки-и-блоки)
15
+ 8. [PDF и печать](#8-pdf-и-печать)
16
+ 9. [Интерфейс и локализация](#9-интерфейс-и-локализация)
17
+ 10. [Шрифты](#10-шрифты)
18
+ 11. [Next.js и SSR](#11-nextjs-и-ssr)
19
+ 12. [Справочник API](#12-справочник-api)
20
+ 13. [Ограничения](#13-ограничения)
21
+ 14. [Ошибки и проверка интеграции](#14-ошибки-и-проверка-интеграции)
22
+
23
+ ## 1. Выбор компонента
24
+
25
+ | Задача | Компонент | Результат |
26
+ | --- | --- | --- |
27
+ | DOCX → DOCX | `AzDocumentEditor` | `await ref.current.save()` |
28
+ | HTML-фрагмент → HTML-фрагмент | `AzHtmlEditor` | `await ref.current.exportHtml()` |
29
+ | DOCX → ограниченный HTML/JSON | `AzDocumentEditor` | `await ref.current.exportHujjat(options)` |
30
+
31
+ `AzDocumentEditor` редактирует документ Word в памяти браузера. Для нового документа
32
+ передайте `document="blank"` или не задавайте `document`.
33
+
34
+ `AzHtmlEditor` принимает один HTML-фрагмент, создаёт внутренний документ для
35
+ редактирования и возвращает HTML по запросу приложения. Это отдельный черновик:
36
+ редактор не заменяет значение формы при каждом нажатии клавиши.
37
+
38
+ Библиотека предоставляет поверхность документа, форматирование, импорт, экспорт
39
+ и диагностику. Приложение отвечает за выбор файлов, хранение, авторизацию, версии,
40
+ маршрутизацию, модальные окна и сообщения пользователю. PDF-сервис подключается
41
+ отдельно. HTTP-помощники не вызываются автоматически при монтировании компонента;
42
+ шрифты и ресурсы движка могут загружаться браузером.
43
+
44
+ Если уже используется другой редактор, храните HTML в общем состоянии формы.
45
+ Открывайте Az Editor с копией значения. После «Применить» заменяйте HTML формы;
46
+ после «Отмена» оставляйте прежнее значение. Пока открыт черновик, блокируйте правки
47
+ той же версии в другом редакторе или разрешайте конфликты явно.
48
+
49
+ ## 2. Установка и сборщик
50
+
51
+ ### Требования
52
+
53
+ - React `^19.0.0` и React DOM `^19.0.0`. React 18 не входит в peer-контракт этой версии.
54
+ - Node.js `^24.15.0 || >=26.0.0` для установки и сборки. Node 25 не входит в этот диапазон.
55
+ - Современный браузер с DOM, WebAssembly, File/Blob, `fetch`, `AbortSignal.any` и `AbortSignal.timeout`.
56
+ - Сборщик ESM, обрабатывающий CSS, шрифты и WASM. Проверенная конфигурация — Vite.
57
+ - Контейнер с явно заданной высотой.
58
+
59
+ В существующем приложении:
60
+
61
+ ```sh
62
+ npm install @makhmudov.kay/az-editor@beta
63
+ ```
64
+
65
+ Для фиксации опубликованной версии:
66
+
67
+ ```sh
68
+ npm install --save-exact @makhmudov.kay/az-editor@0.1.0-beta.3
69
+ ```
70
+
71
+ Тег `beta` может указывать на более новую версию. Сохраняйте lockfile и проверяйте
72
+ обновления на своих документах. Вход в npm для установки публичного пакета не нужен.
73
+ React должен быть одним общим экземпляром приложения и библиотеки.
74
+
75
+ ### Новое приложение React + Vite
76
+
77
+ ```sh
78
+ npm create vite@latest editor-example -- --template react-ts
79
+ cd editor-example
80
+ npm install
81
+ npm install @makhmudov.kay/az-editor@beta
82
+ ```
83
+
84
+ Проверьте, что созданное приложение использует React 19. В `vite.config.ts`:
85
+
86
+ ```ts
87
+ import { defineConfig } from 'vite';
88
+ import react from '@vitejs/plugin-react';
89
+
90
+ export default defineConfig({
91
+ plugins: [react()],
92
+ optimizeDeps: { exclude: ['@docx-editor.dev/fonts'] },
93
+ });
94
+ ```
95
+
96
+ Исключение пакета шрифтов из предварительной сборки используется в конфигурации
97
+ Az Editor. Если `optimizeDeps` уже задан, объедините списки, сохранив настройки
98
+ приложения. [Описание `optimizeDeps.exclude`](https://vite.dev/config/dep-optimization-options#optimizedeps-exclude).
99
+
100
+ Один раз подключите CSS, например в клиентском `src/main.tsx`:
101
+
102
+ ```ts
103
+ import '@makhmudov.kay/az-editor/styles.css';
104
+ ```
105
+
106
+ Собственные переопределения импортируйте после CSS библиотеки. Стили демонстрационного
107
+ приложения не нужны. Проверьте загрузку файлов шрифтов и WASM в production, включая
108
+ размещение приложения в подкаталоге.
109
+
110
+ ### Первое подключение
111
+
112
+ ```tsx
113
+ import { useState } from 'react';
114
+ import { AzDocumentEditor, type EditorStatus } from '@makhmudov.kay/az-editor';
115
+ import '@makhmudov.kay/az-editor/styles.css';
116
+
117
+ export function DocumentView() {
118
+ const [status, setStatus] = useState<EditorStatus>({ loading: true, pages: 0, error: null });
119
+ return <section>
120
+ <p role="status">{status.loading ? 'Загрузка…' : `Страниц: ${status.pages}`}</p>
121
+ {status.error && <p role="alert">{status.error}</p>}
122
+ <AzDocumentEditor document="blank" title="Новый документ.docx"
123
+ style={{ height: 700 }} onStatusChange={setStatus} />
124
+ </section>;
125
+ }
126
+ ```
127
+
128
+ ## 3. Открытие и сохранение DOCX
129
+
130
+ ### Полный пример
131
+
132
+ Скопируйте [DocxEditorExample.tsx](../examples/integration/DocxEditorExample.tsx).
133
+ Он содержит выбор файла, валидацию, скачивание, ошибки, блокировку повторных
134
+ операций и предупреждение о несохранённых правках:
135
+
136
+ ```tsx
137
+ import { DocxEditorExample } from './DocxEditorExample';
138
+
139
+ export default function App() {
140
+ return <DocxEditorExample />;
141
+ }
142
+ ```
143
+
144
+ Примеры входят в npm-архив как исходники для копирования. Путь `examples/...` не
145
+ является экспортом пакета. После копирования используйте относительные импорты.
146
+
147
+ ### Открытие файла
148
+
149
+ Для локального файла используйте `<input type="file" accept=".docx">`, затем
150
+ `const opened = await readDocxFile(file)`. Передайте `opened.original` в `document`.
151
+ Помощник проверяет имя, размер и структуру DOCX до замены текущего документа.
152
+
153
+ Храните байты в state или стабильном объекте. Не создавайте `new Uint8Array(...)`
154
+ на каждом рендере: смена идентичности `document` может пересоздать внутренний редактор.
155
+ При открытии другого документа меняйте React `key` сессии, предварительно получив
156
+ решение пользователя о несохранённых правках. Не меняйте `key` на каждом `onChange`.
157
+
158
+ Для серверного маршрута, который возвращает бинарный DOCX:
159
+
160
+ ```ts
161
+ import { DOCX_MIME, readDocxFile } from '@makhmudov.kay/az-editor';
162
+
163
+ export async function loadDocx(url: string, filename: string, signal?: AbortSignal) {
164
+ const response = await fetch(url, { credentials: 'same-origin', signal });
165
+ if (!response.ok) throw new Error(`Не удалось открыть документ: HTTP ${response.status}`);
166
+ const bytes = await response.arrayBuffer();
167
+ return readDocxFile(new File([bytes], filename, { type: DOCX_MIME }));
168
+ }
169
+ ```
170
+
171
+ `filename` должен оканчиваться на `.docx`. Авторизацию и CSRF подключите через свой
172
+ HTTP-клиент. Отменяйте загрузку при уходе со страницы; не применяйте старый ответ
173
+ поверх новой сессии. Ограничьте размер ответа на сервере/шлюзе: этот пример загружает
174
+ байты до проверки размера через `readDocxFile`.
175
+
176
+ `opened.metadata` содержит число абзацев/таблиц/секций, список шрифтов и признаки
177
+ колонтитулов/рецензирования. `savedPageCount` — число страниц из метаданных исходного
178
+ файла; оно может быть устаревшим. Текущее число страниц берите из `onStatusChange`.
179
+
180
+ ### Скачивание и сохранение на сервере
181
+
182
+ Создайте `useRef<AzDocumentEditorHandle>(null)` и передайте его в компонент.
183
+ `save()` возвращает текущий DOCX как `Promise<ArrayBuffer>`:
184
+
185
+ ```ts
186
+ import { downloadDocx, type AzDocumentEditorHandle } from '@makhmudov.kay/az-editor';
187
+
188
+ export async function saveToDisk(editor: AzDocumentEditorHandle, filename: string) {
189
+ const bytes = await editor.save();
190
+ downloadDocx(bytes, filename);
191
+ }
192
+ ```
193
+
194
+ `onSave` вызывается через меню и Ctrl/Cmd+S. Передавайте обработчик, который
195
+ перехватывает ошибки асинхронной операции. До готовности и на время сохранения
196
+ используйте `actionsDisabled`; чтобы блокировать и сам текст, задайте `mode="view"`.
197
+ `actionsDisabled` сам по себе не делает документ read-only.
198
+
199
+ Для своего сервера скопируйте [server-api.ts](../examples/integration/server-api.ts):
200
+
201
+ ```ts
202
+ import type { AzDocumentEditorHandle } from '@makhmudov.kay/az-editor';
203
+ import { saveDocxToApi } from './server-api';
204
+
205
+ export async function saveToServer(editor: AzDocumentEditorHandle, id: string, signal?: AbortSignal) {
206
+ const bytes = await editor.save();
207
+ await saveDocxToApi(`/api/documents/${encodeURIComponent(id)}/file`, bytes, 'document.docx', signal);
208
+ }
209
+ ```
210
+
211
+ Маршрут в этом примере реализует приложение: `POST multipart/form-data`, поле `file`,
212
+ успешный ответ `2xx`. Библиотека не создаёт endpoint. Не задавайте `Content-Type`
213
+ для `FormData` вручную: браузер формирует multipart boundary.
214
+
215
+ Сбрасывайте признак несохранённых правок после успешного ответа API. Если правки
216
+ разрешены во время запроса, сравните локальную ревизию до запроса и после ответа,
217
+ чтобы не пометить новые изменения сохранёнными. В DOCX-примере показан такой `revision`.
218
+ `downloadDocx` лишь инициирует скачивание: браузер не подтверждает запись файла на диск.
219
+ Для автосохранения приложение реализует очередь и контроль серверных версий.
220
+
221
+ ## 4. HTML: черновик, применение и отмена
222
+
223
+ ```text
224
+ HTML формы → initialHtml → сессия AzHtmlEditor → exportHtml()
225
+ ↑ ↓
226
+ └──── применение после проверки ← HTML + предупреждения
227
+ ```
228
+
229
+ `initialHtml` читается один раз при монтировании. Новый prop в существующей сессии
230
+ не загружает другой документ. Меняйте React `key` после решения о несохранённых
231
+ правках. Нет props `value`/`onChange(html)` и синхронного `getContent()`.
232
+
233
+ `onChange()` только сообщает о правке. Получайте HTML при применении, сохранении
234
+ или подготовке PDF через `await ref.current.exportHtml()`, не на каждую клавишу.
235
+
236
+ ### Полный пример формы
237
+
238
+ Скопируйте [HtmlFormExample.tsx](../examples/integration/HtmlFormExample.tsx) и
239
+ [HtmlDraft.tsx](../examples/integration/HtmlDraft.tsx) в одну папку:
240
+
241
+ ```tsx
242
+ import { HtmlFormExample } from './HtmlFormExample';
243
+
244
+ export default function App() {
245
+ return <HtmlFormExample />;
246
+ }
247
+ ```
248
+
249
+ `textarea` в примере представляет HTML формы; его можно заменить другим редактором.
250
+ `HtmlDraft` получает `initialHtml`, `variables`, `onApply(html)` и `onCancel()`.
251
+ Родитель размонтирует сессию после применения или отмены.
252
+
253
+ При предупреждениях результат хранится в `pending` до подтверждения, редактирование
254
+ временно блокируется. Так пользователь подтверждает именно тот снимок, который
255
+ затем попадёт в форму. Применение без правок закрывает черновик без замены HTML.
256
+
257
+ ### Результат
258
+
259
+ ```ts
260
+ type HtmlResult = {
261
+ html: string;
262
+ variables: string[];
263
+ warnings: { code: string; message: string; severity: 'warning' | 'loss' }[];
264
+ changed: boolean;
265
+ };
266
+ ```
267
+
268
+ - Без правок возвращается исходный `initialHtml`, `changed: false`. Повторной очистки строки нет.
269
+ - После правок возвращается преобразованный HTML, `changed: true`.
270
+ - `dirty`/`changed` отражают факт правок, а не сравнение строк; undo до исходного вида не гарантирует `dirty: false`.
271
+ - Экспорт не сбрасывает `dirty` и не подтверждает запись на сервере.
272
+ - Повторный одновременный экспорт, экспорт до готовности, после закрытия или из изменившегося во время операции черновика отклоняется.
273
+
274
+ ### Сохранение и закрытие
275
+
276
+ `saveHtmlToApi` из [server-api.ts](../examples/integration/server-api.ts) задаёт пример
277
+ контракта `PUT { html }` → `200 { html }`. Передавайте уже применённое значение формы.
278
+ При ошибке сохранения оставляйте его для повторной попытки. `onApply` сам по себе
279
+ не означает, что данные записаны на сервер.
280
+
281
+ После успешного ответа используйте возвращённый сервером HTML как новую исходную
282
+ версию: сервер мог его нормализовать. Закройте черновик или перемонтируйте с новым `key`.
283
+
284
+ Закрытие модального окна по Esc/подложке, переключение языка и переход по маршруту
285
+ должны проходить через решение о применении/отмене. Размонтирование не запускает
286
+ автоматический экспорт. `beforeunload` защищает уход из вкладки только в пределах
287
+ возможностей браузера; для SPA-переходов нужен механизм вашего роутера.
288
+
289
+ ## 5. Диагностика преобразования
290
+
291
+ | Severity | Значение | Действие |
292
+ | --- | --- | --- |
293
+ | `warning` | Нормализация/приближение оформления, например замена шрифта | Показать диагностику |
294
+ | `loss` | Неподдерживаемое содержимое или существенная потеря | По умолчанию операция отклоняется |
295
+
296
+ Импорт с потерями переводит HTML-редактор в `phase: 'blocked'`. Покажите `warnings`;
297
+ только по решению пользователя вызовите `acceptImportLoss()`. Это разрешает открыть
298
+ черновик, но не разрешает все будущие сохранения с потерями.
299
+
300
+ При экспорте `HujjatCompatibilityError` содержит `warnings`. Простой помощник:
301
+
302
+ ```ts
303
+ import { HujjatCompatibilityError, type AzHtmlEditorHandle } from '@makhmudov.kay/az-editor';
304
+
305
+ export async function exportWithDecision(editor: AzHtmlEditorHandle) {
306
+ try {
307
+ return await editor.exportHtml();
308
+ } catch (error) {
309
+ if (!(error instanceof HujjatCompatibilityError) ||
310
+ !error.warnings.some(warning => warning.severity === 'loss')) throw error;
311
+ const accepted = window.confirm(error.warnings.map(warning => warning.message).join('\n'));
312
+ if (!accepted) return null;
313
+ return editor.exportHtml({ allowLossy: true });
314
+ }
315
+ }
316
+ ```
317
+
318
+ Для асинхронного диалога блокируйте редактирование на время подтверждения: решение
319
+ не должно относиться к уже устаревшему результату. Полный пример `HtmlDraft`
320
+ подготавливает снимок с `allowLossy: true`, но не применяет его до подтверждения.
321
+ Не включайте этот флаг безусловно в сохранении формы.
322
+
323
+ `sanitizeHujjatHtml` и `withHujjatLocale` возвращают диагностику, но сами не блокируют
324
+ потери. Проверяйте `warnings`, включая сообщения о некорректных переменных.
325
+
326
+ HTML в ветке «без правок» остаётся исходным и может содержать неподдерживаемые
327
+ конструкции. Редактор и `changed: false` не заменяют серверную очистку/валидацию.
328
+ Произвольный HTML в примерах показан в `textarea`, не через `dangerouslySetInnerHTML`.
329
+
330
+ ## 6. Переменные
331
+
332
+ Оба редактора поддерживают `ref.current.insertVariable('client.name')`. Передавайте
333
+ ключ без фигурных скобок: вставится `{client.name}`. Это обычный редактируемый текст,
334
+ не защищённый элемент. Подстановка значений выполняется вашим сервером.
335
+
336
+ Для внешней кнопки предотвращайте смену фокуса при `mousedown`, чтобы сохранить
337
+ позицию вставки. Готовый селектор есть в `HtmlDraft`.
338
+
339
+ `validateHujjatVariable(key)` проверяет ASCII-буквы, цифры, `_` и точки между
340
+ непустыми сегментами; длина до 100 символов. Сегменты `__proto__`, `prototype`,
341
+ `constructor` запрещены. Ключ `client.name` допустим, `{client.name}` — нет.
342
+
343
+ `exportHtml().variables` содержит найденные ключи без скобок; сверяйте их со своим
344
+ каталогом и валидатором. Для JSON PDF-помощника данные вложенные:
345
+ `{ client: { name: 'Иван' } }`. Сервер экранирует подставляемые значения.
346
+
347
+ ## 7. JSON, языки и блоки
348
+
349
+ `AzHtmlEditor` не требует определённой структуры JSON. Передайте нужную строку,
350
+ после применения замените только её. Остальные языки, блоки и метаданные сохраняйте
351
+ в состоянии приложения независимо.
352
+
353
+ ### Встроенный формат
354
+
355
+ Помощники с префиксом `Hujjat` — публичные имена API для ограниченного контракта:
356
+
357
+ ```json
358
+ {
359
+ "uz": { "blocks": [{ "type": "html", "props": { "html": "<p>Shartnoma</p>" } }] },
360
+ "ru": { "blocks": [{ "type": "html", "props": { "html": "<p>Договор</p>" } }] }
361
+ }
362
+ ```
363
+
364
+ Поддерживаются `uz`, `ru`, `uz_cyrl`, `en` (`HUJJAT_LOCALES`). Это ключи JSON,
365
+ не язык интерфейса. Для других ключей управляйте JSON самостоятельно и используйте
366
+ `AzHtmlEditor` для отдельной строки.
367
+
368
+ Пример импорта/экспорта HTML-only локали через DOCX-компонент:
369
+
370
+ ```ts
371
+ import {
372
+ importHujjatTemplate, type AzDocumentEditorHandle, type HujjatLocales,
373
+ } from '@makhmudov.kay/az-editor';
374
+
375
+ export function openRussianTemplate(locales: HujjatLocales) {
376
+ // Передайте result.document в AzDocumentEditor и покажите result.warnings.
377
+ return importHujjatTemplate(locales, 'ru');
378
+ }
379
+
380
+ export function exportRussianTemplate(editor: AzDocumentEditorHandle, locales: HujjatLocales) {
381
+ return editor.exportHujjat({ locale: 'ru', locales });
382
+ }
383
+ ```
384
+
385
+ `readHujjatHtml` объединяет HTML-блоки выбранной локали. Смешанные блоки, блоки с
386
+ непустым внешним `style` и отсутствующая локаль отклоняются. Подстановки другого
387
+ языка не происходит. `importHujjatTemplate` возвращает `{ document, html, variables, warnings }`.
388
+
389
+ `withHujjatLocale` и `exportHujjat({ locales, locale })` заменяют **всю выбранную
390
+ локаль одним HTML-блоком**, сохраняя копии остальных локалей. Для одного блока
391
+ среди изображений и других блоков обновляйте только его:
392
+
393
+ ```ts
394
+ import type { HujjatLocale, HujjatLocales } from '@makhmudov.kay/az-editor';
395
+
396
+ export function replaceHtmlBlock(
397
+ locales: HujjatLocales, locale: HujjatLocale, index: number, html: string,
398
+ ): HujjatLocales {
399
+ const next = structuredClone(locales);
400
+ const block = next[locale]?.blocks[index];
401
+ if (!block || block.type !== 'html') throw new Error('Выбранный блок не является HTML.');
402
+ block.props = { ...block.props, html };
403
+ return next;
404
+ }
405
+ ```
406
+
407
+ Передавайте уже принятый пользователем HTML. Внешние стили блока не воспроизводятся
408
+ автоматически, если в редактор передан только `props.html`.
409
+
410
+ `createHujjatTemplatePayload(metadata, locales)` необязателен: он формирует
411
+ `{ product_id, name, require_myid, locales }`. Используйте его только для совместимого
412
+ API. `productId` и непустое `name` до 255 символов обязательны; в JSON обязательна
413
+ локаль `uz`, `requireMyid` по умолчанию `false`. Для своего контракта формируйте payload
414
+ самостоятельно. Помощник ничего не отправляет на сервер.
415
+
416
+ ## 8. PDF и печать
417
+
418
+ ### Локальная печать
419
+
420
+ `await documentRef.current.print()` открывает предпросмотр всех страниц, затем
421
+ диалог печати браузера. Пользователь выбирает принтер или сохранение в PDF. Это
422
+ интерактивная печать, а не автоматическое получение PDF Blob. Метод доступен у
423
+ `AzDocumentEditor`; ошибки подготовки компонент показывает в собственной строке ошибки.
424
+
425
+ `ui.print: false` скрывает пункт и отключает перехват Ctrl/Cmd+P адаптером. Браузер
426
+ может обработать сочетание сам. Прямой вызов `ref.print()` настройкой UI не запрещается.
427
+
428
+ ### DOCX → серверный PDF
429
+
430
+ Для PDF текущего состояния сначала вызовите `save()`:
431
+
432
+ ```ts
433
+ import {
434
+ requestPdf, downloadPdf, pdfRenderProfile, type AzDocumentEditorHandle,
435
+ } from '@makhmudov.kay/az-editor';
436
+
437
+ export async function downloadCurrentPdf(editor: AzDocumentEditorHandle, signal?: AbortSignal) {
438
+ const bytes = await editor.save();
439
+ const result = await requestPdf(bytes, 'document.docx', pdfRenderProfile.id, {
440
+ url: '/api/documents/export/pdf', signal,
441
+ });
442
+ downloadPdf(result);
443
+ return result.warnings;
444
+ }
445
+ ```
446
+
447
+ В React свяжите эту функцию с `onExportPdf`, обработайте ошибки и блокировку повторных
448
+ вызовов так же, как при сохранении. Для неподвижного снимка можно временно установить
449
+ `mode="view"` на время операции.
450
+
451
+ Контракт сервиса для `requestPdf`:
452
+
453
+ | Часть | Значение |
454
+ | --- | --- |
455
+ | Метод | `POST` |
456
+ | Тело | `multipart/form-data`: `file` (DOCX), `renderProfile` (строка) |
457
+ | Успех | `2xx`, `Content-Type: application/pdf`, непустой бинарный PDF |
458
+ | Имя файла | Необязательный `Content-Disposition` с `filename*=UTF-8''...` |
459
+ | Предупреждения | Необязательный `X-PDF-Warnings`, коды через запятую |
460
+ | Ошибка | `4xx/5xx`, JSON `{ "error": { "message": "...", "requestId": "..." } }` |
461
+
462
+ URL по умолчанию `/api/documents/export/pdf`. `pdfRenderProfile.id` — идентификатор
463
+ согласованного с сервером профиля рендеринга; передача строки не устанавливает
464
+ PDF-сервис автоматически. Для другого контракта используйте собственный HTTP-клиент.
465
+
466
+ `PdfExportOptions`: `url`, `headers`, `signal`, `fetcher`. Timeout — 95 секунд;
467
+ переданный сигнал может отменить запрос раньше. По умолчанию используются
468
+ `credentials: 'same-origin'`. Для другого origin с cookie:
469
+
470
+ ```ts
471
+ import { requestPdf } from '@makhmudov.kay/az-editor';
472
+
473
+ export function requestCrossOriginPdf(bytes: ArrayBuffer, profile: string) {
474
+ return requestPdf(bytes, 'document.docx', profile, {
475
+ url: 'https://api.example.com/documents/pdf',
476
+ fetcher: (input, init) => fetch(input, { ...init, credentials: 'include' }),
477
+ });
478
+ }
479
+ ```
480
+
481
+ Настройте CORS, credentials и CSRF на своём сервере. Для чтения заголовков имени/
482
+ предупреждений из браузера включите их в `Access-Control-Expose-Headers`.
483
+ Секрет внутреннего PDF-сервиса храните на сервере приложения, не в браузерном коде.
484
+
485
+ ### HTML/JSON → PDF через mPDF или другой движок
486
+
487
+ `requestHujjatPreview` использует формат JSON из раздела 7. Подключаемый маршрут
488
+ может работать на mPDF, если принимает это тело и возвращает PDF:
489
+
490
+ ```ts
491
+ import {
492
+ requestHujjatPreview, downloadPdf, type HujjatLocales,
493
+ } from '@makhmudov.kay/az-editor';
494
+
495
+ export async function previewTemplate(locales: HujjatLocales, signal?: AbortSignal) {
496
+ const result = await requestHujjatPreview({
497
+ url: '/api/templates/preview/pdf',
498
+ locales, locale: 'ru', data: { client: { name: 'Иван' } }, signal,
499
+ });
500
+ downloadPdf(result);
501
+ }
502
+ ```
503
+
504
+ Запрос: `POST application/json` с `{ locales, locale, data }`. Успех: `2xx`,
505
+ `Content-Type: application/pdf`, байты начинаются с `%PDF-`. Ошибки сервера могут
506
+ использовать JSON-поля `message` и `errors`. Результат имеет имя `preview.pdf` и
507
+ пустой массив `warnings`. Помощник не сохраняет шаблон или его версию.
508
+
509
+ Для PDF текущих правок экспортируйте и примите HTML, обновите нужный блок в копии
510
+ JSON, затем передайте эту копию в preview. Иначе PDF будет построен из старой формы.
511
+
512
+ `HujjatPreviewOptions` также поддерживает `headers`, `credentials`, `signal`, `fetcher`.
513
+ Timeout — 95 секунд; `credentials` по умолчанию `same-origin`, `locale` — `uz`,
514
+ `data` — `{}`. Оба PDF-помощника только вызывают сервис; его реализация не входит
515
+ в библиотеку. Серверный PDF остаётся итоговым предпросмотром при использовании mPDF.
516
+
517
+ ## 9. Интерфейс и локализация
518
+
519
+ ### Размеры контейнера
520
+
521
+ Используйте `style={{ height: 700 }}` либо родителя с определённой высотой.
522
+ Для flex-размещения задайте `minHeight: 0` и `minWidth: 0`:
523
+
524
+ ```tsx
525
+ import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
526
+
527
+ export function EmbeddedEditor() {
528
+ return <div style={{ height: '80vh', display: 'flex', flexDirection: 'column', minWidth: 0 }}>
529
+ <div>Панель приложения</div>
530
+ <AzDocumentEditor style={{ flex: 1, minHeight: 0, minWidth: 0 }} />
531
+ </div>;
532
+ }
533
+ ```
534
+
535
+ ### Параметр `ui`
536
+
537
+ | Опция | Назначение |
538
+ | --- | --- |
539
+ | `chrome` | Заголовок, меню и панель форматирования вместе |
540
+ | `menu` | Меню редактора |
541
+ | `rulers` | Линейки |
542
+ | `navigation` | Панель навигации и поиска |
543
+ | `contextMenu` | Контекстное меню |
544
+ | `print` | Печать в меню и перехват Ctrl/Cmd+P |
545
+
546
+ У `AzDocumentEditor` все опции по умолчанию включены. У `AzHtmlEditor` по умолчанию
547
+ выключены `menu` и `print`, остальные включены. `ui={false}` скрывает все опциональные
548
+ элементы, но не отключает редактирование. Для просмотра задайте `mode="view"`.
549
+
550
+ ```tsx
551
+ import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
552
+
553
+ export function MinimalEditor() {
554
+ return <AzDocumentEditor ui={false} style={{ height: 600 }} />;
555
+ }
556
+
557
+ export function Viewer() {
558
+ return <AzDocumentEditor mode="view" ui={{ menu: false, rulers: false }} style={{ height: 600 }} />;
559
+ }
560
+ ```
561
+
562
+ `chrome: false` оставляет поверхность движка без его рамки, в том числе встроенных
563
+ линеек и навигации. Для отдельных переключателей сохраняйте `chrome: true`.
564
+ UI и режим можно менять без замены документа.
565
+
566
+ ### Действия приложения
567
+
568
+ `onOpen`, `onSave`, `onExportPdf` добавляют пункты меню «Файл». Без обработчика пункт
569
+ отсутствует. `labels` меняет подписи, `fileActions` добавляет дополнительные действия:
570
+
571
+ ```tsx
572
+ import { AzDocumentEditor } from '@makhmudov.kay/az-editor';
573
+
574
+ export function EditorWithActions({ onHistory }: { onHistory: () => void }) {
575
+ return <AzDocumentEditor style={{ height: 700 }}
576
+ fileActions={[{ id: 'history', label: 'История версий', onSelect: onHistory }]}
577
+ labels={{ open: 'Загрузить файл', save: 'Сохранить', exportPdf: 'Получить PDF', print: 'Печать' }} />;
578
+ }
579
+ ```
580
+
581
+ Ctrl/Cmd+S/O/P обрабатываются адаптером внутри активного редактора, не во всём
582
+ приложении. Скрытое меню само по себе не отключает файловые сочетания клавиш.
583
+ Кнопкам своего UI внутри `<form>` задавайте `type="button"`.
584
+
585
+ ### Масштаб, тема и язык
586
+
587
+ - `zoom={1}` — 100%; `zoom={1.25}` — 125%. `zoomMode={{ type: 'fixed' }}` закрепляет режим.
588
+ - `zoomMode="auto"` — автоматическая подгонка. Расширенный объект описан типом `AzDocumentEditorProps['zoomMode']`.
589
+ - `colorMode`: `light` (по умолчанию), `dark`, `system`; меняется UI, не лист документа.
590
+ - `locale`, по умолчанию `ru-RU`, задаёт региональные правила дат и подписей, не язык меню.
591
+ - `i18n` принимает каталог переводов движка. По умолчанию — частичный русский каталог
592
+ с запасными английскими строками. Диалог локальной печати использует русский.
593
+
594
+ Собственный каталог проверяйте типом `AzDocumentEditorProps['i18n']`, например
595
+ для полей `toolbar.open` и `toolbar.save`. `renderTitleBarLeft` и
596
+ `renderTitleBarRight` вставляют элементы приложения в заголовок; при скрытом `chrome`
597
+ заголовок не отображается.
598
+
599
+ Передайте `className="my-editor"` и ограничивайте свои стили этим классом:
600
+
601
+ ```css
602
+ .my-editor .docx-editor { --doc-primary: #2563eb; }
603
+ ```
604
+
605
+ Глобальные правила для `table`, `p`, `button`, `canvas` могут изменить документ и UI.
606
+ CSS-переменные движка проверяйте при обновлении версии.
607
+
608
+ ## 10. Шрифты
609
+
610
+ По умолчанию подключены поставляемые с движком замены распространённых шрифтов Word.
611
+ Для собственного набора используйте `fonts`. Один CSS `@font-face` не передаёт
612
+ движку байты для расчёта строк: требуется конфигурация источников.
613
+
614
+ Скопируйте [loadFonts.ts](../examples/integration/loadFonts.ts). Помощник загружает
615
+ TTF/OTF и вычисляет SHA-256. Пример набора:
616
+
617
+ ```ts
618
+ import { loadEditorFonts } from './loadFonts';
619
+
620
+ export function loadTemplateFonts(signal?: AbortSignal) {
621
+ return loadEditorFonts([
622
+ { url: '/fonts/FreeSerif.ttf', family: 'FreeSerif', weight: 400, style: 'normal' },
623
+ { url: '/fonts/FreeSerifBold.ttf', family: 'FreeSerif', weight: 700, style: 'normal' },
624
+ { url: '/fonts/FreeSerifItalic.ttf', family: 'FreeSerif', weight: 400, style: 'italic' },
625
+ { url: '/fonts/FreeSerifBoldItalic.ttf', family: 'FreeSerif', weight: 700, style: 'italic' },
626
+ ], signal);
627
+ }
628
+ ```
629
+
630
+ URL — пример: разместите реальные разрешённые к распространению файлы. Имена и
631
+ начертания должны соответствовать их метаданным. Для TTC пример с `faceIndex: 0`
632
+ нужно адаптировать. `crypto.subtle` требует HTTPS или поддерживаемый браузером localhost.
633
+
634
+ Дождитесь загрузки, сохраните результат в state и смонтируйте редактор с
635
+ `fonts={loadedFonts}`. Сохраняйте объект стабильным всю сессию. Замена конфигурации
636
+ может пересоздать редактор; предварительно сохраните или отмените правки.
637
+ Пользовательский набор заменяет переданную конфигурацию: предоставьте все нужные
638
+ семейства/начертания, не только regular. Показывайте ошибки `onFontError` пользователю.
639
+
640
+ HTML-экспорт нормализует семейства в `freeserif`, `freesans`, `freemono`. Имена в HTML
641
+ сами не загружают файлы. Для близкого оформления используйте одинаковые файлы,
642
+ начертания, размер страницы и поля на клиенте и сервере. Даже так Word, браузерный
643
+ движок и mPDF могут по-разному разбивать строки и страницы. Проверяйте итоговый PDF.
644
+
645
+ ## 11. Next.js и SSR
646
+
647
+ Редактор использует DOM. В App Router одной директивы `'use client'` недостаточно
648
+ для отключения предварительного серверного рендера. Используйте `next/dynamic`
649
+ с `ssr: false` из клиентского компонента.
650
+ [Документация Next.js](https://nextjs.org/docs/app/guides/lazy-loading#skipping-ssr).
651
+
652
+ После копирования `DocxEditorExample.tsx` создайте `EditorClient.tsx`:
653
+
654
+ ```tsx
655
+ 'use client';
656
+
657
+ import dynamic from 'next/dynamic';
658
+ import '@makhmudov.kay/az-editor/styles.css';
659
+
660
+ const Editor = dynamic(
661
+ () => import('./DocxEditorExample').then(module => module.DocxEditorExample),
662
+ { ssr: false, loading: () => <p role="status">Загрузка редактора…</p> },
663
+ );
664
+
665
+ export default function EditorClient() {
666
+ return <Editor />;
667
+ }
668
+ ```
669
+
670
+ В файле страницы:
671
+
672
+ ```tsx
673
+ import EditorClient from './EditorClient';
674
+
675
+ export default function Page() {
676
+ return <EditorClient />;
677
+ }
678
+ ```
679
+
680
+ Импорт CSS можно оставить в одном общем клиентском модуле. Next.js поддерживает
681
+ [CSS сторонних компонентов](https://nextjs.org/docs/app/getting-started/css#external-stylesheets).
682
+ Не выполняйте библиотечную HTML/DOCX-конвертацию в Server Component: она также
683
+ использует DOM. DOCX удобнее загружать после монтирования клиентского компонента.
684
+
685
+ Раздел описывает схему клиентского подключения. Сборку конкретного Next.js-приложения,
686
+ его Turbopack/Webpack и загрузку WASM/шрифтов проверяйте отдельно. В поставляемом
687
+ примере потребителя проверяется Vite.
688
+
689
+ ## 12. Справочник API
690
+
691
+ ### `AzDocumentEditorProps`
692
+
693
+ | Поле | Тип / значение по умолчанию | Назначение |
694
+ | --- | --- | --- |
695
+ | `document` | `Uint8Array \| ArrayBuffer \| 'blank'`, `'blank'` | Исходный DOCX |
696
+ | `title` | `string`, `'Документ.docx'` | Заголовок |
697
+ | `className`, `style` | `string`, `CSSProperties` | Контейнер и размеры |
698
+ | `ui` | `false \| EditorUI` | Видимость интерфейса |
699
+ | `mode` | `'edit' \| 'view'`, `'edit'` | Редактирование/просмотр |
700
+ | `locale` | `string`, `'ru-RU'` | Региональные правила |
701
+ | `i18n` | `AzDocumentEditorProps['i18n']` | Каталог переводов |
702
+ | `colorMode` | `'light' \| 'dark' \| 'system'`, `'light'` | Тема |
703
+ | `zoom`, `zoomMode` | Типы движка | Масштаб и подгонка |
704
+ | `fonts` | `AzDocumentEditorProps['fonts']` | Источники шрифтов |
705
+ | `fileActions` | `readonly EditorFileAction[]` | `{ id, label, onSelect, disabled? }` |
706
+ | `actionsDisabled` | `boolean` | Блокировка файловых действий |
707
+ | `labels` | `{ open?, save?, exportPdf?, print? }` | Подписи действий |
708
+ | `renderTitleBarLeft`, `renderTitleBarRight` | Функции рендера движка | Элементы заголовка |
709
+ | `onOpen`, `onSave`, `onExportPdf` | `() => void` | Действия приложения |
710
+ | `onChange` | `() => void` | Правка, без исходной загрузки |
711
+ | `onStatusChange` | `(status: EditorStatus) => void` | `{ loading, pages, error }` |
712
+ | `onFontError` | `(message: string) => void` | Ошибка шрифтов |
713
+
714
+ `EditorStatus.error` — `string | null`. Ошибки собственных асинхронных действий
715
+ приложение обрабатывает отдельно, не полагаясь на `onStatusChange`.
716
+
717
+ ### `AzDocumentEditorHandle`
718
+
719
+ | Метод | Результат |
720
+ | --- | --- |
721
+ | `save()` | `Promise<ArrayBuffer>` с текущим DOCX |
722
+ | `exportHujjat({ locale, locales?, allowLossy? })` | `Promise<{ html, variables, warnings, locales }>` |
723
+ | `insertVariable(key)` | Вставляет `{key}`, может выбросить ошибку |
724
+ | `focus()` | Фокус в редакторе |
725
+ | `print()` | `Promise<void>`, локальный предпросмотр/печать |
726
+
727
+ ### `AzHtmlEditorProps` и ref
728
+
729
+ Наследует общие props DOCX-компонента, кроме `document`, `onChange`, `onStatusChange`.
730
+
731
+ | Поле | Назначение |
732
+ | --- | --- |
733
+ | `initialHtml: string` | Один фрагмент на сессию |
734
+ | `onChange?: () => void` | Сигнал о правке, без HTML |
735
+ | `onStatusChange?: (status: AzHtmlEditorStatus) => void` | Состояние сессии |
736
+ | `renderFallback?: (status) => ReactNode` | UI загрузки/ошибки/блокировки до открытия документа |
737
+
738
+ `AzHtmlEditorHandle` предоставляет `exportHtml({ allowLossy? })`, `acceptImportLoss()`,
739
+ `insertVariable(key)`, `focus()`. Методов `save()`/`print()` на этом ref нет.
740
+ Наследуемые `onSave`/`onExportPdf` нужно связать с HTML-экспортом и своим серверным процессом.
741
+
742
+ `AzHtmlEditorStatus`: `phase`, `dirty`, `pages`, `error: string | null`, `warnings`.
743
+
744
+ | Phase | Действие |
745
+ | --- | --- |
746
+ | `loading` | Дождаться импорта/открытия |
747
+ | `blocked` | Показать потери, отменить или вызвать `acceptImportLoss()` |
748
+ | `ready` | Редактирование, вставка переменных, экспорт |
749
+ | `exporting` | Дождаться результата; документ временно доступен для просмотра |
750
+ | `error` | Показать ошибку, открыть исправленный источник новой сессией |
751
+
752
+ `acceptImportLoss()` допустим только в `blocked`. `exportHtml()` возвращает
753
+ `Promise<AzHtmlExportResult>` и может отклониться, не переводя весь редактор в `error`.
754
+ Перехватывайте исключения через `try/catch`.
755
+
756
+ ### Файловые и PDF-помощники
757
+
758
+ | Экспорт | Контракт |
759
+ | --- | --- |
760
+ | `readDocxFile(file)` | `Promise<OpenedDocument>`; принимает `name`, `size`, `arrayBuffer()` |
761
+ | `OpenedDocument` | `{ name, original: Uint8Array, metadata }` |
762
+ | `DocumentMetadata` | `paragraphs`, `tables`, `sections`, `savedPageCount`, `hasHeaders`, `hasFooters`, `hasReviewContent`, `fonts` |
763
+ | `downloadDocx(bytes, filename)` | `void`; `ArrayBuffer` или `Uint8Array<ArrayBuffer>` |
764
+ | `exportFilename(name)` | Очищает имя, добавляет суффикс ` — редакция.docx` |
765
+ | `DOCX_MIME`, `MAX_DOCX_BYTES` | MIME DOCX и лимит 30 МиБ |
766
+ | `requestPdf(bytes, filename, renderProfile, options?)` | `Promise<PdfExportResult>` |
767
+ | `PdfExportResult` | `{ blob: Blob, filename: string, warnings: string[] }` |
768
+ | `downloadPdf(result)` | `void`, скачивает `PdfExportResult` |
769
+ | `pdfRenderProfile` | Readonly-профиль с `id` |
770
+
771
+ ### Ограниченный HTML/JSON
772
+
773
+ | Экспорт | Контракт |
774
+ | --- | --- |
775
+ | `sanitizeHujjatHtml(html)` | `{ html, variables, warnings }`; требуется DOM |
776
+ | `readHujjatHtml(locales, locale)` | Чтение HTML-only локали |
777
+ | `withHujjatLocale(locales, locale, html)` | Очистка HTML, замена всей выбранной локали |
778
+ | `importHujjatTemplate(locales, locale, options?)` | `Promise<{ document, html, variables, warnings }>`; `options: { allowLossy? }` |
779
+ | `exportHujjatDocx(bytes, options)` | DOCX → HTML/JSON без React-компонента; требуется браузерный DOM |
780
+ | `requestHujjatPreview(options)` | JSON → серверный PDF, раздел 8 |
781
+ | `createHujjatTemplatePayload(metadata, locales)` | Формирует и валидирует payload, без HTTP |
782
+ | `validateHujjatVariable(key)` | `boolean` |
783
+ | `HujjatCompatibilityError` | `Error` с `warnings` |
784
+ | `HUJJAT_LOCALES`, `HUJJAT_MAX_HTML_BYTES`, `HUJJAT_PAGE` | Локали, лимит HTML, размер/поля страницы |
785
+
786
+ Типы из корня пакета: `HujjatLocale`, `HujjatLocales`, `HujjatBlock`, `HujjatWarning`,
787
+ `HujjatHtmlResult`, `HujjatExportOptions`, `HujjatExportResult`, `HujjatImportResult`,
788
+ `HujjatPreviewOptions`, `HujjatTemplateMetadata`, `PdfExportOptions`, `PdfExportResult`,
789
+ `AzHtmlExportResult` и типы компонентов из таблиц. Используйте `import type`, не
790
+ импорты через внутренние пути `dist/...`.
791
+
792
+ ## 13. Ограничения
793
+
794
+ ### Поддерживаемый HTML
795
+
796
+ ```text
797
+ div p span br hr b strong i em u s sub sup
798
+ h1 h2 h3 h4 h5 h6 ul ol li
799
+ table thead tbody tfoot tr td th
800
+ ```
801
+
802
+ Сохраняются разрешённые inline-стили и `colspan`/`rowspan` ячеек в пределах 1–100.
803
+ Произвольные `class`, `id`, обработчики событий и `data-*` в контракт не входят.
804
+ `a` превращается в текст: ссылка как ссылка не сохраняется. `img`, `svg`, `script`,
805
+ `style`, `iframe`, формы и медиа удаляются с диагностикой потери.
806
+
807
+ CSS ограничен шрифтом/цветом, базовым оформлением, выравниванием, интервалами,
808
+ размерами, полями, отступами, рамками, таблицами и `page-break-*`. `url(...)`,
809
+ `expression`, `!important`, CSS-переменные, `position`, flex/grid и внешние стили
810
+ не поддерживаются. Даже прошедший очистку CSS может преобразоваться приближённо
811
+ при переходе HTML → документ → HTML.
812
+
813
+ | Содержимое | В HTML-режиме / при DOCX → HTML |
814
+ | --- | --- |
815
+ | Текст, абзацы, базовое оформление | Поддерживаются с нормализацией |
816
+ | Таблицы, обычные границы/заливки/объединения | Поддерживаются; сложные случаи проверяйте отдельно |
817
+ | Простой bullet/decimal список | Поддерживается |
818
+ | Многоуровневая нумерация, нестандартный старт/продолжение | Возможны потери; атрибут `start` удаляется |
819
+ | Явный разрыв страницы | Передаётся, автоматическая пагинация рассчитывается заново |
820
+ | Картинки, фигуры, текстовые блоки | Не входят в HTML-контракт |
821
+ | Колонтитулы, сноски, комментарии, исправления, поля | Не сохраняются как интерактивные HTML-объекты |
822
+ | Скрытый текст | Не раскрывается при экспорте; удаление требует принятия потерь |
823
+
824
+ Это ограничения HTML-адаптера, а не перечень возможностей DOCX-режима. Возможности
825
+ конкретного DOCX проверяются отдельно. Наличие кнопки форматирования не гарантирует
826
+ сохранение её результата в ограниченном HTML.
827
+
828
+ ### Таблицы и межстрочный интервал
829
+
830
+ В HTML-режиме ширины ячеек в `pt`, `px` и процентах формируют общую сетку
831
+ колонок DOCX. Ширина таблицы рассчитывается относительно доступной области
832
+ страницы, а вложенной таблицы — относительно родительской ячейки с учётом
833
+ её padding. Для фиксированной таблицы при экспорте используется `tblGrid`;
834
+ пропорции колонок, `colspan` и `rowspan` сохраняются при повторном открытии.
835
+ Противоречивые размеры нормализуются с предупреждением `table-width-normalized`.
836
+
837
+ Абзацы внутри ячеек не получают дополнительный отступ 6pt после текста;
838
+ явный `margin-bottom` сохраняется. Значение `line-height` в абсолютных единицах
839
+ задаёт минимальную, а не жёсткую высоту строки (`atLeast`), чтобы крупный текст
840
+ не перекрывал соседнюю строку. Числа и проценты задают множитель обычной строки.
841
+ Множитель меньше 1 приводится к 1. При DOCX → HTML интервал ниже размера шрифта
842
+ нормализуется, включая крупные фрагменты внутри абзаца; это отмечается
843
+ предупреждением `line-height-normalized`. Достаточные интервалы сохраняются.
844
+
845
+ `AzHtmlEditor.exportHtml()` без правок возвращает исходный HTML. Нормализованный
846
+ HTML появляется после редактирования. Для окончательного PDF также нужны
847
+ совпадающие шрифты, поля страницы и правила CSS на сервере.
848
+
849
+ ### Лимиты и оформление
850
+
851
+ - DOCX до 30 МиБ; DOC, DOCM, PDF и защищённые паролем файлы не являются входом.
852
+ - DOCX-проверка/преобразование ограничивают число частей 2000 и читаемые XML-части 8 МиБ.
853
+ - HTML до 1 048 576 байт UTF-8 на фрагмент; это байты, не `string.length`.
854
+ - Очистка HTML ограничивает глубину 100 и число обходящихся узлов 50 000.
855
+ - При преобразовании дополнительно проверяются сложность таблиц и размер создаваемого XML.
856
+ - Документ из HTML: A4 210×297 мм; поля сверху 15, справа/слева 20, снизу 35 мм.
857
+ - Исходный DOCX сохраняет собственные параметры до преобразования.
858
+ - Лимиты вашего HTTP-сервера задаются отдельно и могут быть меньше клиентских.
859
+
860
+ Совпадение с Microsoft Word один в один не гарантируется. Для документов с критичными
861
+ переносами, подписями, таблицами и нумерацией сравнивайте исходник и конечный PDF
862
+ на реальном корпусе документов с production-шрифтами и серверным рендерером.
863
+
864
+ ## 14. Ошибки и проверка интеграции
865
+
866
+ | Симптом | Что проверить |
867
+ | --- | --- |
868
+ | Пустой редактор / нулевая высота | CSS импортирован, высота контейнера задана, у flex-родителей есть `min-height: 0` |
869
+ | `window/document is not defined` | SSR отключён; конвертация выполняется в браузере |
870
+ | Шрифты/WASM не загружаются | Network, URL ресурсов, MIME, CORS, CSP, `base` сборщика, `onFontError` |
871
+ | Правки пропадают при рендере | `key`, объект `document` или `fonts` не меняются без необходимости |
872
+ | Новый `initialHtml` не появился | Открыта новая сессия с новым `key` после решения о правках |
873
+ | `onChange` не возвращает HTML | Используется `await exportHtml()` |
874
+ | Экспорт недоступен | Достигнут `ready`, нет повторного запроса/размонтирования, ошибка обработана |
875
+ | `phase: blocked` | Показаны `warnings`, принято решение об импорте |
876
+ | PDF-кнопки нет | Передан `onExportPdf`, меню включено |
877
+ | Сервер возвращает HTML/JSON вместо PDF | Маршрут, авторизация, прокси, формат ответа |
878
+ | HTML после сохранения отличается | Диагностика преобразования и серверная нормализация |
879
+ | Исчезли соседние блоки | Для одного блока не используется замена всей локали |
880
+ | Переносы отличаются | Шрифты, начертания, поля, размер страницы, возможности рендерера |
881
+ | Ошибка React hooks | React 19, один экземпляр React |
882
+
883
+ Перед выпуском приложения проверьте:
884
+
885
+ 1. Открытие и правки DOCX, скачивание и повторное открытие результата.
886
+ 2. Сохранность текущего документа при открытии повреждённого файла.
887
+ 3. HTML без правок, с правками, с отменой и с подтверждением потерь.
888
+ 4. Ошибку API: правки должны остаться доступными для повторной попытки.
889
+ 5. Смену документа/языка, закрытие окна и переход по маршруту с правками.
890
+ 6. Сохранность других языков, блоков и метаданных JSON.
891
+ 7. PDF текущих правок на реальном сервере с реальными шрифтами.
892
+ 8. Production-сборку и загрузку всех ресурсов после деплоя.
893
+
894
+ В исходниках библиотеки доступны `npm run typecheck`, `npm test`, `npm run build`,
895
+ `npm run test:e2e`. `npm pack` включает это руководство и примеры. Проверяйте
896
+ подключение из архива или npm, чтобы не зависеть от локальных aliases репозитория.