@betterstart/cli 0.1.2 → 0.1.4

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 (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -354
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -260
  6. package/dist/index.js +4 -11373
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-G4KI4DVB.js +0 -179
  215. package/dist/chunk-G4KI4DVB.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,374 @@
1
+ 'use client'
2
+
3
+ import { useEditorImageUpload } from '@cms/hooks/use-editor-image-upload'
4
+ import { cn } from '@cms/utils/cn'
5
+ import { Highlight } from '@tiptap/extension-highlight'
6
+ import { Image } from '@tiptap/extension-image'
7
+ import { TaskItem, TaskList } from '@tiptap/extension-list'
8
+ import Placeholder from '@tiptap/extension-placeholder'
9
+ import { Subscript } from '@tiptap/extension-subscript'
10
+ import { Superscript } from '@tiptap/extension-superscript'
11
+ import { TextAlign } from '@tiptap/extension-text-align'
12
+ import { Typography } from '@tiptap/extension-typography'
13
+ import { Selection } from '@tiptap/extensions'
14
+ import { EditorContent, EditorContext, useEditor } from '@tiptap/react'
15
+ import { StarterKit } from '@tiptap/starter-kit'
16
+ import { Loader2 } from 'lucide-react'
17
+ import * as React from 'react'
18
+ import { Markdown, type MarkdownStorage } from 'tiptap-markdown'
19
+ // --- TipTap Extensions ---
20
+ import { NodeBackground } from './tiptap/tiptap-extension/node-background-extension'
21
+ // --- TipTap Nodes ---
22
+ import { HorizontalRule } from './tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension'
23
+ import { ImageUploadNode } from './tiptap/tiptap-node/image-upload-node/image-upload-node-extension'
24
+ import { Button } from './tiptap/tiptap-ui-primitive/button'
25
+ // --- TipTap UI Components ---
26
+ import { Spacer } from './tiptap/tiptap-ui-primitive/spacer'
27
+ import { Toolbar, ToolbarGroup, ToolbarSeparator } from './tiptap/tiptap-ui-primitive/toolbar'
28
+ import './tiptap/tiptap-node/blockquote-node/blockquote-node.scss'
29
+ import './tiptap/tiptap-node/code-block-node/code-block-node.scss'
30
+ import './tiptap/tiptap-node/heading-node/heading-node.scss'
31
+ import './tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss'
32
+ import './tiptap/tiptap-node/image-node/image-node.scss'
33
+ import './tiptap/tiptap-node/list-node/list-node.scss'
34
+ import './tiptap/tiptap-node/paragraph-node/paragraph-node.scss'
35
+
36
+ // --- TipTap Hooks ---
37
+ import { useIsBreakpoint } from './tiptap/hooks/use-is-breakpoint'
38
+ // --- TipTap Icons ---
39
+ import { ArrowLeftIcon } from './tiptap/tiptap-icons/arrow-left-icon'
40
+ import { HighlighterIcon } from './tiptap/tiptap-icons/highlighter-icon'
41
+ import { LinkIcon } from './tiptap/tiptap-icons/link-icon'
42
+ // --- TipTap UI Controls ---
43
+ import { BlockquoteButton } from './tiptap/tiptap-ui/blockquote-button'
44
+ import { CodeBlockButton } from './tiptap/tiptap-ui/code-block-button'
45
+ import {
46
+ ColorHighlightPopover,
47
+ ColorHighlightPopoverButton,
48
+ ColorHighlightPopoverContent
49
+ } from './tiptap/tiptap-ui/color-highlight-popover'
50
+ import { HeadingDropdownMenu } from './tiptap/tiptap-ui/heading-dropdown-menu'
51
+ import { ImageUploadButton } from './tiptap/tiptap-ui/image-upload-button'
52
+ import { LinkButton, LinkContent, LinkPopover } from './tiptap/tiptap-ui/link-popover'
53
+ import { ListDropdownMenu } from './tiptap/tiptap-ui/list-dropdown-menu'
54
+ import { MarkButton } from './tiptap/tiptap-ui/mark-button'
55
+
56
+ // --- TipTap Styles ---
57
+ import './tiptap/styles/_variables.scss'
58
+ import './tiptap/styles/_keyframe-animations.scss'
59
+
60
+ export interface RichTextEditorProps {
61
+ value?: string
62
+ onChange?: (value: string) => void
63
+ placeholder?: string
64
+ disabled?: boolean
65
+ className?: string
66
+ }
67
+
68
+ const MainToolbarContent = ({
69
+ onHighlighterClick,
70
+ onLinkClick,
71
+ isMobile
72
+ }: {
73
+ onHighlighterClick: () => void
74
+ onLinkClick: () => void
75
+ isMobile: boolean
76
+ }) => (
77
+ <>
78
+ <Spacer />
79
+
80
+ <ToolbarGroup>
81
+ <HeadingDropdownMenu portal />
82
+ <ListDropdownMenu types={['bulletList', 'orderedList', 'taskList']} portal />
83
+ <BlockquoteButton />
84
+ <CodeBlockButton />
85
+ </ToolbarGroup>
86
+
87
+ <ToolbarSeparator />
88
+
89
+ <ToolbarGroup>
90
+ <MarkButton type="bold" />
91
+ <MarkButton type="italic" />
92
+ <MarkButton type="strike" />
93
+ <MarkButton type="code" />
94
+ <MarkButton type="underline" />
95
+ {!isMobile ? (
96
+ <ColorHighlightPopover />
97
+ ) : (
98
+ <ColorHighlightPopoverButton onClick={onHighlighterClick} />
99
+ )}
100
+ {!isMobile ? <LinkPopover /> : <LinkButton onClick={onLinkClick} />}
101
+ </ToolbarGroup>
102
+
103
+ <ToolbarSeparator />
104
+
105
+ <ToolbarGroup>
106
+ <ImageUploadButton text="Image" />
107
+ </ToolbarGroup>
108
+
109
+ <Spacer />
110
+ </>
111
+ )
112
+
113
+ const MobileToolbarContent = ({
114
+ type,
115
+ onBack
116
+ }: {
117
+ type: 'highlighter' | 'link'
118
+ onBack: () => void
119
+ }) => (
120
+ <>
121
+ <ToolbarGroup>
122
+ <Button variant="ghost" onClick={onBack}>
123
+ <ArrowLeftIcon className="tiptap-button-icon" />
124
+ {type === 'highlighter' ? (
125
+ <HighlighterIcon className="tiptap-button-icon" />
126
+ ) : (
127
+ <LinkIcon className="tiptap-button-icon" />
128
+ )}
129
+ </Button>
130
+ </ToolbarGroup>
131
+
132
+ <ToolbarSeparator />
133
+
134
+ {type === 'highlighter' ? <ColorHighlightPopoverContent /> : <LinkContent />}
135
+ </>
136
+ )
137
+
138
+ export const RichTextEditor = ({
139
+ value = '',
140
+ onChange,
141
+ placeholder,
142
+ disabled = false,
143
+ className
144
+ }: RichTextEditorProps) => {
145
+ const isInternalChange = React.useRef(false)
146
+ const editorRef = React.useRef<ReturnType<typeof useEditor>>(null)
147
+ const isMobile = useIsBreakpoint()
148
+ const [mobileView, setMobileView] = React.useState<'main' | 'highlighter' | 'link'>('main')
149
+
150
+ const { isUploading, progress, uploadImages } = useEditorImageUpload({
151
+ onImagesUploaded: (images) => {
152
+ const ed = editorRef.current
153
+ if (!ed) return
154
+ for (const img of images) {
155
+ ed.chain().focus().setImage({ src: img.url, alt: img.filename }).run()
156
+ }
157
+ }
158
+ })
159
+
160
+ const uploadImagesRef = React.useRef(uploadImages)
161
+ uploadImagesRef.current = uploadImages
162
+
163
+ // Image upload handler for the ImageUploadNode extension
164
+ const handleImageUpload = React.useCallback(
165
+ async (
166
+ file: File,
167
+ onProgress?: (event: { progress: number }) => void,
168
+ _abortSignal?: AbortSignal
169
+ ): Promise<string> => {
170
+ return new Promise((resolve, reject) => {
171
+ const formData = new FormData()
172
+ formData.append('file0', file)
173
+ formData.append('prefix', 'images')
174
+
175
+ const xhr = new XMLHttpRequest()
176
+ xhr.open('POST', '/api/upload')
177
+
178
+ xhr.upload.addEventListener('progress', (e) => {
179
+ if (e.lengthComputable) {
180
+ onProgress?.({ progress: Math.round((e.loaded / e.total) * 100) })
181
+ }
182
+ })
183
+
184
+ xhr.addEventListener('load', () => {
185
+ if (xhr.status >= 200 && xhr.status < 300) {
186
+ try {
187
+ const result = JSON.parse(xhr.responseText)
188
+ if (result.success && result.files?.[0]?.url) {
189
+ resolve(result.files[0].url)
190
+ } else {
191
+ reject(new Error('Upload failed: No URL returned'))
192
+ }
193
+ } catch {
194
+ reject(new Error('Failed to parse upload response'))
195
+ }
196
+ } else {
197
+ reject(new Error(`Upload failed with status ${xhr.status}`))
198
+ }
199
+ })
200
+
201
+ xhr.addEventListener('error', () => reject(new Error('Upload failed')))
202
+ xhr.send(formData)
203
+ })
204
+ },
205
+ []
206
+ )
207
+
208
+ const editor = useEditor({
209
+ immediatelyRender: false,
210
+ editorProps: {
211
+ attributes: {
212
+ autocomplete: 'off',
213
+ autocorrect: 'off',
214
+ autocapitalize: 'off',
215
+ class: 'simple-editor'
216
+ },
217
+ handlePaste: (_view, event) => {
218
+ const items = event.clipboardData?.items
219
+ if (!items) return false
220
+ const imageFiles: File[] = []
221
+ for (const item of items) {
222
+ if (item.type.startsWith('image/')) {
223
+ const file = item.getAsFile()
224
+ if (file) imageFiles.push(file)
225
+ }
226
+ }
227
+ if (imageFiles.length > 0) {
228
+ event.preventDefault()
229
+ uploadImagesRef.current(imageFiles)
230
+ return true
231
+ }
232
+ return false
233
+ },
234
+ handleDrop: (_view, event) => {
235
+ const files = Array.from(event.dataTransfer?.files || []).filter((f) =>
236
+ f.type.startsWith('image/')
237
+ )
238
+ if (files.length > 0) {
239
+ event.preventDefault()
240
+ uploadImagesRef.current(files)
241
+ return true
242
+ }
243
+ return false
244
+ }
245
+ },
246
+ extensions: [
247
+ StarterKit.configure({
248
+ horizontalRule: false,
249
+ link: {
250
+ openOnClick: false,
251
+ enableClickSelection: true
252
+ }
253
+ }),
254
+ HorizontalRule,
255
+ TextAlign.configure({ types: ['heading', 'paragraph'] }),
256
+ TaskList,
257
+ TaskItem.configure({ nested: true }),
258
+ Highlight.configure({ multicolor: true }),
259
+ Image,
260
+ Typography,
261
+ Superscript,
262
+ Subscript,
263
+ Selection,
264
+ NodeBackground,
265
+ Placeholder.configure({ placeholder: placeholder || 'Start writing...' }),
266
+ Markdown.configure({
267
+ html: false,
268
+ transformCopiedText: true,
269
+ transformPastedText: true
270
+ }),
271
+ ImageUploadNode.configure({
272
+ accept: 'image/*',
273
+ maxSize: 10 * 1024 * 1024,
274
+ limit: 3,
275
+ upload: handleImageUpload,
276
+ onError: (error: Error) => console.error('Upload failed:', error)
277
+ })
278
+ ],
279
+ content: value,
280
+ editable: !disabled,
281
+ onUpdate: ({ editor: e }) => {
282
+ isInternalChange.current = true
283
+ const md = (e.storage as unknown as Record<string, MarkdownStorage>).markdown.getMarkdown()
284
+ onChange?.(md)
285
+ }
286
+ })
287
+
288
+ editorRef.current = editor
289
+
290
+ // Sync external value changes (e.g., form reset) without disrupting typing
291
+ React.useEffect(() => {
292
+ if (!editor) return
293
+ if (isInternalChange.current) {
294
+ isInternalChange.current = false
295
+ return
296
+ }
297
+ const currentMd = (
298
+ editor.storage as unknown as Record<string, MarkdownStorage>
299
+ ).markdown.getMarkdown()
300
+ if (currentMd !== value) {
301
+ editor.commands.setContent(value)
302
+ }
303
+ }, [value, editor])
304
+
305
+ // Update editable state
306
+ React.useEffect(() => {
307
+ if (editor) {
308
+ editor.setEditable(!disabled)
309
+ }
310
+ }, [disabled, editor])
311
+
312
+ // Reset mobile view when switching to desktop
313
+ React.useEffect(() => {
314
+ if (!isMobile && mobileView !== 'main') {
315
+ setMobileView('main')
316
+ }
317
+ }, [isMobile, mobileView])
318
+
319
+ if (!editor) return null
320
+
321
+ const overallProgress =
322
+ progress.length > 0
323
+ ? Math.round(progress.reduce((sum, p) => sum + p.progress, 0) / progress.length)
324
+ : 0
325
+
326
+ return (
327
+ <div
328
+ className={cn(
329
+ 'rich-text-editor-wrapper border rounded-md overflow-hidden flex flex-col resize-y min-h-[200px] max-h-[90vh] relative',
330
+ { 'opacity-50 pointer-events-none': disabled },
331
+ className
332
+ )}
333
+ >
334
+ <EditorContext.Provider value={{ editor }}>
335
+ <Toolbar>
336
+ {mobileView === 'main' ? (
337
+ <MainToolbarContent
338
+ onHighlighterClick={() => setMobileView('highlighter')}
339
+ onLinkClick={() => setMobileView('link')}
340
+ isMobile={isMobile}
341
+ />
342
+ ) : (
343
+ <MobileToolbarContent
344
+ type={mobileView === 'highlighter' ? 'highlighter' : 'link'}
345
+ onBack={() => setMobileView('main')}
346
+ />
347
+ )}
348
+ </Toolbar>
349
+
350
+ <EditorContent
351
+ editor={editor}
352
+ role="presentation"
353
+ className="simple-editor-content flex-1 min-h-0 overflow-auto p-5 bg-input"
354
+ />
355
+ </EditorContext.Provider>
356
+
357
+ {/* Upload progress overlay */}
358
+ {isUploading && (
359
+ <div className="absolute inset-0 bg-background/60 flex items-center justify-center z-10">
360
+ <div className="flex flex-col items-center gap-2">
361
+ <Loader2 className="size-6 animate-spin text-primary" />
362
+ <div className="text-sm text-muted-foreground">Uploading... {overallProgress}%</div>
363
+ <div className="w-48 h-1.5 bg-muted rounded-full overflow-hidden">
364
+ <div
365
+ className="h-full bg-primary rounded-full transition-all duration-200"
366
+ style={{ width: `${overallProgress}%` }}
367
+ />
368
+ </div>
369
+ </div>
370
+ </div>
371
+ )}
372
+ </div>
373
+ )
374
+ }
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@cms/utils/cn'
4
+ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
5
+ import * as React from 'react'
6
+
7
+ const ScrollArea = React.forwardRef<
8
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
10
+ >(({ className, children, ...props }, ref) => (
11
+ <ScrollAreaPrimitive.Root
12
+ ref={ref}
13
+ className={cn('relative overflow-hidden', className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
17
+ {children}
18
+ </ScrollAreaPrimitive.Viewport>
19
+ <ScrollBar />
20
+ <ScrollAreaPrimitive.Corner />
21
+ </ScrollAreaPrimitive.Root>
22
+ ))
23
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
24
+
25
+ const ScrollBar = React.forwardRef<
26
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
27
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
28
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
29
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
30
+ ref={ref}
31
+ orientation={orientation}
32
+ className={cn(
33
+ 'flex touch-none select-none transition-colors',
34
+ orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
35
+ orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
41
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
42
+ ))
43
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
44
+
45
+ export { ScrollArea, ScrollBar }
@@ -0,0 +1,151 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@cms/utils/cn'
4
+ import * as SelectPrimitive from '@radix-ui/react-select'
5
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
6
+ import * as React from 'react'
7
+
8
+ const Select = SelectPrimitive.Root
9
+
10
+ const SelectGroup = SelectPrimitive.Group
11
+
12
+ const SelectValue = SelectPrimitive.Value
13
+
14
+ const SelectTrigger = React.forwardRef<
15
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
16
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
17
+ >(({ className, children, ...props }, ref) => (
18
+ <SelectPrimitive.Trigger
19
+ ref={ref}
20
+ className={cn(
21
+ 'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input-border bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
22
+ className
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ <SelectPrimitive.Icon asChild>
28
+ <ChevronDown className="h-4 w-4 opacity-50" />
29
+ </SelectPrimitive.Icon>
30
+ </SelectPrimitive.Trigger>
31
+ ))
32
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
33
+
34
+ const SelectScrollUpButton = React.forwardRef<
35
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
36
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
37
+ >(({ className, ...props }, ref) => (
38
+ <SelectPrimitive.ScrollUpButton
39
+ ref={ref}
40
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
41
+ {...props}
42
+ >
43
+ <ChevronUp className="h-4 w-4" />
44
+ </SelectPrimitive.ScrollUpButton>
45
+ ))
46
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
47
+
48
+ const SelectScrollDownButton = React.forwardRef<
49
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
50
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
51
+ >(({ className, ...props }, ref) => (
52
+ <SelectPrimitive.ScrollDownButton
53
+ ref={ref}
54
+ className={cn('flex cursor-default items-center justify-center py-1', className)}
55
+ {...props}
56
+ >
57
+ <ChevronDown className="h-4 w-4" />
58
+ </SelectPrimitive.ScrollDownButton>
59
+ ))
60
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
61
+
62
+ const SelectContent = React.forwardRef<
63
+ React.ElementRef<typeof SelectPrimitive.Content>,
64
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
65
+ >(({ className, children, position = 'popper', ...props }, ref) => (
66
+ <SelectPrimitive.Portal>
67
+ <SelectPrimitive.Content
68
+ ref={ref}
69
+ className={cn(
70
+ 'relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]',
71
+ position === 'popper' &&
72
+ 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
73
+ className
74
+ )}
75
+ position={position}
76
+ {...props}
77
+ >
78
+ <SelectScrollUpButton />
79
+ <SelectPrimitive.Viewport
80
+ className={cn(
81
+ 'p-1',
82
+ position === 'popper' &&
83
+ 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
84
+ )}
85
+ >
86
+ {children}
87
+ </SelectPrimitive.Viewport>
88
+ <SelectScrollDownButton />
89
+ </SelectPrimitive.Content>
90
+ </SelectPrimitive.Portal>
91
+ ))
92
+ SelectContent.displayName = SelectPrimitive.Content.displayName
93
+
94
+ const SelectLabel = React.forwardRef<
95
+ React.ElementRef<typeof SelectPrimitive.Label>,
96
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
97
+ >(({ className, ...props }, ref) => (
98
+ <SelectPrimitive.Label
99
+ ref={ref}
100
+ className={cn('px-2 py-1.5 text-sm font-semibold', className)}
101
+ {...props}
102
+ />
103
+ ))
104
+ SelectLabel.displayName = SelectPrimitive.Label.displayName
105
+
106
+ const SelectItem = React.forwardRef<
107
+ React.ElementRef<typeof SelectPrimitive.Item>,
108
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
109
+ >(({ className, children, ...props }, ref) => (
110
+ <SelectPrimitive.Item
111
+ ref={ref}
112
+ className={cn(
113
+ 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
114
+ className
115
+ )}
116
+ {...props}
117
+ >
118
+ <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
119
+ <SelectPrimitive.ItemIndicator>
120
+ <Check className="h-4 w-4" />
121
+ </SelectPrimitive.ItemIndicator>
122
+ </span>
123
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
124
+ </SelectPrimitive.Item>
125
+ ))
126
+ SelectItem.displayName = SelectPrimitive.Item.displayName
127
+
128
+ const SelectSeparator = React.forwardRef<
129
+ React.ElementRef<typeof SelectPrimitive.Separator>,
130
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
131
+ >(({ className, ...props }, ref) => (
132
+ <SelectPrimitive.Separator
133
+ ref={ref}
134
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
135
+ {...props}
136
+ />
137
+ ))
138
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName
139
+
140
+ export {
141
+ Select,
142
+ SelectGroup,
143
+ SelectValue,
144
+ SelectTrigger,
145
+ SelectContent,
146
+ SelectLabel,
147
+ SelectItem,
148
+ SelectSeparator,
149
+ SelectScrollUpButton,
150
+ SelectScrollDownButton
151
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@cms/utils/cn'
4
+ import * as SeparatorPrimitive from '@radix-ui/react-separator'
5
+ import * as React from 'react'
6
+
7
+ const Separator = React.forwardRef<
8
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
10
+ >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
11
+ <SeparatorPrimitive.Root
12
+ ref={ref}
13
+ decorative={decorative}
14
+ orientation={orientation}
15
+ className={cn(
16
+ 'shrink-0 bg-border',
17
+ orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ Separator.displayName = SeparatorPrimitive.Root.displayName
24
+
25
+ export { Separator }