@cloud-ru/ds-markdown 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.
Files changed (212) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +273 -0
  4. package/dist/cjs/components/Markdown/CodeBlock.d.ts +9 -0
  5. package/dist/cjs/components/Markdown/CodeBlock.js +58 -0
  6. package/dist/cjs/components/Markdown/Markdown.d.ts +2 -0
  7. package/dist/cjs/components/Markdown/Markdown.js +25 -0
  8. package/dist/cjs/components/Markdown/index.d.ts +1 -0
  9. package/dist/cjs/components/Markdown/index.js +17 -0
  10. package/dist/cjs/components/Markdown/styles.module.css +205 -0
  11. package/dist/cjs/components/MarkdownEditor/MarkdownEditor.d.ts +2 -0
  12. package/dist/cjs/components/MarkdownEditor/MarkdownEditor.js +176 -0
  13. package/dist/cjs/components/MarkdownEditor/index.d.ts +1 -0
  14. package/dist/cjs/components/MarkdownEditor/index.js +17 -0
  15. package/dist/cjs/components/MarkdownEditor/styles.module.css +273 -0
  16. package/dist/cjs/components/Toolbar/Toolbar.d.ts +9 -0
  17. package/dist/cjs/components/Toolbar/Toolbar.js +173 -0
  18. package/dist/cjs/components/Toolbar/constants.d.ts +13 -0
  19. package/dist/cjs/components/Toolbar/constants.js +40 -0
  20. package/dist/cjs/components/Toolbar/hooks/index.d.ts +2 -0
  21. package/dist/cjs/components/Toolbar/hooks/index.js +18 -0
  22. package/dist/cjs/components/Toolbar/hooks/useToolbarHotkeys.d.ts +21 -0
  23. package/dist/cjs/components/Toolbar/hooks/useToolbarHotkeys.js +59 -0
  24. package/dist/cjs/components/Toolbar/hooks/useToolbarOverflow.d.ts +14 -0
  25. package/dist/cjs/components/Toolbar/hooks/useToolbarOverflow.js +87 -0
  26. package/dist/cjs/components/Toolbar/index.d.ts +2 -0
  27. package/dist/cjs/components/Toolbar/index.js +20 -0
  28. package/dist/cjs/components/Toolbar/styles.module.css +17 -0
  29. package/dist/cjs/components/index.d.ts +2 -0
  30. package/dist/cjs/components/index.js +20 -0
  31. package/dist/cjs/constants.d.ts +65 -0
  32. package/dist/cjs/constants.js +67 -0
  33. package/dist/cjs/helperComponents/ButtonHeading/ButtonHeading.d.ts +6 -0
  34. package/dist/cjs/helperComponents/ButtonHeading/ButtonHeading.js +61 -0
  35. package/dist/cjs/helperComponents/ButtonHeading/index.d.ts +1 -0
  36. package/dist/cjs/helperComponents/ButtonHeading/index.js +17 -0
  37. package/dist/cjs/helperComponents/ButtonHeading/styles.module.css +3 -0
  38. package/dist/cjs/helperComponents/Buttons/Buttons.d.ts +10 -0
  39. package/dist/cjs/helperComponents/Buttons/Buttons.js +21 -0
  40. package/dist/cjs/helperComponents/Buttons/index.d.ts +1 -0
  41. package/dist/cjs/helperComponents/Buttons/index.js +17 -0
  42. package/dist/cjs/helperComponents/Buttons/styles.module.css +17 -0
  43. package/dist/cjs/helperComponents/Modals/CustomizeTableModal.d.ts +8 -0
  44. package/dist/cjs/helperComponents/Modals/CustomizeTableModal.js +39 -0
  45. package/dist/cjs/helperComponents/Modals/ImageModal.d.ts +7 -0
  46. package/dist/cjs/helperComponents/Modals/ImageModal.js +44 -0
  47. package/dist/cjs/helperComponents/Modals/LinkModal.d.ts +13 -0
  48. package/dist/cjs/helperComponents/Modals/LinkModal.js +42 -0
  49. package/dist/cjs/helperComponents/Modals/index.d.ts +3 -0
  50. package/dist/cjs/helperComponents/Modals/index.js +19 -0
  51. package/dist/cjs/helperComponents/Modals/styles.module.css +17 -0
  52. package/dist/cjs/helperComponents/PrivateButton/PrivateButton.d.ts +17 -0
  53. package/dist/cjs/helperComponents/PrivateButton/PrivateButton.js +27 -0
  54. package/dist/cjs/helperComponents/PrivateButton/index.d.ts +1 -0
  55. package/dist/cjs/helperComponents/PrivateButton/index.js +17 -0
  56. package/dist/cjs/helperComponents/PrivateButton/styles.module.css +140 -0
  57. package/dist/cjs/helperComponents/TableSelectItem/TableSelectItem.d.ts +11 -0
  58. package/dist/cjs/helperComponents/TableSelectItem/TableSelectItem.js +12 -0
  59. package/dist/cjs/helperComponents/TableSelectItem/index.d.ts +1 -0
  60. package/dist/cjs/helperComponents/TableSelectItem/index.js +17 -0
  61. package/dist/cjs/helperComponents/TableSelectItem/styles.module.css +101 -0
  62. package/dist/cjs/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.d.ts +4 -0
  63. package/dist/cjs/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.js +27 -0
  64. package/dist/cjs/helperComponents/TableSelectSettingsGrid/index.d.ts +1 -0
  65. package/dist/cjs/helperComponents/TableSelectSettingsGrid/index.js +17 -0
  66. package/dist/cjs/helperComponents/TableSelectSettingsGrid/styles.module.css +54 -0
  67. package/dist/cjs/helperComponents/index.d.ts +6 -0
  68. package/dist/cjs/helperComponents/index.js +22 -0
  69. package/dist/cjs/index.d.ts +4 -0
  70. package/dist/cjs/index.js +20 -0
  71. package/dist/cjs/locale/index.d.ts +199 -0
  72. package/dist/cjs/locale/index.js +104 -0
  73. package/dist/cjs/lowlight.d.ts +14 -0
  74. package/dist/cjs/lowlight.js +35 -0
  75. package/dist/cjs/mixins.css +0 -0
  76. package/dist/cjs/package.json +3 -0
  77. package/dist/cjs/rawMarkdownCommands.d.ts +16 -0
  78. package/dist/cjs/rawMarkdownCommands.js +145 -0
  79. package/dist/cjs/style.css +875 -0
  80. package/dist/cjs/styles/syntax.module.css +47 -0
  81. package/dist/cjs/toolbarApi.d.ts +33 -0
  82. package/dist/cjs/toolbarApi.js +100 -0
  83. package/dist/cjs/types.d.ts +52 -0
  84. package/dist/cjs/types.js +2 -0
  85. package/dist/esm/components/Markdown/CodeBlock.d.ts +9 -0
  86. package/dist/esm/components/Markdown/CodeBlock.js +52 -0
  87. package/dist/esm/components/Markdown/Markdown.d.ts +2 -0
  88. package/dist/esm/components/Markdown/Markdown.js +19 -0
  89. package/dist/esm/components/Markdown/index.d.ts +1 -0
  90. package/dist/esm/components/Markdown/index.js +1 -0
  91. package/dist/esm/components/Markdown/styles.module.css +205 -0
  92. package/dist/esm/components/MarkdownEditor/MarkdownEditor.d.ts +2 -0
  93. package/dist/esm/components/MarkdownEditor/MarkdownEditor.js +170 -0
  94. package/dist/esm/components/MarkdownEditor/index.d.ts +1 -0
  95. package/dist/esm/components/MarkdownEditor/index.js +1 -0
  96. package/dist/esm/components/MarkdownEditor/styles.module.css +273 -0
  97. package/dist/esm/components/Toolbar/Toolbar.d.ts +9 -0
  98. package/dist/esm/components/Toolbar/Toolbar.js +167 -0
  99. package/dist/esm/components/Toolbar/constants.d.ts +13 -0
  100. package/dist/esm/components/Toolbar/constants.js +37 -0
  101. package/dist/esm/components/Toolbar/hooks/index.d.ts +2 -0
  102. package/dist/esm/components/Toolbar/hooks/index.js +2 -0
  103. package/dist/esm/components/Toolbar/hooks/useToolbarHotkeys.d.ts +21 -0
  104. package/dist/esm/components/Toolbar/hooks/useToolbarHotkeys.js +56 -0
  105. package/dist/esm/components/Toolbar/hooks/useToolbarOverflow.d.ts +14 -0
  106. package/dist/esm/components/Toolbar/hooks/useToolbarOverflow.js +84 -0
  107. package/dist/esm/components/Toolbar/index.d.ts +2 -0
  108. package/dist/esm/components/Toolbar/index.js +2 -0
  109. package/dist/esm/components/Toolbar/styles.module.css +17 -0
  110. package/dist/esm/components/index.d.ts +2 -0
  111. package/dist/esm/components/index.js +4 -0
  112. package/dist/esm/constants.d.ts +65 -0
  113. package/dist/esm/constants.js +61 -0
  114. package/dist/esm/helperComponents/ButtonHeading/ButtonHeading.d.ts +6 -0
  115. package/dist/esm/helperComponents/ButtonHeading/ButtonHeading.js +55 -0
  116. package/dist/esm/helperComponents/ButtonHeading/index.d.ts +1 -0
  117. package/dist/esm/helperComponents/ButtonHeading/index.js +1 -0
  118. package/dist/esm/helperComponents/ButtonHeading/styles.module.css +3 -0
  119. package/dist/esm/helperComponents/Buttons/Buttons.d.ts +10 -0
  120. package/dist/esm/helperComponents/Buttons/Buttons.js +13 -0
  121. package/dist/esm/helperComponents/Buttons/index.d.ts +1 -0
  122. package/dist/esm/helperComponents/Buttons/index.js +1 -0
  123. package/dist/esm/helperComponents/Buttons/styles.module.css +17 -0
  124. package/dist/esm/helperComponents/Modals/CustomizeTableModal.d.ts +8 -0
  125. package/dist/esm/helperComponents/Modals/CustomizeTableModal.js +33 -0
  126. package/dist/esm/helperComponents/Modals/ImageModal.d.ts +7 -0
  127. package/dist/esm/helperComponents/Modals/ImageModal.js +38 -0
  128. package/dist/esm/helperComponents/Modals/LinkModal.d.ts +13 -0
  129. package/dist/esm/helperComponents/Modals/LinkModal.js +36 -0
  130. package/dist/esm/helperComponents/Modals/index.d.ts +3 -0
  131. package/dist/esm/helperComponents/Modals/index.js +3 -0
  132. package/dist/esm/helperComponents/Modals/styles.module.css +17 -0
  133. package/dist/esm/helperComponents/PrivateButton/PrivateButton.d.ts +17 -0
  134. package/dist/esm/helperComponents/PrivateButton/PrivateButton.js +21 -0
  135. package/dist/esm/helperComponents/PrivateButton/index.d.ts +1 -0
  136. package/dist/esm/helperComponents/PrivateButton/index.js +1 -0
  137. package/dist/esm/helperComponents/PrivateButton/styles.module.css +140 -0
  138. package/dist/esm/helperComponents/TableSelectItem/TableSelectItem.d.ts +11 -0
  139. package/dist/esm/helperComponents/TableSelectItem/TableSelectItem.js +6 -0
  140. package/dist/esm/helperComponents/TableSelectItem/index.d.ts +1 -0
  141. package/dist/esm/helperComponents/TableSelectItem/index.js +1 -0
  142. package/dist/esm/helperComponents/TableSelectItem/styles.module.css +101 -0
  143. package/dist/esm/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.d.ts +4 -0
  144. package/dist/esm/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.js +21 -0
  145. package/dist/esm/helperComponents/TableSelectSettingsGrid/index.d.ts +1 -0
  146. package/dist/esm/helperComponents/TableSelectSettingsGrid/index.js +1 -0
  147. package/dist/esm/helperComponents/TableSelectSettingsGrid/styles.module.css +54 -0
  148. package/dist/esm/helperComponents/index.d.ts +6 -0
  149. package/dist/esm/helperComponents/index.js +6 -0
  150. package/dist/esm/index.d.ts +4 -0
  151. package/dist/esm/index.js +4 -0
  152. package/dist/esm/locale/index.d.ts +199 -0
  153. package/dist/esm/locale/index.js +101 -0
  154. package/dist/esm/lowlight.d.ts +14 -0
  155. package/dist/esm/lowlight.js +29 -0
  156. package/dist/esm/mixins.css +0 -0
  157. package/dist/esm/rawMarkdownCommands.d.ts +16 -0
  158. package/dist/esm/rawMarkdownCommands.js +135 -0
  159. package/dist/esm/style.css +875 -0
  160. package/dist/esm/styles/syntax.module.css +47 -0
  161. package/dist/esm/toolbarApi.d.ts +33 -0
  162. package/dist/esm/toolbarApi.js +96 -0
  163. package/dist/esm/types.d.ts +52 -0
  164. package/dist/esm/types.js +1 -0
  165. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  166. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  167. package/package.json +84 -0
  168. package/src/components/Markdown/CodeBlock.tsx +96 -0
  169. package/src/components/Markdown/Markdown.tsx +47 -0
  170. package/src/components/Markdown/index.ts +1 -0
  171. package/src/components/Markdown/styles.module.scss +105 -0
  172. package/src/components/MarkdownEditor/MarkdownEditor.tsx +322 -0
  173. package/src/components/MarkdownEditor/index.ts +1 -0
  174. package/src/components/MarkdownEditor/styles.module.scss +201 -0
  175. package/src/components/Toolbar/Toolbar.tsx +292 -0
  176. package/src/components/Toolbar/constants.tsx +63 -0
  177. package/src/components/Toolbar/hooks/index.ts +2 -0
  178. package/src/components/Toolbar/hooks/useToolbarHotkeys.ts +74 -0
  179. package/src/components/Toolbar/hooks/useToolbarOverflow.ts +102 -0
  180. package/src/components/Toolbar/index.ts +2 -0
  181. package/src/components/Toolbar/styles.module.scss +29 -0
  182. package/src/components/index.ts +4 -0
  183. package/src/constants.ts +67 -0
  184. package/src/helperComponents/ButtonHeading/ButtonHeading.tsx +100 -0
  185. package/src/helperComponents/ButtonHeading/index.ts +1 -0
  186. package/src/helperComponents/ButtonHeading/styles.module.scss +5 -0
  187. package/src/helperComponents/Buttons/Buttons.tsx +30 -0
  188. package/src/helperComponents/Buttons/index.ts +1 -0
  189. package/src/helperComponents/Buttons/styles.module.scss +26 -0
  190. package/src/helperComponents/Modals/CustomizeTableModal.tsx +82 -0
  191. package/src/helperComponents/Modals/ImageModal.tsx +84 -0
  192. package/src/helperComponents/Modals/LinkModal.tsx +92 -0
  193. package/src/helperComponents/Modals/index.ts +3 -0
  194. package/src/helperComponents/Modals/styles.module.scss +21 -0
  195. package/src/helperComponents/PrivateButton/PrivateButton.tsx +88 -0
  196. package/src/helperComponents/PrivateButton/index.ts +1 -0
  197. package/src/helperComponents/PrivateButton/styles.module.scss +77 -0
  198. package/src/helperComponents/TableSelectItem/TableSelectItem.tsx +43 -0
  199. package/src/helperComponents/TableSelectItem/index.ts +1 -0
  200. package/src/helperComponents/TableSelectItem/styles.module.scss +26 -0
  201. package/src/helperComponents/TableSelectSettingsGrid/TableSelectSettingsGrid.tsx +48 -0
  202. package/src/helperComponents/TableSelectSettingsGrid/index.ts +1 -0
  203. package/src/helperComponents/TableSelectSettingsGrid/styles.module.scss +65 -0
  204. package/src/helperComponents/index.ts +6 -0
  205. package/src/index.ts +4 -0
  206. package/src/locale/index.ts +105 -0
  207. package/src/lowlight.ts +30 -0
  208. package/src/mixins.scss +123 -0
  209. package/src/rawMarkdownCommands.ts +179 -0
  210. package/src/styles/syntax.module.scss +54 -0
  211. package/src/toolbarApi.ts +152 -0
  212. package/src/types.ts +56 -0
