@inkbrick/react 0.0.0-stage → 0.1.0-alpha.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.
Files changed (131) hide show
  1. package/dist/FormulaPalette-WFRPRHS5.js +320 -0
  2. package/dist/FormulaPalette-WFRPRHS5.js.map +1 -0
  3. package/dist/chunk-TPUX4C45.js +2309 -0
  4. package/dist/chunk-TPUX4C45.js.map +1 -0
  5. package/dist/index.d.ts +1431 -0
  6. package/dist/index.js +4880 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/mathCss-FLPE6QBM.js +361 -0
  9. package/dist/mathCss-FLPE6QBM.js.map +1 -0
  10. package/package.json +46 -3
  11. package/src/block-handle/BlockHandleController.ts +93 -0
  12. package/src/block-handle/BlockHandleHost.tsx +108 -0
  13. package/src/block-handle/DefaultBlockHandle.tsx +71 -0
  14. package/src/block-handle/DefaultBlockMenu.tsx +114 -0
  15. package/src/block-handle/DropIndicatorHost.tsx +58 -0
  16. package/src/block-handle/actions.tsx +146 -0
  17. package/src/block-handle/blockDrag.ts +215 -0
  18. package/src/block-handle/geometry.ts +64 -0
  19. package/src/block-handle/gripGesture.ts +108 -0
  20. package/src/block-handle/types.ts +65 -0
  21. package/src/block-handle/useBlockDrag.ts +21 -0
  22. package/src/block-handle/useBlockHandles.ts +125 -0
  23. package/src/blocks/BlockChildren.tsx +17 -0
  24. package/src/blocks/BlockProps.ts +5 -0
  25. package/src/blocks/CalloutBlock.tsx +61 -0
  26. package/src/blocks/CodeBlock.tsx +96 -0
  27. package/src/blocks/DividerBlock.tsx +15 -0
  28. package/src/blocks/Editable.tsx +84 -0
  29. package/src/blocks/FormulaBlock.tsx +173 -0
  30. package/src/blocks/HeadingBlock.tsx +22 -0
  31. package/src/blocks/ImageBlock.tsx +362 -0
  32. package/src/blocks/ListBlock.tsx +86 -0
  33. package/src/blocks/NoteBlock.tsx +10 -0
  34. package/src/blocks/PageBlock.tsx +10 -0
  35. package/src/blocks/ParagraphBlock.tsx +20 -0
  36. package/src/blocks/QuoteBlock.tsx +18 -0
  37. package/src/blocks/UnknownBlockView.tsx +27 -0
  38. package/src/blocks/defineBlock.ts +49 -0
  39. package/src/clipboard/html/blocks.ts +379 -0
  40. package/src/clipboard/html/inline.ts +92 -0
  41. package/src/clipboard/html/style.ts +92 -0
  42. package/src/clipboard/html/tags.ts +77 -0
  43. package/src/clipboard/htmlToFragment.ts +41 -0
  44. package/src/clipboard/useClipboard.ts +296 -0
  45. package/src/code/highlight.ts +56 -0
  46. package/src/code/languages.ts +236 -0
  47. package/src/code/scan/css.ts +164 -0
  48. package/src/code/scan/generic.ts +328 -0
  49. package/src/code/scan/html.ts +129 -0
  50. package/src/code/scan/markdown.ts +58 -0
  51. package/src/code/scan/shared.ts +81 -0
  52. package/src/code/scan/yaml.ts +34 -0
  53. package/src/code/types.ts +34 -0
  54. package/src/context/EditorContext.ts +9 -0
  55. package/src/context/EditorProvider.tsx +172 -0
  56. package/src/controllers/adapters.tsx +63 -0
  57. package/src/editing/BlockSelectionController.ts +27 -0
  58. package/src/editing/CaretController.ts +27 -0
  59. package/src/editing/EditorView.ts +108 -0
  60. package/src/editing/ViewportController.ts +63 -0
  61. package/src/editing/beforeInput.ts +276 -0
  62. package/src/editing/deleteAtBoundary.ts +16 -0
  63. package/src/editing/inputActions.ts +109 -0
  64. package/src/editing/renderCode.ts +69 -0
  65. package/src/editing/renderInline.ts +144 -0
  66. package/src/editing/selection.ts +262 -0
  67. package/src/editing/useEditorInput.ts +223 -0
  68. package/src/editing/useSelectionState.ts +46 -0
  69. package/src/formula/FormulaController.ts +51 -0
  70. package/src/formula/FormulaPalette.tsx +79 -0
  71. package/src/formula/FormulaPopover.tsx +181 -0
  72. package/src/formula/LazyFormulaPalette.tsx +42 -0
  73. package/src/formula/actions.ts +138 -0
  74. package/src/formula/snippet.ts +70 -0
  75. package/src/formula/symbols.ts +264 -0
  76. package/src/hooks/useBlock.ts +21 -0
  77. package/src/hooks/useBlockController.ts +30 -0
  78. package/src/hooks/useChildren.ts +16 -0
  79. package/src/hooks/useDocument.ts +16 -0
  80. package/src/hooks/useEditor.ts +13 -0
  81. package/src/hooks/useEditorView.ts +13 -0
  82. package/src/hooks/useHistory.ts +46 -0
  83. package/src/hooks/useReadOnly.ts +21 -0
  84. package/src/hooks/useT.ts +10 -0
  85. package/src/i18n/en.ts +182 -0
  86. package/src/i18n/ru.ts +174 -0
  87. package/src/i18n/translate.ts +77 -0
  88. package/src/icons/ICONS_LICENSE.md +50 -0
  89. package/src/icons/Icon.tsx +41 -0
  90. package/src/icons/lucide.ts +234 -0
  91. package/src/images/ImageError.ts +39 -0
  92. package/src/images/UploadTracker.ts +49 -0
  93. package/src/images/insertImages.ts +142 -0
  94. package/src/images/types.ts +22 -0
  95. package/src/images/upload.ts +62 -0
  96. package/src/index.ts +102 -0
  97. package/src/keymap/defaultKeymap.ts +189 -0
  98. package/src/keymap/keymap.ts +42 -0
  99. package/src/keymap/shortcut.ts +105 -0
  100. package/src/keymap/types.ts +40 -0
  101. package/src/math/FormulaView.tsx +62 -0
  102. package/src/math/mathCss.ts +4 -0
  103. package/src/math/renderer.ts +198 -0
  104. package/src/registry/defaultBlockComponents.ts +26 -0
  105. package/src/registry/resolveBlockComponent.ts +14 -0
  106. package/src/renderer/BlockRenderer.tsx +63 -0
  107. package/src/renderer/DocumentRenderer.tsx +33 -0
  108. package/src/renderer/DocumentView.tsx +94 -0
  109. package/src/renderer/virtual/VirtualChildList.tsx +355 -0
  110. package/src/renderer/virtual/estimate.ts +62 -0
  111. package/src/renderer/virtual/virtualRenderer.tsx +35 -0
  112. package/src/slash/DefaultSlashMenu.tsx +119 -0
  113. package/src/slash/SlashMenuController.ts +66 -0
  114. package/src/slash/SlashMenuHost.tsx +48 -0
  115. package/src/slash/applySlashItem.ts +57 -0
  116. package/src/slash/defaultSlashItems.tsx +186 -0
  117. package/src/slash/filter.ts +29 -0
  118. package/src/slash/items.ts +30 -0
  119. package/src/slash/syncSlashMenu.ts +67 -0
  120. package/src/slash/types.ts +21 -0
  121. package/src/toolbar/DefaultToolbar.tsx +281 -0
  122. package/src/toolbar/ToolbarController.ts +100 -0
  123. package/src/toolbar/ToolbarHost.tsx +81 -0
  124. package/src/toolbar/formatting.ts +75 -0
  125. package/src/toolbar/rich.ts +58 -0
  126. package/src/toolbar/syncToolbar.ts +39 -0
  127. package/src/toolbar/types.ts +21 -0
  128. package/src/ui/EditorUIContext.ts +26 -0
  129. package/src/ui/presentation.ts +24 -0
  130. package/src/ui/types.ts +70 -0
  131. package/README.md +0 -3
