@kodaeditor/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +83 -0
- package/README.md +94 -0
- package/THIRD-PARTY-LICENSES.txt +1811 -0
- package/dist/commands.d.ts +15 -0
- package/dist/docx-alignment.d.ts +14 -0
- package/dist/docx-breaks.d.ts +31 -0
- package/dist/docx-forms.d.ts +28 -0
- package/dist/docx-headers.d.ts +22 -0
- package/dist/docx-images.d.ts +41 -0
- package/dist/docx-import.d.ts +36 -0
- package/dist/docx-math.d.ts +18 -0
- package/dist/docx-styles.d.ts +53 -0
- package/dist/docx-symbols.d.ts +19 -0
- package/dist/docx-text-styles.d.ts +27 -0
- package/dist/editor.d.ts +372 -0
- package/dist/fallback.d.ts +13 -0
- package/dist/floatingToolbar.d.ts +7 -0
- package/dist/group-icons.d.ts +17 -0
- package/dist/i18n.d.ts +214 -0
- package/dist/icons.d.ts +1 -0
- package/dist/image-view.d.ts +27 -0
- package/dist/import-formats.d.ts +41 -0
- package/dist/index.d.ts +15 -0
- package/dist/kodaeditor-core.umd.cjs +1 -0
- package/dist/kodaeditor.js +1 -0
- package/dist/kodaeditor.umd.cjs +1 -0
- package/dist/license.d.ts +90 -0
- package/dist/markdown.d.ts +4 -0
- package/dist/menubar.d.ts +60 -0
- package/dist/offline-license.d.ts +36 -0
- package/dist/pagination.d.ts +11 -0
- package/dist/paste.d.ts +54 -0
- package/dist/pdf-import-cdn.d.ts +2 -0
- package/dist/pdf-import.d.ts +8 -0
- package/dist/print.d.ts +18 -0
- package/dist/rtf-images.d.ts +24 -0
- package/dist/schema.d.ts +14 -0
- package/dist/search.d.ts +17 -0
- package/dist/serialize.d.ts +20 -0
- package/dist/slashMenu.d.ts +8 -0
- package/dist/styles.d.ts +8 -0
- package/dist/table-split.d.ts +26 -0
- package/dist/table-view.d.ts +34 -0
- package/dist/tableColumnTrade.d.ts +15 -0
- package/dist/tableToolbar.d.ts +14 -0
- package/dist/toc-view.d.ts +23 -0
- package/dist/toolbar-editor.d.ts +89 -0
- package/dist/validate-signature.d.ts +64 -0
- package/dist/vendor/pdf.min.mjs +29 -0
- package/dist/vendor/pdf.worker.min.mjs +29 -0
- package/dist/vendor/xlsx.full.min.js +24 -0
- package/dist/web-component.d.ts +383 -0
- package/dist/xlsx-import-cdn.d.ts +1 -0
- package/dist/xlsx-import.d.ts +2 -0
- package/package.json +88 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Состояния лицензии.
|
|
3
|
+
*
|
|
4
|
+
* 'invalid' и 'unreachable' РАЗНЫЕ, и различие не косметическое: первое
|
|
5
|
+
* означает «сервер сказал нет», второе - «мы не смогли спросить». Наказывать
|
|
6
|
+
* покупателя за нашу недоступность нельзя, поэтому пометка в выгрузках
|
|
7
|
+
* ставится только при 'invalid' и 'unconfigured' (см. web-component).
|
|
8
|
+
*/
|
|
9
|
+
export type LicenseStatus = 'valid' | 'grace' | 'invalid' | 'unreachable' | 'unconfigured';
|
|
10
|
+
export interface LicenseResult {
|
|
11
|
+
status: LicenseStatus;
|
|
12
|
+
/**
|
|
13
|
+
* Список из ответа сервера. НИЧЕГО НЕ ОГРАНИЧИВАЕТ: ключ открывает все
|
|
14
|
+
* возможности редактора, деления на тарифы нет, и ни одна функция не
|
|
15
|
+
* сверяется с этим списком - проверено по всему ядру 12.09.2026.
|
|
16
|
+
*
|
|
17
|
+
* Поле осталось от замысла продавать возможности по отдельности и сейчас
|
|
18
|
+
* служит только одному: оно входит в подписываемую строку ответа, поэтому
|
|
19
|
+
* убрать его нельзя, не меняя протокол подписи на обеих сторонах.
|
|
20
|
+
*
|
|
21
|
+
* Комментарий здесь раньше обещал feature-gating. Это было неправдой и
|
|
22
|
+
* вводило в заблуждение любого, кто читал ответ /validate и видел там два
|
|
23
|
+
* названия вместо всех возможностей, за которые заплатил.
|
|
24
|
+
*/
|
|
25
|
+
plugins: string[];
|
|
26
|
+
/** Показывать ли водяной знак. */
|
|
27
|
+
watermark: boolean;
|
|
28
|
+
message?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface LicenseOptions {
|
|
31
|
+
/** Лицензионный ключ клиента. */
|
|
32
|
+
key?: string;
|
|
33
|
+
/**
|
|
34
|
+
* URL license-сервера. По умолчанию - наш боевой адрес (DEFAULT_ENDPOINT),
|
|
35
|
+
* задавать его клиенту НЕ нужно. Переопределяют только те, кто проксирует
|
|
36
|
+
* проверку через собственный домен.
|
|
37
|
+
*/
|
|
38
|
+
endpoint?: string;
|
|
39
|
+
/** Сколько дней жить по кэшу при недоступной сети. */
|
|
40
|
+
graceDays?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Адрес проверки лицензий по умолчанию.
|
|
44
|
+
*
|
|
45
|
+
* Зашит намеренно: раньше адрес был обязательным атрибутом, и во всех наших
|
|
46
|
+
* же сниппетах стоял плейсхолдер вида https://your-domain.com/validate, то
|
|
47
|
+
* есть домен ПОКУПАТЕЛЯ, где такого эндпоинта нет. Клиент подключал по
|
|
48
|
+
* документации и получал водяной знак на проде. Адрес наш и меняться не
|
|
49
|
+
* должен: он остаётся в коде у всех выданных лицензий бессрочно.
|
|
50
|
+
*/
|
|
51
|
+
export declare const DEFAULT_ENDPOINT = "https://api.kodaeditor.com/validate";
|
|
52
|
+
/**
|
|
53
|
+
* Проверяет лицензию.
|
|
54
|
+
*
|
|
55
|
+
* Порядок: свежий кэш (моложе суток) отвечает сразу и обновляется в фоне;
|
|
56
|
+
* иначе идём на сервер; если сервер недоступен - живём по кэшу в пределах
|
|
57
|
+
* grace-периода; иначе деградация (водяной знак, редактор продолжает работать).
|
|
58
|
+
*
|
|
59
|
+
* Сознательно НЕ делаем поблажку «первая установка при недоступном сервере»:
|
|
60
|
+
* ключ на этом этапе ещё ни разу не подтверждён, и любое такое окно означало
|
|
61
|
+
* бы, что произвольная строка в атрибуте license плюс закрытый доступ к нашему
|
|
62
|
+
* домену дают чистый редактор. Доступность сервера - наша задача, а не повод
|
|
63
|
+
* ослабить проверку.
|
|
64
|
+
*/
|
|
65
|
+
export declare function validateLicense(opts: LicenseOptions): Promise<LicenseResult>;
|
|
66
|
+
export interface OfflineLicenseOptions {
|
|
67
|
+
/**
|
|
68
|
+
* URL подписанного файла лицензии (обычно свой же домен: /license.json).
|
|
69
|
+
* Взаимоисключимо с fileContent - если задан fileContent, fileUrl
|
|
70
|
+
* игнорируется.
|
|
71
|
+
*/
|
|
72
|
+
fileUrl?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Содержимое файла лицензии ИНЛАЙНОМ, без fetch. Обязательно для
|
|
75
|
+
* страниц, открытых напрямую file:// (там fetch() к соседнему файлу не
|
|
76
|
+
* работает вовсе - браузерное ограничение, не наше) - сценарий агента
|
|
77
|
+
* в поле без интернета и без локального веб-сервера: HTML/JS/подпись
|
|
78
|
+
* поставляются одним пакетом, ничего не грузится по сети.
|
|
79
|
+
*/
|
|
80
|
+
fileContent?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Полностью офлайн-проверка: подпись файла проверяется локально (Web
|
|
84
|
+
* Crypto API), без обращения к license-серверу. Содержимое либо
|
|
85
|
+
* загружается (fileUrl - для same-origin/localhost развёртываний), либо
|
|
86
|
+
* передаётся сразу (fileContent - для true-offline file:// страниц, где
|
|
87
|
+
* fetch физически недоступен). Никогда не бросает - при любой проблеме
|
|
88
|
+
* возвращает деградацию (водяной знак), как и онлайн-путь validateLicense().
|
|
89
|
+
*/
|
|
90
|
+
export declare function validateLicenseOffline(opts: OfflineLicenseOptions): Promise<LicenseResult>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Верхнее меню (menubar) — структурированный доступ к работе с компонентами
|
|
3
|
+
* документа: Файл / Правка / Вставка / Формат / Таблица / Вид. Каждое меню —
|
|
4
|
+
* выпадающая панель пунктов; пункт может иметь подменю (Таблица → Строка/Столбец/
|
|
5
|
+
* Ячейка). Действия переиспользуют те же методы редактора, что и тулбар, —
|
|
6
|
+
* поэтому menubar НИЧЕГО не дублирует по логике, только даёт второй способ доступа.
|
|
7
|
+
*
|
|
8
|
+
* Тот же модуль строит контекстное меню (правый клик) — это просто всплывающая
|
|
9
|
+
* панель из тех же MenuItemSpec в точке курсора.
|
|
10
|
+
*/
|
|
11
|
+
import type { Strings } from './i18n';
|
|
12
|
+
export interface MenuItemSpec {
|
|
13
|
+
label?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
separator?: boolean;
|
|
16
|
+
action?: () => void;
|
|
17
|
+
submenu?: MenuItemSpec[];
|
|
18
|
+
enabled?: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MenuSpec {
|
|
21
|
+
label: string;
|
|
22
|
+
items: MenuItemSpec[];
|
|
23
|
+
}
|
|
24
|
+
export interface MenubarHost {
|
|
25
|
+
shell: HTMLElement;
|
|
26
|
+
strings: Strings;
|
|
27
|
+
}
|
|
28
|
+
export declare class Menubar {
|
|
29
|
+
private menus;
|
|
30
|
+
private host;
|
|
31
|
+
private bar?;
|
|
32
|
+
private openPanels;
|
|
33
|
+
private activeTop?;
|
|
34
|
+
private tops;
|
|
35
|
+
private onAway;
|
|
36
|
+
private onKey;
|
|
37
|
+
constructor(menus: MenuSpec[], host: MenubarHost);
|
|
38
|
+
/** Полоса меню (для вставки над тулбаром). */
|
|
39
|
+
render(): HTMLElement;
|
|
40
|
+
/** Roving-фокус на верхний пункт i (обновляет tabindex). */
|
|
41
|
+
private focusTop;
|
|
42
|
+
private currentTopIndex;
|
|
43
|
+
/** Клавиатура на верхних пунктах menubar. */
|
|
44
|
+
private onTopKey;
|
|
45
|
+
private toggleTop;
|
|
46
|
+
private openTop;
|
|
47
|
+
/** Включённые пункты панели (для клавиатурной навигации). */
|
|
48
|
+
private itemsOf;
|
|
49
|
+
/** Фокус на пункт панели по индексу (отрицательный = с конца). */
|
|
50
|
+
private focusPanelItem;
|
|
51
|
+
/** Клавиатура внутри панели меню/подменю/контекста (WAI-ARIA menu). */
|
|
52
|
+
private onPanelKey;
|
|
53
|
+
/** Контекстное меню (правый клик): панель из items в точке (clientX, clientY). */
|
|
54
|
+
openContext(items: MenuItemSpec[], clientX: number, clientY: number): void;
|
|
55
|
+
private buildPanel;
|
|
56
|
+
private openSubmenu;
|
|
57
|
+
/** Закрыть все панели глубже указанной (для переключения подменю на одном уровне). */
|
|
58
|
+
private closeDeeperThan;
|
|
59
|
+
closeAll(): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Офлайн-активация: подписанный ECDSA (P-256) файл лицензии вместо живого
|
|
3
|
+
* запроса на license-сервер — для корпоративных клиентов без исходящего
|
|
4
|
+
* доступа в интернет (не грейс-кэш, а изначально нулевая сетевая
|
|
5
|
+
* зависимость от нашей инфраструктуры).
|
|
6
|
+
*
|
|
7
|
+
* Приватный ключ подписи НИКОГДА не попадает в этот бандл — генерируется и
|
|
8
|
+
* хранится отдельно (см. apps/license-server/src/offline-license-cli.mjs).
|
|
9
|
+
* Публичный ключ ниже — не секрет по своей природе (нужен только для
|
|
10
|
+
* проверки подписи, не для её создания), безопасно шить в открытый код.
|
|
11
|
+
*
|
|
12
|
+
* Формат подписи — raw IEEE-P1363 (r||s, 64 байта для P-256), НЕ DER: это
|
|
13
|
+
* то, что ждёт браузерный Web Crypto API. Node по умолчанию подписывает в
|
|
14
|
+
* DER — CLI обязан использовать { dsaEncoding: 'ieee-p1363' }, иначе
|
|
15
|
+
* подпись здесь не пройдёт верификацию (проверено сквозным тестом).
|
|
16
|
+
*/
|
|
17
|
+
export interface OfflineLicensePayload {
|
|
18
|
+
v: 1;
|
|
19
|
+
key: string;
|
|
20
|
+
customer: string;
|
|
21
|
+
domains: string[];
|
|
22
|
+
plugins: string[];
|
|
23
|
+
issuedAt: string;
|
|
24
|
+
expiresAt: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface OfflineLicenseResult {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
plugins: string[];
|
|
29
|
+
reason?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Проверяет содержимое офлайн-файла лицензии для текущего домена. Никогда
|
|
33
|
+
* не бросает исключение — любая проблема (битый файл, неверная подпись,
|
|
34
|
+
* домен не совпал, срок истёк) возвращается как `{valid:false, reason}`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function verifyOfflineLicense(fileText: string, domain: string): Promise<OfflineLicenseResult>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Страничный вид (pagination) — безопасный режим: НЕ меняет модель документа и
|
|
3
|
+
* не вмешивается в транзакции (исключает риск циклов/поломки редактирования).
|
|
4
|
+
* Плагин лишь рисует визуальные границы страниц A4 в отдельном DOM-оверлее
|
|
5
|
+
* поверх содержимого, измеряя реальную высоту блоков. Включается переключателем
|
|
6
|
+
* (stateRef.on); выключен по умолчанию.
|
|
7
|
+
*/
|
|
8
|
+
import { Plugin } from 'prosemirror-state';
|
|
9
|
+
export declare function paginationPlugin(stateRef: {
|
|
10
|
+
on: boolean;
|
|
11
|
+
}): Plugin;
|
package/dist/paste.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart-paste: очистка HTML, вставленного из Microsoft Word / Excel / Google Docs.
|
|
3
|
+
*
|
|
4
|
+
* Главная боль ЭДО - пользователи копируют документы из Word, и обычные
|
|
5
|
+
* редакторы тащат за собой `mso-` стили, `<o:p>`, условные комментарии и
|
|
6
|
+
* мусорные span-ы. Здесь это вычищается ДО парсинга в модель документа,
|
|
7
|
+
* сохраняя реальную структуру (заголовки, списки, форматирование).
|
|
8
|
+
*
|
|
9
|
+
* Премиум-функция: включается лицензией (plugin "smart-paste").
|
|
10
|
+
*/
|
|
11
|
+
export declare function inertCreate<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K];
|
|
12
|
+
/**
|
|
13
|
+
* Убирает дубль-таблицы Word `mc:AlternateContent`. Word кладёт в буфер ОДИН и
|
|
14
|
+
* тот же блок дважды - основная вёрстка (Choice) и запасная (Fallback, часто с
|
|
15
|
+
* упрощёнными/битыми ширинами). Здесь среди верхнеуровневых таблиц с идентичным
|
|
16
|
+
* текстом оставляем ПЕРВУЮ (Choice - она структурнее), последующие дубли убираем.
|
|
17
|
+
*/
|
|
18
|
+
export declare function dedupeAlternateTables(root: HTMLElement): void;
|
|
19
|
+
/**
|
|
20
|
+
* Удаляет узлы-комментарии из DOM. Безопасно для структуры: комментарий -
|
|
21
|
+
* отдельный узел и не может содержать элементы, поэтому вложенные таблицы не
|
|
22
|
+
* пострадают (в отличие от строкового regex `<!--…-->`, который мог «съесть»
|
|
23
|
+
* таблицу, оказавшуюся между двумя Word-комментариями).
|
|
24
|
+
*/
|
|
25
|
+
export declare function stripComments(root: HTMLElement): void;
|
|
26
|
+
export declare function cleanWordHTML(html: string): string;
|
|
27
|
+
export declare function normalizeWordWhitespace(root: HTMLElement): void;
|
|
28
|
+
export declare function inlineWordStyles(root: HTMLElement): void;
|
|
29
|
+
/** Переносит высоту строк (<tr height>) на ячейки - высота полей форм. */
|
|
30
|
+
export declare function copyRowHeights(root: HTMLElement): void;
|
|
31
|
+
export declare function normalizeTableWidths(root: HTMLElement): void;
|
|
32
|
+
export declare function reconstructWordFootnotes(root: HTMLElement): void;
|
|
33
|
+
/**
|
|
34
|
+
* Восстанавливает списки Word во ВСЁМ поддереве.
|
|
35
|
+
*
|
|
36
|
+
* Раньше обход шёл только по прямым детям root, и это молча теряло списки в
|
|
37
|
+
* двух совершенно обычных случаях:
|
|
38
|
+
*
|
|
39
|
+
* - абзацы внутри обёртки `<div class=WordSection1>`, которую Word кладёт
|
|
40
|
+
* вокруг документа: при вставке из буфера она доходит до нас (там же
|
|
41
|
+
* приезжает <style>, ради которого написан inlineWordStyles);
|
|
42
|
+
* - абзацы внутри ЯЧЕЕК ТАБЛИЦ - в договорах и актах это встречается
|
|
43
|
+
* постоянно.
|
|
44
|
+
*
|
|
45
|
+
* В обоих случаях пункты оставались абзацами вида «1.Первый пункт» с номером
|
|
46
|
+
* внутри текста. Корпусная проверка на 6000 документов этого не поймала,
|
|
47
|
+
* потому что шла через docx-import: там разметку порождает конвертер, обёрток
|
|
48
|
+
* Word в ней нет, а списки в ячейках приходят уже настоящими <ol>/<ul>.
|
|
49
|
+
* То есть 6000 документов проверили путь ИМПОРТА ФАЙЛА, а не путь БУФЕРА.
|
|
50
|
+
*
|
|
51
|
+
* Контейнеры собираются ДО правок: обработка переставляет узлы, и обход по
|
|
52
|
+
* живой коллекции пропускал бы элементы.
|
|
53
|
+
*/
|
|
54
|
+
export declare function reconstructWordLists(root: HTMLElement): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Извлекает текст PDF в HTML-абзацы. Строки группируются по Y-координате,
|
|
3
|
+
* абзацы разделяются по увеличенному вертикальному промежутку (эвристика —
|
|
4
|
+
* качество зависит от PDF; сканы-картинки текста не дают).
|
|
5
|
+
*/
|
|
6
|
+
export declare function pdfToHtml(data: ArrayBuffer): Promise<string>;
|
|
7
|
+
/** Рендерит страницы PDF в PNG data-URL (scale: чёткость). */
|
|
8
|
+
export declare function pdfToImages(data: ArrayBuffer, scale?: number): Promise<string[]>;
|
package/dist/print.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Экспорт документа в PDF средствами браузера (диалог печати → «Сохранить как
|
|
3
|
+
* PDF»). Полностью CLIENT-SIDE: содержимое НЕ покидает машину пользователя —
|
|
4
|
+
* важно для ЭДО (документы не уходят на сервер). Рендер выполняет движок самого
|
|
5
|
+
* браузера, поэтому PDF выглядит как контент в редакторе (WYSIWYG).
|
|
6
|
+
*
|
|
7
|
+
* Печать идёт в скрытом <iframe>, а не в текущей странице, чтобы напечатать
|
|
8
|
+
* ТОЛЬКО документ (без тулбара/UI хоста) и не зависеть от Shadow DOM.
|
|
9
|
+
*/
|
|
10
|
+
/** Собирает самостоятельный HTML-документ для печати из контента редактора. */
|
|
11
|
+
export declare function buildPrintHtml(contentHtml: string, lang?: string, title?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Печатает HTML-контент через скрытый iframe. Дожидается загрузки картинок
|
|
14
|
+
* (data:/удалённых) перед вызовом print(), но не висит дольше таймаута. iframe
|
|
15
|
+
* снимается после закрытия диалога печати (afterprint) либо по страховочному
|
|
16
|
+
* таймауту.
|
|
17
|
+
*/
|
|
18
|
+
export declare function printDocument(ownerDoc: Document, contentHtml: string, lang?: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Восстановление картинок при вставке из Word через RTF-флейвор буфера.
|
|
3
|
+
*
|
|
4
|
+
* Боль: в `text/html` буфера Word кладёт картинки ссылками `file:///C:/...`
|
|
5
|
+
* (локальные пути, сайту недоступны) или VML-плейсхолдерами — поэтому при
|
|
6
|
+
* обычной вставке картинки теряются. Но тот же буфер содержит `text/rtf`, где
|
|
7
|
+
* изображения встроены hex-кодом в группах `{\pict ...}`. Этот же приём
|
|
8
|
+
* используют PowerPaste (TinyMCE) и PasteFromOffice (CKEditor).
|
|
9
|
+
*
|
|
10
|
+
* Здесь RTF разбирается, PNG/JPEG-картинки достаются как data-URI и
|
|
11
|
+
* подставляются в плейсхолдеры HTML по порядку. Метафайлы (WMF/EMF) браузер
|
|
12
|
+
* не рисует — такие позиции остаются null, чтобы не сбить порядок маппинга.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Извлекает картинки из RTF по порядку. Поддерживаются PNG и JPEG;
|
|
16
|
+
* неподдержанные форматы (метафайлы) дают null, сохраняя порядок.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractRtfImages(rtf: string): (string | null)[];
|
|
19
|
+
/**
|
|
20
|
+
* Подставляет извлечённые из RTF картинки в плейсхолдеры HTML по порядку.
|
|
21
|
+
* Обрабатывает и обычные `<img>`, и VML `<v:imagedata>` (Word рисует ими),
|
|
22
|
+
* конвертируя последние в `<img>`, чтобы пережить санитайзинг.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergeRtfImagesIntoHtml(root: HTMLElement, images: (string | null)[]): void;
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Схема документа KodaEditor.
|
|
3
|
+
*
|
|
4
|
+
* Документ хранится как структурированная модель ProseMirror, а не как
|
|
5
|
+
* «сырой» HTML из contenteditable. Благодаря этому вывод всегда чистый —
|
|
6
|
+
* никаких `<span style>` и пустых тегов, которыми грешит TinyMCE.
|
|
7
|
+
*
|
|
8
|
+
* За основу взята проверенная prosemirror-schema-basic, расширенная
|
|
9
|
+
* списками и дополнительными марками (подчёркивание, зачёркивание),
|
|
10
|
+
* которые корпоративные редакторы ожидают по умолчанию.
|
|
11
|
+
*/
|
|
12
|
+
import { Schema, type MarkSpec, type NodeSpec } from 'prosemirror-model';
|
|
13
|
+
export declare const editorSchema: Schema<keyof import("orderedmap").default<NodeSpec>, keyof import("orderedmap").default<MarkSpec>>;
|
|
14
|
+
export type EditorSchema = typeof editorSchema;
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Найти/заменить: плагин ProseMirror, который хранит запрос и подсвечивает
|
|
3
|
+
* совпадения декорациями (активное — отдельным классом). Поиск по тексту,
|
|
4
|
+
* регистронезависимый. Навигация/замена — методами Editor (см. editor.ts).
|
|
5
|
+
*/
|
|
6
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
7
|
+
export interface Match {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SearchState {
|
|
12
|
+
query: string;
|
|
13
|
+
matches: Match[];
|
|
14
|
+
active: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const searchKey: PluginKey<SearchState>;
|
|
17
|
+
export declare function searchPlugin(): Plugin<SearchState>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сериализация и парсинг документа.
|
|
3
|
+
*
|
|
4
|
+
* Выход — чистый HTML, построенный из модели документа через
|
|
5
|
+
* официальный DOMSerializer ProseMirror. Вход — строгий парсинг входного
|
|
6
|
+
* HTML обратно в модель, что попутно отбрасывает небезопасный/мусорный
|
|
7
|
+
* разметочный шум (база для будущего paste-санитайзера).
|
|
8
|
+
*/
|
|
9
|
+
import { type Node as PMNode } from 'prosemirror-model';
|
|
10
|
+
/** Документ ProseMirror -> чистая HTML-строка. */
|
|
11
|
+
export declare function docToHTML(doc: PMNode): string;
|
|
12
|
+
/**
|
|
13
|
+
* HTML-строка -> документ ProseMirror (по схеме редактора).
|
|
14
|
+
*
|
|
15
|
+
* Перед парсингом HTML прогоняется через DOMPurify: это отсекает
|
|
16
|
+
* исполняемые векторы (`<script>`, `onerror=`, `<svg onload>` и т.п.),
|
|
17
|
+
* которые иначе срабатывают при присваивании в innerHTML — даже несмотря
|
|
18
|
+
* на то, что ProseMirror потом отбросит сами узлы. Защита от XSS на входе.
|
|
19
|
+
*/
|
|
20
|
+
export declare function htmlToDoc(html: string): PMNode;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-меню (Notion-UX): по вводу «/» в начале блока всплывает меню блоков
|
|
3
|
+
* (заголовки, списки, цитата, код, таблица) с фильтрацией по тексту, выбором
|
|
4
|
+
* стрелками/Enter и кликом. Отличитель от TinyMCE.
|
|
5
|
+
*/
|
|
6
|
+
import { Plugin } from 'prosemirror-state';
|
|
7
|
+
import type { Strings } from './i18n';
|
|
8
|
+
export declare function slashMenuPlugin(strings: Strings): Plugin;
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Стили KodaEditor как строка (инлайнятся в компонент — без внешних запросов,
|
|
3
|
+
* что удобно для строгой CSP и self-hosted поставки).
|
|
4
|
+
*
|
|
5
|
+
* Дизайн-токены через CSS-переменные, чтобы клиент мог переопределить тему
|
|
6
|
+
* одной строкой, не трогая внутренности.
|
|
7
|
+
*/
|
|
8
|
+
export declare const editorStyles = "\n:host {\n display: block;\n}\n.euz {\n --koda-bg: #ffffff;\n --koda-fg: #1a1a1f;\n --koda-muted: #6b7280;\n --koda-border: #e4e4e7;\n --koda-accent: #2f5bff;\n --koda-accent-soft: #eef2ff;\n --koda-toolbar-bg: #fafafa;\n --koda-radius: 10px;\n --koda-font: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n\n display: flex;\n flex-direction: column;\n position: relative;\n border: 1px solid var(--koda-border);\n border-radius: var(--koda-radius);\n background: var(--koda-bg);\n color: var(--koda-fg);\n font-family: var(--koda-font);\n /* \u042F\u0432\u043D\u044B\u0435 font-size/line-height \u043D\u0430 \u043A\u043E\u0440\u043D\u0435: \u044D\u0442\u043E \u041D\u0410\u0421\u041B\u0415\u0414\u0423\u0415\u041C\u042B\u0415 \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430, \u043E\u043D\u0438 \u043F\u0440\u043E\u0445\u043E\u0434\u044F\u0442\n \u0441\u043A\u0432\u043E\u0437\u044C \u0433\u0440\u0430\u043D\u0438\u0446\u0443 Shadow DOM \u043E\u0442 \u0445\u043E\u0441\u0442\u0430. \u0411\u0435\u0437 \u043D\u0438\u0445 \u0447\u0443\u0436\u043E\u0439 body{font-size:Npx!important}\n \u0441\u0434\u0432\u0438\u0433\u0430\u043B \u0431\u044B \u0431\u0430\u0437\u043E\u0432\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0442\u0443\u043B\u0431\u0430\u0440\u0430/\u043C\u0435\u043D\u044E. .koda-content \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043D\u0430 15px. */\n font-size: 14px;\n line-height: 1.45;\n overflow: hidden;\n}\n\n.koda-toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 2px;\n padding: 6px 8px;\n background: var(--koda-toolbar-bg);\n border-bottom: 1px solid var(--koda-border);\n flex-shrink: 0;\n}\n\n.koda-btn {\n appearance: none;\n border: 1px solid transparent;\n background: transparent;\n color: var(--koda-fg);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 34px;\n height: 34px;\n padding: 0 8px;\n border-radius: 7px;\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n transition: background 120ms ease, color 120ms ease, border-color 120ms ease;\n}\n.koda-btn svg { width: 18px; height: 18px; display: block; }\n/* \u0421\u0430\u043C\u043E\u043E\u0447\u0435\u0432\u0438\u0434\u043D\u044B\u0435 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0435 \u043A\u043D\u043E\u043F\u043A\u0438: B \u0436\u0438\u0440\u043D\u0430\u044F, I \u043A\u0443\u0440\u0441\u0438\u0432, U \u043F\u043E\u0434\u0447\u0451\u0440\u043A\u043D\u0443\u0442\u0430, S \u0437\u0430\u0447\u0451\u0440\u043A\u043D\u0443\u0442\u0430. */\n.koda-btn[data-tb=\"bold\"] { font-weight: 800; }\n.koda-btn[data-tb=\"italic\"] { font-style: italic; font-family: Georgia, \"Times New Roman\", serif; }\n.koda-btn[data-tb=\"underline\"] { text-decoration: underline; text-underline-offset: 2px; }\n.koda-btn[data-tb=\"strike\"] { text-decoration: line-through; }\n/* \u0412\u044B\u043F\u0430\u0434\u0430\u0448\u043A\u0438 \u043F\u0430\u043D\u0435\u043B\u0438 (\u0441\u0442\u0438\u043B\u044C/\u0448\u0440\u0438\u0444\u0442/\u0440\u0430\u0437\u043C\u0435\u0440) */\n.koda-select {\n appearance: none;\n -webkit-appearance: none;\n border: 1px solid transparent;\n background-color: transparent;\n color: var(--koda-fg);\n height: 34px;\n min-width: 62px;\n max-width: 170px;\n padding: 0 26px 0 9px;\n border-radius: 7px;\n font: 600 13px var(--koda-font);\n cursor: pointer;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 12px;\n transition: background-color 120ms ease, border-color 120ms ease;\n}\n.koda-select:hover { background-color: #efefef; }\n.koda-select:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: 1px; }\n.koda-btn:hover { background: #efefef; }\n.koda-btn:active { transform: translateY(0.5px); }\n.koda-btn:focus-visible {\n outline: 2px solid var(--koda-accent);\n outline-offset: 1px;\n}\n.koda-btn[aria-pressed=\"true\"] {\n /* \u041A\u043E\u043D\u0442\u0440\u0430\u0441\u0442 \u22654.5:1 (#1a3bbf \u043D\u0430 #e4e9ff \u2248 5.4:1) \u2014 WCAG 1.4.3 */\n background: #e4e9ff;\n color: #1a3bbf;\n border-color: #c4ceff;\n}\n.koda-sep {\n width: 1px;\n height: 18px;\n margin: 0 4px;\n background: var(--koda-border);\n}\n\n.koda-content {\n position: relative;\n font-size: 15px;\n line-height: 1.6;\n /* \u0410\u0434\u0430\u043F\u0442\u0438\u0432\u043D\u0430\u044F \u0432\u044B\u0441\u043E\u0442\u0430 (\u0434\u0435\u0444\u043E\u043B\u0442): \u0440\u0430\u0441\u0442\u0451\u0442 \u0441 \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0434\u043E \u0434\u043E\u043B\u0438 \u044D\u043A\u0440\u0430\u043D\u0430, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014\n \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0439 \u0441\u043A\u0440\u043E\u043B\u043B. min(A4, 80vh): \u043D\u0430 \u0432\u044B\u0441\u043E\u043A\u043E\u043C \u044D\u043A\u0440\u0430\u043D\u0435 \u0443\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432 \u043B\u0438\u0441\u0442 A4\n (1123px), \u043D\u0430 \u043D\u0438\u0437\u043A\u043E\u043C \u2014 \u0432 80% \u0432\u044B\u0441\u043E\u0442\u044B \u043E\u043A\u043D\u0430, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432\u043B\u0435\u0437\u0430\u0435\u0442 \u043D\u0430 \u043B\u044E\u0431\u043E\u043C\n \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0435 \u0438 \u0442\u0443\u043B\u0431\u0430\u0440 \u0432\u0441\u0435\u0433\u0434\u0430 \u0432\u0438\u0434\u0435\u043D. \u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --koda-max-height\n (\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 none = autogrow; \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u0430\u044F \u0432\u044B\u0441\u043E\u0442\u0430 = \u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C). */\n overflow-y: auto;\n max-height: var(--koda-max-height, min(1123px, 80vh));\n /* \u00AB\u0421\u0442\u043E\u043B\u00BB \u043F\u043E\u0434 \u043B\u0438\u0441\u0442\u043E\u043C */\n background: #eceef1;\n padding: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n/* \u041B\u0438\u0441\u0442 A4: \u0448\u0438\u0440\u0438\u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B, \u043F\u043E\u043B\u044F, \u0442\u0435\u043D\u044C; \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u043D\u0435\u0431\u043E\u043B\u044C\u0448\u0438\u043C, \u0440\u0430\u0441\u0442\u0451\u0442 \u043F\u043E \u0442\u0435\u043A\u0441\u0442\u0443. */\n.koda-content .ProseMirror {\n width: min(794px, 100%);\n box-sizing: border-box;\n /* \u0425\u043E\u043B\u0441\u0442-\u00AB\u043B\u0438\u0441\u0442\u00BB \u0437\u0430\u043C\u0435\u0442\u043D\u043E \u043A\u0440\u0443\u043F\u043D\u0435\u0435 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0438 \u0430\u0434\u0430\u043F\u0442\u0438\u0432\u0435\u043D: \u0440\u0430\u0441\u0442\u0451\u0442 \u0441 \u0432\u044B\u0441\u043E\u0442\u043E\u0439\n \u043E\u043A\u043D\u0430 (52vh), \u043D\u043E \u0432 \u0440\u0430\u0437\u0443\u043C\u043D\u044B\u0445 \u0433\u0440\u0430\u043D\u0438\u0446\u0430\u0445 320\u2013640px. \u0411\u043E\u043B\u044C\u0448\u0435 \u043C\u0435\u0441\u0442\u0430 \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u0438\n \u0447\u0442\u043E\u0431\u044B \u0432\u044B\u043F\u0430\u0434\u0430\u0448\u043A\u0438/\u0434\u0438\u0430\u043B\u043E\u0433\u0438 \u043E\u0449\u0443\u0449\u0430\u043B\u0438\u0441\u044C \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E. */\n min-height: clamp(320px, 52vh, 640px);\n /* \u0412 flex-\u043A\u043E\u043B\u043E\u043D\u043A\u0435 \u043B\u0438\u0441\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E (flex-shrink:1) \u0443\u0436\u0438\u043C\u0430\u043B\u0441\u044F \u043F\u043E\u0434 \u0432\u044B\u0441\u043E\u0442\u0443\n \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430, \u0438 \u043A\u043E\u043D\u0442\u0435\u043D\u0442 \u0432\u044B\u043B\u0435\u0437\u0430\u043B \u0437\u0430 \u0431\u0435\u043B\u043E\u0435 \u043F\u043E\u043B\u0435. flex-shrink:0 \u2014 \u043B\u0438\u0441\u0442 \u0440\u0430\u0441\u0442\u0451\u0442\n \u043F\u043E \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u043C\u0443, \u0430 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0439 \u0441\u043A\u0440\u043E\u043B\u043B \u0431\u0435\u0440\u0451\u0442 \u043D\u0430 \u0441\u0435\u0431\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 (.koda-content). */\n flex-shrink: 0;\n background: #fff;\n padding: 48px 56px;\n box-shadow: 0 1px 10px rgba(0, 0, 0, 0.12);\n border-radius: 2px;\n}\n/* \u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 \u043D\u0435 \u0432\u044B\u043B\u0435\u0437\u0430\u044E\u0442 \u0437\u0430 \u043B\u0438\u0441\u0442 */\n.koda-content .ProseMirror img {\n max-width: 100%;\n height: auto;\n}\n\n/* \u0413\u0430\u0441\u0438\u043C \u0434\u0435\u0444\u043E\u043B\u0442\u043D\u044B\u0439 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043D\u044B\u0439 outline \u043D\u0430 \u0441\u0430\u043C\u043E\u043C ProseMirror, \u043D\u043E \u0440\u0438\u0441\u0443\u0435\u043C\n \u0432\u0438\u0434\u0438\u043C\u043E\u0435 \u0444\u043E\u043A\u0443\u0441-\u043A\u043E\u043B\u044C\u0446\u043E \u043D\u0430 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435 \u2014 WCAG 2.4.7 / 2.4.11 */\n.koda-content .ProseMirror { outline: none; }\n.koda-content .ProseMirror:focus { outline: none; }\n.koda-content:focus-within {\n outline: 2px solid var(--koda-accent);\n outline-offset: -2px;\n border-radius: 4px;\n}\n\n/* \u0412\u0438\u0437\u0443\u0430\u043B\u044C\u043D\u043E \u0441\u043A\u0440\u044B\u0442\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u0441\u043A\u0440\u0438\u043D\u0440\u0438\u0434\u0435\u0440\u043E\u0432 */\n.koda-sr-only {\n position: absolute;\n width: 1px; height: 1px;\n padding: 0; margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n.koda-content .ProseMirror p { margin: 0 0 0.7em; }\n.koda-content .ProseMirror h1 { font-size: 1.8em; line-height: 1.2; margin: 0.4em 0 0.3em; }\n.koda-content .ProseMirror h2 { font-size: 1.45em; line-height: 1.25; margin: 0.4em 0 0.3em; }\n.koda-content .ProseMirror h3 { font-size: 1.2em; margin: 0.4em 0 0.3em; }\n.koda-content .ProseMirror blockquote {\n margin: 0.6em 0;\n padding: 0.2em 0 0.2em 1em;\n border-left: 3px solid var(--koda-accent);\n color: var(--koda-muted);\n}\n.koda-content .ProseMirror pre {\n background: #0f172a;\n color: #e2e8f0;\n padding: 12px 14px;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 13px;\n}\n.koda-content .ProseMirror code {\n background: #f1f5f9;\n padding: 1px 5px;\n border-radius: 4px;\n font-size: 0.9em;\n}\n.koda-content .ProseMirror pre code { background: transparent; padding: 0; }\n.koda-content .ProseMirror ul,\n.koda-content .ProseMirror ol { padding-left: 1.4em; margin: 0.4em 0; }\n.koda-content .ProseMirror a { color: var(--koda-accent); }\n.koda-content .ProseMirror img { max-width: 100%; height: auto; border-radius: 4px; }\n.koda-content .ProseMirror img.ProseMirror-selectednode { outline: 2px solid var(--koda-accent); }\n\n/* \u0422\u0430\u0431\u043B\u0438\u0446\u044B */\n.koda-content .tableWrapper { overflow-x: auto; margin: 0.6em 0; }\n.koda-content .ProseMirror table {\n border-collapse: collapse;\n table-layout: fixed;\n /* \u0428\u0438\u0440\u0438\u043D\u0443 \u0434\u0435\u0440\u0436\u0438\u0442 ResponsiveTableView (width:100% + colgroup \u0432 %) \u2014 \u0442\u0430\u0431\u043B\u0438\u0446\u0430\n \u0440\u0435\u0437\u0438\u043D\u043E\u0432\u0430\u044F \u043D\u0430 \u0448\u0438\u0440\u0438\u043D\u0443 \u043B\u0438\u0441\u0442\u0430, \u043F\u0440\u043E\u043F\u043E\u0440\u0446\u0438\u0438 \u043A\u043E\u043B\u043E\u043D\u043E\u043A \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B. */\n width: 100%;\n max-width: 100%;\n overflow: hidden;\n}\n.koda-content .ProseMirror td,\n.koda-content .ProseMirror th {\n border: 1px solid var(--koda-border);\n padding: 6px 9px;\n vertical-align: top;\n position: relative;\n min-width: 2em;\n}\n.koda-content .ProseMirror th {\n background: #f4f4f5;\n font-weight: 600;\n text-align: left;\n}\n.koda-content .ProseMirror td > *,\n.koda-content .ProseMirror th > * { margin-bottom: 0; }\n.koda-content .ProseMirror .selectedCell::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: rgba(47, 91, 255, 0.12);\n pointer-events: none;\n}\n.koda-content .ProseMirror .column-resize-handle {\n position: absolute;\n right: -3px;\n top: 0;\n bottom: 0;\n width: 6px;\n background: var(--koda-accent);\n opacity: 0.7;\n border-radius: 3px;\n pointer-events: none;\n z-index: 15;\n}\n.koda-content .ProseMirror.resize-cursor { cursor: col-resize; }\n/* \u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043D\u0430 \u0442\u0430\u0431\u043B\u0438\u0446\u0443 \u043F\u0440\u0430\u0432\u044B\u0435 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u044F\u0447\u0435\u0435\u043A \u0447\u0443\u0442\u044C \u043F\u043E\u0434\u0441\u0432\u0435\u0447\u0438\u0432\u0430\u044E\u0442\u0441\u044F */\n.koda-content .ProseMirror table:hover td,\n.koda-content .ProseMirror table:hover th { transition: border-color 120ms ease; }\n\n/* \u041F\u043B\u0435\u0439\u0441\u0445\u043E\u043B\u0434\u0435\u0440: \u043A\u043B\u0430\u0441\u0441 is-empty + data-placeholder \u0432\u0435\u0448\u0430\u0435\u0442 \u043F\u043B\u0430\u0433\u0438\u043D \u0434\u0435\u043A\u043E\u0440\u0430\u0446\u0438\u0439\n \u043D\u0430 \u043F\u0443\u0441\u0442\u043E\u0439 \u043F\u0435\u0440\u0432\u044B\u0439 \u0431\u043B\u043E\u043A (\u0441\u043C. placeholderPlugin \u0432 editor.ts) */\n.koda-content .ProseMirror .is-empty::before {\n content: attr(data-placeholder);\n color: var(--koda-muted);\n pointer-events: none;\n height: 0;\n float: left;\n}\n\n/* \u0412\u043E\u0434\u044F\u043D\u043E\u0439 \u0437\u043D\u0430\u043A \u0434\u0435\u043C\u043E/\u043D\u0435\u0432\u0430\u043B\u0438\u0434\u043D\u043E\u0439 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0438.\n \u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C: \u0434\u0438\u0430\u0433\u043E\u043D\u0430\u043B\u044C\u043D\u043E\u0435 \u0437\u0430\u043C\u043E\u0449\u0435\u043D\u0438\u0435 \u043F\u043E \u0432\u0441\u0435\u0439 \u043A\u0430\u043D\u0432\u0435 (\u0444\u043E\u043D \u0440\u0430\u0441\u0442\u0451\u0442 \u0441\n \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u043C \u0438 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u0432\u0441\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u043F\u0440\u0438 \u0441\u043A\u0440\u043E\u043B\u043B\u0435) + sticky-\u043F\u0438\u043B\u044E\u043B\u044F \u0441\u0432\u0435\u0440\u0445\u0443 \u0434\u043B\u044F\n \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u0438 (role=status). pointer-events:none \u2014 \u043F\u0435\u0447\u0430\u0442\u0430\u0442\u044C \u043C\u043E\u0436\u043D\u043E. */\n.koda-content.koda-demo .ProseMirror {\n background-image: var(--koda-wm);\n background-repeat: repeat;\n /* \u0427\u0442\u043E\u0431\u044B \u0437\u0430\u043C\u043E\u0449\u0435\u043D\u0438\u0435 \u043F\u0435\u0447\u0430\u0442\u0430\u043B\u043E\u0441\u044C (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u044B \u0433\u0430\u0441\u044F\u0442 \u0444\u043E\u043D \u043F\u0440\u0438 \u043F\u0435\u0447\u0430\u0442\u0438). */\n -webkit-print-color-adjust: exact;\n print-color-adjust: exact;\n}\n.koda-watermark {\n position: sticky;\n top: 8px;\n z-index: 30;\n width: fit-content;\n max-width: 92%;\n margin: 0 auto 6px;\n padding: 5px 14px;\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.03em;\n color: #b91c1c;\n background: #fff1f2;\n border: 1px solid #fecaca;\n border-radius: 999px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n text-align: center;\n pointer-events: none;\n}\n\n/* Slash-\u043C\u0435\u043D\u044E (Notion-UX) */\n.koda-slash {\n position: absolute;\n z-index: 20;\n min-width: 210px;\n max-height: 280px;\n overflow-y: auto;\n background: #fff;\n border: 1px solid var(--koda-border);\n border-radius: 10px;\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.12);\n padding: 4px;\n}\n.koda-slash-item {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 7px 9px;\n border: none;\n background: transparent;\n border-radius: 7px;\n font-size: 14px;\n text-align: left;\n cursor: pointer;\n color: var(--koda-fg);\n}\n.koda-slash-item:hover,\n.koda-slash-item.is-active { background: var(--koda-accent-soft); }\n.koda-slash-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: 1px solid var(--koda-border);\n border-radius: 6px;\n font-size: 12px;\n font-weight: 600;\n color: var(--koda-muted);\n flex: none;\n}\n\n/* Floating toolbar (Notion-UX) */\n.koda-floating {\n position: absolute;\n z-index: 20;\n display: flex;\n gap: 2px;\n background: #1a1a1f;\n border-radius: 8px;\n padding: 4px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);\n}\n.koda-floating .koda-btn { color: #fff; height: 28px; min-width: 28px; }\n.koda-floating .koda-btn:hover { background: rgba(255, 255, 255, 0.14); }\n.koda-floating .koda-btn[aria-pressed=\"true\"] {\n background: var(--koda-accent);\n color: #fff;\n border-color: var(--koda-accent);\n}\n\n/* \u041A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u043D\u0430\u044F \u043F\u0430\u043D\u0435\u043B\u044C \u0442\u0430\u0431\u043B\u0438\u0446\u044B \u2014 \u0432\u0441\u043F\u043B\u044B\u0432\u0430\u0435\u0442 \u043D\u0430\u0434 \u0442\u0430\u0431\u043B\u0438\u0446\u0435\u0439, \u043A\u043E\u0433\u0434\u0430 \u043A\u0443\u0440\u0441\u043E\u0440 \u0432\u043D\u0443\u0442\u0440\u0438 */\n.koda-table-toolbar {\n position: absolute;\n z-index: 19;\n display: flex;\n gap: 2px;\n background: #1a1a1f;\n border-radius: 8px;\n padding: 4px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);\n}\n.koda-table-toolbar .koda-btn { color: #fff; height: 28px; min-width: 28px; }\n.koda-table-toolbar .koda-btn:hover { background: rgba(255, 255, 255, 0.14); }\n\n/* \u0414\u0438\u0430\u043B\u043E\u0433 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0430\u043D\u0435\u043B\u0438 (\u00AB\u043A\u0430\u0440\u0430\u043D\u0434\u0430\u0448\u00BB) */\n.koda-customizer-list {\n max-height: 320px; overflow-y: auto;\n display: flex; flex-direction: column; gap: 4px; margin: 10px 0;\n}\n.koda-customizer-row {\n display: flex; align-items: center; gap: 6px;\n padding: 4px 6px; border: 1px solid #e2e2e6; border-radius: 6px; background: #fff;\n}\n.koda-customizer-ctl {\n width: 26px; height: 26px; flex: 0 0 auto;\n border: 1px solid #e2e2e6; border-radius: 4px; background: #fff; cursor: pointer;\n font-size: 12px; line-height: 1;\n}\n.koda-customizer-ctl:hover:not(:disabled) { background: #f2f2f5; }\n.koda-customizer-ctl:disabled { opacity: 0.3; cursor: default; }\n.koda-customizer-label { margin-left: 6px; }\n.koda-customizer-add { min-width: 160px; height: 34px; }\n\n/* \u0414\u0438\u0430\u043B\u043E\u0433 (\u0432\u0441\u0442\u0430\u0432\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438) */\n.koda-loading {\n position: absolute; inset: 0; z-index: 40;\n display: flex; align-items: center; justify-content: center;\n background: rgba(255,255,255,0.6);\n}\n.koda-loading-box {\n display: flex; align-items: center; gap: 10px;\n background: #111827; color: #fff; padding: 12px 18px;\n border-radius: 10px; font-size: 14px; box-shadow: 0 8px 30px rgba(0,0,0,0.25);\n}\n.koda-spinner {\n width: 16px; height: 16px; border-radius: 50%;\n border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff;\n animation: koda-spin 0.7s linear infinite;\n}\n@keyframes koda-spin { to { transform: rotate(360deg); } }\n/* \u0422\u0440\u0430\u043D\u0437\u0438\u0435\u043D\u0442\u043D\u0430\u044F \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430 (toast) \u2014 \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u00AB\u0432\u044B\u0434\u0435\u043B\u0438\u0442\u0435 2+ \u044F\u0447\u0435\u0439\u043A\u0438\u00BB. */\n.koda-toast {\n position: absolute;\n left: 50%;\n bottom: 16px;\n transform: translateX(-50%);\n z-index: 80;\n max-width: 90%;\n padding: 9px 14px;\n background: #1f2937;\n color: #fff;\n font: 500 13px var(--koda-font);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);\n opacity: 0;\n animation: koda-toast-in 180ms ease-out forwards;\n pointer-events: none;\n}\n@keyframes koda-toast-in {\n from { opacity: 0; transform: translate(-50%, 8px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n.koda-dialog-backdrop {\n /* fixed (\u043D\u0435 absolute): \u0434\u0438\u0430\u043B\u043E\u0433 \u0446\u0435\u043D\u0442\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0432\u043E \u0412\u042C\u042E\u041F\u041E\u0420\u0422\u0415 \u0438 \u043D\u0435 \u043E\u0431\u0440\u0435\u0437\u0430\u0435\u0442\u0441\u044F\n \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u043C .euz{overflow:hidden} \u043F\u0440\u0438 \u043A\u043E\u0440\u043E\u0442\u043A\u043E\u043C \u0445\u043E\u043B\u0441\u0442\u0435. z-index \u0432\u044B\u0448\u0435 \u0432\u0441\u0435\u0445\n \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0445 \u043E\u0432\u0435\u0440\u043B\u0435\u0435\u0432 (\u043C\u0435\u043D\u044E/\u0442\u0443\u043B\u0431\u0430\u0440), \u0447\u0442\u043E\u0431\u044B \u0431\u044B\u0442\u044C \u043F\u043E\u0432\u0435\u0440\u0445. */\n position: fixed;\n inset: 0;\n z-index: 2147483001;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.18);\n}\n.koda-dialog {\n width: min(460px, 92%);\n background: #fff;\n border: 1px solid var(--koda-border);\n border-radius: 14px;\n box-shadow: 0 20px 60px rgba(15, 23, 42, 0.22);\n padding: 22px;\n font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;\n}\n.koda-dialog-title {\n font-weight: 700;\n font-size: 16px;\n line-height: 1.3;\n color: #0f172a;\n letter-spacing: -0.01em;\n margin: 0 0 16px;\n}\n.koda-dialog-input {\n width: 100%;\n box-sizing: border-box;\n padding: 10px 12px;\n border: 1px solid var(--koda-border);\n border-radius: 9px;\n font-size: 14px;\n color: #0f172a;\n outline: none;\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n.koda-dialog-input::placeholder { color: #94a3b8; }\n.koda-dialog-input:focus {\n border-color: var(--koda-accent);\n box-shadow: 0 0 0 3px rgba(47, 107, 255, 0.18);\n}\n.koda-dialog-row {\n display: flex;\n flex-wrap: wrap; /* \u043C\u043D\u043E\u0433\u043E \u043A\u043D\u043E\u043F\u043E\u043A (\u044D\u043A\u0441\u043F\u043E\u0440\u0442: 7) \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u044F\u0442\u0441\u044F, \u0430 \u043D\u0435 \u0432\u044B\u043B\u0435\u0437\u0430\u044E\u0442 \u0437\u0430 \u043F\u043E\u043F\u0430\u043F */\n gap: 8px;\n justify-content: center;\n margin-top: 18px;\n}\n.koda-dialog-btn {\n min-width: 96px;\n padding: 10px 16px;\n border: 1px solid var(--koda-border);\n background: #fff;\n color: #1f2937;\n border-radius: 10px;\n font-size: 13px;\n font-weight: 600;\n white-space: nowrap;\n text-align: center;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s, transform 0.08s, box-shadow 0.15s;\n}\n.koda-dialog-btn:hover { background: #f8fafc; border-color: #cbd5e1; }\n.koda-dialog-btn:active { transform: translateY(1px); }\n.koda-dialog-btn:focus-visible {\n outline: 2px solid var(--koda-accent);\n outline-offset: 2px;\n}\n.koda-dialog-btn.primary {\n background: var(--koda-accent);\n color: #fff;\n border-color: var(--koda-accent);\n box-shadow: 0 2px 10px rgba(47, 107, 255, 0.3);\n}\n.koda-dialog-btn.primary:hover { background: var(--koda-accent); filter: brightness(1.06); }\n\n/* \u0420\u0435\u0441\u0430\u0439\u0437 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A: \u043E\u0431\u0451\u0440\u0442\u043A\u0430 + \u0440\u0443\u0447\u043A\u0430 \u0432 \u043F\u0440\u0430\u0432\u043E\u043C-\u043D\u0438\u0436\u043D\u0435\u043C \u0443\u0433\u043B\u0443 */\n.koda-content .ProseMirror .koda-img-wrap { display: inline-block; position: relative; line-height: 0; max-width: 100%; }\n.koda-content .ProseMirror .koda-img-wrap img { display: inline-block; max-width: 100%; height: auto; vertical-align: bottom; }\n.koda-content .ProseMirror .koda-img-resize {\n position: absolute; right: 2px; bottom: 2px; width: 12px; height: 12px;\n background: var(--koda-accent); border: 2px solid #fff; border-radius: 2px;\n box-sizing: border-box; cursor: nwse-resize; display: none; z-index: 2;\n}\n.koda-content .ProseMirror .koda-img-wrap:hover .koda-img-resize,\n.koda-content .ProseMirror .koda-img-wrap.ProseMirror-selectednode .koda-img-resize { display: block; }\n.koda-content .ProseMirror .koda-img-crop {\n position: absolute; left: 2px; top: 2px; width: 20px; height: 20px;\n background: rgba(0,0,0,0.55); color: #fff; border-radius: 3px;\n font-size: 13px; line-height: 20px; text-align: center; cursor: pointer;\n display: none; z-index: 2; user-select: none;\n}\n.koda-content .ProseMirror .koda-img-wrap:hover .koda-img-crop,\n.koda-content .ProseMirror .koda-img-wrap.ProseMirror-selectednode .koda-img-crop { display: block; }\n.koda-content .ProseMirror .koda-crop-overlay {\n position: absolute; inset: 0; z-index: 5; cursor: crosshair;\n background: rgba(0,0,0,0.30);\n}\n.koda-content .ProseMirror .koda-crop-sel {\n position: absolute; display: none; border: 1px dashed #fff;\n box-shadow: 0 0 0 9999px rgba(0,0,0,0.45); pointer-events: none;\n}\n.koda-content .ProseMirror .koda-crop-bar {\n position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%);\n display: flex; gap: 6px; z-index: 6;\n}\n\n/* \u041D\u0430\u0439\u0442\u0438/\u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C */\n.koda-search { display: flex; flex-direction: column; gap: 6px; padding: 8px; background: var(--koda-toolbar-bg); border-bottom: 1px solid var(--koda-border); flex-shrink: 0; }\n.koda-search[hidden] { display: none; }\n.koda-search-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }\n.koda-search-input { flex: 1 1 160px; min-width: 110px; height: 30px; box-sizing: border-box; padding: 0 9px; border: 1px solid var(--koda-border); border-radius: 7px; font: 400 14px var(--koda-font); color: var(--koda-fg); background: #fff; }\n.koda-search-input:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: 1px; }\n.koda-search-count { font: 600 12px var(--koda-font); color: var(--koda-muted); min-width: 42px; text-align: center; }\n.koda-search-btn { height: 30px; min-width: 30px; box-sizing: border-box; padding: 0 10px; border: 1px solid var(--koda-border); background: #fff; border-radius: 7px; font: 600 13px var(--koda-font); color: var(--koda-fg); cursor: pointer; transition: background 120ms ease; }\n.koda-search-btn:hover { background: #efefef; }\n.koda-search-btn:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: 1px; }\n.koda-content .ProseMirror .koda-search-match { background: rgba(255, 213, 0, 0.45); border-radius: 2px; }\n.koda-content .ProseMirror .koda-search-active { background: #ff9632; border-radius: 2px; }\n\n/* \u041F\u043E\u043B\u043D\u043E\u044D\u043A\u0440\u0430\u043D\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C (maximize \u0431\u0435\u0437 Fullscreen API \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u043C) */\n:host([data-fullscreen]) { position: fixed; inset: 0; z-index: 2147483000; background: var(--koda-bg); }\n:host([data-fullscreen]) .euz { height: 100%; border-radius: 0; border: none; }\n:host([data-fullscreen]) .koda-content { max-height: none; flex: 1 1 auto; }\n\n/* === \u041A\u043D\u043E\u043F\u043A\u0430-\u0433\u0440\u0443\u043F\u043F\u0430 \u043D\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 (\u0440\u0430\u0441\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043C\u0435\u043D\u044E) === */\n.koda-grp-btn { display: inline-flex; align-items: center; gap: 5px; width: auto; padding: 0 8px; }\n.koda-grp-btn .koda-grp-label { font: 600 13px var(--koda-font); white-space: nowrap; }\n.koda-grp-btn .koda-grp-caret { font-size: 10px; opacity: 0.7; }\n.koda-grp-logo { width: 18px; height: 18px; object-fit: contain; display: block; }\n.koda-grp-emoji { font-size: 16px; line-height: 1; }\n.koda-grp-menu { position: fixed; z-index: 2147483000; min-width: 180px; max-height: 92vh; overflow-y: auto; padding: 5px; background: var(--koda-bg); border: 1px solid var(--koda-border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.14); display: flex; flex-direction: column; gap: 2px; }\n.koda-grp-menu-item { display: flex; align-items: center; gap: 8px; padding: 3px 6px; border-radius: 7px; cursor: pointer; }\n.koda-grp-menu-item:hover { background: var(--koda-accent-soft); }\n.koda-grp-menu-item .koda-btn { pointer-events: none; }\n.koda-grp-menu-label { font: 500 13px var(--koda-font); color: var(--koda-fg); }\n\n/* === \u0420\u0435\u0436\u0438\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u043F\u0430\u043D\u0435\u043B\u0438 (\u043A\u0430\u0440\u0430\u043D\u0434\u0430\u0448) === */\n.koda-edit { display: flex; flex-direction: column; gap: 8px; padding: 10px; background: var(--koda-toolbar-bg); border-bottom: 1px solid var(--koda-border); }\n.koda-edit-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n.koda-edit-title { font: 700 14px var(--koda-font); color: var(--koda-fg); }\n.koda-edit-spacer { flex: 1 1 auto; }\n.koda-edit-action { height: 32px; padding: 0 12px; border: 1px solid var(--koda-border); background: #fff; border-radius: 8px; font: 600 13px var(--koda-font); color: var(--koda-fg); cursor: pointer; transition: background 120ms ease, border-color 120ms ease; }\n.koda-edit-action:hover { background: #f1f5ff; border-color: var(--koda-accent); }\n.koda-edit-action--primary { background: var(--koda-accent); border-color: var(--koda-accent); color: #fff; }\n.koda-edit-action--primary:hover { background: #2347d8; }\n.koda-edit-action--ghost { color: var(--koda-muted); }\n.koda-edit-hint { font: 400 12px var(--koda-font); color: var(--koda-muted); }\n.koda-edit-row, .koda-palette { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 6px; padding: 8px; border: 1px dashed var(--koda-border); border-radius: 10px; background: var(--koda-bg); min-height: 46px; }\n.koda-palette { background: #f6f7fb; border-style: solid; }\n.koda-palette-title { font: 600 12px var(--koda-font); color: var(--koda-muted); margin-top: 2px; }\n\n/* \u0424\u0438\u0448\u043A\u0430 \u043A\u043D\u043E\u043F\u043A\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 */\n.koda-chip { position: relative; display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 10px; border: 1px solid var(--koda-border); border-radius: 8px; background: #fff; font: 500 13px var(--koda-font); color: var(--koda-fg); cursor: grab; user-select: none; touch-action: none; transition: box-shadow 120ms ease, border-color 120ms ease; }\n.koda-chip:hover { border-color: var(--koda-accent); box-shadow: 0 1px 4px rgba(47,91,255,0.12); }\n.koda-chip:active { cursor: grabbing; }\n.koda-chip .koda-chip-ic { display: inline-flex; width: 18px; height: 18px; }\n.koda-chip .koda-chip-ic svg { width: 18px; height: 18px; }\n.koda-chip-lb { white-space: nowrap; }\n.koda-chip--sep { padding: 0 12px; }\n.koda-chip-bar { display: inline-block; width: 2px; height: 18px; background: var(--koda-border); border-radius: 1px; }\n.koda-chip-x { position: absolute; top: -7px; right: -7px; width: 18px; height: 18px; padding: 0; border: none; border-radius: 50%; background: #ef4444; color: #fff; font: 700 13px/16px var(--koda-font); cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 1px 3px rgba(0,0,0,0.25); }\n.koda-chip:hover > .koda-chip-x { display: inline-flex; }\n.koda-chip--pal { background: #fff; cursor: grab; }\n.koda-chip--ghost { position: fixed; z-index: 9999; pointer-events: none; opacity: 0.92; box-shadow: 0 8px 24px rgba(0,0,0,0.22); border-color: var(--koda-accent); margin: 0; }\n.koda-chip--placeholder { opacity: 0.35; border-style: dashed; }\n\n/* \u041A\u0430\u043F\u0441\u0443\u043B\u0430 \u0433\u0440\u0443\u043F\u043F\u044B \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 */\n.koda-grp-chip { flex-direction: column; align-items: stretch; gap: 6px; padding: 8px; height: auto; background: var(--koda-accent-soft); border-color: #c7d2fe; cursor: default; }\n.koda-grp-head { display: flex; align-items: center; gap: 6px; cursor: grab; }\n.koda-grp-head .koda-grp-ic { width: 18px; height: 18px; }\n.koda-grp-head .koda-grp-ic svg, .koda-grp-head .koda-grp-ic img { width: 18px; height: 18px; }\n.koda-grp-lb { font: 600 13px var(--koda-font); flex: 1 1 auto; white-space: nowrap; }\n.koda-grp-ctl { width: 22px; height: 22px; padding: 0; border: 1px solid transparent; border-radius: 6px; background: rgba(255,255,255,0.6); color: var(--koda-fg); cursor: pointer; font-size: 13px; line-height: 1; }\n.koda-grp-ctl:hover { border-color: var(--koda-accent); background: #fff; }\n.koda-grp-items { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; min-width: 60px; min-height: 40px; padding: 5px; border: 1px dashed #c7d2fe; border-radius: 7px; background: rgba(255,255,255,0.55); }\n\n/* \u041B\u043E\u0433\u043E-\u043F\u0438\u043A\u0435\u0440 \u0433\u0440\u0443\u043F\u043F\u044B */\n.koda-iconpick { max-width: 460px; }\n.koda-iconpick-top { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }\n.koda-iconpick-preview { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; border: 1px solid var(--koda-border); border-radius: 9px; background: var(--koda-toolbar-bg); flex-shrink: 0; }\n.koda-iconpick-preview svg, .koda-iconpick-preview img { width: 24px; height: 24px; }\n.koda-iconpick-preview .koda-grp-emoji { font-size: 22px; }\n.koda-iconpick-top .koda-dialog-input { flex: 1 1 auto; }\n.koda-iconpick-tabs { display: flex; gap: 4px; margin-bottom: 8px; }\n.koda-iconpick-tab { height: 30px; padding: 0 12px; border: 1px solid var(--koda-border); background: #fff; border-radius: 7px; font: 600 12px var(--koda-font); color: var(--koda-muted); cursor: pointer; }\n.koda-iconpick-tab.is-active { background: var(--koda-accent); border-color: var(--koda-accent); color: #fff; }\n.koda-iconpick-grid { display: flex; flex-wrap: wrap; gap: 6px; max-height: 220px; overflow-y: auto; padding: 4px; }\n.koda-iconpick-cell { width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--koda-border); border-radius: 8px; background: #fff; cursor: pointer; }\n.koda-iconpick-cell:hover { border-color: var(--koda-accent); background: var(--koda-accent-soft); }\n.koda-iconpick-cell.is-sel { border-color: var(--koda-accent); box-shadow: 0 0 0 2px var(--koda-accent-soft); }\n.koda-iconpick-cell svg { width: 22px; height: 22px; }\n.koda-iconpick-cell .koda-grp-emoji { font-size: 20px; }\n.koda-iconpick-file { font: 400 13px var(--koda-font); }\n\n/* === \u0412\u0435\u0440\u0445\u043D\u0435\u0435 \u043C\u0435\u043D\u044E (menubar) === */\n.koda-menubar { display: flex; align-items: stretch; gap: 1px; padding: 2px 6px; background: var(--koda-bg); border-bottom: 1px solid var(--koda-border); flex-wrap: wrap; }\n.koda-menu-top { height: 30px; padding: 0 11px; border: none; background: transparent; border-radius: 6px; font: 500 13.5px var(--koda-font); color: var(--koda-fg); cursor: pointer; }\n.koda-menu-top:hover { background: var(--koda-toolbar-bg); }\n.koda-menu-top.is-active { background: var(--koda-accent-soft); color: var(--koda-accent); }\n.koda-menu-top:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: -2px; }\n\n/* \u0412\u044B\u043F\u0430\u0434\u0430\u044E\u0449\u0430\u044F \u043F\u0430\u043D\u0435\u043B\u044C \u043C\u0435\u043D\u044E (\u0438 \u043F\u043E\u0434\u043C\u0435\u043D\u044E, \u0438 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u043D\u043E\u0435 \u043C\u0435\u043D\u044E) */\n/* fixed: \u043C\u0435\u043D\u044E \u043F\u043E\u0437\u0438\u0446\u0438\u043E\u043D\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u043F\u043E \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0443 (menubar.ts) \u0438 \u043D\u0435 \u0440\u0435\u0436\u0435\u0442\u0441\u044F \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u043C\n \u0445\u043E\u043B\u0441\u0442\u043E\u043C. max-height/overflow \u2014 \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044E \u0432\u044B\u0448\u0435 \u043E\u043A\u043D\u0430, \u043E\u043D\u043E \u0441\u043A\u0440\u043E\u043B\u043B\u0438\u0442\u0441\u044F. */\n.koda-menu { position: fixed; z-index: 2147483000; min-width: 190px; max-width: 320px; max-height: 92vh; overflow-y: auto; padding: 5px; background: var(--koda-bg); border: 1px solid var(--koda-border); border-radius: 10px; box-shadow: 0 12px 34px rgba(0,0,0,0.16); display: flex; flex-direction: column; gap: 1px; }\n.koda-menu-item { display: flex; align-items: center; gap: 9px; width: 100%; height: 32px; padding: 0 9px; border: none; background: transparent; border-radius: 7px; font: 500 13px var(--koda-font); color: var(--koda-fg); cursor: pointer; text-align: left; }\n.koda-menu-item:hover:not(:disabled) { background: var(--koda-accent-soft); }\n.koda-menu-item:disabled { opacity: 0.4; cursor: default; }\n.koda-menu-item:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: -2px; }\n.koda-menu-ic { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex-shrink: 0; color: var(--koda-muted); }\n.koda-menu-ic svg { width: 17px; height: 17px; }\n.koda-menu-lb { flex: 1 1 auto; white-space: nowrap; }\n.koda-menu-arrow { margin-left: auto; font-size: 11px; color: var(--koda-muted); }\n.koda-menu-sep { height: 1px; margin: 4px 6px; background: var(--koda-border); }\n\n/* === \u0412\u0441\u0442\u0430\u0432\u043A\u0430 \u0442\u0430\u0431\u043B\u0438\u0446\u044B: grid-picker (\u043A\u0430\u043A \u0432 Word) === */\n/* Picker \u041D\u0415 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u0448\u0438\u0440\u0438\u043D\u0443 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E \u0434\u0438\u0430\u043B\u043E\u0433\u0430 (460px) \u2014 \u0443\u0436\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u043F\u043E\u0434 \u0433\u0440\u0438\u0434. */\n.koda-tablepicker { display: inline-block; width: auto; max-width: 92vw; }\n/* \u0411\u0415\u0417 overflow/max-width: \u0438\u043D\u0430\u0447\u0435 \u0441\u0435\u0442\u043A\u0430-\u0432\u044B\u0431\u043E\u0440 \u0440\u0435\u0437\u0430\u043B\u0430\u0441\u044C \u0441\u043F\u0440\u0430\u0432\u0430 (scroll-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u043D\u0430\n 2px \u0443\u0436\u0435 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430). \u0414\u0438\u0430\u043B\u043E\u0433 .koda-tablepicker \u0438\u043C\u0435\u0435\u0442 width:auto \u0438 \u0441\u0430\u043C \u0440\u0430\u0441\u0442\u0451\u0442 \u043F\u043E\u0434\n \u0441\u0435\u0442\u043A\u0443 \u2014 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0430\u0434\u0430\u043F\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044F, \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u043E\u0431\u0440\u0435\u0437\u0430\u0435\u0442\u0441\u044F. */\n.koda-tp-grid { display: grid; gap: 2px; margin: 6px 0 4px; user-select: none; justify-content: start; }\n/* box-sizing:border-box \u2014 \u0431\u043E\u0440\u0434\u044E\u0440 \u0432\u0445\u043E\u0434\u0438\u0442 \u0432 18px, \u0438\u043D\u0430\u0447\u0435 \u044F\u0447\u0435\u0439\u043A\u0430 \u0440\u0435\u043D\u0434\u0435\u0440\u0438\u0442\u0441\u044F 20px,\n \u043D\u0435 \u0432\u043B\u0435\u0437\u0430\u0435\u0442 \u0432 18px-\u0442\u0440\u0435\u043A \u0433\u0440\u0438\u0434\u0430, \u0438 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0441\u0442\u043E\u043B\u0431\u0435\u0446 \u0440\u0435\u0436\u0435\u0442\u0441\u044F \u0441\u043F\u0440\u0430\u0432\u0430 \u043D\u0430 2px. */\n.koda-tp-cell { box-sizing: border-box; width: 18px; height: 18px; border: 1px solid var(--koda-border); border-radius: 2px; background: #fff; cursor: pointer; transition: background 60ms; }\n.koda-tp-cell.on { background: var(--koda-accent-soft); border-color: var(--koda-accent); }\n.koda-tp-label { text-align: center; font: 600 13px var(--koda-font); color: var(--koda-fg); margin-bottom: 6px; min-height: 18px; }\n.koda-tp-manual { display: flex; align-items: flex-end; gap: 10px; margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--koda-border); }\n.koda-tp-field { display: flex; flex-direction: column; gap: 3px; font: 500 12px var(--koda-font); color: var(--koda-muted); }\n.koda-tp-field input { width: 64px; height: 30px; box-sizing: border-box; padding: 0 8px; border: 1px solid var(--koda-border); border-radius: 7px; font: 400 13px var(--koda-font); color: var(--koda-fg); background: #fff; }\n.koda-tp-manual .koda-dialog-btn { margin-left: auto; }\n\n/* === \u0414\u0438\u0430\u043B\u043E\u0433\u0438 \u0441\u0432\u043E\u0439\u0441\u0442\u0432 \u0442\u0430\u0431\u043B\u0438\u0446\u044B / \u044F\u0447\u0435\u0439\u043A\u0438 (A3) === */\n.koda-props { min-width: 320px; max-width: 380px; }\n.koda-props-row { display: flex; align-items: center; gap: 12px; margin: 8px 0; }\n.koda-props-label { flex: 0 0 130px; font: 500 13px var(--koda-font); color: var(--koda-fg); }\n.koda-props-field { flex: 1 1 auto; height: 32px; box-sizing: border-box; padding: 0 8px; border: 1px solid var(--koda-border); border-radius: 7px; font: 400 13px var(--koda-font); color: var(--koda-fg); background: #fff; }\ninput.koda-props-field[type=color] { padding: 2px; height: 32px; cursor: pointer; }\ninput.koda-props-field[type=number] { max-width: 90px; }\n.koda-props-bg { display: flex; align-items: center; gap: 10px; border: none; padding: 0; height: auto; }\n.koda-props-bg input[type=color] { flex: 0 0 48px; }\n.koda-props-bgoff { display: inline-flex; align-items: center; gap: 4px; font: 400 12.5px var(--koda-font); color: var(--koda-muted); white-space: nowrap; }\n.koda-props-check { width: 15px; height: 15px; }\n\n/* === \u041F\u043E\u043B\u044F \u0448\u0430\u0431\u043B\u043E\u043D\u0430 (merge fields) \u0434\u043B\u044F \u042D\u0414\u041E (B1) === */\n.koda-content .ProseMirror .koda-merge-field {\n display: inline-block; padding: 1px 8px; margin: 0 1px; border-radius: 5px;\n background: var(--koda-accent-soft); color: var(--koda-accent);\n border: 1px solid color-mix(in oklab, var(--koda-accent) 35%, transparent);\n font: 600 0.92em var(--koda-font); white-space: nowrap; cursor: default;\n user-select: all; vertical-align: baseline;\n}\n.koda-content .ProseMirror .koda-merge-field::before { content: '\u2B21 '; opacity: 0.6; font-size: 0.85em; }\n.koda-merge-picker { min-width: 300px; max-width: 420px; }\n.koda-merge-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 6px 0 12px; }\n.koda-merge-pick {\n display: flex; flex-direction: column; align-items: flex-start; gap: 2px;\n padding: 8px 10px; border: 1px solid var(--koda-border); border-radius: 8px;\n background: #fff; cursor: pointer; text-align: left; transition: border-color 120ms ease, background 120ms ease;\n}\n.koda-merge-pick:hover { border-color: var(--koda-accent); background: var(--koda-accent-soft); }\n.koda-merge-pick-lb { font: 600 13px var(--koda-font); color: var(--koda-fg); }\n.koda-merge-pick-key { font: 400 11px ui-monospace, monospace; color: var(--koda-muted); }\n\n/* === \u0421\u043D\u043E\u0441\u043A\u0438 (B2): \u0430\u0432\u0442\u043E-\u043D\u0443\u043C\u0435\u0440\u0430\u0446\u0438\u044F CSS-\u0441\u0447\u0451\u0442\u0447\u0438\u043A\u043E\u043C === */\n.koda-content .ProseMirror { counter-reset: koda-fn; }\n.koda-content .ProseMirror .koda-footnote {\n counter-increment: koda-fn; cursor: help; color: var(--koda-accent);\n font-weight: 700; vertical-align: super; font-size: 0.72em; padding: 0 1px; user-select: all;\n}\n.koda-content .ProseMirror .koda-footnote::after { content: counter(koda-fn); }\n\n/* === \u041E\u0433\u043B\u0430\u0432\u043B\u0435\u043D\u0438\u0435 (B2) === */\n.koda-content .ProseMirror .koda-toc {\n border: 1px solid var(--koda-border); border-left: 3px solid var(--koda-accent);\n border-radius: 8px; padding: 12px 16px; margin: 14px 0; background: var(--koda-toolbar-bg);\n user-select: none;\n}\n.koda-toc-title { font: 700 14px var(--koda-font); color: var(--koda-fg); margin-bottom: 8px; }\n.koda-toc-item { font: 400 13px var(--koda-font); padding: 2px 0; cursor: pointer; color: var(--koda-fg); line-height: 1.5; }\n.koda-toc-item:hover { color: var(--koda-accent); }\n.koda-toc-num { color: var(--koda-muted); font-weight: 600; }\n.koda-toc-empty { color: var(--koda-muted); font-size: 13px; }\n\n/* === \u0413\u0430\u043B\u0435\u0440\u0435\u044F \u0441\u0442\u0438\u043B\u0435\u0439 (\u0441\u0438\u0441\u0442\u0435\u043C\u0430 \u0441\u0442\u0438\u043B\u0435\u0439) === */\n.koda-style-gallery {\n position: absolute; z-index: 60; min-width: 220px; max-height: 380px; overflow-y: auto;\n padding: 5px; background: var(--koda-bg); border: 1px solid var(--koda-border);\n border-radius: 10px; box-shadow: 0 12px 34px rgba(0,0,0,0.16);\n display: flex; flex-direction: column; gap: 2px;\n}\n.koda-style-item { display: block; width: 100%; padding: 7px 11px; border: none; background: transparent; border-radius: 7px; cursor: pointer; text-align: left; }\n.koda-style-item:hover { background: var(--koda-accent-soft); }\n.koda-style-item:focus-visible { outline: 2px solid var(--koda-accent); outline-offset: -2px; }\n.koda-style-prev { display: block; color: var(--koda-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n\n/* === \u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u0440\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442\u0430 (C1) === */\n.koda-content .ProseMirror .koda-comment {\n background: rgba(255, 200, 0, 0.28); border-bottom: 2px solid rgba(230, 160, 0, 0.65);\n cursor: pointer; border-radius: 2px;\n}\n.koda-content .ProseMirror .koda-comment:hover { background: rgba(255, 200, 0, 0.45); }\n.koda-comment-popup {\n position: absolute; z-index: 65; max-width: 280px; min-width: 160px; padding: 10px 12px;\n background: var(--koda-bg); border: 1px solid var(--koda-border); border-radius: 10px;\n box-shadow: 0 10px 30px rgba(0,0,0,0.16);\n}\n.koda-comment-text { font: 400 13px var(--koda-font); color: var(--koda-fg); margin-bottom: 8px; white-space: pre-wrap; }\n.koda-comment-resolve {\n height: 28px; padding: 0 12px; border: 1px solid var(--koda-border); background: var(--koda-accent-soft);\n color: var(--koda-accent); border-radius: 7px; font: 600 12px var(--koda-font); cursor: pointer;\n}\n.koda-comment-resolve:hover { background: var(--koda-accent); color: #fff; }\n.koda-dialog-hint { font: 400 12px var(--koda-font); color: #ef4444; margin: 4px 0 0; min-height: 14px; }\n\n/* === Track changes (C2): \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u043C\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 === */\n.koda-content .ProseMirror .koda-ins {\n color: #15803d; text-decoration: underline; text-decoration-color: rgba(21,128,61,0.5);\n background: rgba(21,128,61,0.08);\n}\n.koda-content .ProseMirror .koda-del {\n color: #b91c1c; text-decoration: line-through; text-decoration-color: rgba(185,28,28,0.75);\n background: rgba(185,28,28,0.08);\n}\n/* \u0418\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u043E\u0433\u043E \u0440\u0435\u0436\u0438\u043C\u0430 \u0440\u0435\u0446\u0435\u043D\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u2014 \u0442\u043E\u043D\u043A\u0430\u044F \u043F\u043E\u043B\u043E\u0441\u0430 \u0441\u0432\u0435\u0440\u0445\u0443 \u043A\u043E\u043D\u0442\u0435\u043D\u0442\u0430 */\n.koda-content .ProseMirror[data-track=\"1\"] { box-shadow: inset 0 3px 0 -1px var(--koda-accent); }\n\n/* \u041A\u0438\u0441\u0442\u044C \u0444\u043E\u0440\u043C\u0430\u0442\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u0430 (D1): \u044F\u0432\u043D\u0430\u044F \u0441\u043F\u043B\u043E\u0448\u043D\u0430\u044F \u0437\u0430\u043B\u0438\u0432\u043A\u0430 + \u0431\u0435\u043B\u0430\u044F \u0438\u043A\u043E\u043D\u043A\u0430 + \u043C\u044F\u0433\u043A\u043E\u0435\n \u043A\u043E\u043B\u044C\u0446\u043E \u2014 \u043D\u0435\u0434\u0432\u0443\u0441\u043C\u044B\u0441\u043B\u0435\u043D\u043D\u044B\u0439 \u00AB\u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u00BB, \u043F\u043E\u043A\u0430 \u043A\u043E\u043F\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0444\u043E\u0440\u043C\u0430\u0442.\n \u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430/\u043A\u043E\u043B\u044C\u0446\u0430 \u043D\u0430 var(--koda-accent) (\u043D\u0430\u0434\u0451\u0436\u043D\u043E \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u0442\u0441\u044F). \u041F\u0443\u043B\u044C\u0441\u0430\u0446\u0438\u044F \u2014\n \u043D\u0435\u043D\u0430\u0432\u044F\u0437\u0447\u0438\u0432\u044B\u0439 \u0441\u0438\u0433\u043D\u0430\u043B, \u0447\u0442\u043E \u043A\u0438\u0441\u0442\u044C \u00AB\u0436\u0434\u0451\u0442\u00BB \u043A\u043B\u0438\u043A; \u0433\u0430\u0441\u0438\u0442\u0441\u044F prefers-reduced-motion. */\n.koda-btn.is-armed,\n.koda-btn.is-armed:hover {\n background: var(--koda-accent);\n color: #fff;\n border-color: var(--koda-accent);\n box-shadow: 0 0 0 2px var(--koda-accent-soft), 0 0 0 3px var(--koda-accent);\n animation: koda-armed-pulse 1.4s ease-in-out infinite;\n}\n.koda-btn.is-armed svg { color: #fff; stroke: #fff; }\n@keyframes koda-armed-pulse {\n 0%, 100% { box-shadow: 0 0 0 2px var(--koda-accent-soft), 0 0 0 3px var(--koda-accent); }\n 50% { box-shadow: 0 0 0 4px var(--koda-accent-soft), 0 0 0 3px var(--koda-accent); }\n}\n@media (prefers-reduced-motion: reduce) {\n .koda-btn.is-armed { animation: none; }\n}\n\n/* === \u0421\u0442\u0440\u0430\u043D\u0438\u0447\u043D\u044B\u0439 \u0432\u0438\u0434 (D5) === */\n.koda-content.koda-paged { position: relative; background: #ececed; overflow: visible; max-height: none; padding: 16px 0; }\n.koda-content.koda-paged .ProseMirror {\n max-width: 794px; min-height: 1000px; margin: 0 auto; background: #fff;\n padding: 56px 64px; box-shadow: 0 2px 16px rgba(0,0,0,0.12); box-sizing: border-box;\n}\n.koda-page-overlay { position: absolute; inset: 16px 0; pointer-events: none; z-index: 3; max-width: 794px; margin: 0 auto; left: 0; right: 0; }\n.koda-page-divider { position: absolute; left: 0; right: 0; border-top: 2px dashed var(--koda-accent); opacity: 0.55; }\n.koda-page-num {\n position: absolute; right: 6px; top: 4px; font: 600 11px var(--koda-font); color: var(--koda-accent);\n background: var(--koda-bg); padding: 1px 7px; border-radius: 6px; border: 1px solid var(--koda-border);\n}\n\n/* === \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u0438 (D2) === */\n.koda-a11y { min-width: 320px; max-width: 420px; }\n.koda-a11y-list { margin: 8px 0; display: flex; flex-direction: column; gap: 6px; }\n.koda-a11y-ok { font: 600 14px var(--koda-font); color: #15803d; padding: 12px; text-align: center; }\n.koda-a11y-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--koda-border); border-radius: 8px; font: 400 13px var(--koda-font); color: var(--koda-fg); }\n.koda-a11y-count { flex-shrink: 0; min-width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; background: #fef3c7; color: #b45309; border-radius: 6px; font-weight: 700; font-size: 12px; }\n\n/* === \u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0432\u0435\u0440\u0441\u0438\u0439 (C3) === */\n.koda-versions { min-width: 360px; max-width: 440px; }\n.koda-versions-list { max-height: 280px; overflow-y: auto; margin: 8px 0; }\n.koda-versions-empty { color: var(--koda-muted); font: 400 13px var(--koda-font); padding: 18px; text-align: center; }\n.koda-version-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--koda-border); border-radius: 8px; margin-bottom: 6px; }\n.koda-version-meta { flex: 1 1 auto; min-width: 0; }\n.koda-version-name { font: 600 13px var(--koda-font); color: var(--koda-fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.koda-version-date { font: 400 11px var(--koda-font); color: var(--koda-muted); }\n.koda-version-del { width: 26px; height: 26px; border: none; background: transparent; color: var(--koda-muted); border-radius: 6px; cursor: pointer; font-size: 13px; flex-shrink: 0; }\n.koda-version-del:hover { background: rgba(239,68,68,0.12); color: #ef4444; }\n";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word-подобное «Разделить ячейку»: делит ОДНУ ячейку на сетку N столбцов × M
|
|
3
|
+
* строк, не затрагивая остальные ячейки таблицы.
|
|
4
|
+
*
|
|
5
|
+
* prosemirror-tables даёт только `splitCell` (разъединяет ранее объединённую
|
|
6
|
+
* ячейку обратно в 1×1). Для деления обычной ячейки на произвольную сетку — как
|
|
7
|
+
* в Word по «Разделить ячейку → Столбцов/Строк» — мы:
|
|
8
|
+
* 1) вставляем столбцы/строки во ВСЮ таблицу (addColumn/addRow);
|
|
9
|
+
* 2) «сливаем обратно» новые ячейки во всех строках/столбцах ВНЕ области целевой
|
|
10
|
+
* ячейки, увеличивая colspan/rowspan соседа — так делится только она.
|
|
11
|
+
*
|
|
12
|
+
* Надёжность: каждое структурное изменение — ОТДЕЛЬНАЯ транзакция (view.dispatch).
|
|
13
|
+
* Хелперы prosemirror-tables ожидают координаты «шага 0» и маппят через tr.mapping;
|
|
14
|
+
* накапливать несколько структурных правок в одной транзакции нельзя — позиции
|
|
15
|
+
* разъезжаются. Таблицы малы, серия мелких диспатчей дёшева и предсказуема.
|
|
16
|
+
*
|
|
17
|
+
* Безопасность: только одиночная неслитая ячейка (1×1). Для уже объединённой
|
|
18
|
+
* ячейки и любой нештатной ситуации — return false: вызывающий код применит
|
|
19
|
+
* встроенный splitCell.
|
|
20
|
+
*/
|
|
21
|
+
import type { EditorView } from 'prosemirror-view';
|
|
22
|
+
/**
|
|
23
|
+
* Делит текущую ОДИНОЧНУЮ ячейку (1×1) на nCols × nRows.
|
|
24
|
+
* Возвращает false, если ячейка уже объединена или сценарий нештатный.
|
|
25
|
+
*/
|
|
26
|
+
export declare function splitCellInto(view: EditorView, nCols: number, nRows: number): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Node as PMNode } from 'prosemirror-model';
|
|
2
|
+
import type { NodeView, ViewMutationRecord } from 'prosemirror-view';
|
|
3
|
+
/**
|
|
4
|
+
* Резиновый TableView для prosemirror-tables.
|
|
5
|
+
*
|
|
6
|
+
* Штатный TableView при фиксированных ширинах колонок пишет таблице
|
|
7
|
+
* `style.width = <сумма>px` и colgroup в пикселях. Из-за этого таблица,
|
|
8
|
+
* вставленная/импортированная из Word, не растягивалась на ширину листа и
|
|
9
|
+
* обрезалась справа (см. скриншоты сравнения с TinyMCE).
|
|
10
|
+
*
|
|
11
|
+
* Здесь colgroup строится в ПРОЦЕНТАХ (пропорции сохраняются), а таблица
|
|
12
|
+
* получает width:100% — она заполняет текстовую область листа, как в Word и
|
|
13
|
+
* как делает TinyMCE. data-colwidth (px) остаётся в модели для column-resize:
|
|
14
|
+
* во время перетаскивания библиотека временно рисует px, после отпускания
|
|
15
|
+
* update() возвращает проценты.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ResponsiveTableView implements NodeView {
|
|
18
|
+
readonly dom: HTMLElement;
|
|
19
|
+
readonly table: HTMLTableElement;
|
|
20
|
+
readonly colgroup: HTMLElement;
|
|
21
|
+
readonly contentDOM: HTMLElement;
|
|
22
|
+
private node;
|
|
23
|
+
constructor(node: PMNode, defaultCellMinWidth: number);
|
|
24
|
+
update(node: PMNode): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Свойства таблицы (диалог «Свойства таблицы»): выравнивание (margin) и рамка
|
|
27
|
+
* применяются всегда; пользовательская ширина переопределяет авто-ширину
|
|
28
|
+
* responsive-логики (updateColumns вызывается ДО — поэтому width из tableStyle
|
|
29
|
+
* имеет приоритет). Снятие свойства возвращает responsive-поведение.
|
|
30
|
+
*/
|
|
31
|
+
private applyTableStyle;
|
|
32
|
+
ignoreMutation(record: ViewMutationRecord): boolean;
|
|
33
|
+
private updateColumns;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* «Обмен с соседом» при ресайзе колонки таблицы.
|
|
3
|
+
*
|
|
4
|
+
* Таблицы у нас резиновые (width:100%), ширины колонок — доли от суммы. Штатный
|
|
5
|
+
* ресайз prosemirror-tables меняет ширину ТОЛЬКО перетаскиваемой колонки, из-за
|
|
6
|
+
* чего сумма меняется, и при пересчёте долей «едут» ВСЕ колонки (жалоба заказчика
|
|
7
|
+
* «меняешь одно — другое непредсказуемо»).
|
|
8
|
+
*
|
|
9
|
+
* Здесь после ресайза соседняя колонка компенсируется на ту же дельту — СУММА
|
|
10
|
+
* остаётся постоянной. Тогда резина сохраняется (таблица на 100%), но двигаются
|
|
11
|
+
* только перетаскиваемая колонка и её сосед, как в Word. Работает ТОЛЬКО в момент
|
|
12
|
+
* завершения ресайза (по состоянию columnResizing), поэтому обычный ввод не грузит.
|
|
13
|
+
*/
|
|
14
|
+
import { Plugin } from 'prosemirror-state';
|
|
15
|
+
export declare function tableColumnTradePlugin(): Plugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Контекстная панель таблицы (Google Docs / Notion UX): когда курсор внутри
|
|
3
|
+
* таблицы — над ней всплывает мини-панель с операциями (строки/колонки,
|
|
4
|
+
* объединение/разделение, удаление таблицы). Действия «на месте», а не теряются
|
|
5
|
+
* в общем тулбаре. Вне таблицы панель скрыта.
|
|
6
|
+
*/
|
|
7
|
+
import { Plugin } from 'prosemirror-state';
|
|
8
|
+
import type { Strings } from './i18n';
|
|
9
|
+
/** Колбэки, чтобы split/merge открывали диалог/подсказку вместо «голой» команды. */
|
|
10
|
+
export interface TableToolbarCallbacks {
|
|
11
|
+
onSplitCell?: () => void;
|
|
12
|
+
onMergeCells?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function tableToolbarPlugin(strings: Strings, callbacks?: TableToolbarCallbacks): Plugin;
|