@react-x11/components 0.8.0 → 0.9.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 (323) hide show
  1. package/README.md +95 -34
  2. package/dist/charts/node.d.ts +1 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +6 -2
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/embed/client.d.ts +48 -3
  7. package/dist/embed/client.d.ts.map +1 -1
  8. package/dist/embed/client.js +79 -5
  9. package/dist/embed/client.js.map +1 -1
  10. package/dist/embed/index.d.ts +1 -1
  11. package/dist/embed/index.d.ts.map +1 -1
  12. package/dist/embed/index.js +1 -1
  13. package/dist/embed/index.js.map +1 -1
  14. package/dist/html/node.d.ts +3 -1
  15. package/dist/html/node.d.ts.map +1 -1
  16. package/dist/html/node.js +7 -5
  17. package/dist/html/node.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  23. package/dist/internal/markdown/ast.d.ts.map +1 -0
  24. package/dist/internal/markdown/ast.js +22 -0
  25. package/dist/internal/markdown/ast.js.map +1 -0
  26. package/dist/internal/markdown/parse.d.ts.map +1 -0
  27. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  28. package/dist/internal/markdown/parse.js.map +1 -0
  29. package/dist/internal/markdown/stringify.d.ts +4 -0
  30. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  31. package/dist/internal/markdown/stringify.js +684 -0
  32. package/dist/internal/markdown/stringify.js.map +1 -0
  33. package/dist/internal/markdown/tags.d.ts.map +1 -0
  34. package/dist/internal/markdown/tags.js.map +1 -0
  35. package/dist/internal/scroll.d.ts +3 -2
  36. package/dist/internal/scroll.d.ts.map +1 -1
  37. package/dist/internal/scroll.js +1 -0
  38. package/dist/internal/scroll.js.map +1 -1
  39. package/dist/maps/anchors.d.ts +98 -0
  40. package/dist/maps/anchors.d.ts.map +1 -0
  41. package/dist/maps/anchors.js +531 -0
  42. package/dist/maps/anchors.js.map +1 -0
  43. package/dist/maps/controller.d.ts +283 -0
  44. package/dist/maps/controller.d.ts.map +1 -0
  45. package/dist/maps/controller.js +743 -0
  46. package/dist/maps/controller.js.map +1 -0
  47. package/dist/maps/gl/buckets.d.ts +95 -0
  48. package/dist/maps/gl/buckets.d.ts.map +1 -0
  49. package/dist/maps/gl/buckets.js +362 -0
  50. package/dist/maps/gl/buckets.js.map +1 -0
  51. package/dist/maps/gl/build-worker.d.ts +13 -0
  52. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  53. package/dist/maps/gl/build-worker.js +44 -0
  54. package/dist/maps/gl/build-worker.js.map +1 -0
  55. package/dist/maps/gl/color.d.ts +8 -0
  56. package/dist/maps/gl/color.d.ts.map +1 -0
  57. package/dist/maps/gl/color.js +57 -0
  58. package/dist/maps/gl/color.js.map +1 -0
  59. package/dist/maps/gl/cover.d.ts +51 -0
  60. package/dist/maps/gl/cover.d.ts.map +1 -0
  61. package/dist/maps/gl/cover.js +124 -0
  62. package/dist/maps/gl/cover.js.map +1 -0
  63. package/dist/maps/gl/index.d.ts +3 -0
  64. package/dist/maps/gl/index.d.ts.map +1 -0
  65. package/dist/maps/gl/index.js +20 -0
  66. package/dist/maps/gl/index.js.map +1 -0
  67. package/dist/maps/gl/markers.d.ts +28 -0
  68. package/dist/maps/gl/markers.d.ts.map +1 -0
  69. package/dist/maps/gl/markers.js +60 -0
  70. package/dist/maps/gl/markers.js.map +1 -0
  71. package/dist/maps/gl/overlays.d.ts +68 -0
  72. package/dist/maps/gl/overlays.d.ts.map +1 -0
  73. package/dist/maps/gl/overlays.js +300 -0
  74. package/dist/maps/gl/overlays.js.map +1 -0
  75. package/dist/maps/gl/pane.d.ts +12 -0
  76. package/dist/maps/gl/pane.d.ts.map +1 -0
  77. package/dist/maps/gl/pane.js +37 -0
  78. package/dist/maps/gl/pane.js.map +1 -0
  79. package/dist/maps/gl/placement.d.ts +111 -0
  80. package/dist/maps/gl/placement.d.ts.map +1 -0
  81. package/dist/maps/gl/placement.js +567 -0
  82. package/dist/maps/gl/placement.js.map +1 -0
  83. package/dist/maps/gl/renderer.d.ts +352 -0
  84. package/dist/maps/gl/renderer.d.ts.map +1 -0
  85. package/dist/maps/gl/renderer.js +1127 -0
  86. package/dist/maps/gl/renderer.js.map +1 -0
  87. package/dist/maps/gl/shaders.d.ts +120 -0
  88. package/dist/maps/gl/shaders.d.ts.map +1 -0
  89. package/dist/maps/gl/shaders.js +451 -0
  90. package/dist/maps/gl/shaders.js.map +1 -0
  91. package/dist/maps/gl/store.d.ts +115 -0
  92. package/dist/maps/gl/store.d.ts.map +1 -0
  93. package/dist/maps/gl/store.js +469 -0
  94. package/dist/maps/gl/store.js.map +1 -0
  95. package/dist/maps/gl/text.d.ts +154 -0
  96. package/dist/maps/gl/text.d.ts.map +1 -0
  97. package/dist/maps/gl/text.js +382 -0
  98. package/dist/maps/gl/text.js.map +1 -0
  99. package/dist/maps/gl/view.d.ts +54 -0
  100. package/dist/maps/gl/view.d.ts.map +1 -0
  101. package/dist/maps/gl/view.js +974 -0
  102. package/dist/maps/gl/view.js.map +1 -0
  103. package/dist/maps/index.d.ts +1 -1
  104. package/dist/maps/index.d.ts.map +1 -1
  105. package/dist/maps/index.js +216 -52
  106. package/dist/maps/index.js.map +1 -1
  107. package/dist/maps/labels.d.ts +37 -16
  108. package/dist/maps/labels.d.ts.map +1 -1
  109. package/dist/maps/labels.js +188 -119
  110. package/dist/maps/labels.js.map +1 -1
  111. package/dist/maps/node.d.ts +162 -56
  112. package/dist/maps/node.d.ts.map +1 -1
  113. package/dist/maps/node.js +607 -515
  114. package/dist/maps/node.js.map +1 -1
  115. package/dist/maps/overlay.d.ts +72 -9
  116. package/dist/maps/overlay.d.ts.map +1 -1
  117. package/dist/maps/overlay.js +108 -28
  118. package/dist/maps/overlay.js.map +1 -1
  119. package/dist/maps/paint.d.ts +3 -0
  120. package/dist/maps/paint.d.ts.map +1 -1
  121. package/dist/maps/paint.js.map +1 -1
  122. package/dist/maps/proj.d.ts +14 -0
  123. package/dist/maps/proj.d.ts.map +1 -1
  124. package/dist/maps/proj.js +28 -0
  125. package/dist/maps/proj.js.map +1 -1
  126. package/dist/maps/renderer.d.ts +69 -0
  127. package/dist/maps/renderer.d.ts.map +1 -0
  128. package/dist/maps/renderer.js +66 -0
  129. package/dist/maps/renderer.js.map +1 -0
  130. package/dist/maps/sources.d.ts +25 -3
  131. package/dist/maps/sources.d.ts.map +1 -1
  132. package/dist/maps/sources.js +17 -0
  133. package/dist/maps/sources.js.map +1 -1
  134. package/dist/maps/theme.d.ts +19 -0
  135. package/dist/maps/theme.d.ts.map +1 -0
  136. package/dist/maps/theme.js +44 -0
  137. package/dist/maps/theme.js.map +1 -0
  138. package/dist/maps/tiles.d.ts +117 -17
  139. package/dist/maps/tiles.d.ts.map +1 -1
  140. package/dist/maps/tiles.js +163 -30
  141. package/dist/maps/tiles.js.map +1 -1
  142. package/dist/maps/types.d.ts +188 -20
  143. package/dist/maps/types.d.ts.map +1 -1
  144. package/dist/markdown/expressions.d.ts +1 -1
  145. package/dist/markdown/expressions.d.ts.map +1 -1
  146. package/dist/markdown/expressions.js.map +1 -1
  147. package/dist/markdown/index.d.ts +7 -6
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +9 -7
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/spans.d.ts +1 -1
  152. package/dist/markdown/spans.d.ts.map +1 -1
  153. package/dist/media-player/index.d.ts +11 -3
  154. package/dist/media-player/index.d.ts.map +1 -1
  155. package/dist/media-player/index.js +13 -2
  156. package/dist/media-player/index.js.map +1 -1
  157. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  158. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  159. package/dist/rich-text-editor/clipboard.js +307 -0
  160. package/dist/rich-text-editor/clipboard.js.map +1 -0
  161. package/dist/rich-text-editor/collab.d.ts +32 -0
  162. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  163. package/dist/rich-text-editor/collab.js +55 -0
  164. package/dist/rich-text-editor/collab.js.map +1 -0
  165. package/dist/rich-text-editor/commands.d.ts +50 -0
  166. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  167. package/dist/rich-text-editor/commands.js +376 -0
  168. package/dist/rich-text-editor/commands.js.map +1 -0
  169. package/dist/rich-text-editor/drag.d.ts +68 -0
  170. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  171. package/dist/rich-text-editor/drag.js +148 -0
  172. package/dist/rich-text-editor/drag.js.map +1 -0
  173. package/dist/rich-text-editor/html.d.ts +145 -0
  174. package/dist/rich-text-editor/html.d.ts.map +1 -0
  175. package/dist/rich-text-editor/html.js +516 -0
  176. package/dist/rich-text-editor/html.js.map +1 -0
  177. package/dist/rich-text-editor/index.d.ts +263 -0
  178. package/dist/rich-text-editor/index.d.ts.map +1 -0
  179. package/dist/rich-text-editor/index.js +822 -0
  180. package/dist/rich-text-editor/index.js.map +1 -0
  181. package/dist/rich-text-editor/inline.d.ts +94 -0
  182. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  183. package/dist/rich-text-editor/inline.js +252 -0
  184. package/dist/rich-text-editor/inline.js.map +1 -0
  185. package/dist/rich-text-editor/keymap.d.ts +71 -0
  186. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  187. package/dist/rich-text-editor/keymap.js +393 -0
  188. package/dist/rich-text-editor/keymap.js.map +1 -0
  189. package/dist/rich-text-editor/keys.d.ts +29 -0
  190. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  191. package/dist/rich-text-editor/keys.js +93 -0
  192. package/dist/rich-text-editor/keys.js.map +1 -0
  193. package/dist/rich-text-editor/look.d.ts +71 -0
  194. package/dist/rich-text-editor/look.d.ts.map +1 -0
  195. package/dist/rich-text-editor/look.js +342 -0
  196. package/dist/rich-text-editor/look.js.map +1 -0
  197. package/dist/rich-text-editor/markdown.d.ts +18 -0
  198. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  199. package/dist/rich-text-editor/markdown.js +496 -0
  200. package/dist/rich-text-editor/markdown.js.map +1 -0
  201. package/dist/rich-text-editor/nodes.d.ts +135 -0
  202. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  203. package/dist/rich-text-editor/nodes.js +397 -0
  204. package/dist/rich-text-editor/nodes.js.map +1 -0
  205. package/dist/rich-text-editor/render.d.ts +68 -0
  206. package/dist/rich-text-editor/render.d.ts.map +1 -0
  207. package/dist/rich-text-editor/render.js +500 -0
  208. package/dist/rich-text-editor/render.js.map +1 -0
  209. package/dist/rich-text-editor/schema.d.ts +16 -0
  210. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  211. package/dist/rich-text-editor/schema.js +386 -0
  212. package/dist/rich-text-editor/schema.js.map +1 -0
  213. package/dist/rich-text-editor/store.d.ts +11 -0
  214. package/dist/rich-text-editor/store.d.ts.map +1 -0
  215. package/dist/rich-text-editor/store.js +43 -0
  216. package/dist/rich-text-editor/store.js.map +1 -0
  217. package/dist/rich-text-editor/suggest.d.ts +127 -0
  218. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  219. package/dist/rich-text-editor/suggest.js +0 -0
  220. package/dist/rich-text-editor/suggest.js.map +1 -0
  221. package/dist/rich-text-editor/tables.d.ts +41 -0
  222. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  223. package/dist/rich-text-editor/tables.js +204 -0
  224. package/dist/rich-text-editor/tables.js.map +1 -0
  225. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  226. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  227. package/dist/rich-text-editor/toolbar.js +548 -0
  228. package/dist/rich-text-editor/toolbar.js.map +1 -0
  229. package/dist/rich-text-editor/view.d.ts +368 -0
  230. package/dist/rich-text-editor/view.d.ts.map +1 -0
  231. package/dist/rich-text-editor/view.js +1968 -0
  232. package/dist/rich-text-editor/view.js.map +1 -0
  233. package/dist/rich-text-editor/virtual.d.ts +54 -0
  234. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  235. package/dist/rich-text-editor/virtual.js +262 -0
  236. package/dist/rich-text-editor/virtual.js.map +1 -0
  237. package/dist/richtext/node.d.ts +25 -2
  238. package/dist/richtext/node.d.ts.map +1 -1
  239. package/dist/richtext/node.js +46 -24
  240. package/dist/richtext/node.js.map +1 -1
  241. package/dist/terminal/backends.d.ts +4 -1
  242. package/dist/terminal/backends.d.ts.map +1 -1
  243. package/dist/terminal/backends.js.map +1 -1
  244. package/dist/terminal/index.d.ts +18 -7
  245. package/dist/terminal/index.d.ts.map +1 -1
  246. package/dist/terminal/index.js +28 -11
  247. package/dist/terminal/index.js.map +1 -1
  248. package/package.json +23 -4
  249. package/src/charts/node.ts +6 -2
  250. package/src/embed/client.ts +102 -8
  251. package/src/embed/index.ts +2 -0
  252. package/src/html/node.ts +7 -5
  253. package/src/index.ts +2 -0
  254. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  255. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  256. package/src/internal/markdown/stringify.ts +772 -0
  257. package/src/internal/scroll.ts +5 -3
  258. package/src/maps/anchors.ts +607 -0
  259. package/src/maps/controller.ts +999 -0
  260. package/src/maps/gl/buckets.ts +457 -0
  261. package/src/maps/gl/build-worker.ts +55 -0
  262. package/src/maps/gl/color.ts +62 -0
  263. package/src/maps/gl/cover.ts +170 -0
  264. package/src/maps/gl/index.ts +23 -0
  265. package/src/maps/gl/markers.ts +83 -0
  266. package/src/maps/gl/overlays.ts +379 -0
  267. package/src/maps/gl/pane.ts +42 -0
  268. package/src/maps/gl/placement.ts +816 -0
  269. package/src/maps/gl/renderer.ts +1576 -0
  270. package/src/maps/gl/shaders.ts +486 -0
  271. package/src/maps/gl/store.ts +566 -0
  272. package/src/maps/gl/text.ts +495 -0
  273. package/src/maps/gl/view.ts +1224 -0
  274. package/src/maps/index.ts +270 -81
  275. package/src/maps/labels.ts +214 -121
  276. package/src/maps/node.ts +763 -629
  277. package/src/maps/overlay.ts +148 -43
  278. package/src/maps/paint.ts +3 -0
  279. package/src/maps/proj.ts +31 -0
  280. package/src/maps/renderer.ts +155 -0
  281. package/src/maps/sources.ts +38 -3
  282. package/src/maps/theme.ts +51 -0
  283. package/src/maps/tiles.ts +191 -33
  284. package/src/maps/types.ts +188 -20
  285. package/src/markdown/expressions.ts +5 -1
  286. package/src/markdown/index.ts +14 -11
  287. package/src/markdown/spans.ts +1 -1
  288. package/src/media-player/index.ts +18 -4
  289. package/src/rich-text-editor/clipboard.ts +439 -0
  290. package/src/rich-text-editor/collab.ts +91 -0
  291. package/src/rich-text-editor/commands.ts +448 -0
  292. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  293. package/src/rich-text-editor/drag.ts +211 -0
  294. package/src/rich-text-editor/html.ts +619 -0
  295. package/src/rich-text-editor/index.ts +1331 -0
  296. package/src/rich-text-editor/inline.ts +345 -0
  297. package/src/rich-text-editor/keymap.ts +510 -0
  298. package/src/rich-text-editor/keys.ts +119 -0
  299. package/src/rich-text-editor/look.ts +427 -0
  300. package/src/rich-text-editor/markdown.ts +653 -0
  301. package/src/rich-text-editor/nodes.ts +497 -0
  302. package/src/rich-text-editor/render.ts +800 -0
  303. package/src/rich-text-editor/schema.ts +431 -0
  304. package/src/rich-text-editor/store.ts +39 -0
  305. package/src/rich-text-editor/suggest.ts +597 -0
  306. package/src/rich-text-editor/tables.ts +272 -0
  307. package/src/rich-text-editor/toolbar.ts +724 -0
  308. package/src/rich-text-editor/view.ts +2287 -0
  309. package/src/rich-text-editor/virtual.ts +347 -0
  310. package/src/richtext/node.ts +59 -30
  311. package/src/terminal/backends.ts +4 -1
  312. package/src/terminal/index.ts +43 -15
  313. package/dist/markdown/ast.d.ts.map +0 -1
  314. package/dist/markdown/ast.js +0 -17
  315. package/dist/markdown/ast.js.map +0 -1
  316. package/dist/markdown/parse.d.ts.map +0 -1
  317. package/dist/markdown/parse.js.map +0 -1
  318. package/dist/markdown/tags.d.ts.map +0 -1
  319. package/dist/markdown/tags.js.map +0 -1
  320. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  321. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  322. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  323. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -0,0 +1,724 @@