@@ -0,0 +1,201 @@
1
+ @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/markdown.module' as md;
3
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/field.module' as field;
4
+ @use '@cloud-ru/ds-figma-variables/build/scss/components/scroll.module' as scroll;
5
+ @use '../../mixins' as mx;
6
+
7
+ // Внутренние отступы контента поверх паддинга fieldContainer (его задаёт FieldShell):
8
+ // повторяют contentWrapper.verticalPadding + inputLine.horizontalPadding из field-токенов.
9
+ $content-padding-vertical: field.$sn-field-anatomy-size-m-fieldContainer-contentWrapper-verticalPadding;
10
+ $content-padding-horizontal: field.$sn-field-anatomy-size-m-fieldContainer-paddingRight;
11
+ $min-height: field.$sn-field-anatomy-size-m-fieldContainer-multiLine-minHeight;
12
+ $scrollbar-width: base.simple-var(scroll.$scroll, 'anatomy', 'container', 'size', 's', 'width');
13
+ $button-slot: field.$sn-field-anatomy-size-m-fieldContainer-centeredWrapper-maxHeight;
14
+ $input-line-padding: field.$sn-field-anatomy-size-m-fieldContainer-contentWrapper-inputLine-horizontalPadding;
15
+
16
+ .root {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: md.$sn-markdown-anatomy-size-container-gap;
20
+ box-sizing: border-box;
21
+ }
22
+
23
+ // Шапка (headlineWrapper) — вне поля: тогл Preview слева, подпись справа.
24
+ .header {
25
+ display: flex;
26
+ gap: base.$sn-primitive-dimension-8;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+
30
+ font-family: md.$sn-brand-font-fontFamily-regular-body;
31
+ font-size: md.$sn-density-typography-roleBody-fontSizeS;
32
+ line-height: md.$sn-density-typography-roleBody-lineHeightS;
33
+ color: md.$sn-theme-color-available-version-textSecondary;
34
+ }
35
+
36
+ .previewToggle {
37
+ cursor: pointer;
38
+ user-select: none;
39
+
40
+ display: inline-flex;
41
+ gap: md.$sn-markdown-anatomy-size-container-headlineWrapper-headline-gap;
42
+ align-items: center;
43
+ }
44
+
45
+ // Подпись тогла крупнее подписи справа: в мастере это body/m, а не body/s шапки.
46
+ .previewToggleLabel {
47
+ @include base.composite-var(base.$sn-regular, 'body', 'm');
48
+
49
+ color: base.$sn-theme-color-neutral-text;
50
+ }
51
+
52
+ .label {
53
+ color: md.$sn-theme-color-available-version-textTertiary;
54
+ }
55
+
56
+ // Слот тулбара в header-слоте оболочки: растягивается на всю ширину и даёт тулбару
57
+ // сжиматься (Toolbar-root имеет flex-shrink: 0), иначе overflow в More не срабатывает.
58
+ .toolbarSlot {
59
+ flex: 1 1 auto;
60
+ min-width: 0;
61
+ }
62
+
63
+ // Контентная область поля: общий контейнер для textarea (raw) и EditorContent (preview).
64
+ // position: relative — якорь для overlay-кнопки очистки.
65
+ .contentArea {
66
+ position: relative;
67
+
68
+ display: flex;
69
+
70
+ box-sizing: border-box;
71
+ width: 100%;
72
+ padding: $content-padding-vertical $content-padding-horizontal;
73
+ }
74
+
75
+ // Общий resize-бокс: один и тот же DOM-узел в обоих режимах, поэтому заданная ресайзом
76
+ // высота сохраняется при переключении preview. Потомок (textarea/EditorContent) скроллится
77
+ // внутри (native overflow), бокс лишь задаёт высоту.
78
+ .resizeBox {
79
+ display: flex;
80
+ flex: 1 1 auto;
81
+ flex-direction: column;
82
+
83
+ min-width: 0;
84
+ min-height: $min-height;
85
+ }
86
+
87
+ // При resizable — вертикальный CSS-resize со стартовой высотой min-height (иначе resize
88
+ // не от чего отсчитывать); overflow != visible нужен, чтобы появился resize-handle.
89
+ .resizeBoxResizable {
90
+ resize: vertical;
91
+ overflow: hidden;
92
+ height: $min-height;
93
+ }
94
+
95
+ .postfix {
96
+ $square: field.$sn-field-anatomy-size-m-fieldContainer-centeredWrapper-maxHeight;
97
+ $input-line-padding: field.$sn-field-anatomy-size-m-fieldContainer-contentWrapper-inputLine-horizontalPadding;
98
+ $line-height: base.$sn-regular-body-m-lineHeight;
99
+ $vertical-padding: field.$sn-field-anatomy-size-m-fieldContainer-contentWrapper-verticalPadding;
100
+
101
+ position: absolute;
102
+
103
+ // Кнопка центрируется по первой строке текста: verticalPadding (отступ контента сверху)
104
+ // + центровка слота в line-height. Так 32px-кнопка верхним краем садится ровно на
105
+ // внутреннюю границу поля (Figma), а не выступает над ней.
106
+ top: calc(#{$vertical-padding} + (#{$line-height} - #{$square}) / 2);
107
+ right: calc(#{$scrollbar-width} + #{$input-line-padding});
108
+
109
+ display: grid;
110
+ grid-auto-rows: $square;
111
+
112
+ // Единственный явный column-track шириной с контейнер: без него implicit-колонка
113
+ // растягивается (auto) под кнопку, и кнопка перестаёт центрироваться по слоту.
114
+ grid-template-columns: 100%;
115
+ row-gap: base.$sn-primitive-dimension-2;
116
+ place-items: center center;
117
+
118
+ width: $square;
119
+ }
120
+
121
+ .rawInputWrapper {
122
+ display: flex;
123
+ width: 100%;
124
+ height: 100%;
125
+ }
126
+
127
+ // raw-режим: «сырой» markdown в textarea. Native-скролл при переполнении resize-бокса.
128
+ .rawInput {
129
+ resize: none;
130
+
131
+ overflow-y: auto;
132
+ flex: 1 1 auto;
133
+
134
+ min-height: 0;
135
+ padding: 0;
136
+ padding-right: calc(#{$button-slot} + #{$scrollbar-width} + #{$input-line-padding});
137
+ border: none;
138
+
139
+ font-family: md.$sn-brand-font-fontFamily-regular-body;
140
+ font-size: md.$sn-density-typography-roleBody-fontSizeM;
141
+ line-height: md.$sn-density-typography-roleBody-lineHeightM;
142
+ color: md.$sn-theme-color-available-version-textMain;
143
+
144
+ background: transparent;
145
+ outline: none;
146
+
147
+ &::placeholder {
148
+ color: base.$sn-theme-color-available-version-textDisabled;
149
+ }
150
+ }
151
+
152
+ // preview-режим: WYSIWYG (EditorContent). Заполняет resize-бокс, скроллится при переполнении.
153
+ .content {
154
+ overflow-y: auto;
155
+ flex: 1 1 auto;
156
+
157
+ min-height: 0;
158
+
159
+ font-family: md.$sn-brand-font-fontFamily-regular-body;
160
+ font-size: md.$sn-density-typography-roleBody-fontSizeM;
161
+ line-height: md.$sn-density-typography-roleBody-lineHeightM;
162
+ color: md.$sn-theme-color-available-version-textMain;
163
+
164
+ :global(.ProseMirror) {
165
+ min-height: 100%;
166
+ padding-right: calc(#{$button-slot} + #{$scrollbar-width} + #{$input-line-padding});
167
+ outline: none;
168
+
169
+ @include mx.markdown-content;
170
+ @include mx.markdown-block-spacings;
171
+
172
+ pre {
173
+ overflow-x: auto;
174
+ padding: base.$sn-primitive-dimension-12;
175
+ border-radius: md.$sn-density-radius-2xs;
176
+ background: md.$sn-theme-color-neutral-background2Level;
177
+
178
+ code {
179
+ min-height: 0;
180
+ padding: 0;
181
+ background: transparent;
182
+ }
183
+ }
184
+
185
+ :global(p.is-editor-empty:first-child)::before {
186
+ pointer-events: none;
187
+ content: attr(data-placeholder);
188
+
189
+ float: left;
190
+
191
+ height: 0;
192
+
193
+ color: base.$sn-theme-color-available-version-textDisabled;
194
+ }
195
+
196
+ img {
197
+ max-width: 100%;
198
+ height: auto;
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,292 @@
1
+ import { ChevronRightSVG, MoreSVG } from '@cloud-ru/ds-icons/interface/system';
2
+ import { Droplist, DroplistProps, ItemProps } from '@cloud-ru/ds-list';
3
+ import { usePortalContext } from '@cloud-ru/ds-portal-context';
4
+ import { Tooltip } from '@cloud-ru/ds-tooltip';
5
+ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
6
+
7
+ import { TEST_IDS, TOOLBAR_ITEM, toolbarButtonTestId } from '../../constants';
8
+ import {
9
+ ButtonHeading,
10
+ Buttons,
11
+ ButtonsDivider,
12
+ ButtonsGroup,
13
+ CustomizeTableModal,
14
+ ImageModal,
15
+ LinkModal,
16
+ LinkProps,
17
+ PrivateButton,
18
+ PrivateButtonProps,
19
+ TableSelectSettingsGrid,
20
+ } from '../../helperComponents';
21
+ import { markdownLocale } from '../../locale';
22
+ import { ToolbarApi } from '../../toolbarApi';
23
+ import { ToolbarItemId } from '../../types';
24
+ import { BUTTONS, GROUPS } from './constants';
25
+ import { useToolbarHotkeys, useToolbarOverflow } from './hooks';
26
+ import styles from './styles.module.scss';
27
+
28
+ type ToolbarProps = {
29
+ /** Бэкенд команд: WYSIWYG (TipTap) в preview-режиме либо markdown-исходник (textarea) в raw-режиме. */
30
+ api: ToolbarApi;
31
+ items: ToolbarItemId[];
32
+ };
33
+
34
+ export function Toolbar({ api, items }: ToolbarProps) {
35
+ const portalContext = usePortalContext();
36
+ const { t } = markdownLocale.useTranslations();
37
+
38
+ // Overflow тулбара (сколько кнопок видно, остальные — в «Ещё») вынесен в хук.
39
+ const { rootRef, moreButtonRef, visibleCount } = useToolbarOverflow(items);
40
+ const [moreOpen, setMoreOpen] = useState(false);
41
+ const [tableOpen, setTableOpen] = useState(false);
42
+ const [linkInitial, setLinkInitial] = useState<LinkProps | undefined>(undefined);
43
+
44
+ const [toolbarItemModal, setToolbarItemModal] = useState<'image' | 'link' | 'table'>();
45
+
46
+ // Закрывает оба table-picker'а и открывает модалку настройки размеров.
47
+ const openCustomize = useCallback(() => {
48
+ setTableOpen(false);
49
+ setMoreOpen(false);
50
+ setToolbarItemModal('table');
51
+ }, []);
52
+
53
+ const openLink = useCallback(() => {
54
+ setMoreOpen(false);
55
+
56
+ // raw-режим: ссылка добавляется как сырой markdown-шаблон прямо в textarea, без модалки.
57
+ if (api.mode === 'raw') {
58
+ api.setLink({});
59
+ return;
60
+ }
61
+
62
+ setLinkInitial({
63
+ href: api.getLinkHref(),
64
+ title: api.getLinkTitle(),
65
+ });
66
+ setToolbarItemModal('link');
67
+ }, [api]);
68
+
69
+ const openImage = useCallback(() => {
70
+ setMoreOpen(false);
71
+ setToolbarItemModal('image');
72
+ }, []);
73
+
74
+ const closeToolbarItemModal = () => {
75
+ setToolbarItemModal(undefined);
76
+ api.focus();
77
+ };
78
+
79
+ // Принудительный re-render тулбара на selection/transaction → переключаются checked-стейты кнопок.
80
+ const [, setTick] = useState(0);
81
+ useEffect(() => api.subscribe(() => setTick(n => n + 1)), [api]);
82
+
83
+ useToolbarHotkeys({ rootRef, items, api, onLink: openLink, onImage: openImage });
84
+
85
+ const toolbarTableItems = useMemo<DroplistProps['items']>(
86
+ () => [
87
+ {
88
+ content: (
89
+ <TableSelectSettingsGrid
90
+ onPick={(rows, cols) => {
91
+ api.insertTable(rows, cols);
92
+ setTableOpen(false);
93
+ setMoreOpen(false);
94
+ }}
95
+ />
96
+ ),
97
+ inactive: true,
98
+ },
99
+ { type: 'group', content: '', divider: true },
100
+ {
101
+ content: {
102
+ label: t('table.customize'),
103
+ className: styles.tableCustomizeOption,
104
+ },
105
+ onClick: openCustomize,
106
+ afterContent: <ChevronRightSVG />,
107
+ 'data-test-id': TEST_IDS.tableCustomize,
108
+ },
109
+ ],
110
+ [api, t, openCustomize],
111
+ );
112
+
113
+ const renderItem = (id: ToolbarItemId) => {
114
+ const spec = BUTTONS[id];
115
+
116
+ if (!spec) {
117
+ return null;
118
+ }
119
+
120
+ const name = t(spec.nameKey);
121
+
122
+ const commonButtonProps: PrivateButtonProps = {
123
+ id,
124
+ icon: spec.icon,
125
+ 'data-test-id': toolbarButtonTestId(id),
126
+ 'aria-label': name,
127
+ tip: { title: name, caption: spec.hotkey },
128
+ checked: api.isActive(id),
129
+ onClick: () => api.toggle(id),
130
+ };
131
+
132
+ if (id === TOOLBAR_ITEM.Heading) {
133
+ return <ButtonHeading key={id} api={api} />;
134
+ }
135
+
136
+ if (id === TOOLBAR_ITEM.Link && spec) {
137
+ commonButtonProps.onClick = openLink;
138
+ }
139
+
140
+ if (id === TOOLBAR_ITEM.Image) {
141
+ commonButtonProps.onClick = openImage;
142
+ }
143
+
144
+ if (id === TOOLBAR_ITEM.Table && spec) {
145
+ return (
146
+ <Droplist
147
+ size='m'
148
+ container={portalContext}
149
+ key={id}
150
+ open={tableOpen}
151
+ onOpenChange={setTableOpen}
152
+ placement='bottom-start'
153
+ triggerClassName={styles.tooltipTrigger}
154
+ items={toolbarTableItems}
155
+ >
156
+ <PrivateButton key={id} {...commonButtonProps} />
157
+ </Droplist>
158
+ );
159
+ }
160
+
161
+ return <PrivateButton key={id} {...commonButtonProps} />;
162
+ };
163
+
164
+ // Решаем, какие items видимы, группируем под GROUPS, рисуем дивайдеры между группами.
165
+ const visibleItems = new Set(items.slice(0, visibleCount));
166
+ const overflowItems = items.slice(visibleCount);
167
+ const presentGroups = GROUPS.map(group => group.filter(id => visibleItems.has(id))).filter(g => g.length > 0);
168
+
169
+ const handleMoreSelect = (id: ToolbarItemId) => {
170
+ if (id === TOOLBAR_ITEM.Table) {
171
+ // Table-picker открывается вторым уровнем, якорь — More-кнопка (childless Dropdown + triggerRef).
172
+ setTableOpen(true);
173
+ return;
174
+ }
175
+
176
+ if (id === TOOLBAR_ITEM.Link) {
177
+ openLink();
178
+ } else if (id === TOOLBAR_ITEM.Image) {
179
+ openImage();
180
+ } else {
181
+ setMoreOpen(false);
182
+ api.toggle(id);
183
+ }
184
+ };
185
+
186
+ const handleSubmitCustomizeTable: ToolbarApi['insertTable'] = (rows, cols) => {
187
+ api.insertTable(rows, cols);
188
+ closeToolbarItemModal();
189
+ setMoreOpen(false);
190
+ };
191
+
192
+ const handleLinkSubmit: ToolbarApi['setLink'] = linkProps => {
193
+ api.setLink(linkProps);
194
+ closeToolbarItemModal();
195
+ };
196
+
197
+ const handleImageSubmit: ToolbarApi['insertImage'] = (url, alt) => {
198
+ api.insertImage(url, alt);
199
+ closeToolbarItemModal();
200
+ };
201
+
202
+ // Строки More-списка: иконка + подпись + хоткей справа (caption).
203
+ const moreItems = overflowItems.flatMap((id): ItemProps[] => {
204
+ const spec = BUTTONS[id];
205
+
206
+ if (!spec) return [];
207
+
208
+ const base = {
209
+ id,
210
+ beforeContent: spec?.icon,
211
+ content: { label: t(spec.nameKey), caption: spec.hotkey },
212
+ onClick: () => handleMoreSelect(id),
213
+ 'data-test-id': toolbarButtonTestId(id),
214
+ };
215
+
216
+ if (id === TOOLBAR_ITEM.Table) {
217
+ return [
218
+ {
219
+ ...base,
220
+ type: 'next-list',
221
+ items: toolbarTableItems,
222
+ },
223
+ ];
224
+ }
225
+
226
+ return [
227
+ {
228
+ ...base,
229
+ beforeContent: spec.icon,
230
+ content: { label: t(spec.nameKey), caption: spec.hotkey },
231
+ checked: api.isActive(id),
232
+ },
233
+ ];
234
+ });
235
+
236
+ return (
237
+ <div ref={rootRef} className={styles.root}>
238
+ <Buttons>
239
+ {presentGroups.map((group, gi) => (
240
+ <Fragment key={`g-${gi}`}>
241
+ {gi > 0 && <ButtonsDivider />}
242
+ <ButtonsGroup>{group.map(renderItem)}</ButtonsGroup>
243
+ </Fragment>
244
+ ))}
245
+
246
+ {overflowItems.length > 0 && (
247
+ <>
248
+ <ButtonsDivider />
249
+
250
+ <Droplist
251
+ size='m'
252
+ container={portalContext}
253
+ open={moreOpen}
254
+ onOpenChange={setMoreOpen}
255
+ placement='bottom-end'
256
+ marker={false}
257
+ items={moreItems}
258
+ triggerElemRef={moreButtonRef}
259
+ triggerClassName={styles.tooltipTrigger}
260
+ data-test-id={TEST_IDS.toolbarMoreList}
261
+ >
262
+ <Tooltip tip={t('toolbar.more')} triggerClassName={styles.tooltipTrigger}>
263
+ <PrivateButton
264
+ icon={<MoreSVG />}
265
+ data-test-id={TEST_IDS.toolbarMore}
266
+ aria-label={t('toolbar.more')}
267
+ aria-haspopup='menu'
268
+ aria-expanded={moreOpen}
269
+ />
270
+ </Tooltip>
271
+ </Droplist>
272
+ </>
273
+ )}
274
+ </Buttons>
275
+
276
+ <CustomizeTableModal
277
+ open={toolbarItemModal === 'table'}
278
+ onClose={closeToolbarItemModal}
279
+ onSubmit={handleSubmitCustomizeTable}
280
+ />
281
+
282
+ <LinkModal
283
+ open={toolbarItemModal === 'link'}
284
+ initial={linkInitial}
285
+ onClose={closeToolbarItemModal}
286
+ onSubmit={handleLinkSubmit}
287
+ />
288
+
289
+ <ImageModal open={toolbarItemModal === 'image'} onClose={closeToolbarItemModal} onSubmit={handleImageSubmit} />
290
+ </div>
291
+ );
292
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ BlockCodeSVG,
3
+ BlockQuoteSVG,
4
+ BoldSVG,
5
+ BulletListSVG,
6
+ HeadingSVG,
7
+ ImageSVG,
8
+ InlineCodeSVG,
9
+ ItalicSVG,
10
+ LinkSVG,
11
+ OrderedListSVG,
12
+ StrikeSVG,
13
+ TableSVG,
14
+ } from '@cloud-ru/ds-icons/interface/system';
15
+ import { ReactNode } from 'react';
16
+
17
+ import { TOOLBAR_ITEM } from '../../constants';
18
+ import { MarkdownMessages } from '../../locale';
19
+ import { ToolbarItemId } from '../../types';
20
+
21
+ // Группировка: text-style | inline | lists | blocks | media.
22
+ export const GROUPS: ToolbarItemId[][] = [
23
+ [TOOLBAR_ITEM.Heading, TOOLBAR_ITEM.Bold, TOOLBAR_ITEM.Italic, TOOLBAR_ITEM.Strikethrough],
24
+ [TOOLBAR_ITEM.Link, TOOLBAR_ITEM.InlineCode, TOOLBAR_ITEM.BulletList, TOOLBAR_ITEM.OrderedList],
25
+ [TOOLBAR_ITEM.BlockQuote, TOOLBAR_ITEM.BlockCode, TOOLBAR_ITEM.Table, TOOLBAR_ITEM.Image],
26
+ ];
27
+
28
+ type ButtonSpec = {
29
+ icon: ReactNode;
30
+ nameKey: `toolbar.${Extract<keyof MarkdownMessages['toolbar'], string>}`;
31
+ /** Хоткей для строки в More-списке. Только реально работающие биндинги TipTap. */
32
+ hotkey?: string;
33
+ };
34
+
35
+ export const BUTTONS: Record<ToolbarItemId, ButtonSpec> = {
36
+ [TOOLBAR_ITEM.Heading]: { icon: <HeadingSVG />, nameKey: 'toolbar.heading' },
37
+ [TOOLBAR_ITEM.Bold]: { icon: <BoldSVG />, nameKey: 'toolbar.bold', hotkey: 'Ctrl+B' },
38
+ [TOOLBAR_ITEM.Italic]: { icon: <ItalicSVG />, nameKey: 'toolbar.italic', hotkey: 'Ctrl+I' },
39
+ [TOOLBAR_ITEM.Strikethrough]: { icon: <StrikeSVG />, nameKey: 'toolbar.strikethrough', hotkey: 'Ctrl+Shift+X' },
40
+ [TOOLBAR_ITEM.Link]: { icon: <LinkSVG />, nameKey: 'toolbar.link', hotkey: 'Ctrl+K' },
41
+ [TOOLBAR_ITEM.Image]: { icon: <ImageSVG />, nameKey: 'toolbar.image', hotkey: 'Ctrl+Shift+I' },
42
+ [TOOLBAR_ITEM.Table]: { icon: <TableSVG />, nameKey: 'toolbar.table' },
43
+ [TOOLBAR_ITEM.InlineCode]: { icon: <InlineCodeSVG />, nameKey: 'toolbar.inlineCode', hotkey: 'Ctrl+E' },
44
+ [TOOLBAR_ITEM.BulletList]: { icon: <BulletListSVG />, nameKey: 'toolbar.bulletList', hotkey: 'Ctrl+Shift+8' },
45
+ [TOOLBAR_ITEM.OrderedList]: { icon: <OrderedListSVG />, nameKey: 'toolbar.orderedList', hotkey: 'Ctrl+Shift+7' },
46
+ [TOOLBAR_ITEM.BlockQuote]: { icon: <BlockQuoteSVG />, nameKey: 'toolbar.blockQuote', hotkey: 'Ctrl+Shift+B' },
47
+ [TOOLBAR_ITEM.BlockCode]: { icon: <BlockCodeSVG />, nameKey: 'toolbar.blockCode', hotkey: 'Ctrl+Alt+C' },
48
+ };
49
+
50
+ export const DEFAULT_TOOLBAR: ToolbarItemId[] = [
51
+ TOOLBAR_ITEM.Heading,
52
+ TOOLBAR_ITEM.Bold,
53
+ TOOLBAR_ITEM.Italic,
54
+ TOOLBAR_ITEM.Strikethrough,
55
+ TOOLBAR_ITEM.Link,
56
+ TOOLBAR_ITEM.InlineCode,
57
+ TOOLBAR_ITEM.BulletList,
58
+ TOOLBAR_ITEM.OrderedList,
59
+ TOOLBAR_ITEM.BlockQuote,
60
+ TOOLBAR_ITEM.BlockCode,
61
+ TOOLBAR_ITEM.Table,
62
+ TOOLBAR_ITEM.Image,
63
+ ];
@@ -0,0 +1,2 @@
1
+ export * from './useToolbarHotkeys';
2
+ export * from './useToolbarOverflow';
@@ -0,0 +1,74 @@
1
+ import { RefObject, useEffect } from 'react';
2
+
3
+ import { TOOLBAR_ITEM } from '../../../constants';
4
+ import { ToolbarApi } from '../../../toolbarApi';
5
+ import { ToolbarItemId } from '../../../types';
6
+ import { BUTTONS } from '../constants';
7
+
8
+ // Сопоставляет KeyboardEvent с хоткеем вида 'Ctrl+Shift+I' (Cmd подменяет Ctrl на macOS).
9
+ const matchesHotkey = (event: KeyboardEvent, hotkey: string) => {
10
+ const parts = hotkey.toLowerCase().split('+');
11
+ const last = parts[parts.length - 1];
12
+ // С Shift цифры дают символ (Shift+8 → '*'), поэтому помимо event.key матчим и по event.code
13
+ // (Digit8 / KeyB) — иначе Ctrl+Shift+8/7 не распознались бы.
14
+ const code = event.code.toLowerCase();
15
+ const keyMatches = event.key.toLowerCase() === last || code === `key${last}` || code === `digit${last}`;
16
+ return (
17
+ (event.ctrlKey || event.metaKey) === parts.includes('ctrl') &&
18
+ event.shiftKey === parts.includes('shift') &&
19
+ event.altKey === parts.includes('alt') &&
20
+ keyMatches
21
+ );
22
+ };
23
+
24
+ type ToolbarHotkeysParams = {
25
+ /** Корень тулбара (из useToolbarOverflow) — от его родителя ищем scope-предка для listener'а. */
26
+ rootRef: RefObject<HTMLDivElement | null>;
27
+ items: ToolbarItemId[];
28
+ api: ToolbarApi;
29
+ /** Действие кнопки Link (открыть модалку или вставить сырой markdown). */
30
+ onLink(): void;
31
+ /** Действие кнопки Image (открыть модалку). */
32
+ onImage(): void;
33
+ };
34
+
35
+ /**
36
+ * Вешает хоткеи тулбара на корень редактора (scope), а не на конкретный input:
37
+ * - Link/Image открывают UI (модалку/пикер), а не команды TipTap → биндим всегда;
38
+ * - остальные кнопки (bold/italic/…) в WYSIWYG обрабатывает сам keymap TipTap, а в raw-режиме
39
+ * textarea их не знает, поэтому в raw биндим все hotkey'и на api-команды вручную.
40
+ */
41
+ export function useToolbarHotkeys({ rootRef, items, api, onLink, onImage }: ToolbarHotkeysParams) {
42
+ useEffect(() => {
43
+ // Ищем scope от РОДИТЕЛЯ тулбара: у самого toolbar-root есть data-test-id, и closest вернул бы
44
+ // его же — а input (textarea/ProseMirror) лежит рядом, не внутри тулбара. Нужен общий предок
45
+ // (оболочка поля), который содержит и тулбар, и input, чтобы keydown из input всплывал к нему.
46
+ const scope = rootRef.current?.parentElement?.closest<HTMLElement>('[data-test-id]');
47
+ if (!scope) return;
48
+
49
+ const bindings: { hotkey: string; action: () => void }[] = [];
50
+ for (const id of items) {
51
+ const hotkey = BUTTONS[id]?.hotkey;
52
+ if (!hotkey) continue;
53
+
54
+ if (id === TOOLBAR_ITEM.Link) {
55
+ bindings.push({ hotkey, action: onLink });
56
+ } else if (id === TOOLBAR_ITEM.Image) {
57
+ bindings.push({ hotkey, action: onImage });
58
+ } else if (api.mode === 'raw') {
59
+ bindings.push({ hotkey, action: () => api.toggle(id) });
60
+ }
61
+ }
62
+
63
+ const handleKeyDown = (event: KeyboardEvent) => {
64
+ const hit = bindings.find(binding => matchesHotkey(event, binding.hotkey));
65
+ if (hit) {
66
+ event.preventDefault();
67
+ hit.action();
68
+ }
69
+ };
70
+
71
+ scope.addEventListener('keydown', handleKeyDown);
72
+ return () => scope.removeEventListener('keydown', handleKeyDown);
73
+ }, [items, api, onLink, onImage, rootRef]);
74
+ }