@crystallize/design-system 1.3.2 → 1.4.1

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 (246) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/TableComponent-I2YOOYOU.css +281 -0
  3. package/dist/TableComponent-QINOO453.mjs +1377 -0
  4. package/dist/arrow-clockwise-Z2G6UEGP.svg +1 -0
  5. package/dist/arrow-counterclockwise-2O5EYVJT.svg +1 -0
  6. package/dist/bg-color-HB2WDYGO.svg +1 -0
  7. package/dist/camera-CR7D2PNH.svg +1 -0
  8. package/dist/caret-right-fill-FFBNEXVX.svg +1 -0
  9. package/dist/chat-square-quote-CI6PUJHH.svg +1 -0
  10. package/dist/chevron-down-3FRWSIKS.svg +1 -0
  11. package/dist/chunk-VUXQZRSP.mjs +737 -0
  12. package/dist/clipboard-OSEFDF25.svg +1 -0
  13. package/dist/close-FH57ZMJF.svg +1 -0
  14. package/dist/code-SEVR6TIQ.svg +1 -0
  15. package/dist/copy-DMGDODUL.svg +1 -0
  16. package/dist/diagram-2-CEJUD2B4.svg +1 -0
  17. package/dist/download-JXUGIUCX.svg +1 -0
  18. package/dist/draggable-block-menu-KKHDNKJA.svg +1 -0
  19. package/dist/dropdown-more-BHZ5COKX.svg +1 -0
  20. package/dist/file-image-TIQPFJX4.svg +1 -0
  21. package/dist/filetype-gif-OG2BEYYK.svg +1 -0
  22. package/dist/font-color-J4GA3ZJO.svg +1 -0
  23. package/dist/font-family-ZU5N6TTE.svg +1 -0
  24. package/dist/gear-ICMT4NTP.svg +1 -0
  25. package/dist/horizontal-rule-N6RD2V7H.svg +1 -0
  26. package/dist/indent-MJ6JIMCK.svg +1 -0
  27. package/dist/index.css +2711 -315
  28. package/dist/index.d.ts +145 -40
  29. package/dist/index.js +10376 -1481
  30. package/dist/index.mjs +7609 -746
  31. package/dist/journal-code-XUT44HDV.svg +1 -0
  32. package/dist/justify-J7X5JEEX.svg +1 -0
  33. package/dist/link-W52N4JKZ.svg +1 -0
  34. package/dist/list-ol-2ZEUN4Z7.svg +1 -0
  35. package/dist/list-ul-DVKNUP47.svg +1 -0
  36. package/dist/lock-WCYOZOHW.svg +1 -0
  37. package/dist/lock-fill-JZSKOSHK.svg +1 -0
  38. package/dist/markdown-4BGQNLLT.svg +1 -0
  39. package/dist/mic-H5FNOMM7.svg +1 -0
  40. package/dist/outdent-2LUMUMIP.svg +1 -0
  41. package/dist/paint-bucket-VCISMZTH.svg +1 -0
  42. package/dist/palette-SWGFPRWZ.svg +1 -0
  43. package/dist/pencil-fill-STFSC26F.svg +1 -0
  44. package/dist/plug-HGGGEVS3.svg +1 -0
  45. package/dist/plug-fill-OTG3U4TN.svg +1 -0
  46. package/dist/plus-CQISIKEC.svg +1 -0
  47. package/dist/plus-slash-minus-N22JU4TI.svg +1 -0
  48. package/dist/prettier-WUJ7B5NV.svg +1 -0
  49. package/dist/prettier-error-DYJSLYDP.svg +1 -0
  50. package/dist/square-check-UTG6FU6D.svg +1 -0
  51. package/dist/success-YVXUMPEZ.svg +1 -0
  52. package/dist/table-BR6DI4ZQ.svg +1 -0
  53. package/dist/text-center-UQI6PAEF.svg +1 -0
  54. package/dist/text-left-KT2B6TR3.svg +1 -0
  55. package/dist/text-paragraph-MFTUIIQG.svg +1 -0
  56. package/dist/text-right-SKELPISG.svg +1 -0
  57. package/dist/trash-UOM6D7TD.svg +1 -0
  58. package/dist/type-bold-PY7COC3N.svg +1 -0
  59. package/dist/type-h1-6KJP7YOM.svg +1 -0
  60. package/dist/type-h2-VHI2USC3.svg +1 -0
  61. package/dist/type-h3-JIU77CHO.svg +1 -0
  62. package/dist/type-h4-P5EHKDAL.svg +1 -0
  63. package/dist/type-h5-CS2KYVRG.svg +1 -0
  64. package/dist/type-h6-J2O74LJZ.svg +1 -0
  65. package/dist/type-italic-3DSFOSG2.svg +1 -0
  66. package/dist/type-strikethrough-E2KKQFSX.svg +1 -0
  67. package/dist/type-subscript-BMPTRIBU.svg +1 -0
  68. package/dist/type-superscript-EDF6EPAA.svg +1 -0
  69. package/dist/type-underline-CBFA5VLF.svg +1 -0
  70. package/dist/upload-Q6KICGZW.svg +1 -0
  71. package/dist/user-EOI2NEFZ.svg +1 -0
  72. package/package.json +30 -6
  73. package/src/dialog/dialog.tsx +1 -0
  74. package/src/icon-button/icon-button.css +16 -14
  75. package/src/index.ts +4 -4
  76. package/src/input/input.css +1 -1
  77. package/src/input-with-label/input-with-label.css +1 -1
  78. package/src/rich-text-editor/appSettings.ts +28 -0
  79. package/src/rich-text-editor/context/SettingsContext.tsx +71 -0
  80. package/src/rich-text-editor/context/SharedAutocompleteContext.tsx +60 -0
  81. package/src/rich-text-editor/context/SharedHistoryContext.tsx +25 -0
  82. package/src/rich-text-editor/hooks/useReport.ts +64 -0
  83. package/src/rich-text-editor/images/cat-typing.gif +0 -0
  84. package/src/rich-text-editor/images/emoji/1F600.png +0 -0
  85. package/src/rich-text-editor/images/emoji/1F641.png +0 -0
  86. package/src/rich-text-editor/images/emoji/1F642.png +0 -0
  87. package/src/rich-text-editor/images/emoji/2764.png +0 -0
  88. package/src/rich-text-editor/images/emoji/LICENSE.md +5 -0
  89. package/src/rich-text-editor/images/icons/LICENSE.md +5 -0
  90. package/src/rich-text-editor/images/icons/arrow-clockwise.svg +1 -0
  91. package/src/rich-text-editor/images/icons/arrow-counterclockwise.svg +1 -0
  92. package/src/rich-text-editor/images/icons/bg-color.svg +1 -0
  93. package/src/rich-text-editor/images/icons/camera.svg +1 -0
  94. package/src/rich-text-editor/images/icons/card-checklist.svg +1 -0
  95. package/src/rich-text-editor/images/icons/caret-right-fill.svg +1 -0
  96. package/src/rich-text-editor/images/icons/chat-left-text.svg +1 -0
  97. package/src/rich-text-editor/images/icons/chat-right-dots.svg +1 -0
  98. package/src/rich-text-editor/images/icons/chat-right-text.svg +1 -0
  99. package/src/rich-text-editor/images/icons/chat-right.svg +1 -0
  100. package/src/rich-text-editor/images/icons/chat-square-quote.svg +1 -0
  101. package/src/rich-text-editor/images/icons/chevron-down.svg +1 -0
  102. package/src/rich-text-editor/images/icons/clipboard.svg +1 -0
  103. package/src/rich-text-editor/images/icons/close.svg +1 -0
  104. package/src/rich-text-editor/images/icons/code.svg +1 -0
  105. package/src/rich-text-editor/images/icons/comments.svg +1 -0
  106. package/src/rich-text-editor/images/icons/copy.svg +1 -0
  107. package/src/rich-text-editor/images/icons/diagram-2.svg +1 -0
  108. package/src/rich-text-editor/images/icons/download.svg +1 -0
  109. package/src/rich-text-editor/images/icons/draggable-block-menu.svg +1 -0
  110. package/src/rich-text-editor/images/icons/dropdown-more.svg +1 -0
  111. package/src/rich-text-editor/images/icons/figma.svg +1 -0
  112. package/src/rich-text-editor/images/icons/file-image.svg +1 -0
  113. package/src/rich-text-editor/images/icons/filetype-gif.svg +1 -0
  114. package/src/rich-text-editor/images/icons/font-color.svg +1 -0
  115. package/src/rich-text-editor/images/icons/font-family.svg +1 -0
  116. package/src/rich-text-editor/images/icons/gear.svg +1 -0
  117. package/src/rich-text-editor/images/icons/horizontal-rule.svg +1 -0
  118. package/src/rich-text-editor/images/icons/indent.svg +1 -0
  119. package/src/rich-text-editor/images/icons/journal-code.svg +1 -0
  120. package/src/rich-text-editor/images/icons/journal-text.svg +1 -0
  121. package/src/rich-text-editor/images/icons/justify.svg +1 -0
  122. package/src/rich-text-editor/images/icons/link.svg +1 -0
  123. package/src/rich-text-editor/images/icons/list-ol.svg +1 -0
  124. package/src/rich-text-editor/images/icons/list-ul.svg +1 -0
  125. package/src/rich-text-editor/images/icons/lock-fill.svg +1 -0
  126. package/src/rich-text-editor/images/icons/lock.svg +1 -0
  127. package/src/rich-text-editor/images/icons/markdown.svg +1 -0
  128. package/src/rich-text-editor/images/icons/mic.svg +1 -0
  129. package/src/rich-text-editor/images/icons/outdent.svg +1 -0
  130. package/src/rich-text-editor/images/icons/paint-bucket.svg +1 -0
  131. package/src/rich-text-editor/images/icons/palette.svg +1 -0
  132. package/src/rich-text-editor/images/icons/pencil-fill.svg +1 -0
  133. package/src/rich-text-editor/images/icons/plug-fill.svg +1 -0
  134. package/src/rich-text-editor/images/icons/plug.svg +1 -0
  135. package/src/rich-text-editor/images/icons/plus-slash-minus.svg +1 -0
  136. package/src/rich-text-editor/images/icons/plus.svg +1 -0
  137. package/src/rich-text-editor/images/icons/prettier-error.svg +1 -0
  138. package/src/rich-text-editor/images/icons/prettier.svg +1 -0
  139. package/src/rich-text-editor/images/icons/send.svg +1 -0
  140. package/src/rich-text-editor/images/icons/square-check.svg +1 -0
  141. package/src/rich-text-editor/images/icons/sticky.svg +1 -0
  142. package/src/rich-text-editor/images/icons/success.svg +1 -0
  143. package/src/rich-text-editor/images/icons/table.svg +1 -0
  144. package/src/rich-text-editor/images/icons/text-center.svg +1 -0
  145. package/src/rich-text-editor/images/icons/text-left.svg +1 -0
  146. package/src/rich-text-editor/images/icons/text-paragraph.svg +1 -0
  147. package/src/rich-text-editor/images/icons/text-right.svg +1 -0
  148. package/src/rich-text-editor/images/icons/trash.svg +1 -0
  149. package/src/rich-text-editor/images/icons/trash3.svg +1 -0
  150. package/src/rich-text-editor/images/icons/tweet.svg +1 -0
  151. package/src/rich-text-editor/images/icons/type-bold.svg +1 -0
  152. package/src/rich-text-editor/images/icons/type-h1.svg +1 -0
  153. package/src/rich-text-editor/images/icons/type-h2.svg +1 -0
  154. package/src/rich-text-editor/images/icons/type-h3.svg +1 -0
  155. package/src/rich-text-editor/images/icons/type-h4.svg +1 -0
  156. package/src/rich-text-editor/images/icons/type-h5.svg +1 -0
  157. package/src/rich-text-editor/images/icons/type-h6.svg +1 -0
  158. package/src/rich-text-editor/images/icons/type-italic.svg +1 -0
  159. package/src/rich-text-editor/images/icons/type-strikethrough.svg +1 -0
  160. package/src/rich-text-editor/images/icons/type-subscript.svg +1 -0
  161. package/src/rich-text-editor/images/icons/type-superscript.svg +1 -0
  162. package/src/rich-text-editor/images/icons/type-underline.svg +1 -0
  163. package/src/rich-text-editor/images/icons/upload.svg +1 -0
  164. package/src/rich-text-editor/images/icons/user.svg +1 -0
  165. package/src/rich-text-editor/images/icons/youtube.svg +1 -0
  166. package/src/rich-text-editor/images/image/LICENSE.md +5 -0
  167. package/src/rich-text-editor/images/landscape.jpg +0 -0
  168. package/src/rich-text-editor/images/logo.svg +1 -0
  169. package/src/rich-text-editor/images/yellow-flower-small.jpg +0 -0
  170. package/src/rich-text-editor/images/yellow-flower.jpg +0 -0
  171. package/src/rich-text-editor/index.ts +1 -0
  172. package/src/rich-text-editor/model/crystallize-rich-text-types/code.ts +39 -0
  173. package/src/rich-text-editor/model/crystallize-rich-text-types/headings.ts +12 -0
  174. package/src/rich-text-editor/model/crystallize-rich-text-types/index.ts +69 -0
  175. package/src/rich-text-editor/model/crystallize-rich-text-types/link.ts +9 -0
  176. package/src/rich-text-editor/model/crystallize-rich-text-types/table.ts +16 -0
  177. package/src/rich-text-editor/model/crystallize-to-lexical.ts +186 -0
  178. package/src/rich-text-editor/model/lexical-to-crystallize.ts +232 -0
  179. package/src/rich-text-editor/nodes/AutocompleteNode.tsx +96 -0
  180. package/src/rich-text-editor/nodes/BaseNodes.ts +45 -0
  181. package/src/rich-text-editor/nodes/KeywordNode.ts +73 -0
  182. package/src/rich-text-editor/nodes/TableCellNodes.ts +31 -0
  183. package/src/rich-text-editor/nodes/TableComponent.tsx +1547 -0
  184. package/src/rich-text-editor/nodes/TableNode.tsx +398 -0
  185. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +83 -0
  186. package/src/rich-text-editor/plugins/AutoLinkPlugin/index.tsx +47 -0
  187. package/src/rich-text-editor/plugins/AutocompletePlugin/index.tsx +2536 -0
  188. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +60 -0
  189. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.css +14 -0
  190. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +140 -0
  191. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.css +46 -0
  192. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +155 -0
  193. package/src/rich-text-editor/plugins/CodeHighlightPlugin/index.ts +21 -0
  194. package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +320 -0
  195. package/src/rich-text-editor/plugins/DragDropPastePlugin/index.ts +40 -0
  196. package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.css +36 -0
  197. package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.tsx +368 -0
  198. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.css +40 -0
  199. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +305 -0
  200. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.css +128 -0
  201. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +351 -0
  202. package/src/rich-text-editor/plugins/LinkPlugin/index.tsx +16 -0
  203. package/src/rich-text-editor/plugins/ListMaxIndentLevelPlugin/index.ts +86 -0
  204. package/src/rich-text-editor/plugins/MarkdownShortcutPlugin/index.tsx +16 -0
  205. package/src/rich-text-editor/plugins/MarkdownTransformers/index.ts +195 -0
  206. package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +49 -0
  207. package/src/rich-text-editor/plugins/SpeechToTextPlugin/index.ts +113 -0
  208. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +65 -0
  209. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +481 -0
  210. package/src/rich-text-editor/plugins/TableCellResizer/index.css +12 -0
  211. package/src/rich-text-editor/plugins/TableCellResizer/index.tsx +386 -0
  212. package/src/rich-text-editor/plugins/TablePlugin.tsx +190 -0
  213. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +726 -0
  214. package/src/rich-text-editor/plugins/TreeViewPlugin/index.tsx +25 -0
  215. package/src/rich-text-editor/plugins/TypingPerfPlugin/index.ts +117 -0
  216. package/src/rich-text-editor/rich-text-editor.css +1396 -0
  217. package/src/rich-text-editor/rich-text-editor.stories.tsx +385 -0
  218. package/src/rich-text-editor/rich-text-editor.tsx +228 -0
  219. package/src/rich-text-editor/tests/rich-text-editor-basic-rendering.test.tsx +47 -0
  220. package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +39 -0
  221. package/src/rich-text-editor/tests/rich-text-editor-model-basics.test.tsx +56 -0
  222. package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +195 -0
  223. package/src/rich-text-editor/tests/rich-text-editor-onchange.test.tsx +37 -0
  224. package/src/rich-text-editor/tests/rich-text-editor-quote.test.tsx +36 -0
  225. package/src/rich-text-editor/tests/rich-text-editor-text-formats.test.tsx +135 -0
  226. package/src/rich-text-editor/tests/rich-text-editor-typing.test.tsx +73 -0
  227. package/src/rich-text-editor/tests/utils.ts +23 -0
  228. package/src/rich-text-editor/themes/PlaygroundEditorTheme.css +433 -0
  229. package/src/rich-text-editor/themes/PlaygroundEditorTheme.ts +113 -0
  230. package/src/rich-text-editor/types.ts +5 -0
  231. package/src/rich-text-editor/ui/ContentEditable.css +13 -0
  232. package/src/rich-text-editor/ui/ContentEditable.tsx +15 -0
  233. package/src/rich-text-editor/ui/LinkPreview.css +57 -0
  234. package/src/rich-text-editor/ui/LinkPreview.tsx +169 -0
  235. package/src/rich-text-editor/utils/environment.ts +1 -0
  236. package/src/rich-text-editor/utils/getDOMRangeRect.ts +42 -0
  237. package/src/rich-text-editor/utils/getSelectedNode.ts +27 -0
  238. package/src/rich-text-editor/utils/guard.ts +10 -0
  239. package/src/rich-text-editor/utils/isMobileWidth.ts +7 -0
  240. package/src/rich-text-editor/utils/joinClasses.ts +13 -0
  241. package/src/rich-text-editor/utils/point.ts +55 -0
  242. package/src/rich-text-editor/utils/rect.ts +158 -0
  243. package/src/rich-text-editor/utils/setFloatingElemPosition.ts +46 -0
  244. package/src/rich-text-editor/utils/swipe.ts +127 -0
  245. package/src/rich-text-editor/utils/url.ts +33 -0
  246. package/src/Tokens.stories.tsx +0 -18
