@betterstart/cli 0.1.3 → 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 -367
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -266
  6. package/dist/index.js +4 -11378
  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-EIH4RRIJ.js +0 -183
  215. package/dist/chunk-EIH4RRIJ.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,522 @@
1
+ 'use client'
2
+
3
+ import type { NodeViewProps } from '@tiptap/react'
4
+ import { NodeViewWrapper } from '@tiptap/react'
5
+ import { useRef, useState } from 'react'
6
+ import { CloseIcon } from '../../tiptap-icons/close-icon'
7
+ import { Button } from '../../tiptap-ui-primitive/button'
8
+ import './image-upload-node.scss'
9
+ import { focusNextNode, isValidPosition } from '../../lib/tiptap-utils'
10
+
11
+ export interface FileItem {
12
+ /**
13
+ * Unique identifier for the file item
14
+ */
15
+ id: string
16
+ /**
17
+ * The actual File object being uploaded
18
+ */
19
+ file: File
20
+ /**
21
+ * Current upload progress as a percentage (0-100)
22
+ */
23
+ progress: number
24
+ /**
25
+ * Current status of the file upload process
26
+ * @default "uploading"
27
+ */
28
+ status: 'uploading' | 'success' | 'error'
29
+
30
+ /**
31
+ * URL to the uploaded file, available after successful upload
32
+ * @optional
33
+ */
34
+ url?: string
35
+ /**
36
+ * Controller that can be used to abort the upload process
37
+ * @optional
38
+ */
39
+ abortController?: AbortController
40
+ }
41
+
42
+ export interface UploadOptions {
43
+ /**
44
+ * Maximum allowed file size in bytes
45
+ */
46
+ maxSize: number
47
+ /**
48
+ * Maximum number of files that can be uploaded
49
+ */
50
+ limit: number
51
+ /**
52
+ * String specifying acceptable file types (MIME types or extensions)
53
+ * @example ".jpg,.png,image/jpeg" or "image/*"
54
+ */
55
+ accept: string
56
+ /**
57
+ * Function that handles the actual file upload process
58
+ * @param {File} file - The file to be uploaded
59
+ * @param {Function} onProgress - Callback function to report upload progress
60
+ * @param {AbortSignal} signal - Signal that can be used to abort the upload
61
+ * @returns {Promise<string>} Promise resolving to the URL of the uploaded file
62
+ */
63
+ upload: (
64
+ file: File,
65
+ onProgress: (event: { progress: number }) => void,
66
+ signal: AbortSignal
67
+ ) => Promise<string>
68
+ /**
69
+ * Callback triggered when a file is uploaded successfully
70
+ * @param {string} url - URL of the successfully uploaded file
71
+ * @optional
72
+ */
73
+ onSuccess?: (url: string) => void
74
+ /**
75
+ * Callback triggered when an error occurs during upload
76
+ * @param {Error} error - The error that occurred
77
+ * @optional
78
+ */
79
+ onError?: (error: Error) => void
80
+ }
81
+
82
+ /**
83
+ * Custom hook for managing multiple file uploads with progress tracking and cancellation
84
+ */
85
+ function useFileUpload(options: UploadOptions) {
86
+ const [fileItems, setFileItems] = useState<FileItem[]>([])
87
+
88
+ const uploadFile = async (file: File): Promise<string | null> => {
89
+ if (file.size > options.maxSize) {
90
+ const error = new Error(
91
+ `File size exceeds maximum allowed (${options.maxSize / 1024 / 1024}MB)`
92
+ )
93
+ options.onError?.(error)
94
+ return null
95
+ }
96
+
97
+ const abortController = new AbortController()
98
+ const fileId = crypto.randomUUID()
99
+
100
+ const newFileItem: FileItem = {
101
+ id: fileId,
102
+ file,
103
+ progress: 0,
104
+ status: 'uploading',
105
+ abortController
106
+ }
107
+
108
+ setFileItems((prev) => [...prev, newFileItem])
109
+
110
+ try {
111
+ if (!options.upload) {
112
+ throw new Error('Upload function is not defined')
113
+ }
114
+
115
+ const url = await options.upload(
116
+ file,
117
+ (event: { progress: number }) => {
118
+ setFileItems((prev) =>
119
+ prev.map((item) => (item.id === fileId ? { ...item, progress: event.progress } : item))
120
+ )
121
+ },
122
+ abortController.signal
123
+ )
124
+
125
+ if (!url) throw new Error('Upload failed: No URL returned')
126
+
127
+ if (!abortController.signal.aborted) {
128
+ setFileItems((prev) =>
129
+ prev.map((item) =>
130
+ item.id === fileId ? { ...item, status: 'success', url, progress: 100 } : item
131
+ )
132
+ )
133
+ options.onSuccess?.(url)
134
+ return url
135
+ }
136
+
137
+ return null
138
+ } catch (error) {
139
+ if (!abortController.signal.aborted) {
140
+ setFileItems((prev) =>
141
+ prev.map((item) =>
142
+ item.id === fileId ? { ...item, status: 'error', progress: 0 } : item
143
+ )
144
+ )
145
+ options.onError?.(error instanceof Error ? error : new Error('Upload failed'))
146
+ }
147
+ return null
148
+ }
149
+ }
150
+
151
+ const uploadFiles = async (files: File[]): Promise<string[]> => {
152
+ if (!files || files.length === 0) {
153
+ options.onError?.(new Error('No files to upload'))
154
+ return []
155
+ }
156
+
157
+ if (options.limit && files.length > options.limit) {
158
+ options.onError?.(
159
+ new Error(`Maximum ${options.limit} file${options.limit === 1 ? '' : 's'} allowed`)
160
+ )
161
+ return []
162
+ }
163
+
164
+ // Upload all files concurrently
165
+ const uploadPromises = files.map((file) => uploadFile(file))
166
+ const results = await Promise.all(uploadPromises)
167
+
168
+ // Filter out null results (failed uploads)
169
+ return results.filter((url): url is string => url !== null)
170
+ }
171
+
172
+ const removeFileItem = (fileId: string) => {
173
+ setFileItems((prev) => {
174
+ const fileToRemove = prev.find((item) => item.id === fileId)
175
+ if (fileToRemove?.abortController) {
176
+ fileToRemove.abortController.abort()
177
+ }
178
+ if (fileToRemove?.url) {
179
+ URL.revokeObjectURL(fileToRemove.url)
180
+ }
181
+ return prev.filter((item) => item.id !== fileId)
182
+ })
183
+ }
184
+
185
+ const clearAllFiles = () => {
186
+ fileItems.forEach((item) => {
187
+ if (item.abortController) {
188
+ item.abortController.abort()
189
+ }
190
+ if (item.url) {
191
+ URL.revokeObjectURL(item.url)
192
+ }
193
+ })
194
+ setFileItems([])
195
+ }
196
+
197
+ return {
198
+ fileItems,
199
+ uploadFiles,
200
+ removeFileItem,
201
+ clearAllFiles
202
+ }
203
+ }
204
+
205
+ const CloudUploadIcon: React.FC = () => (
206
+ <svg
207
+ width="24"
208
+ height="24"
209
+ viewBox="0 0 24 24"
210
+ className="tiptap-image-upload-icon"
211
+ fill="currentColor"
212
+ xmlns="http://www.w3.org/2000/svg"
213
+ >
214
+ <path
215
+ d="M11.1953 4.41771C10.3478 4.08499 9.43578 3.94949 8.5282 4.02147C7.62062 4.09345 6.74133 4.37102 5.95691 4.83316C5.1725 5.2953 4.50354 5.92989 4.00071 6.68886C3.49788 7.44783 3.17436 8.31128 3.05465 9.2138C2.93495 10.1163 3.0222 11.0343 3.3098 11.8981C3.5974 12.7619 4.07781 13.5489 4.71463 14.1995C5.10094 14.5942 5.09414 15.2274 4.69945 15.6137C4.30476 16 3.67163 15.9932 3.28532 15.5985C2.43622 14.731 1.79568 13.6816 1.41221 12.5299C1.02875 11.3781 0.91241 10.1542 1.07201 8.95084C1.23162 7.74748 1.66298 6.59621 2.33343 5.58425C3.00387 4.57229 3.89581 3.72617 4.9417 3.10998C5.98758 2.4938 7.15998 2.1237 8.37008 2.02773C9.58018 1.93176 10.7963 2.11243 11.9262 2.55605C13.0561 2.99968 14.0703 3.69462 14.8919 4.58825C15.5423 5.29573 16.0585 6.11304 16.4177 7.00002H17.4999C18.6799 6.99991 19.8288 7.37933 20.7766 8.08222C21.7245 8.78515 22.4212 9.7743 22.7637 10.9036C23.1062 12.0328 23.0765 13.2423 22.6788 14.3534C22.2812 15.4644 21.5367 16.4181 20.5554 17.0736C20.0962 17.3803 19.4752 17.2567 19.1684 16.7975C18.8617 16.3382 18.9853 15.7172 19.4445 15.4105C20.069 14.9934 20.5427 14.3865 20.7958 13.6794C21.0488 12.9724 21.0678 12.2027 20.8498 11.4841C20.6318 10.7655 20.1885 10.136 19.5853 9.6887C18.9821 9.24138 18.251 8.99993 17.5001 9.00002H15.71C15.2679 9.00002 14.8783 8.70973 14.7518 8.28611C14.4913 7.41374 14.0357 6.61208 13.4195 5.94186C12.8034 5.27164 12.0427 4.75043 11.1953 4.41771Z"
216
+ fill="currentColor"
217
+ />
218
+ <path
219
+ d="M11 14.4142V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V14.4142L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L12.7078 11.2936C12.7054 11.2912 12.703 11.2888 12.7005 11.2864C12.5208 11.1099 12.2746 11.0008 12.003 11L12 11L11.997 11C11.8625 11.0004 11.7343 11.0273 11.6172 11.0759C11.502 11.1236 11.3938 11.1937 11.2995 11.2864C11.297 11.2888 11.2946 11.2912 11.2922 11.2936L7.29289 15.2929C6.90237 15.6834 6.90237 16.3166 7.29289 16.7071C7.68342 17.0976 8.31658 17.0976 8.70711 16.7071L11 14.4142Z"
220
+ fill="currentColor"
221
+ />
222
+ </svg>
223
+ )
224
+
225
+ const FileIcon: React.FC = () => (
226
+ <svg
227
+ width="43"
228
+ height="57"
229
+ viewBox="0 0 43 57"
230
+ fill="currentColor"
231
+ className="tiptap-image-upload-dropzone-rect-primary"
232
+ xmlns="http://www.w3.org/2000/svg"
233
+ >
234
+ <path
235
+ d="M0.75 10.75C0.75 5.64137 4.89137 1.5 10 1.5H32.3431C33.2051 1.5 34.0317 1.84241 34.6412 2.4519L40.2981 8.10876C40.9076 8.71825 41.25 9.5449 41.25 10.4069V46.75C41.25 51.8586 37.1086 56 32 56H10C4.89137 56 0.75 51.8586 0.75 46.75V10.75Z"
236
+ fill="currentColor"
237
+ fillOpacity="0.11"
238
+ stroke="currentColor"
239
+ strokeWidth="1.5"
240
+ />
241
+ </svg>
242
+ )
243
+
244
+ const FileCornerIcon: React.FC = () => (
245
+ <svg
246
+ width="10"
247
+ height="10"
248
+ className="tiptap-image-upload-dropzone-rect-secondary"
249
+ viewBox="0 0 10 10"
250
+ fill="currentColor"
251
+ xmlns="http://www.w3.org/2000/svg"
252
+ >
253
+ <path
254
+ d="M0 0.75H0.343146C1.40401 0.75 2.42143 1.17143 3.17157 1.92157L8.82843 7.57843C9.57857 8.32857 10 9.34599 10 10.4069V10.75H4C1.79086 10.75 0 8.95914 0 6.75V0.75Z"
255
+ fill="currentColor"
256
+ />
257
+ </svg>
258
+ )
259
+
260
+ interface ImageUploadDragAreaProps {
261
+ /**
262
+ * Callback function triggered when files are dropped or selected
263
+ * @param {File[]} files - Array of File objects that were dropped or selected
264
+ */
265
+ onFile: (files: File[]) => void
266
+ /**
267
+ * Optional child elements to render inside the drag area
268
+ * @optional
269
+ * @default undefined
270
+ */
271
+ children?: React.ReactNode
272
+ }
273
+
274
+ /**
275
+ * A component that creates a drag-and-drop area for image uploads
276
+ */
277
+ const ImageUploadDragArea: React.FC<ImageUploadDragAreaProps> = ({ onFile, children }) => {
278
+ const [isDragOver, setIsDragOver] = useState(false)
279
+ const [isDragActive, setIsDragActive] = useState(false)
280
+
281
+ const handleDragEnter = (e: React.DragEvent) => {
282
+ e.preventDefault()
283
+ e.stopPropagation()
284
+ setIsDragActive(true)
285
+ }
286
+
287
+ const handleDragLeave = (e: React.DragEvent) => {
288
+ e.preventDefault()
289
+ e.stopPropagation()
290
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
291
+ setIsDragActive(false)
292
+ setIsDragOver(false)
293
+ }
294
+ }
295
+
296
+ const handleDragOver = (e: React.DragEvent) => {
297
+ e.preventDefault()
298
+ e.stopPropagation()
299
+ setIsDragOver(true)
300
+ }
301
+
302
+ const handleDrop = (e: React.DragEvent) => {
303
+ e.preventDefault()
304
+ e.stopPropagation()
305
+ setIsDragActive(false)
306
+ setIsDragOver(false)
307
+
308
+ const files = Array.from(e.dataTransfer.files)
309
+ if (files.length > 0) {
310
+ onFile(files)
311
+ }
312
+ }
313
+
314
+ return (
315
+ // biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop target
316
+ <div
317
+ className={`tiptap-image-upload-drag-area ${isDragActive ? 'drag-active' : ''} ${isDragOver ? 'drag-over' : ''}`}
318
+ onDragEnter={handleDragEnter}
319
+ onDragLeave={handleDragLeave}
320
+ onDragOver={handleDragOver}
321
+ onDrop={handleDrop}
322
+ >
323
+ {children}
324
+ </div>
325
+ )
326
+ }
327
+
328
+ interface ImageUploadPreviewProps {
329
+ /**
330
+ * The file item to preview
331
+ */
332
+ fileItem: FileItem
333
+ /**
334
+ * Callback to remove this file from upload queue
335
+ */
336
+ onRemove: () => void
337
+ }
338
+
339
+ /**
340
+ * Component that displays a preview of an uploading file with progress
341
+ */
342
+ const ImageUploadPreview: React.FC<ImageUploadPreviewProps> = ({ fileItem, onRemove }) => {
343
+ const formatFileSize = (bytes: number) => {
344
+ if (bytes === 0) return '0 Bytes'
345
+ const k = 1024
346
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
347
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
348
+ return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
349
+ }
350
+
351
+ return (
352
+ <div className="tiptap-image-upload-preview">
353
+ {fileItem.status === 'uploading' && (
354
+ <div className="tiptap-image-upload-progress" style={{ width: `${fileItem.progress}%` }} />
355
+ )}
356
+
357
+ <div className="tiptap-image-upload-preview-content">
358
+ <div className="tiptap-image-upload-file-info">
359
+ <div className="tiptap-image-upload-file-icon">
360
+ <CloudUploadIcon />
361
+ </div>
362
+ <div className="tiptap-image-upload-details">
363
+ <span className="tiptap-image-upload-text">{fileItem.file.name}</span>
364
+ <span className="tiptap-image-upload-subtext">
365
+ {formatFileSize(fileItem.file.size)}
366
+ </span>
367
+ </div>
368
+ </div>
369
+ <div className="tiptap-image-upload-actions">
370
+ {fileItem.status === 'uploading' && (
371
+ <span className="tiptap-image-upload-progress-text">{fileItem.progress}%</span>
372
+ )}
373
+ <Button
374
+ type="button"
375
+ variant="ghost"
376
+ onClick={(e) => {
377
+ e.stopPropagation()
378
+ onRemove()
379
+ }}
380
+ >
381
+ <CloseIcon className="tiptap-button-icon" />
382
+ </Button>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ )
387
+ }
388
+
389
+ const DropZoneContent: React.FC<{ maxSize: number; limit: number }> = ({ maxSize, limit }) => (
390
+ <>
391
+ <div className="tiptap-image-upload-dropzone">
392
+ <FileIcon />
393
+ <FileCornerIcon />
394
+ <div className="tiptap-image-upload-icon-container">
395
+ <CloudUploadIcon />
396
+ </div>
397
+ </div>
398
+
399
+ <div className="tiptap-image-upload-content">
400
+ <span className="tiptap-image-upload-text">
401
+ <em>Click to upload</em> or drag and drop
402
+ </span>
403
+ <span className="tiptap-image-upload-subtext">
404
+ Maximum {limit} file{limit === 1 ? '' : 's'}, {maxSize / 1024 / 1024}MB each.
405
+ </span>
406
+ </div>
407
+ </>
408
+ )
409
+
410
+ export const ImageUploadNode: React.FC<NodeViewProps> = (props) => {
411
+ const { accept, limit, maxSize } = props.node.attrs
412
+ const inputRef = useRef<HTMLInputElement>(null)
413
+ const extension = props.extension
414
+
415
+ const uploadOptions: UploadOptions = {
416
+ maxSize,
417
+ limit,
418
+ accept,
419
+ upload: extension.options.upload,
420
+ onSuccess: extension.options.onSuccess,
421
+ onError: extension.options.onError
422
+ }
423
+
424
+ const { fileItems, uploadFiles, removeFileItem, clearAllFiles } = useFileUpload(uploadOptions)
425
+
426
+ const handleUpload = async (files: File[]) => {
427
+ const urls = await uploadFiles(files)
428
+
429
+ if (urls.length > 0) {
430
+ const pos = props.getPos()
431
+
432
+ if (isValidPosition(pos)) {
433
+ const imageNodes = urls.map((url, index) => {
434
+ const filename = files[index]?.name.replace(/\.[^/.]+$/, '') || 'unknown'
435
+ return {
436
+ type: extension.options.type,
437
+ attrs: {
438
+ ...extension.options,
439
+ src: url,
440
+ alt: filename,
441
+ title: filename
442
+ }
443
+ }
444
+ })
445
+
446
+ props.editor
447
+ .chain()
448
+ .focus()
449
+ .deleteRange({ from: pos, to: pos + props.node.nodeSize })
450
+ .insertContentAt(pos, imageNodes)
451
+ .run()
452
+
453
+ focusNextNode(props.editor)
454
+ }
455
+ }
456
+ }
457
+
458
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
459
+ const files = e.target.files
460
+ if (!files || files.length === 0) {
461
+ extension.options.onError?.(new Error('No file selected'))
462
+ return
463
+ }
464
+ handleUpload(Array.from(files))
465
+ }
466
+
467
+ const handleClick = () => {
468
+ if (inputRef.current && fileItems.length === 0) {
469
+ inputRef.current.value = ''
470
+ inputRef.current.click()
471
+ }
472
+ }
473
+
474
+ const hasFiles = fileItems.length > 0
475
+
476
+ return (
477
+ <NodeViewWrapper className="tiptap-image-upload" tabIndex={0} onClick={handleClick}>
478
+ {!hasFiles && (
479
+ <ImageUploadDragArea onFile={handleUpload}>
480
+ <DropZoneContent maxSize={maxSize} limit={limit} />
481
+ </ImageUploadDragArea>
482
+ )}
483
+
484
+ {hasFiles && (
485
+ <div className="tiptap-image-upload-previews">
486
+ {fileItems.length > 1 && (
487
+ <div className="tiptap-image-upload-header">
488
+ <span>Uploading {fileItems.length} files</span>
489
+ <Button
490
+ type="button"
491
+ variant="ghost"
492
+ onClick={(e) => {
493
+ e.stopPropagation()
494
+ clearAllFiles()
495
+ }}
496
+ >
497
+ Clear All
498
+ </Button>
499
+ </div>
500
+ )}
501
+ {fileItems.map((fileItem) => (
502
+ <ImageUploadPreview
503
+ key={fileItem.id}
504
+ fileItem={fileItem}
505
+ onRemove={() => removeFileItem(fileItem.id)}
506
+ />
507
+ ))}
508
+ </div>
509
+ )}
510
+
511
+ <input
512
+ ref={inputRef}
513
+ name="file"
514
+ accept={accept}
515
+ type="file"
516
+ multiple={limit > 1}
517
+ onChange={handleChange}
518
+ onClick={(e: React.MouseEvent<HTMLInputElement>) => e.stopPropagation()}
519
+ />
520
+ </NodeViewWrapper>
521
+ )
522
+ }
@@ -0,0 +1 @@
1
+ export * from './image-upload-node-extension'