@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,50 @@
1
+ # Иконки
2
+
3
+ Файл `lucide.ts` сгенерирован из иконок [Lucide](https://lucide.dev). Часть из них унаследована от проекта Feather,
4
+ поэтому ниже приведён полный текст лицензии Lucide (ISC и MIT для Feather).
5
+
6
+ ```text
7
+ ISC License
8
+
9
+ Copyright (c) 2026 Lucide Icons and Contributors
10
+
11
+ Permission to use, copy, modify, and/or distribute this software for any
12
+ purpose with or without fee is hereby granted, provided that the above
13
+ copyright notice and this permission notice appear in all copies.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
16
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
18
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
21
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22
+
23
+ ---
24
+
25
+ The following Lucide icons are derived from the Feather project:
26
+
27
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
28
+
29
+ The MIT License (MIT) (for the icons listed above)
30
+
31
+ Copyright (c) 2013-present Cole Bemis
32
+
33
+ Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ of this software and associated documentation files (the "Software"), to deal
35
+ in the Software without restriction, including without limitation the rights
36
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ copies of the Software, and to permit persons to whom the Software is
38
+ furnished to do so, subject to the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be included in all
41
+ copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
49
+ SOFTWARE.
50
+ ```
@@ -0,0 +1,41 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ import { ICON_SHAPES, type IconName } from './lucide';
4
+
5
+ export type { IconName };
6
+
7
+ export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'name' | 'children'> {
8
+ readonly name: IconName;
9
+ }
10
+
11
+ /**
12
+ * Иконка из набора Lucide (ISC). Рисуется текущим цветом текста; размер задаётся CSS (`--inkbrick-icon-size`
13
+ * или `width`/`height` у `[data-inkbrick-icon]`). Годится и для пользовательских пунктов slash-меню и меню блока.
14
+ */
15
+ export function Icon({ name, ...props }: IconProps) {
16
+ const shapes = ICON_SHAPES[name];
17
+
18
+ return (
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ width={16}
22
+ height={16}
23
+ viewBox="0 0 24 24"
24
+ fill="none"
25
+ stroke="currentColor"
26
+ strokeWidth={2}
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
+ aria-hidden="true"
30
+ focusable="false"
31
+ data-inkbrick-icon={name}
32
+ {...props}
33
+ >
34
+ {shapes.map(([tag, attributes], index) => {
35
+ const Shape = tag as 'path';
36
+
37
+ return <Shape key={index} {...attributes} />;
38
+ })}
39
+ </svg>
40
+ );
41
+ }
@@ -0,0 +1,234 @@
1
+ // Файл сгенерирован из иконок Lucide (https://lucide.dev, лицензия ISC — см. ICONS_LICENSE.md). Не правьте вручную.
2
+ export type IconShape = readonly [tag: string, attributes: Readonly<Record<string, string>>];
3
+
4
+ export const ICON_SHAPES = {
5
+ bold: [['path', { d: 'M6 12h9a4 4 0 010 8H7a1 1 0 01-1-1V5a1 1 0 011-1h7a4 4 0 010 8' }]],
6
+ italic: [
7
+ ['line', { x1: '19', x2: '10', y1: '4', y2: '4' }],
8
+ ['line', { x1: '14', x2: '5', y1: '20', y2: '20' }],
9
+ ['line', { x1: '15', x2: '9', y1: '4', y2: '20' }],
10
+ ],
11
+ underline: [
12
+ ['path', { d: 'M6 4v6a6 6 0 0 0 12 0V4' }],
13
+ ['line', { x1: '4', x2: '20', y1: '20', y2: '20' }],
14
+ ],
15
+ strike: [
16
+ ['path', { d: 'M16 4H9a3 3 0 0 0-2.83 4' }],
17
+ ['path', { d: 'M14 12a4 4 0 0 1 0 8H6' }],
18
+ ['line', { x1: '4', x2: '20', y1: '12', y2: '12' }],
19
+ ],
20
+ code: [
21
+ ['path', { d: 'm18 16 4-4-4-4' }],
22
+ ['path', { d: 'm6 8-4 4 4 4' }],
23
+ ['path', { d: 'm14.5 4-5 16' }],
24
+ ],
25
+ link: [
26
+ ['path', { d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71' }],
27
+ ['path', { d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71' }],
28
+ ],
29
+ unlink: [
30
+ [
31
+ 'path',
32
+ {
33
+ d: 'm18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71',
34
+ },
35
+ ],
36
+ [
37
+ 'path',
38
+ { d: 'm5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71' },
39
+ ],
40
+ ['line', { x1: '8', x2: '8', y1: '2', y2: '5' }],
41
+ ['line', { x1: '2', x2: '5', y1: '8', y2: '8' }],
42
+ ['line', { x1: '16', x2: '16', y1: '19', y2: '22' }],
43
+ ['line', { x1: '19', x2: '22', y1: '16', y2: '16' }],
44
+ ],
45
+ check: [['path', { d: 'M20 6 9 17l-5-5' }]],
46
+ x: [
47
+ ['path', { d: 'M18 6 6 18' }],
48
+ ['path', { d: 'm6 6 12 12' }],
49
+ ],
50
+ plus: [
51
+ ['path', { d: 'M5 12h14' }],
52
+ ['path', { d: 'M12 5v14' }],
53
+ ],
54
+ grip: [
55
+ ['circle', { cx: '9', cy: '12', r: '1' }],
56
+ ['circle', { cx: '9', cy: '5', r: '1' }],
57
+ ['circle', { cx: '9', cy: '19', r: '1' }],
58
+ ['circle', { cx: '15', cy: '12', r: '1' }],
59
+ ['circle', { cx: '15', cy: '5', r: '1' }],
60
+ ['circle', { cx: '15', cy: '19', r: '1' }],
61
+ ],
62
+ trash: [
63
+ ['path', { d: 'M10 11v6' }],
64
+ ['path', { d: 'M14 11v6' }],
65
+ ['path', { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' }],
66
+ ['path', { d: 'M3 6h18' }],
67
+ ['path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }],
68
+ ],
69
+ copy: [
70
+ ['rect', { width: '14', height: '14', x: '8', y: '8', rx: '2', ry: '2' }],
71
+ ['path', { d: 'M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2' }],
72
+ ],
73
+ 'arrow-up': [
74
+ ['path', { d: 'm5 12 7-7 7 7' }],
75
+ ['path', { d: 'M12 19V5' }],
76
+ ],
77
+ 'arrow-down': [
78
+ ['path', { d: 'M12 5v14' }],
79
+ ['path', { d: 'm19 12-7 7-7-7' }],
80
+ ],
81
+ 'chevron-down': [['path', { d: 'm6 9 6 6 6-6' }]],
82
+ text: [
83
+ ['path', { d: 'M13 4v16' }],
84
+ ['path', { d: 'M17 4v16' }],
85
+ ['path', { d: 'M19 4H9.5a4.5 4.5 0 0 0 0 9H13' }],
86
+ ],
87
+ 'heading-1': [
88
+ ['path', { d: 'M4 12h8' }],
89
+ ['path', { d: 'M4 18V6' }],
90
+ ['path', { d: 'M12 18V6' }],
91
+ ['path', { d: 'm17 12 3-2v8' }],
92
+ ],
93
+ 'heading-2': [
94
+ ['path', { d: 'M4 12h8' }],
95
+ ['path', { d: 'M4 18V6' }],
96
+ ['path', { d: 'M12 18V6' }],
97
+ ['path', { d: 'M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1' }],
98
+ ],
99
+ 'heading-3': [
100
+ ['path', { d: 'M4 12h8' }],
101
+ ['path', { d: 'M4 18V6' }],
102
+ ['path', { d: 'M12 18V6' }],
103
+ ['path', { d: 'M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2' }],
104
+ ['path', { d: 'M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2' }],
105
+ ],
106
+ 'heading-4': [
107
+ ['path', { d: 'M12 18V6' }],
108
+ ['path', { d: 'M17 10v3a1 1 0 0 0 1 1h3' }],
109
+ ['path', { d: 'M21 10v8' }],
110
+ ['path', { d: 'M4 12h8' }],
111
+ ['path', { d: 'M4 18V6' }],
112
+ ],
113
+ 'heading-5': [
114
+ ['path', { d: 'M4 12h8' }],
115
+ ['path', { d: 'M4 18V6' }],
116
+ ['path', { d: 'M12 18V6' }],
117
+ ['path', { d: 'M17 13v-3h4' }],
118
+ ['path', { d: 'M17 17.7c.4.2.8.3 1.3.3 1.5 0 2.7-1.1 2.7-2.5S19.8 13 18.3 13H17' }],
119
+ ],
120
+ 'heading-6': [
121
+ ['path', { d: 'M4 12h8' }],
122
+ ['path', { d: 'M4 18V6' }],
123
+ ['path', { d: 'M12 18V6' }],
124
+ ['circle', { cx: '19', cy: '16', r: '2' }],
125
+ ['path', { d: 'M20 10c-2 2-3 3.5-3 6' }],
126
+ ],
127
+ list: [
128
+ ['path', { d: 'M3 5h.01' }],
129
+ ['path', { d: 'M3 12h.01' }],
130
+ ['path', { d: 'M3 19h.01' }],
131
+ ['path', { d: 'M8 5h13' }],
132
+ ['path', { d: 'M8 12h13' }],
133
+ ['path', { d: 'M8 19h13' }],
134
+ ],
135
+ 'list-ordered': [
136
+ ['path', { d: 'M11 5h10' }],
137
+ ['path', { d: 'M11 12h10' }],
138
+ ['path', { d: 'M11 19h10' }],
139
+ ['path', { d: 'M4 4h1v5' }],
140
+ ['path', { d: 'M4 9h2' }],
141
+ ['path', { d: 'M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02' }],
142
+ ],
143
+ 'list-todo': [
144
+ ['path', { d: 'M13 5h8' }],
145
+ ['path', { d: 'M13 12h8' }],
146
+ ['path', { d: 'M13 19h8' }],
147
+ ['path', { d: 'm3 17 2 2 4-4' }],
148
+ ['path', { d: 'm3 7 2 2 4-4' }],
149
+ ],
150
+ quote: [
151
+ ['path', { d: 'M17 5H3' }],
152
+ ['path', { d: 'M21 12H8' }],
153
+ ['path', { d: 'M21 19H8' }],
154
+ ['path', { d: 'M3 12v7' }],
155
+ ],
156
+ divider: [['path', { d: 'M5 12h14' }]],
157
+ info: [
158
+ ['circle', { cx: '12', cy: '12', r: '10' }],
159
+ ['path', { d: 'M12 16v-4' }],
160
+ ['path', { d: 'M12 8h.01' }],
161
+ ],
162
+ warning: [
163
+ ['path', { d: 'm21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3' }],
164
+ ['path', { d: 'M12 9v4' }],
165
+ ['path', { d: 'M12 17h.01' }],
166
+ ],
167
+ success: [
168
+ ['circle', { cx: '12', cy: '12', r: '10' }],
169
+ ['path', { d: 'm16 9-5.5 5.5L8 12' }],
170
+ ],
171
+ danger: [
172
+ ['path', { d: 'm15 9-6 6' }],
173
+ [
174
+ 'path',
175
+ {
176
+ d: 'M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z',
177
+ },
178
+ ],
179
+ ['path', { d: 'm9 9 6 6' }],
180
+ ],
181
+ image: [
182
+ ['rect', { width: '18', height: '18', x: '3', y: '3', rx: '2', ry: '2' }],
183
+ ['circle', { cx: '9', cy: '9', r: '2' }],
184
+ ['path', { d: 'm21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21' }],
185
+ ],
186
+ replace: [
187
+ ['path', { d: 'M14 4a1 1 0 0 1 1-1' }],
188
+ ['path', { d: 'M15 10a1 1 0 0 1-1-1' }],
189
+ ['path', { d: 'M21 4a1 1 0 0 0-1-1' }],
190
+ ['path', { d: 'M21 9a1 1 0 0 1-1 1' }],
191
+ ['path', { d: 'm3 7 3 3 3-3' }],
192
+ ['path', { d: 'M6 10V5a2 2 0 0 1 2-2h2' }],
193
+ ['rect', { x: '3', y: '14', width: '7', height: '7', rx: '1' }],
194
+ ],
195
+ 'align-left': [
196
+ ['path', { d: 'M21 5H3' }],
197
+ ['path', { d: 'M15 12H3' }],
198
+ ['path', { d: 'M17 19H3' }],
199
+ ],
200
+ 'align-center': [
201
+ ['path', { d: 'M21 5H3' }],
202
+ ['path', { d: 'M17 12H7' }],
203
+ ['path', { d: 'M19 19H5' }],
204
+ ],
205
+ 'align-right': [
206
+ ['path', { d: 'M21 5H3' }],
207
+ ['path', { d: 'M21 12H9' }],
208
+ ['path', { d: 'M21 19H7' }],
209
+ ],
210
+ 'align-justify': [
211
+ ['path', { d: 'M3 5h18' }],
212
+ ['path', { d: 'M3 12h18' }],
213
+ ['path', { d: 'M3 19h18' }],
214
+ ],
215
+ baseline: [
216
+ ['path', { d: 'M4 20h16' }],
217
+ ['path', { d: 'm6 16 6-12 6 12' }],
218
+ ['path', { d: 'M8 12h8' }],
219
+ ],
220
+ highlighter: [
221
+ ['path', { d: 'm9 11-6 6v3h9l3-3' }],
222
+ ['path', { d: 'm22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4' }],
223
+ ],
224
+ sigma: [
225
+ [
226
+ 'path',
227
+ {
228
+ d: 'M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2',
229
+ },
230
+ ],
231
+ ],
232
+ } as const satisfies Record<string, readonly IconShape[]>;
233
+
234
+ export type IconName = keyof typeof ICON_SHAPES;
@@ -0,0 +1,39 @@
1
+ import type { MessageKey } from '../i18n/en';
2
+ import type { MessageParams, TFunction } from '../i18n/translate';
3
+
4
+ export type ImageErrorCode = 'type' | 'size' | 'uploadAddress' | 'read';
5
+
6
+ /**
7
+ * Ошибка добавления картинки. `message` — английский текст для разработчика (попадает в `onError` и консоль);
8
+ * в интерфейсе ошибка показывается переведённой по `code`.
9
+ */
10
+ export class ImageError extends Error {
11
+ constructor(
12
+ readonly code: ImageErrorCode,
13
+ message: string,
14
+ readonly params?: MessageParams,
15
+ ) {
16
+ super(message);
17
+ this.name = 'ImageError';
18
+ }
19
+ }
20
+
21
+ export interface ImageErrorInfo {
22
+ readonly message: string;
23
+ readonly code?: ImageErrorCode;
24
+ readonly params?: MessageParams;
25
+ }
26
+
27
+ export function toImageErrorInfo(error: unknown): ImageErrorInfo {
28
+ if (error instanceof ImageError) {
29
+ return { message: error.message, code: error.code, params: error.params };
30
+ }
31
+
32
+ return { message: error instanceof Error ? error.message : '' };
33
+ }
34
+
35
+ export function formatImageError(info: ImageErrorInfo, t: TFunction): string {
36
+ if (info.code) return t(`image.error.${info.code}` as MessageKey, info.params);
37
+
38
+ return info.message || t('image.error.generic');
39
+ }
@@ -0,0 +1,49 @@
1
+ import type { ImageErrorInfo } from './ImageError';
2
+
3
+ /**
4
+ * Какие блоки-картинки сейчас загружаются и какие ошибки загрузки показать. Состояние живёт здесь, а не в
5
+ * компоненте: пока файл обрабатывается, выбор файла заменяется индикатором и локальное состояние пропало бы.
6
+ */
7
+ export class UploadTracker {
8
+ private readonly active = new Set<string>();
9
+
10
+ private readonly errors = new Map<string, ImageErrorInfo>();
11
+
12
+ private readonly listeners = new Set<() => void>();
13
+
14
+ has = (blockId: string): boolean => this.active.has(blockId);
15
+
16
+ errorOf = (blockId: string): string | undefined => this.errors.get(blockId)?.message;
17
+
18
+ errorDetailsOf = (blockId: string): ImageErrorInfo | undefined => this.errors.get(blockId);
19
+
20
+ subscribe = (listener: () => void): (() => void) => {
21
+ this.listeners.add(listener);
22
+
23
+ return () => {
24
+ this.listeners.delete(listener);
25
+ };
26
+ };
27
+
28
+ start(blockId: string): void {
29
+ this.errors.delete(blockId);
30
+ this.active.add(blockId);
31
+ this.notify();
32
+ }
33
+
34
+ finish(blockId: string, error?: string | ImageErrorInfo): void {
35
+ const changed = this.active.delete(blockId);
36
+
37
+ if (error) this.errors.set(blockId, typeof error === 'string' ? { message: error } : error);
38
+
39
+ if (changed || error) this.notify();
40
+ }
41
+
42
+ clearError(blockId: string): void {
43
+ if (this.errors.delete(blockId)) this.notify();
44
+ }
45
+
46
+ private notify(): void {
47
+ for (const listener of [...this.listeners]) listener();
48
+ }
49
+ }
@@ -0,0 +1,142 @@
1
+ import { flushSync } from 'react-dom';
2
+
3
+ import { createId } from '@inkbrick/document';
4
+ import type { CaretResult, Editor } from '@inkbrick/engine';
5
+
6
+ import type { EditorView } from '../editing/EditorView';
7
+ import { toImageErrorInfo } from './ImageError';
8
+ import type { ImageOptions } from './types';
9
+ import { isAcceptedImage, uploadImage } from './upload';
10
+
11
+ export function topLevelBlockId(editor: Editor, blockId: string): string {
12
+ let current = editor.store.getBlock(blockId);
13
+
14
+ while (current?.parentId) {
15
+ const parent = editor.store.getBlock(current.parentId);
16
+
17
+ if (!parent || parent.flavour === 'block:note' || !parent.parentId) break;
18
+
19
+ current = parent;
20
+ }
21
+
22
+ return current?.id ?? blockId;
23
+ }
24
+
25
+ const altFromName = (file: File) => file.name.replace(/\.[^.]+$/, '').slice(0, 200);
26
+
27
+ function report(options: ImageOptions, error: unknown, file: File): void {
28
+ if (options.onError) options.onError(error, file);
29
+ else console.error('[@inkbrick/react] Could not add the image:', error);
30
+ }
31
+
32
+ export async function fillImageBlock(
33
+ editor: Editor,
34
+ view: EditorView,
35
+ options: ImageOptions,
36
+ blockId: string,
37
+ file: File,
38
+ mergeKey?: string,
39
+ ): Promise<boolean> {
40
+ view.uploads.start(blockId);
41
+
42
+ try {
43
+ const src = await uploadImage(file, options);
44
+
45
+ flushSync(() => {
46
+ editor.dispatch({
47
+ type: 'setBlockProps',
48
+ payload: { blockId, props: { src, alt: altFromName(file) }, mergeKey },
49
+ });
50
+ });
51
+
52
+ view.uploads.finish(blockId);
53
+
54
+ return true;
55
+ } catch (error) {
56
+ view.uploads.finish(blockId, toImageErrorInfo(error));
57
+ report(options, error, file);
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Вставляет картинки из файлов (вставка, перетаскивание) после блока `afterId`. Блок появляется сразу
64
+ * (с индикатором загрузки); если загрузка не удалась, пустой блок убирается. Пустой параграф заменяется картинкой.
65
+ * Возвращает false, если среди файлов нет подходящих изображений.
66
+ */
67
+ export function insertImageFiles(
68
+ editor: Editor,
69
+ view: EditorView,
70
+ options: ImageOptions,
71
+ files: readonly File[],
72
+ afterId: string,
73
+ ): boolean {
74
+ const images = files.filter((file) => isAcceptedImage(file, options));
75
+
76
+ if (images.length === 0 || editor.readOnly) return false;
77
+
78
+ let anchor = topLevelBlockId(editor, afterId);
79
+ let caret: CaretResult | undefined;
80
+
81
+ const mergeKey = `images:${createId()}`;
82
+ const created: Array<{ id: string; file: File }> = [];
83
+
84
+ flushSync(() => {
85
+ editor.batch(
86
+ () => {
87
+ for (const file of images) {
88
+ const target = editor.store.getBlock(anchor);
89
+ const alt = altFromName(file);
90
+
91
+ // Пустой параграф заменяется картинкой (и под ней появляется параграф для продолжения).
92
+ const emptyParagraph =
93
+ target?.flavour === 'block:paragraph' &&
94
+ target.content.length === 0 &&
95
+ target.children.length === 0;
96
+
97
+ if (emptyParagraph) {
98
+ caret = editor.dispatch({
99
+ type: 'convertBlock',
100
+ payload: { blockId: anchor, flavour: 'block:image', props: { alt } },
101
+ }) as CaretResult | undefined;
102
+ created.push({ id: anchor, file });
103
+ } else {
104
+ const inserted = editor.dispatch({
105
+ type: 'insertBlock',
106
+ payload: { flavour: 'block:image', afterId: anchor, props: { alt } },
107
+ }) as CaretResult | undefined;
108
+
109
+ if (!inserted) continue;
110
+
111
+ created.push({ id: inserted.blockId, file });
112
+ anchor = inserted.blockId;
113
+
114
+ if (!editor.document.getNextSibling(anchor)) {
115
+ caret = editor.dispatch({
116
+ type: 'insertBlock',
117
+ payload: { flavour: 'block:paragraph', afterId: anchor },
118
+ }) as CaretResult | undefined;
119
+ } else {
120
+ caret = { blockId: editor.document.getNextSibling(anchor)!.id, offset: 0 };
121
+ }
122
+ }
123
+ }
124
+ },
125
+ { mergeKey },
126
+ );
127
+ });
128
+
129
+ if (caret) view.placeCaret(caret.blockId, caret.offset);
130
+
131
+ for (const { id, file } of created) {
132
+ void fillImageBlock(editor, view, options, id, file, mergeKey).then((ok) => {
133
+ if (!ok && (editor.store.getBlock(id)?.props.src ?? '') === '') {
134
+ flushSync(() => {
135
+ editor.execute('delete-block', { id });
136
+ });
137
+ }
138
+ });
139
+ }
140
+
141
+ return true;
142
+ }
@@ -0,0 +1,22 @@
1
+ export const DEFAULT_IMAGE_TYPES = [
2
+ 'image/png',
3
+ 'image/jpeg',
4
+ 'image/gif',
5
+ 'image/webp',
6
+ 'image/avif',
7
+ ] as const;
8
+
9
+ export const DEFAULT_MAX_INLINE_BYTES = 1_500_000;
10
+
11
+ export interface ImageOptions {
12
+ /**
13
+ * Загружает файл и возвращает адрес картинки (http(s) или относительный). Без него картинки
14
+ * встраиваются в документ как data:-адреса (до `maxInlineBytes`).
15
+ */
16
+ readonly upload?: (file: File) => Promise<string>;
17
+ /** Предел размера для встраивания data:-картинки, байт (по умолчанию 1,5 МБ). */
18
+ readonly maxInlineBytes?: number;
19
+ /** Допустимые MIME-типы (по умолчанию PNG, JPEG, GIF, WebP, AVIF). SVG не поддерживается. */
20
+ readonly acceptedTypes?: readonly string[];
21
+ readonly onError?: (error: unknown, file: File) => void;
22
+ }
@@ -0,0 +1,62 @@
1
+ import { isSafeImageUrl } from '@inkbrick/document';
2
+
3
+ import { ImageError } from './ImageError';
4
+ import { DEFAULT_IMAGE_TYPES, DEFAULT_MAX_INLINE_BYTES, type ImageOptions } from './types';
5
+
6
+ export function isAcceptedImage(file: { type: string }, options: ImageOptions): boolean {
7
+ return (options.acceptedTypes ?? DEFAULT_IMAGE_TYPES).includes(file.type.toLowerCase());
8
+ }
9
+
10
+ function readAsDataUrl(file: File): Promise<string> {
11
+ return new Promise((resolve, reject) => {
12
+ const reader = new FileReader();
13
+
14
+ reader.onload = () => resolve(String(reader.result));
15
+ reader.onerror = () => reject(new ImageError('read', 'Could not read the file'));
16
+ reader.readAsDataURL(file);
17
+ });
18
+ }
19
+
20
+ export async function uploadImage(file: File, options: ImageOptions): Promise<string> {
21
+ if (!isAcceptedImage(file, options)) {
22
+ const type = file.type || 'unknown';
23
+
24
+ throw new ImageError('type', `Unsupported image type "${type}"`, { type });
25
+ }
26
+
27
+ let url: string;
28
+
29
+ if (options.upload) {
30
+ url = await options.upload(file);
31
+ } else {
32
+ const limit = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
33
+
34
+ if (file.size > limit) {
35
+ const size = Math.round(limit / 1024);
36
+
37
+ throw new ImageError(
38
+ 'size',
39
+ `The image is larger than ${size} KB. Provide "images.upload" to store larger images.`,
40
+ { size },
41
+ );
42
+ }
43
+
44
+ url = await readAsDataUrl(file);
45
+ }
46
+
47
+ if (!isSafeImageUrl(url))
48
+ throw new ImageError('uploadAddress', 'The upload returned an unsupported image address');
49
+
50
+ return url;
51
+ }
52
+
53
+ export function normalizeImageUrl(input: string): string | null {
54
+ const value = input.trim();
55
+
56
+ if (value === '') return null;
57
+
58
+ const candidate =
59
+ /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith('/') ? value : `https://${value}`;
60
+
61
+ return isSafeImageUrl(candidate) ? candidate : null;
62
+ }