@@ -0,0 +1,198 @@
1
+ export interface FormulaRenderOptions {
2
+ readonly display: boolean;
3
+ }
4
+
5
+ /**
6
+ * Рисует формулу в `target`: по коду LaTeX строит DOM (MathML, HTML, SVG…). Может быть асинхронной — например, для
7
+ * подгрузки библиотеки. Если код ошибочен, функция должна бросить исключение: редактор покажет исходный текст и
8
+ * сообщение об ошибке. Встроенный `temmlRenderer` строит MathML; KaTeX подключается так:
9
+ * `(latex, target, { display }) => katex.render(latex, target, { displayMode: display, throwOnError: true })`.
10
+ *
11
+ * Результат вставляется в страницу как есть, поэтому используйте только безопасные режимы библиотек
12
+ * (например, `trust: false`): LaTeX приходит из документа и может быть недоверенным.
13
+ */
14
+ export type FormulaRenderer = (
15
+ latex: string,
16
+ target: HTMLElement,
17
+ options: FormulaRenderOptions,
18
+ ) => void | Promise<void>;
19
+
20
+ type TemmlApi = typeof import('temml').default;
21
+
22
+ let temml: Promise<TemmlApi> | undefined;
23
+
24
+ /** Temml (≈ 480 КБ) подгружается при первой формуле: документы без формул её не платят. */
25
+ const loadTemml = () => (temml ??= import('temml').then((module) => module.default));
26
+
27
+ export const temmlRenderer: FormulaRenderer = async (latex, target, { display }) => {
28
+ const api = await loadTemml();
29
+
30
+ api.render(latex, target, {
31
+ displayMode: display,
32
+ throwOnError: true,
33
+ trust: false,
34
+ strict: false,
35
+ maxExpand: 1000,
36
+ });
37
+ };
38
+
39
+ let mathCss: Promise<string> | undefined;
40
+
41
+ const loadMathCss = () => (mathCss ??= import('./mathCss').then((module) => module.MATH_CSS));
42
+
43
+ let shadowSheet: CSSStyleSheet | null | undefined;
44
+
45
+ function constructableSheet(css: string): CSSStyleSheet | null {
46
+ if (shadowSheet !== undefined) return shadowSheet;
47
+
48
+ try {
49
+ if (typeof CSSStyleSheet !== 'undefined' && 'replaceSync' in CSSStyleSheet.prototype) {
50
+ shadowSheet = new CSSStyleSheet();
51
+ shadowSheet.replaceSync(css);
52
+
53
+ return shadowSheet;
54
+ }
55
+ } catch {
56
+ // нет поддержки конструируемых таблиц стилей — ниже используется <style>
57
+ }
58
+
59
+ return (shadowSheet = null);
60
+ }
61
+
62
+ const styled = new WeakSet<Document | ShadowRoot>();
63
+
64
+ function applyMathStyles(scope: Document | ShadowRoot, css: string): void {
65
+ if (typeof ShadowRoot !== 'undefined' && scope instanceof ShadowRoot) {
66
+ const sheet = constructableSheet(css);
67
+
68
+ if (sheet && Array.isArray(scope.adoptedStyleSheets)) {
69
+ scope.adoptedStyleSheets = [...scope.adoptedStyleSheets, sheet];
70
+ } else {
71
+ const style = globalThis.document.createElement('style');
72
+
73
+ style.textContent = css;
74
+ scope.appendChild(style);
75
+ }
76
+
77
+ return;
78
+ }
79
+
80
+ const doc = scope as Document;
81
+
82
+ if (doc.head.querySelector('style[data-inkbrick-math]')) return;
83
+
84
+ const style = doc.createElement('style');
85
+
86
+ style.setAttribute('data-inkbrick-math', '');
87
+ style.textContent = `@layer inkbrick.math {\n${css}\n}`;
88
+ doc.head.appendChild(style);
89
+ }
90
+
91
+ /**
92
+ * Подключает стили MathML (Temml) к документу или теневому корню встроенной формулы. В документе стили кладутся
93
+ * в слой `inkbrick.math`, поэтому любое ваше правило без слоя сильнее. Стили подгружаются при первом вызове
94
+ * и применяются, как только загрузятся (одновременно с самой формулой).
95
+ */
96
+ export function ensureMathStyles(scope: Document | ShadowRoot): void {
97
+ if (styled.has(scope)) return;
98
+
99
+ styled.add(scope);
100
+ void loadMathCss().then((css) => applyMathStyles(scope, css));
101
+ }
102
+
103
+ type Rendered = readonly Node[];
104
+
105
+ const MAX_CACHE = 500;
106
+ const caches = new WeakMap<FormulaRenderer, Map<string, Rendered>>();
107
+
108
+ function cacheOf(renderer: FormulaRenderer): Map<string, Rendered> {
109
+ let cache = caches.get(renderer);
110
+
111
+ if (!cache) caches.set(renderer, (cache = new Map()));
112
+
113
+ return cache;
114
+ }
115
+
116
+ export type FormulaState = 'empty' | 'source' | 'loading' | 'ready' | 'error';
117
+
118
+ export interface MountFormulaOptions {
119
+ readonly display: boolean;
120
+ readonly renderer: FormulaRenderer | null;
121
+ readonly onSettled?: (state: 'ready' | 'error', message?: string) => void;
122
+ }
123
+
124
+ /**
125
+ * Рисует формулу в `target`. Пока идёт расчёт, виден исходный код (`data-state="loading"`); готовый результат кладётся
126
+ * в кэш, поэтому одинаковые формулы (и перерисовка блока на каждое нажатие клавиши) появляются сразу, без мигания.
127
+ * Возвращает функцию отмены: после неё устаревший результат в `target` не попадёт.
128
+ */
129
+ export function mountFormula(
130
+ target: HTMLElement,
131
+ latex: string,
132
+ { display, renderer, onSettled }: MountFormulaOptions,
133
+ ): () => void {
134
+ const source = latex.trim();
135
+
136
+ if (source === '') {
137
+ target.replaceChildren();
138
+ target.setAttribute('data-state', 'empty');
139
+
140
+ return () => undefined;
141
+ }
142
+
143
+ if (!renderer) {
144
+ target.textContent = latex;
145
+ target.setAttribute('data-state', 'source');
146
+
147
+ return () => undefined;
148
+ }
149
+
150
+ const key = `${display ? 'd' : 'i'}:${latex}`;
151
+ const cache = cacheOf(renderer);
152
+ const cached = cache.get(key);
153
+
154
+ if (cached) {
155
+ target.replaceChildren(...cached.map((node) => node.cloneNode(true)));
156
+ target.setAttribute('data-state', 'ready');
157
+ onSettled?.('ready');
158
+
159
+ return () => undefined;
160
+ }
161
+
162
+ let cancelled = false;
163
+
164
+ target.textContent = latex;
165
+ target.setAttribute('data-state', 'loading');
166
+
167
+ const scratch = globalThis.document.createElement('span');
168
+
169
+ void (async () => {
170
+ try {
171
+ await renderer(latex, scratch, { display });
172
+
173
+ if (cancelled) return;
174
+
175
+ const nodes = [...scratch.childNodes];
176
+
177
+ if (cache.size >= MAX_CACHE) cache.delete(cache.keys().next().value as string);
178
+
179
+ cache.set(
180
+ key,
181
+ nodes.map((node) => node.cloneNode(true)),
182
+ );
183
+ target.replaceChildren(...nodes);
184
+ target.setAttribute('data-state', 'ready');
185
+ onSettled?.('ready');
186
+ } catch (error) {
187
+ if (cancelled) return;
188
+
189
+ target.textContent = latex;
190
+ target.setAttribute('data-state', 'error');
191
+ onSettled?.('error', error instanceof Error ? error.message : String(error));
192
+ }
193
+ })();
194
+
195
+ return () => {
196
+ cancelled = true;
197
+ };
198
+ }
@@ -0,0 +1,26 @@
1
+ import type { BlockComponentRegistry } from '../ui/types';
2
+ import { PageBlock } from '../blocks/PageBlock';
3
+ import { NoteBlock } from '../blocks/NoteBlock';
4
+ import { ParagraphBlock } from '../blocks/ParagraphBlock';
5
+ import { HeadingBlock } from '../blocks/HeadingBlock';
6
+ import { ListBlock } from '../blocks/ListBlock';
7
+ import { QuoteBlock } from '../blocks/QuoteBlock';
8
+ import { CodeBlock } from '../blocks/CodeBlock';
9
+ import { DividerBlock } from '../blocks/DividerBlock';
10
+ import { CalloutBlock } from '../blocks/CalloutBlock';
11
+ import { ImageBlock } from '../blocks/ImageBlock';
12
+ import { FormulaBlock } from '../blocks/FormulaBlock';
13
+
14
+ export const defaultBlockComponents: BlockComponentRegistry = Object.freeze({
15
+ 'block:page': PageBlock,
16
+ 'block:note': NoteBlock,
17
+ 'block:paragraph': ParagraphBlock,
18
+ 'block:heading': HeadingBlock,
19
+ 'block:list': ListBlock,
20
+ 'block:quote': QuoteBlock,
21
+ 'block:code': CodeBlock,
22
+ 'block:divider': DividerBlock,
23
+ 'block:callout': CalloutBlock,
24
+ 'block:image': ImageBlock,
25
+ 'block:formula': FormulaBlock,
26
+ });
@@ -0,0 +1,14 @@
1
+ import type { BlockComponent, BlockComponentRegistry } from '../ui/types';
2
+ import { defaultBlockComponents } from './defaultBlockComponents';
3
+
4
+ function lookup(registry: BlockComponentRegistry, flavour: string): BlockComponent | undefined {
5
+ // hasOwn: ключи вроде "constructor" или "__proto__" не должны находить прототипные свойства.
6
+ return Object.hasOwn(registry, flavour) ? registry[flavour] : undefined;
7
+ }
8
+
9
+ export function resolveBlockComponent(
10
+ flavour: string,
11
+ custom: BlockComponentRegistry,
12
+ ): BlockComponent | undefined {
13
+ return lookup(custom, flavour) ?? lookup(defaultBlockComponents, flavour);
14
+ }
@@ -0,0 +1,63 @@
1
+ import { memo, useSyncExternalStore } from 'react';
2
+
3
+ import type { BlockId } from '@inkbrick/document';
4
+
5
+ import { BlockChildren } from '../blocks/BlockChildren';
6
+ import { UnknownBlockView } from '../blocks/UnknownBlockView';
7
+ import { useBlock } from '../hooks/useBlock';
8
+ import { useEditor } from '../hooks/useEditor';
9
+ import { useEditorView } from '../hooks/useEditorView';
10
+ import { useReadOnly } from '../hooks/useReadOnly';
11
+ import { resolveBlockComponent } from '../registry/resolveBlockComponent';
12
+ import { useEditorUI } from '../ui/EditorUIContext';
13
+
14
+ export interface BlockRendererProps {
15
+ readonly id: BlockId;
16
+ }
17
+
18
+ const warned = new Set<string>();
19
+
20
+ export const BlockRenderer = memo(function BlockRenderer({ id }: BlockRendererProps) {
21
+ const editor = useEditor();
22
+ const view = useEditorView();
23
+ const readOnly = useReadOnly();
24
+ const { components, slots } = useEditorUI();
25
+ const selected = useSyncExternalStore(
26
+ view.selection.subscribe,
27
+ () => view.selection.isSelected(id),
28
+ () => false,
29
+ );
30
+ const block = useBlock(id);
31
+
32
+ if (!block) {
33
+ return null;
34
+ }
35
+
36
+ const Component = resolveBlockComponent(block.flavour, components);
37
+
38
+ if (Component) {
39
+ return (
40
+ <Component
41
+ block={block}
42
+ editor={editor}
43
+ selected={selected}
44
+ readOnly={readOnly}
45
+ controller={editor.controllers.get(block.flavour)}
46
+ />
47
+ );
48
+ }
49
+
50
+ // Неизвестный тип блока не должен ронять редактор и не должен подменяться другим типом.
51
+ if (!warned.has(block.flavour)) {
52
+ warned.add(block.flavour);
53
+ console.warn(`[@inkbrick/react] No component registered for block "${block.flavour}"`);
54
+ }
55
+
56
+ const Fallback = slots.UnknownBlock ?? UnknownBlockView;
57
+
58
+ return (
59
+ <Fallback block={block} editor={editor} selected={selected} readOnly={readOnly}>
60
+ <BlockChildren id={block.id} />
61
+ </Fallback>
62
+ );
63
+ });
@@ -0,0 +1,33 @@
1
+ import { createContext, type ComponentType } from 'react';
2
+
3
+ import type { BlockId } from '@inkbrick/document';
4
+
5
+ import { BlockRenderer } from './BlockRenderer';
6
+
7
+ export interface ChildListProps {
8
+ readonly parentId: BlockId;
9
+ readonly ids: readonly BlockId[];
10
+ }
11
+
12
+ /**
13
+ * Способ отрисовки документа: как выводить детей каждого контейнера (страницы, заметки, вложенного списка).
14
+ * Модель и команды от него не зависят. Встроенные варианты — `fullRenderer` (всё дерево, по умолчанию)
15
+ * и `virtualRenderer()` (только блоки около видимой области).
16
+ */
17
+ export interface DocumentRenderer {
18
+ readonly ChildList: ComponentType<ChildListProps>;
19
+ }
20
+
21
+ function FullChildList({ ids }: ChildListProps) {
22
+ return (
23
+ <>
24
+ {ids.map((id) => (
25
+ <BlockRenderer key={id} id={id} />
26
+ ))}
27
+ </>
28
+ );
29
+ }
30
+
31
+ export const fullRenderer: DocumentRenderer = { ChildList: FullChildList };
32
+
33
+ export const DocumentRendererContext = createContext<DocumentRenderer>(fullRenderer);
@@ -0,0 +1,94 @@
1
+ import { useEffect, useLayoutEffect, useRef } from 'react';
2
+
3
+ import { BlockHandleHost } from '../block-handle/BlockHandleHost';
4
+ import { DropIndicatorHost } from '../block-handle/DropIndicatorHost';
5
+ import { useClipboard } from '../clipboard/useClipboard';
6
+ import { useBlockHandles } from '../block-handle/useBlockHandles';
7
+ import { useEditorInput } from '../editing/useEditorInput';
8
+ import { useSelectionState } from '../editing/useSelectionState';
9
+ import { useEditor } from '../hooks/useEditor';
10
+ import { useEditorView } from '../hooks/useEditorView';
11
+ import { useReadOnly } from '../hooks/useReadOnly';
12
+ import { useT } from '../hooks/useT';
13
+ import { useEditorUI } from '../ui/EditorUIContext';
14
+ import { FormulaPopover } from '../formula/FormulaPopover';
15
+ import { preloadFormulaPalette } from '../formula/LazyFormulaPalette';
16
+ import { SlashMenuHost } from '../slash/SlashMenuHost';
17
+ import { ToolbarHost } from '../toolbar/ToolbarHost';
18
+ import { BlockRenderer } from './BlockRenderer';
19
+ import { DocumentRendererContext, fullRenderer, type DocumentRenderer } from './DocumentRenderer';
20
+ import { defaultVirtualRenderer } from './virtual/virtualRenderer';
21
+
22
+ export interface DocumentViewProps {
23
+ readonly label?: string;
24
+ /**
25
+ * Как рисовать блоки: `'full'` (по умолчанию) — всё дерево; `'virtual'` или `virtualRenderer({...})` — только
26
+ * блоки около видимой области (для документов на тысячи блоков); либо свой `DocumentRenderer`.
27
+ */
28
+ readonly renderer?: 'full' | 'virtual' | DocumentRenderer;
29
+ }
30
+
31
+ function resolveRenderer(renderer: DocumentViewProps['renderer']): DocumentRenderer {
32
+ if (renderer === 'virtual') return defaultVirtualRenderer;
33
+ if (!renderer || renderer === 'full') return fullRenderer;
34
+ return renderer;
35
+ }
36
+
37
+ /**
38
+ * Единственный редактируемый элемент документа. Все блоки лежат внутри него, поэтому
39
+ * выделение работает через границы блоков; ввод перехватывается и превращается в команды движка.
40
+ */
41
+ export function DocumentView({ label, renderer }: DocumentViewProps = {}) {
42
+ const editor = useEditor();
43
+ const view = useEditorView();
44
+ const ref = useRef<HTMLDivElement>(null);
45
+
46
+ const { slots } = useEditorUI();
47
+
48
+ const readOnly = useReadOnly();
49
+ const t = useT();
50
+
51
+ useEditorInput(ref, !readOnly);
52
+ useSelectionState(ref);
53
+ useClipboard(ref, readOnly);
54
+ useBlockHandles(ref, slots.BlockHandle !== null && !readOnly);
55
+
56
+ useLayoutEffect(() => view.setRoot(ref.current), [view]);
57
+
58
+ /*
59
+ * Палитру формул (отдельный чанк ~5 КБ) начинаем грузить сразу после первой отрисовки: она не задерживает
60
+ * открытие документа, но готова к первому редактору формулы (Tab из поля кода ведёт в палитру).
61
+ */
62
+ useEffect(() => {
63
+ if (!readOnly) void preloadFormulaPalette();
64
+ }, [readOnly]);
65
+
66
+ return (
67
+ <>
68
+ <div
69
+ ref={ref}
70
+ className="inkbrick-editor"
71
+ data-inkbrick-root=""
72
+ contentEditable={!readOnly}
73
+ suppressContentEditableWarning
74
+ spellCheck={false}
75
+ role="textbox"
76
+ aria-multiline="true"
77
+ aria-label={label ?? t('editor.label')}
78
+ // Документ только для чтения должен получать фокус: так работают Ctrl+A и Ctrl+C с клавиатуры.
79
+ tabIndex={readOnly ? 0 : undefined}
80
+ aria-readonly={readOnly || undefined}
81
+ data-readonly={readOnly || undefined}
82
+ >
83
+ <DocumentRendererContext.Provider value={resolveRenderer(renderer)}>
84
+ <BlockRenderer id={editor.document.rootId} />
85
+ </DocumentRendererContext.Provider>
86
+ </div>
87
+ <SlashMenuHost />
88
+ <ToolbarHost />
89
+ <FormulaPopover />
90
+ <BlockHandleHost />
91
+ <DropIndicatorHost />
92
+ </>
93
+ );
94
+ }