@@ -0,0 +1,385 @@
1
+ /* eslint-disable no-console */
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+
4
+ import type { CrystallizeRichText, CrystallizeRichTextNode } from './model/crystallize-rich-text-types';
5
+ import { RichTextEditor } from './rich-text-editor';
6
+
7
+ const meta: Meta<typeof RichTextEditor> = {
8
+ title: 'Components/RichTextEditor',
9
+ component: RichTextEditor,
10
+ argTypes: {
11
+ initialData: {},
12
+ },
13
+ decorators: [
14
+ Story => (
15
+ <div className="bg-gray-50-900 p-10">
16
+ <div className="shadow-md border-red bg-white max-w-[1100px] mx-auto rounded-md">
17
+ <Story />
18
+ </div>
19
+ </div>
20
+ ),
21
+ ],
22
+ };
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof RichTextEditor>;
27
+
28
+ const fullModel: CrystallizeRichText = [
29
+ {
30
+ kind: 'block',
31
+ type: 'heading1',
32
+ textContent: 'Whoa, an h1 heading?',
33
+ },
34
+ {
35
+ kind: 'block',
36
+ type: 'heading2',
37
+ textContent: 'Yikes, another heading? h2?',
38
+ },
39
+ {
40
+ kind: 'block',
41
+ type: 'heading3',
42
+ textContent: 'Wait, a third heading? h3?',
43
+ },
44
+ {
45
+ kind: 'block',
46
+ type: 'horizontal-line',
47
+ },
48
+ {
49
+ kind: 'block',
50
+ type: 'paragraph',
51
+ children: [
52
+ {
53
+ kind: 'inline',
54
+ textContent: 'You can do ',
55
+ },
56
+ {
57
+ kind: 'inline',
58
+ textContent: 'strong',
59
+ type: 'strong',
60
+ },
61
+ {
62
+ kind: 'inline',
63
+ textContent: ' and ',
64
+ },
65
+ {
66
+ kind: 'inline',
67
+ type: 'emphasized',
68
+ textContent: 'italic',
69
+ },
70
+ {
71
+ kind: 'inline',
72
+ textContent: ', and ',
73
+ },
74
+ {
75
+ kind: 'inline',
76
+ type: 'underlined',
77
+ textContent: 'underlined',
78
+ },
79
+ {
80
+ kind: 'inline',
81
+ textContent: ' text formats',
82
+ },
83
+ ],
84
+ },
85
+ {
86
+ kind: 'block',
87
+ type: 'paragraph',
88
+ children: [
89
+ {
90
+ kind: 'inline',
91
+ textContent: 'Here is a paragraph with a ',
92
+ },
93
+ {
94
+ kind: 'inline',
95
+ type: 'link',
96
+ metadata: {
97
+ href: 'https://crystallize.com',
98
+ },
99
+ textContent: 'link',
100
+ },
101
+ {
102
+ kind: 'inline',
103
+ textContent: ' and a ',
104
+ },
105
+ {
106
+ kind: 'inline',
107
+ type: 'line-break',
108
+ },
109
+ {
110
+ kind: 'inline',
111
+ textContent: 'line break',
112
+ },
113
+ ],
114
+ },
115
+
116
+ {
117
+ kind: 'block',
118
+ type: 'code',
119
+ textContent: 'const you = "can also do code";\nconst thisIs: string = "typescript"',
120
+ metadata: {
121
+ language: 'typescript',
122
+ },
123
+ },
124
+
125
+ {
126
+ kind: 'block',
127
+ type: 'code',
128
+ textContent: '<!DOCTYPE html>\n<html lang="en">\n<h1>But HTML works just as fine</h1>',
129
+ metadata: {
130
+ language: 'html',
131
+ },
132
+ },
133
+
134
+ {
135
+ kind: 'block',
136
+ type: 'paragraph',
137
+ textContent: 'Here is an ordered list',
138
+ },
139
+ {
140
+ kind: 'block',
141
+ type: 'ordered-list',
142
+ children: [
143
+ {
144
+ kind: 'block',
145
+ type: 'list-item',
146
+ textContent: 'first list item',
147
+ },
148
+ {
149
+ kind: 'block',
150
+ type: 'list-item',
151
+ textContent: 'second list item',
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ kind: 'block',
157
+ type: 'paragraph',
158
+ textContent: 'Here is an unordered list',
159
+ },
160
+ {
161
+ kind: 'block',
162
+ type: 'unordered-list',
163
+ children: [
164
+ {
165
+ kind: 'block',
166
+ type: 'list-item',
167
+ textContent: 'first list item',
168
+ },
169
+ {
170
+ kind: 'block',
171
+ type: 'list-item',
172
+ textContent: 'second list item',
173
+ },
174
+ ],
175
+ },
176
+ ];
177
+
178
+ export const AllElements: Story = {
179
+ args: {
180
+ initialData: fullModel,
181
+ onChange() {
182
+ console.log('onChange');
183
+ // console.log(JSON.stringify(data, null, 1));
184
+ },
185
+ },
186
+ };
187
+
188
+ export const UsingSlotPreContent: Story = {
189
+ args: {
190
+ slotPreContent: <div>Hello there 👋</div>,
191
+ },
192
+ };
193
+
194
+ export const SimpleText: Story = {
195
+ args: {
196
+ initialData: [
197
+ {
198
+ type: 'paragraph',
199
+ kind: 'block',
200
+ textContent: 'Hello there',
201
+ },
202
+ ],
203
+ onChange(data) {
204
+ console.log('onChange');
205
+ console.log(JSON.stringify(data, null, 1));
206
+ },
207
+ },
208
+ };
209
+
210
+ export const BlankSheet: Story = {
211
+ args: {
212
+ initialData: [],
213
+ placeholder: 'Some content in here',
214
+ onChange(data) {
215
+ console.log('onChange');
216
+ console.log(JSON.stringify(data, null, 1));
217
+ },
218
+ },
219
+ };
220
+
221
+ export const CodeInlined: Story = {
222
+ args: {
223
+ initialData: [
224
+ {
225
+ kind: 'block',
226
+ type: 'paragraph',
227
+ children: [
228
+ {
229
+ kind: 'inline',
230
+ textContent: 'Here come some ',
231
+ },
232
+ {
233
+ kind: 'inline',
234
+ type: 'code',
235
+ textContent: `var r = 1`,
236
+ metadata: {
237
+ language: 'typescript',
238
+ },
239
+ },
240
+ {
241
+ kind: 'inline',
242
+ textContent: '! Would you look at that?',
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ onChange(data) {
248
+ console.log('onChange');
249
+ console.log(JSON.stringify(data, null, 1));
250
+ },
251
+ },
252
+ };
253
+
254
+ export const CodeBlock: Story = {
255
+ args: {
256
+ initialData: [
257
+ {
258
+ type: 'paragraph',
259
+ kind: 'block',
260
+ textContent: 'const aVariable = 42;',
261
+ },
262
+ ],
263
+ onChange(data) {
264
+ console.log('onChange');
265
+ console.log(JSON.stringify(data, null, 1));
266
+ },
267
+ },
268
+ };
269
+
270
+ export const LineBreak: Story = {
271
+ args: {
272
+ initialData: [
273
+ {
274
+ kind: 'block',
275
+ type: 'paragraph',
276
+ children: [
277
+ {
278
+ kind: 'inline',
279
+ textContent: 'There is a ',
280
+ },
281
+ {
282
+ kind: 'inline',
283
+ type: 'line-break',
284
+ },
285
+ {
286
+ kind: 'inline',
287
+ textContent: 'line break here (as a node)',
288
+ },
289
+ ],
290
+ },
291
+ ],
292
+ onChange(data) {
293
+ console.log('onChange');
294
+ console.log(JSON.stringify(data, null, 1));
295
+ },
296
+ },
297
+ };
298
+
299
+ export const Table: Story = {
300
+ args: {
301
+ initialData: [
302
+ {
303
+ kind: 'block',
304
+ type: 'table',
305
+ children: [
306
+ {
307
+ kind: 'block',
308
+ type: 'table-row',
309
+ children: [
310
+ {
311
+ type: 'table-cell',
312
+ kind: 'inline',
313
+ textContent: 'Hi',
314
+ },
315
+ {
316
+ type: 'table-cell',
317
+ kind: 'inline',
318
+ textContent: 'from',
319
+ },
320
+ ],
321
+ },
322
+ {
323
+ kind: 'block',
324
+ type: 'table-row',
325
+ children: [
326
+ {
327
+ type: 'table-cell',
328
+ kind: 'inline',
329
+ textContent: 'a',
330
+ },
331
+ {
332
+ type: 'table-cell',
333
+ kind: 'inline',
334
+ textContent: 'table',
335
+ },
336
+ ],
337
+ },
338
+ ],
339
+ },
340
+ ],
341
+ onChange(data) {
342
+ console.log('onChange');
343
+ console.log(JSON.stringify(data, null, 1));
344
+ },
345
+ },
346
+ };
347
+
348
+ export const InlineTextFormats: Story = {
349
+ args: {
350
+ initialData: [
351
+ {
352
+ kind: 'block',
353
+ type: 'paragraph',
354
+ children: ['underlined', 'emphasized', 'strong', 'superscripted', 'subscripted', 'deleted'].map(
355
+ type =>
356
+ ({
357
+ kind: 'inline',
358
+ textContent: type,
359
+ type,
360
+ } as CrystallizeRichTextNode),
361
+ ),
362
+ },
363
+ ],
364
+ onChange(data) {
365
+ console.log('onChange');
366
+ console.log(JSON.stringify(data, null, 1));
367
+ },
368
+ },
369
+ };
370
+
371
+ export const Quote: Story = {
372
+ args: {
373
+ initialData: [
374
+ {
375
+ kind: 'block',
376
+ type: 'quote',
377
+ textContent: 'Simon says: convert that into Typescript!',
378
+ },
379
+ ],
380
+ onChange(data) {
381
+ console.log('onChange');
382
+ console.log(JSON.stringify(data, null, 1));
383
+ },
384
+ },
385
+ };
@@ -0,0 +1,228 @@
1
+ import { ReactNode, useEffect, useRef, useState } from 'react';
2
+ import type { EditorState } from 'lexical';
3
+ import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
4
+ import { CharacterLimitPlugin } from '@lexical/react/LexicalCharacterLimitPlugin';
5
+ import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
6
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
7
+ import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
8
+ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
9
+ import { HorizontalRulePlugin } from '@lexical/react/LexicalHorizontalRulePlugin';
10
+ import { ListPlugin } from '@lexical/react/LexicalListPlugin';
11
+ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
12
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
13
+ import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
14
+ import { TablePlugin } from '@lexical/react/LexicalTablePlugin';
15
+
16
+ import './rich-text-editor.css';
17
+ import { useSettings } from './context/SettingsContext';
18
+ import { SharedHistoryContext, useSharedHistoryContext } from './context/SharedHistoryContext';
19
+ import type { CrystallizeRichText } from './model/crystallize-rich-text-types';
20
+ import { composeInitialState } from './model/crystallize-to-lexical';
21
+ import { lexicalToCrystallizeRichText } from './model/lexical-to-crystallize';
22
+ import { BaseNodes } from './nodes/BaseNodes';
23
+ import { BaseNodes as TableCellNodes } from './nodes/TableCellNodes';
24
+ import AutoLinkPlugin from './plugins/AutoLinkPlugin';
25
+ import AutocompletePlugin from './plugins/AutocompletePlugin';
26
+ import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin';
27
+ import CodeHighlightPlugin from './plugins/CodeHighlightPlugin';
28
+ import ComponentPickerPlugin from './plugins/ComponentPickerPlugin';
29
+ import DragDropPaste from './plugins/DragDropPastePlugin';
30
+ import DraggableBlockPlugin from './plugins/DraggableBlockPlugin';
31
+ import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
32
+ import FloatingTextFormatToolbarPlugin from './plugins/FloatingTextFormatToolbarPlugin';
33
+ import LinkPlugin from './plugins/LinkPlugin';
34
+ import ListMaxIndentLevelPlugin from './plugins/ListMaxIndentLevelPlugin';
35
+ import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin';
36
+ import { MaxLengthPlugin } from './plugins/MaxLengthPlugin';
37
+ import SpeechToTextPlugin from './plugins/SpeechToTextPlugin';
38
+ import TabFocusPlugin from './plugins/TabFocusPlugin';
39
+ import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
40
+ import TableCellResizer from './plugins/TableCellResizer';
41
+ import { TableContext, TablePlugin as NewTablePlugin } from './plugins/TablePlugin';
42
+ import ToolbarPlugin from './plugins/ToolbarPlugin';
43
+ import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme';
44
+ import type { CrystallizeRichTextActionMenuItem } from './types';
45
+ import ContentEditable from './ui/ContentEditable';
46
+
47
+ // Types
48
+ export type { CrystallizeRichText } from './model/crystallize-rich-text-types';
49
+ export * from './types';
50
+
51
+ type TRichTextBase = {
52
+ placeholder?: string;
53
+ onChange?: (data: CrystallizeRichText) => void;
54
+ triggerOnChangeOnAutoFocus?: boolean;
55
+ autoFocus?: boolean;
56
+ actionsMenuPrepend?: CrystallizeRichTextActionMenuItem[];
57
+ actionsMenuAppend?: CrystallizeRichTextActionMenuItem[];
58
+ slotPreContent?: ReactNode;
59
+ };
60
+
61
+ export function RichTextEditor({
62
+ initialData,
63
+ editable = true,
64
+ ...rest
65
+ }: TRichTextBase & {
66
+ initialData?: CrystallizeRichText;
67
+ editable?: boolean;
68
+ }) {
69
+ return (
70
+ <LexicalComposer
71
+ initialConfig={{
72
+ namespace: 'crystallize-rich-text-editor',
73
+ nodes: [...BaseNodes],
74
+ editable,
75
+ onError: (error: Error) => {
76
+ throw error;
77
+ },
78
+ theme: PlaygroundEditorTheme,
79
+ editorState: initialData
80
+ ? composeInitialState({
81
+ richText: initialData,
82
+ })
83
+ : undefined,
84
+ }}
85
+ >
86
+ <SharedHistoryContext>
87
+ <TableContext>
88
+ <div className="editor-shell">
89
+ <RichTextEditorWithoutContext {...rest} />
90
+ </div>
91
+ </TableContext>
92
+ </SharedHistoryContext>
93
+ </LexicalComposer>
94
+ );
95
+ }
96
+
97
+ function RichTextEditorWithoutContext({
98
+ placeholder: placeholderText,
99
+ onChange,
100
+ triggerOnChangeOnAutoFocus,
101
+ autoFocus,
102
+ actionsMenuPrepend,
103
+ actionsMenuAppend,
104
+ slotPreContent,
105
+ }: TRichTextBase): JSX.Element {
106
+ const { historyState } = useSharedHistoryContext();
107
+ const {
108
+ settings: { isAutocomplete, isMaxLength, isCharLimit, isCharLimitUtf8 },
109
+ } = useSettings();
110
+ const placeholder = (
111
+ <div className="text-[14px] font-normal text-gray-300-600 italic absolute overflow-hidden text-ellipsis top-0 left-6 right-10 select-none whitespace-nowrap inline-block pointer-events-none ">
112
+ {placeholderText ?? ''}
113
+ </div>
114
+ );
115
+
116
+ const [floatingAnchorElem, setFloatingAnchorElem] = useState<HTMLDivElement | null>(null);
117
+ const [isSmallWidthViewport, setIsSmallWidthViewport] = useState<boolean>(false);
118
+ const firstOnChangeTriggeredRef = useRef(!autoFocus);
119
+
120
+ const onRef = (_floatingAnchorElem: HTMLDivElement) => {
121
+ if (_floatingAnchorElem !== null) {
122
+ setFloatingAnchorElem(_floatingAnchorElem);
123
+ }
124
+ };
125
+
126
+ // Deal with different window sizes
127
+ useEffect(() => {
128
+ const updateViewPortWidth = () => {
129
+ const isNextSmallWidthViewport = window.matchMedia('(max-width: 1025px)').matches;
130
+
131
+ if (isNextSmallWidthViewport !== isSmallWidthViewport) {
132
+ setIsSmallWidthViewport(isNextSmallWidthViewport);
133
+ }
134
+ };
135
+
136
+ window.addEventListener('resize', updateViewPortWidth);
137
+
138
+ return () => {
139
+ window.removeEventListener('resize', updateViewPortWidth);
140
+ };
141
+ }, [isSmallWidthViewport]);
142
+
143
+ function onLocalChange(editorState: EditorState) {
144
+ if (onChange) {
145
+ if (triggerOnChangeOnAutoFocus || firstOnChangeTriggeredRef.current) {
146
+ onChange(lexicalToCrystallizeRichText({ editorState }));
147
+ }
148
+ }
149
+
150
+ firstOnChangeTriggeredRef.current = true;
151
+ }
152
+
153
+ return (
154
+ <>
155
+ <OnChangePlugin onChange={onLocalChange} ignoreSelectionChange />
156
+ <ToolbarPlugin actionsMenuPrepend={actionsMenuPrepend} actionsMenuAppend={actionsMenuAppend} />
157
+ {slotPreContent}
158
+ <div className="editor-container">
159
+ {isMaxLength && <MaxLengthPlugin maxLength={30} />}
160
+ <DragDropPaste />
161
+ {!autoFocus ? null : <AutoFocusPlugin />}
162
+ <ClearEditorPlugin />
163
+ <ComponentPickerPlugin />
164
+ <SpeechToTextPlugin />
165
+ <AutoLinkPlugin />
166
+
167
+ <HistoryPlugin externalHistoryState={historyState} />
168
+
169
+ <RichTextPlugin
170
+ contentEditable={
171
+ <div className="editor-scroller">
172
+ <div className="editor" ref={onRef}>
173
+ <ContentEditable />
174
+ </div>
175
+ </div>
176
+ }
177
+ placeholder={placeholder}
178
+ ErrorBoundary={LexicalErrorBoundary}
179
+ />
180
+ <MarkdownShortcutPlugin />
181
+ <CodeHighlightPlugin />
182
+ <ListPlugin />
183
+ <ListMaxIndentLevelPlugin maxDepth={7} />
184
+ <TablePlugin />
185
+ <TableCellResizer />
186
+ <NewTablePlugin
187
+ cellEditorConfig={{
188
+ namespace: 'Playground',
189
+ nodes: [...TableCellNodes],
190
+ onError: (error: Error) => {
191
+ throw error;
192
+ },
193
+ theme: PlaygroundEditorTheme,
194
+ }}
195
+ >
196
+ {!autoFocus ? <></> : <AutoFocusPlugin />}
197
+ <RichTextPlugin
198
+ contentEditable={<ContentEditable className="TableNode__contentEditable" />}
199
+ placeholder={null}
200
+ ErrorBoundary={LexicalErrorBoundary}
201
+ />
202
+ <HistoryPlugin />
203
+ <LinkPlugin />
204
+ <FloatingTextFormatToolbarPlugin />
205
+ </NewTablePlugin>
206
+ <LinkPlugin />
207
+
208
+ <HorizontalRulePlugin />
209
+ <TabFocusPlugin />
210
+ <TabIndentationPlugin />
211
+ {floatingAnchorElem && !isSmallWidthViewport && (
212
+ <>
213
+ <DraggableBlockPlugin anchorElem={floatingAnchorElem} />
214
+ <CodeActionMenuPlugin anchorElem={floatingAnchorElem} />
215
+ <FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
216
+ <TableCellActionMenuPlugin anchorElem={floatingAnchorElem} />
217
+ <FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
218
+ </>
219
+ )}
220
+
221
+ {(isCharLimit || isCharLimitUtf8) && (
222
+ <CharacterLimitPlugin charset={isCharLimit ? 'UTF-16' : 'UTF-8'} maxLength={5} />
223
+ )}
224
+ {isAutocomplete && <AutocompletePlugin />}
225
+ </div>
226
+ </>
227
+ );
228
+ }
@@ -0,0 +1,47 @@
1
+ /* eslint-disable no-console */
2
+ import { render, screen } from '@testing-library/react';
3
+
4
+ import type { CrystallizeRichText } from '../model/crystallize-rich-text-types';
5
+ import { RichTextEditor } from '../rich-text-editor';
6
+ import { sleep } from './utils';
7
+
8
+ const emptyParagraph: CrystallizeRichText = [{ kind: 'block', type: 'paragraph', textContent: 'rabbit' }];
9
+
10
+ describe('RichTextEditor basic rendering', () => {
11
+ it('shows placeholder when no initial data is passed', async () => {
12
+ const onChange = vi.fn();
13
+ await render(<RichTextEditor placeholder="Get your content in here" />);
14
+ await sleep(1);
15
+
16
+ expect(screen.getByText('Get your content in here')).toBeInTheDocument();
17
+
18
+ expect(onChange).not.toHaveBeenCalled();
19
+ });
20
+
21
+ it('should not trigger onChange on mount when no initialData is passed', async () => {
22
+ const onChange = vi.fn();
23
+ await render(<RichTextEditor onChange={onChange} />);
24
+ await sleep(1);
25
+
26
+ expect(onChange).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it('should not trigger onChange on mount when initialData is passed', async () => {
30
+ const onChange = vi.fn();
31
+ await render(<RichTextEditor initialData={emptyParagraph} onChange={onChange} />);
32
+ await sleep(1);
33
+
34
+ expect(onChange).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('should trigger onChange on mount if triggerOnChangeOnAutoFocus and autoFocus is passed', async () => {
38
+ const onChange = vi.fn();
39
+ await render(
40
+ <RichTextEditor initialData={emptyParagraph} onChange={onChange} triggerOnChangeOnAutoFocus autoFocus />,
41
+ );
42
+ await sleep(1);
43
+
44
+ expect(onChange).toHaveBeenCalledTimes(1);
45
+ expect(onChange).toHaveBeenCalledWith(emptyParagraph);
46
+ });
47
+ });
@@ -0,0 +1,39 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+
4
+ import { RichTextEditor } from '../rich-text-editor';
5
+
6
+ describe('RichTextEditor code node', () => {
7
+ it('can add a code block to an existing paragraph text', async () => {
8
+ const onChange = vi.fn();
9
+
10
+ render(
11
+ <RichTextEditor
12
+ initialData={[
13
+ {
14
+ type: 'paragraph',
15
+ kind: 'block',
16
+ textContent: 'const aVariable = 42;',
17
+ },
18
+ ]}
19
+ onChange={onChange}
20
+ />,
21
+ );
22
+
23
+ const user = userEvent.setup();
24
+ await user.click(screen.getByTestId('toggle-block-format'));
25
+ await user.click(screen.getByTestId('toggle-block-format-code'));
26
+
27
+ expect(onChange).toHaveBeenCalledTimes(1);
28
+ expect(onChange).toHaveBeenLastCalledWith([
29
+ {
30
+ type: 'code',
31
+ kind: 'block',
32
+ textContent: 'const aVariable = 42;',
33
+ metadata: {
34
+ language: 'javascript',
35
+ },
36
+ },
37
+ ]);
38
+ });
39
+ });