1
+ // The toolbar: a row of formatting buttons over the document — the piece of
2
+ // editor chrome most apps want and few want to build.
3
+ //
4
+ // Three rungs on one prop. `toolbar` gives the default set for whatever the
5
+ // schema can do; an array picks and orders — built-in names, `'|'` for a
6
+ // separator, and items of the app's own beside them; a function renders
7
+ // something else entirely, handed the editor to drive. The buttons run the
8
+ // same public commands an app's own toolbar would (`./commands.ts`,
9
+ // `./tables.ts`), and ask the state whether they are on and whether they can
10
+ // run the same way, so the built-in toolbar is a caller of the API rather
11
+ // than a second one.
12
+ //
13
+ // A press never takes focus: a button is not focusable, and its press is
14
+ // consumed before the editor's own press handler sees it — so the caret stays
15
+ // where the user left it, and the command runs on the selection they can see.
16
+ //
17
+ // Some buttons belong to a place in the document: a table's rows and columns
18
+ // mean something only with the caret in a table. Those say so with
19
+ // `visible`, and the bar leaves them out — and any separator left with
20
+ // nothing beside it — everywhere else.
21
+ //
22
+ // Most buttons are a letter or two in the editor's own face. The rest are
23
+ // pictures — undo, redo, a link, a task, a table and its parts — and those
24
+ // are drawn rather than typed: core's icon set is affordances (a chevron, a
25
+ // check) and these are nouns, and no font a desktop happens to have can be
26
+ // counted on for ↶ or ☐ (AGENTS.md, "Affordance glyphs come from core's set;
27
+ // nouns do not").
28
+ import React from 'react';
29
+ import type { ReactElement, ReactNode } from 'react';
30
+ import type { MouseEvent as X11MouseEvent } from 'react-x11';
31
+ import { tint } from 'react-x11/style';
32
+ import type { Style } from 'react-x11/style';
33
+ import { toggleMark } from 'prosemirror-commands';
34
+ import { redo, redoDepth, undo, undoDepth } from 'prosemirror-history';
35
+ import type { Schema } from 'prosemirror-model';
36
+ import type { Command, EditorState } from 'prosemirror-state';
37
+
38
+ import { hx } from '../internal/hx.js';
39
+ import { RICHTEXT_ELEMENT } from '../richtext/index.js';
40
+ import type { TextRun } from '../richtext/index.js';
41
+ import {
42
+ insertHorizontalRule,
43
+ isBlockActive,
44
+ isMarkActive,
45
+ toggleBlockType,
46
+ toggleList,
47
+ toggleTaskList,
48
+ toggleWrap,
49
+ } from './commands.js';
50
+ import type { EditorLook } from './look.js';
51
+ import {
52
+ addColumnAfter,
53
+ addColumnBefore,
54
+ addRowAfter,
55
+ addRowBefore,
56
+ columnAlign,
57
+ deleteColumn,
58
+ deleteRow,
59
+ deleteTable,
60
+ hasTables,
61
+ insertTable,
62
+ isInTable,
63
+ setColumnAlign,
64
+ } from './tables.js';
65
+
66
+ const h = React.createElement;
67
+
68
+ /** One button. */
69
+ export interface ToolbarItem {
70
+ /** Stable, and unique in one toolbar. */
71
+ id: string;
72
+ /** What the button shows when it has no `icon` — a letter or two, set in
73
+ * `labelStyle`. */
74
+ label: string;
75
+ labelStyle?: Omit<TextRun, 'text'>;
76
+ /** Drawn instead of the label: a node, or a function handed the ink the
77
+ * button's state picked — muted when it cannot run, the accent when it is
78
+ * on. An icon library's component goes here. */
79
+ icon?: ReactNode | ((ink: string) => ReactNode);
80
+ /** What a screen reader says it is. */
81
+ title: string;
82
+ run: Command;
83
+ /** Whether it is on here — bold is on inside bold text. */
84
+ active?(state: EditorState): boolean;
85
+ /** Whether it can run here. Default: whether `run` could. */
86
+ enabled?(state: EditorState): boolean;
87
+ /** Whether it is in the bar at all here — a table's rows only in a table.
88
+ * Default: always. */
89
+ visible?(state: EditorState): boolean;
90
+ }
91
+
92
+ /** The built-in items, by name. */
93
+ export type ToolbarItemName =
94
+ | 'undo'
95
+ | 'redo'
96
+ | 'paragraph'
97
+ | 'heading1'
98
+ | 'heading2'
99
+ | 'heading3'
100
+ | 'bold'
101
+ | 'italic'
102
+ | 'strike'
103
+ | 'code'
104
+ | 'link'
105
+ | 'bulletList'
106
+ | 'orderedList'
107
+ | 'taskList'
108
+ | 'blockquote'
109
+ | 'codeBlock'
110
+ | 'rule'
111
+ | 'table'
112
+ | 'rowBefore'
113
+ | 'rowAfter'
114
+ | 'columnBefore'
115
+ | 'columnAfter'
116
+ | 'deleteRow'
117
+ | 'deleteColumn'
118
+ | 'deleteTable'
119
+ | 'alignLeft'
120
+ | 'alignCenter'
121
+ | 'alignRight';
122
+
123
+ /** What a `toolbar` array holds: a built-in by name, `'|'`, or an item. */
124
+ export type ToolbarEntry = ToolbarItemName | '|' | ToolbarItem;
125
+
126
+ /** `toolbar={true}`. Whatever the schema cannot do is left out, and the
127
+ * table's own group shows only with the caret in a table. The column
128
+ * alignments are there by name, for a bar of an app's own. */
129
+ export const DEFAULT_TOOLBAR: readonly ToolbarEntry[] = [
130
+ 'undo',
131
+ 'redo',
132
+ '|',
133
+ 'heading1',
134
+ 'heading2',
135
+ 'heading3',
136
+ '|',
137
+ 'bold',
138
+ 'italic',
139
+ 'strike',
140
+ 'code',
141
+ 'link',
142
+ '|',
143
+ 'bulletList',
144
+ 'orderedList',
145
+ 'taskList',
146
+ '|',
147
+ 'blockquote',
148
+ 'codeBlock',
149
+ 'rule',
150
+ 'table',
151
+ '|',
152
+ 'rowBefore',
153
+ 'rowAfter',
154
+ 'columnBefore',
155
+ 'columnAfter',
156
+ 'deleteRow',
157
+ 'deleteColumn',
158
+ 'deleteTable',
159
+ ];
160
+
161
+ // --- the drawn glyphs -------------------------------------------------------------
162
+
163
+ const GLYPH: Style = { width: 14, height: 14 };
164
+
165
+ /** A curved arrow, back (undo) or — mirrored — forward (redo). */
166
+ function HistoryGlyph({
167
+ ink,
168
+ forward,
169
+ }: {
170
+ ink: string;
171
+ forward?: boolean;
172
+ }): ReactElement {
173
+ return hx('canvas', {
174
+ style: GLYPH,
175
+ onDraw: (ctx, { width }) => {
176
+ const x = (v: number): number => (forward ? width - v : v);
177
+ ctx.strokeStyle = ink;
178
+ ctx.lineWidth = 1.5;
179
+ ctx.lineCap = 'round';
180
+ ctx.lineJoin = 'round';
181
+ ctx.beginPath();
182
+ ctx.moveTo(x(11.5), 11.5);
183
+ ctx.bezierCurveTo(x(11.5), 7.5, x(9), 5.5, x(3.5), 5.5);
184
+ ctx.moveTo(x(6.5), 2.5);
185
+ ctx.lineTo(x(3.5), 5.5);
186
+ ctx.lineTo(x(6.5), 8.5);
187
+ ctx.stroke();
188
+ },
189
+ });
190
+ }
191
+
192
+ /** Two links of a chain on the diagonal, and the bar that joins them. */
193
+ function LinkGlyph({ ink }: { ink: string }): ReactElement {
194
+ return hx('canvas', {
195
+ style: GLYPH,
196
+ onDraw: (ctx) => {
197
+ ctx.strokeStyle = ink;
198
+ ctx.lineWidth = 1.5;
199
+ ctx.lineCap = 'round';
200
+ // a pill from one end's centre to the other's
201
+ const pill = (
202
+ ax: number,
203
+ ay: number,
204
+ bx: number,
205
+ by: number,
206
+ r: number,
207
+ ): void => {
208
+ const len = Math.hypot(bx - ax, by - ay);
209
+ const nx = -(by - ay) / len;
210
+ const ny = (bx - ax) / len;
211
+ const a = Math.atan2(ny, nx);
212
+ ctx.beginPath();
213
+ ctx.moveTo(ax + nx * r, ay + ny * r);
214
+ ctx.lineTo(bx + nx * r, by + ny * r);
215
+ ctx.arc(bx, by, r, a, a - Math.PI, true);
216
+ ctx.lineTo(ax - nx * r, ay - ny * r);
217
+ ctx.arc(ax, ay, r, a - Math.PI, a - 2 * Math.PI, true);
218
+ ctx.closePath();
219
+ ctx.stroke();
220
+ };
221
+ pill(3, 11, 5.5, 8.5, 2.25);
222
+ pill(8.5, 5.5, 11, 3, 2.25);
223
+ ctx.beginPath();
224
+ ctx.moveTo(5.25, 8.75);
225
+ ctx.lineTo(8.75, 5.25);
226
+ ctx.stroke();
227
+ },
228
+ });
229
+ }
230
+
231
+ /** A ticked box. */
232
+ function TaskGlyph({ ink }: { ink: string }): ReactElement {
233
+ return hx('canvas', {
234
+ style: GLYPH,
235
+ onDraw: (ctx, { width, height }) => {
236
+ ctx.strokeStyle = ink;
237
+ ctx.lineWidth = 1.5;
238
+ ctx.lineCap = 'round';
239
+ ctx.lineJoin = 'round';
240
+ ctx.strokeRect(1.75, 1.75, width - 3.5, height - 3.5);
241
+ ctx.beginPath();
242
+ ctx.moveTo(4.5, 7.25);
243
+ ctx.lineTo(6.25, 9);
244
+ ctx.lineTo(9.75, 5);
245
+ ctx.stroke();
246
+ },
247
+ });
248
+ }
249
+
250
+ type TablePart =
251
+ | 'table'
252
+ | 'rowBefore'
253
+ | 'rowAfter'
254
+ | 'columnBefore'
255
+ | 'columnAfter'
256
+ | 'deleteRow'
257
+ | 'deleteColumn'
258
+ | 'deleteTable';
259
+
260
+ /** A table, or the part of one a button adds or takes away: a grid, and a
261
+ * plus where a row or column goes, or a cross over what goes. */
262
+ function TableGlyph({
263
+ ink,
264
+ part,
265
+ }: {
266
+ ink: string;
267
+ part: TablePart;
268
+ }): ReactElement {
269
+ return hx('canvas', {
270
+ style: GLYPH,
271
+ onDraw: (ctx) => {
272
+ ctx.strokeStyle = ink;
273
+ ctx.lineWidth = 1.25;
274
+ ctx.lineCap = 'round';
275
+ const grid = (
276
+ x: number,
277
+ y: number,
278
+ w: number,
279
+ gh: number,
280
+ cols: number,
281
+ rows: number,
282
+ ): void => {
283
+ ctx.strokeRect(x, y, w, gh);
284
+ ctx.beginPath();
285
+ for (let c = 1; c < cols; c++) {
286
+ ctx.moveTo(x + (w * c) / cols, y);
287
+ ctx.lineTo(x + (w * c) / cols, y + gh);
288
+ }
289
+ for (let r = 1; r < rows; r++) {
290
+ ctx.moveTo(x, y + (gh * r) / rows);
291
+ ctx.lineTo(x + w, y + (gh * r) / rows);
292
+ }
293
+ ctx.stroke();
294
+ };
295
+ const mark = (cx: number, cy: number, cross: boolean): void => {
296
+ ctx.beginPath();
297
+ if (cross) {
298
+ ctx.moveTo(cx - 2, cy - 2);
299
+ ctx.lineTo(cx + 2, cy + 2);
300
+ ctx.moveTo(cx + 2, cy - 2);
301
+ ctx.lineTo(cx - 2, cy + 2);
302
+ } else {
303
+ ctx.moveTo(cx - 2.5, cy);
304
+ ctx.lineTo(cx + 2.5, cy);
305
+ ctx.moveTo(cx, cy - 2.5);
306
+ ctx.lineTo(cx, cy + 2.5);
307
+ }
308
+ ctx.stroke();
309
+ };
310
+ switch (part) {
311
+ case 'table':
312
+ grid(1.5, 2.5, 11, 9, 3, 3);
313
+ break;
314
+ case 'rowBefore':
315
+ grid(1.5, 6.5, 11, 6, 3, 2);
316
+ mark(7, 2.5, false);
317
+ break;
318
+ case 'rowAfter':
319
+ grid(1.5, 1.5, 11, 6, 3, 2);
320
+ mark(7, 11.5, false);
321
+ break;
322
+ case 'columnBefore':
323
+ grid(6.5, 1.5, 6, 11, 2, 3);
324
+ mark(2.5, 7, false);
325
+ break;
326
+ case 'columnAfter':
327
+ grid(1.5, 1.5, 6, 11, 2, 3);
328
+ mark(11.5, 7, false);
329
+ break;
330
+ case 'deleteRow':
331
+ ctx.strokeRect(1.5, 4, 11, 6);
332
+ mark(7, 7, true);
333
+ break;
334
+ case 'deleteColumn':
335
+ ctx.strokeRect(4, 1.5, 6, 11);
336
+ mark(7, 7, true);
337
+ break;
338
+ case 'deleteTable':
339
+ grid(1.5, 1.5, 8, 8, 2, 2);
340
+ mark(11, 11, true);
341
+ break;
342
+ }
343
+ },
344
+ });
345
+ }
346
+
347
+ /** Four lines set ragged the way a column's text is. */
348
+ function AlignGlyph({
349
+ ink,
350
+ align,
351
+ }: {
352
+ ink: string;
353
+ align: 'left' | 'center' | 'right';
354
+ }): ReactElement {
355
+ return hx('canvas', {
356
+ style: GLYPH,
357
+ onDraw: (ctx, { width }) => {
358
+ ctx.strokeStyle = ink;
359
+ ctx.lineWidth = 1.5;
360
+ ctx.lineCap = 'round';
361
+ ctx.beginPath();
362
+ [11, 7, 11, 7].forEach((len, i) => {
363
+ const y = 3 + i * 2.75;
364
+ const x =
365
+ align === 'left'
366
+ ? 1.5
367
+ : align === 'right'
368
+ ? width - 1.5 - len
369
+ : (width - len) / 2;
370
+ ctx.moveTo(x, y);
371
+ ctx.lineTo(x + len, y);
372
+ });
373
+ ctx.stroke();
374
+ },
375
+ });
376
+ }
377
+
378
+ // --- the items ------------------------------------------------------------------
379
+
380
+ /**
381
+ * The built-in items for `schema`. `link` is the one that needs the
382
+ * component: a link needs a target to be typed, and the component owns the
383
+ * popup that asks for one.
384
+ */
385
+ export function toolbarItems(
386
+ schema: Schema,
387
+ options: { link?: Command } = {},
388
+ ): Partial<Record<ToolbarItemName, ToolbarItem>> {
389
+ const { marks, nodes } = schema;
390
+ const out: Partial<Record<ToolbarItemName, ToolbarItem>> = {
391
+ undo: {
392
+ id: 'undo',
393
+ label: 'Undo',
394
+ icon: (ink) => h(HistoryGlyph, { ink }),
395
+ title: 'Undo',
396
+ run: undo,
397
+ enabled: (state) => undoDepth(state) > 0,
398
+ },
399
+ redo: {
400
+ id: 'redo',
401
+ label: 'Redo',
402
+ icon: (ink) => h(HistoryGlyph, { ink, forward: true }),
403
+ title: 'Redo',
404
+ run: redo,
405
+ enabled: (state) => redoDepth(state) > 0,
406
+ },
407
+ };
408
+ const mark = (
409
+ id: ToolbarItemName,
410
+ type: (typeof marks)[string] | undefined,
411
+ label: string,
412
+ title: string,
413
+ labelStyle: Omit<TextRun, 'text'>,
414
+ ): void => {
415
+ if (!type) return;
416
+ out[id] = {
417
+ id,
418
+ label,
419
+ title,
420
+ labelStyle,
421
+ run: toggleMark(type),
422
+ active: (state) => isMarkActive(state, type),
423
+ };
424
+ };
425
+ mark('bold', marks.strong, 'B', 'Bold', { weight: 700 });
426
+ mark('italic', marks.em, 'I', 'Italic', { style: 'italic' });
427
+ mark('strike', marks.strike, 'S', 'Strikethrough', {});
428
+ mark('code', marks.code, '</>', 'Inline code', { family: 'monospace' });
429
+ if (marks.link && options.link) {
430
+ const link = marks.link;
431
+ out.link = {
432
+ id: 'link',
433
+ label: 'Link',
434
+ icon: (ink) => h(LinkGlyph, { ink }),
435
+ title: 'Link',
436
+ run: options.link,
437
+ active: (state) => isMarkActive(state, link),
438
+ enabled: () => true,
439
+ };
440
+ }
441
+ if (nodes.paragraph) {
442
+ const para = nodes.paragraph;
443
+ out.paragraph = {
444
+ id: 'paragraph',
445
+ label: '¶',
446
+ title: 'Paragraph',
447
+ run: toggleBlockType(para),
448
+ active: (state) =>
449
+ isBlockActive(state, para) &&
450
+ state.selection.$from.parent.type === para,
451
+ };
452
+ }
453
+ if (nodes.heading) {
454
+ const heading = nodes.heading;
455
+ for (const level of [1, 2, 3] as const) {
456
+ out[`heading${level}`] = {
457
+ id: `heading${level}`,
458
+ label: `H${level}`,
459
+ title: `Heading ${level}`,
460
+ labelStyle: { weight: 700 },
461
+ run: toggleBlockType(heading, { level }),
462
+ active: (state) => isBlockActive(state, heading, { level }),
463
+ };
464
+ }
465
+ }
466
+ const item = nodes.list_item;
467
+ if (item && nodes.bullet_list) {
468
+ const list = nodes.bullet_list;
469
+ out.bulletList = {
470
+ id: 'bulletList',
471
+ label: '•',
472
+ title: 'Bulleted list',
473
+ labelStyle: { weight: 700 },
474
+ run: toggleList(list, item),
475
+ active: (state) =>
476
+ isBlockActive(state, list) &&
477
+ state.selection.$from.node(Math.max(0, state.selection.$from.depth - 1))
478
+ .attrs.checked == null,
479
+ };
480
+ if ('checked' in (item.spec.attrs ?? {})) {
481
+ out.taskList = {
482
+ id: 'taskList',
483
+ label: '[x]',
484
+ icon: (ink) => h(TaskGlyph, { ink }),
485
+ title: 'Task list',
486
+ run: toggleTaskList(schema),
487
+ active: (state) => {
488
+ const { $from } = state.selection;
489
+ for (let d = $from.depth; d > 0; d--) {
490
+ if ($from.node(d).type === item)
491
+ return $from.node(d).attrs.checked != null;
492
+ }
493
+ return false;
494
+ },
495
+ };
496
+ }
497
+ }
498
+ if (item && nodes.ordered_list) {
499
+ const list = nodes.ordered_list;
500
+ out.orderedList = {
501
+ id: 'orderedList',
502
+ label: '1.',
503
+ title: 'Numbered list',
504
+ run: toggleList(list, item),
505
+ active: (state) => isBlockActive(state, list),
506
+ };
507
+ }
508
+ if (nodes.blockquote) {
509
+ const quote = nodes.blockquote;
510
+ out.blockquote = {
511
+ id: 'blockquote',
512
+ label: '“',
513
+ title: 'Quote',
514
+ labelStyle: { weight: 700 },
515
+ run: toggleWrap(quote),
516
+ active: (state) => isBlockActive(state, quote),
517
+ };
518
+ }
519
+ if (nodes.code_block) {
520
+ const code = nodes.code_block;
521
+ out.codeBlock = {
522
+ id: 'codeBlock',
523
+ label: '{ }',
524
+ title: 'Code block',
525
+ labelStyle: { family: 'monospace' },
526
+ run: toggleBlockType(code),
527
+ active: (state) => isBlockActive(state, code),
528
+ };
529
+ }
530
+ if (nodes.horizontal_rule) {
531
+ out.rule = {
532
+ id: 'rule',
533
+ label: '—',
534
+ title: 'Divider',
535
+ run: insertHorizontalRule(nodes.horizontal_rule),
536
+ };
537
+ }
538
+ if (hasTables(schema)) {
539
+ out.table = {
540
+ id: 'table',
541
+ label: 'Table',
542
+ icon: (ink) => h(TableGlyph, { ink, part: 'table' }),
543
+ title: 'Table',
544
+ run: insertTable(),
545
+ active: isInTable,
546
+ };
547
+ const part = (id: TablePart, title: string, run: Command): ToolbarItem => ({
548
+ id,
549
+ label: title,
550
+ icon: (ink) => h(TableGlyph, { ink, part: id }),
551
+ title,
552
+ run,
553
+ visible: isInTable,
554
+ });
555
+ out.rowBefore = part('rowBefore', 'Row above', addRowBefore);
556
+ out.rowAfter = part('rowAfter', 'Row below', addRowAfter);
557
+ out.columnBefore = part('columnBefore', 'Column before', addColumnBefore);
558
+ out.columnAfter = part('columnAfter', 'Column after', addColumnAfter);
559
+ out.deleteRow = part('deleteRow', 'Delete row', deleteRow);
560
+ out.deleteColumn = part('deleteColumn', 'Delete column', deleteColumn);
561
+ out.deleteTable = part('deleteTable', 'Delete table', deleteTable);
562
+ for (const [id, align] of [
563
+ ['alignLeft', 'left'],
564
+ ['alignCenter', 'center'],
565
+ ['alignRight', 'right'],
566
+ ] as const) {
567
+ out[id] = {
568
+ id,
569
+ label: `Align ${align}`,
570
+ icon: (ink) => h(AlignGlyph, { ink, align }),
571
+ title: `Align column ${align}`,
572
+ // pressed again, it takes the alignment off
573
+ run: (state, dispatch) =>
574
+ setColumnAlign(columnAlign(state) === align ? null : align)(
575
+ state,
576
+ dispatch,
577
+ ),
578
+ active: (state) => columnAlign(state) === align,
579
+ visible: isInTable,
580
+ };
581
+ }
582
+ }
583
+ return out;
584
+ }
585
+
586
+ /** Entries as items: names resolved, the ones the schema cannot do dropped,
587
+ * and no separator left leading, trailing or doubled. */
588
+ export function resolveToolbar(
589
+ entries: readonly ToolbarEntry[],
590
+ items: Partial<Record<ToolbarItemName, ToolbarItem>>,
591
+ ): (ToolbarItem | '|')[] {
592
+ const out: (ToolbarItem | '|')[] = [];
593
+ for (const entry of entries) {
594
+ const item =
595
+ typeof entry === 'string' && entry !== '|' ? items[entry] : entry;
596
+ if (!item) continue;
597
+ if (item === '|' && (out.length === 0 || out[out.length - 1] === '|'))
598
+ continue;
599
+ out.push(item);
600
+ }
601
+ while (out[out.length - 1] === '|') out.pop();
602
+ return out;
603
+ }
604
+
605
+ /** The entries in the bar for `state`: the invisible ones out, and the
606
+ * separators that were only between them with them. */
607
+ function shownEntries(
608
+ entries: readonly (ToolbarItem | '|')[],
609
+ state: EditorState,
610
+ ): (ToolbarItem | '|')[] {
611
+ const out: (ToolbarItem | '|')[] = [];
612
+ for (const entry of entries) {
613
+ if (entry !== '|' && entry.visible && !entry.visible(state)) continue;
614
+ if (entry === '|' && (out.length === 0 || out[out.length - 1] === '|'))
615
+ continue;
616
+ out.push(entry);
617
+ }
618
+ while (out[out.length - 1] === '|') out.pop();
619
+ return out;
620
+ }
621
+
622
+ export interface ToolbarProps {
623
+ entries: readonly (ToolbarItem | '|')[];
624
+ state: EditorState;
625
+ run(command: Command): void;
626
+ look: EditorLook;
627
+ disabled?: boolean;
628
+ style?: Style;
629
+ }
630
+
631
+ function button(item: ToolbarItem, props: ToolbarProps): ReactNode {
632
+ const { state, look, run, disabled } = props;
633
+ const active = item.active?.(state) ?? false;
634
+ const enabled =
635
+ !disabled && (item.enabled ? item.enabled(state) : item.run(state));
636
+ // a button that cannot run is drawn in the muted ink — react-x11 styles
637
+ // have no opacity to fade it with
638
+ const ink = !enabled ? look.muted : active ? look.accent : look.text;
639
+ let content: ReactNode;
640
+ if (item.icon != null) {
641
+ content = typeof item.icon === 'function' ? item.icon(ink) : item.icon;
642
+ } else {
643
+ const label: TextRun = {
644
+ text: item.label,
645
+ family: look.family,
646
+ size: look.size,
647
+ color: ink,
648
+ ...item.labelStyle,
649
+ ...(item.id === 'strike' ? { strike: ink } : null),
650
+ };
651
+ if (item.labelStyle?.family === 'monospace') label.family = look.mono;
652
+ content = h(RICHTEXT_ELEMENT, { runs: [label], wrap: false });
653
+ }
654
+ return hx(
655
+ 'box',
656
+ {
657
+ key: item.id,
658
+ role: 'button',
659
+ 'aria-label': item.title,
660
+ 'aria-pressed': active,
661
+ focusable: false,
662
+ style: {
663
+ minWidth: 26,
664
+ height: 26,
665
+ paddingLeft: 6,
666
+ paddingRight: 6,
667
+ borderRadius: 5,
668
+ alignItems: 'center',
669
+ justifyContent: 'center',
670
+ backgroundColor: active ? tint(look.accent, 0.16) : 'transparent',
671
+ ...(enabled
672
+ ? { ':hover': { backgroundColor: tint(look.text, 0.08) } }
673
+ : null),
674
+ },
675
+ onMouseDown: (ev: X11MouseEvent) => {
676
+ // the editor's own press handler is on the root, above: stop here,
677
+ // so the caret stays put, and prevent core's default action too
678
+ ev.preventDefault();
679
+ ev.stopPropagation();
680
+ if (enabled) run(item.run);
681
+ },
682
+ },
683
+ content,
684
+ );
685
+ }
686
+
687
+ export function Toolbar(props: ToolbarProps): ReactElement {
688
+ const { look } = props;
689
+ const entries = shownEntries(props.entries, props.state);
690
+ return hx(
691
+ 'box',
692
+ {
693
+ role: 'toolbar',
694
+ style: {
695
+ flexDirection: 'row',
696
+ flexWrap: 'wrap',
697
+ alignItems: 'center',
698
+ gap: 2,
699
+ paddingLeft: 6,
700
+ paddingRight: 6,
701
+ paddingTop: 4,
702
+ paddingBottom: 4,
703
+ borderBottomWidth: 1,
704
+ borderBottomColor: look.border,
705
+ backgroundColor: look.surface,
706
+ ...props.style,
707
+ },
708
+ },
709
+ ...entries.map((entry, i) =>
710
+ entry === '|'
711
+ ? hx('box', {
712
+ key: `sep${i}`,
713
+ style: {
714
+ width: 1,
715
+ height: 16,
716
+ marginLeft: 4,
717
+ marginRight: 4,
718
+ backgroundColor: look.border,
719
+ },
720
+ })
721
+ : button(entry, props),
722
+ ),
723
+ );
724
+ }