@betterstart/cli 0.1.3 → 0.1.5

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 +13585 -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 +687 -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,587 @@
1
+ import type { Node as PMNode } from '@tiptap/pm/model'
2
+ import type { Transaction } from '@tiptap/pm/state'
3
+ import { AllSelection, NodeSelection, Selection, TextSelection } from '@tiptap/pm/state'
4
+ import { CellSelection, cellAround } from '@tiptap/pm/tables'
5
+ import { type Editor, findParentNodeClosestToPos, type NodeWithPos } from '@tiptap/react'
6
+
7
+ export const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
8
+
9
+ export const MAC_SYMBOLS: Record<string, string> = {
10
+ mod: '⌘',
11
+ command: '⌘',
12
+ meta: '⌘',
13
+ ctrl: '⌃',
14
+ control: '⌃',
15
+ alt: '⌥',
16
+ option: '⌥',
17
+ shift: '⇧',
18
+ backspace: 'Del',
19
+ delete: '⌦',
20
+ enter: '⏎',
21
+ escape: '⎋',
22
+ capslock: '⇪'
23
+ } as const
24
+
25
+ export const SR_ONLY = {
26
+ position: 'absolute',
27
+ width: '1px',
28
+ height: '1px',
29
+ padding: 0,
30
+ margin: '-1px',
31
+ overflow: 'hidden',
32
+ clip: 'rect(0, 0, 0, 0)',
33
+ whiteSpace: 'nowrap',
34
+ borderWidth: 0
35
+ } as const
36
+
37
+ export function cn(...classes: (string | boolean | undefined | null)[]): string {
38
+ return classes.filter(Boolean).join(' ')
39
+ }
40
+
41
+ /**
42
+ * Determines if the current platform is macOS
43
+ * @returns boolean indicating if the current platform is Mac
44
+ */
45
+ export function isMac(): boolean {
46
+ return typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac')
47
+ }
48
+
49
+ /**
50
+ * Formats a shortcut key based on the platform (Mac or non-Mac)
51
+ * @param key - The key to format (e.g., "ctrl", "alt", "shift")
52
+ * @param isMac - Boolean indicating if the platform is Mac
53
+ * @param capitalize - Whether to capitalize the key (default: true)
54
+ * @returns Formatted shortcut key symbol
55
+ */
56
+ export const formatShortcutKey = (key: string, isMac: boolean, capitalize: boolean = true) => {
57
+ if (isMac) {
58
+ const lowerKey = key.toLowerCase()
59
+ return MAC_SYMBOLS[lowerKey] || (capitalize ? key.toUpperCase() : key)
60
+ }
61
+
62
+ return capitalize ? key.charAt(0).toUpperCase() + key.slice(1) : key
63
+ }
64
+
65
+ /**
66
+ * Parses a shortcut key string into an array of formatted key symbols
67
+ * @param shortcutKeys - The string of shortcut keys (e.g., "ctrl-alt-shift")
68
+ * @param delimiter - The delimiter used to split the keys (default: "-")
69
+ * @param capitalize - Whether to capitalize the keys (default: true)
70
+ * @returns Array of formatted shortcut key symbols
71
+ */
72
+ export const parseShortcutKeys = (props: {
73
+ shortcutKeys: string | undefined
74
+ delimiter?: string
75
+ capitalize?: boolean
76
+ }) => {
77
+ const { shortcutKeys, delimiter = '+', capitalize = true } = props
78
+
79
+ if (!shortcutKeys) return []
80
+
81
+ return shortcutKeys
82
+ .split(delimiter)
83
+ .map((key) => key.trim())
84
+ .map((key) => formatShortcutKey(key, isMac(), capitalize))
85
+ }
86
+
87
+ /**
88
+ * Checks if a mark exists in the editor schema
89
+ * @param markName - The name of the mark to check
90
+ * @param editor - The editor instance
91
+ * @returns boolean indicating if the mark exists in the schema
92
+ */
93
+ export const isMarkInSchema = (markName: string, editor: Editor | null): boolean => {
94
+ if (!editor?.schema) return false
95
+ return editor.schema.spec.marks.get(markName) !== undefined
96
+ }
97
+
98
+ /**
99
+ * Checks if a node exists in the editor schema
100
+ * @param nodeName - The name of the node to check
101
+ * @param editor - The editor instance
102
+ * @returns boolean indicating if the node exists in the schema
103
+ */
104
+ export const isNodeInSchema = (nodeName: string, editor: Editor | null): boolean => {
105
+ if (!editor?.schema) return false
106
+ return editor.schema.spec.nodes.get(nodeName) !== undefined
107
+ }
108
+
109
+ /**
110
+ * Moves the focus to the next node in the editor
111
+ * @param editor - The editor instance
112
+ * @returns boolean indicating if the focus was moved
113
+ */
114
+ export function focusNextNode(editor: Editor) {
115
+ const { state, view } = editor
116
+ const { doc, selection } = state
117
+
118
+ const nextSel = Selection.findFrom(selection.$to, 1, true)
119
+ if (nextSel) {
120
+ view.dispatch(state.tr.setSelection(nextSel).scrollIntoView())
121
+ return true
122
+ }
123
+
124
+ const paragraphType = state.schema.nodes.paragraph
125
+ if (!paragraphType) {
126
+ console.warn('No paragraph node type found in schema.')
127
+ return false
128
+ }
129
+
130
+ const end = doc.content.size
131
+ const para = paragraphType.create()
132
+ let tr = state.tr.insert(end, para)
133
+
134
+ // Place the selection inside the new paragraph
135
+ const $inside = tr.doc.resolve(end + 1)
136
+ tr = tr.setSelection(TextSelection.near($inside)).scrollIntoView()
137
+ view.dispatch(tr)
138
+ return true
139
+ }
140
+
141
+ /**
142
+ * Checks if a value is a valid number (not null, undefined, or NaN)
143
+ * @param value - The value to check
144
+ * @returns boolean indicating if the value is a valid number
145
+ */
146
+ export function isValidPosition(pos: number | null | undefined): pos is number {
147
+ return typeof pos === 'number' && pos >= 0
148
+ }
149
+
150
+ /**
151
+ * Checks if one or more extensions are registered in the Tiptap editor.
152
+ * @param editor - The Tiptap editor instance
153
+ * @param extensionNames - A single extension name or an array of names to check
154
+ * @returns True if at least one of the extensions is available, false otherwise
155
+ */
156
+ export function isExtensionAvailable(
157
+ editor: Editor | null,
158
+ extensionNames: string | string[]
159
+ ): boolean {
160
+ if (!editor) return false
161
+
162
+ const names = Array.isArray(extensionNames) ? extensionNames : [extensionNames]
163
+
164
+ const found = names.some((name) =>
165
+ editor.extensionManager.extensions.some((ext) => ext.name === name)
166
+ )
167
+
168
+ if (!found) {
169
+ console.warn(
170
+ `None of the extensions [${names.join(', ')}] were found in the editor schema. Ensure they are included in the editor configuration.`
171
+ )
172
+ }
173
+
174
+ return found
175
+ }
176
+
177
+ /**
178
+ * Finds a node at the specified position with error handling
179
+ * @param editor The Tiptap editor instance
180
+ * @param position The position in the document to find the node
181
+ * @returns The node at the specified position, or null if not found
182
+ */
183
+ export function findNodeAtPosition(editor: Editor, position: number) {
184
+ try {
185
+ const node = editor.state.doc.nodeAt(position)
186
+ if (!node) {
187
+ console.warn(`No node found at position ${position}`)
188
+ return null
189
+ }
190
+ return node
191
+ } catch (error) {
192
+ console.error(`Error getting node at position ${position}:`, error)
193
+ return null
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Finds the position and instance of a node in the document
199
+ * @param props Object containing editor, node (optional), and nodePos (optional)
200
+ * @param props.editor The Tiptap editor instance
201
+ * @param props.node The node to find (optional if nodePos is provided)
202
+ * @param props.nodePos The position of the node to find (optional if node is provided)
203
+ * @returns An object with the position and node, or null if not found
204
+ */
205
+ export function findNodePosition(props: {
206
+ editor: Editor | null
207
+ node?: PMNode | null
208
+ nodePos?: number | null
209
+ }): { pos: number; node: PMNode } | null {
210
+ const { editor, node, nodePos } = props
211
+
212
+ if (!editor || !editor.state?.doc) return null
213
+
214
+ // Zero is valid position
215
+ const hasValidNode = node !== undefined && node !== null
216
+ const hasValidPos = isValidPosition(nodePos)
217
+
218
+ if (!hasValidNode && !hasValidPos) {
219
+ return null
220
+ }
221
+
222
+ // First search for the node in the document if we have a node
223
+ if (hasValidNode) {
224
+ let foundPos = -1
225
+ let foundNode: PMNode | null = null
226
+
227
+ editor.state.doc.descendants((currentNode, pos) => {
228
+ // TODO: Needed?
229
+ // if (currentNode.type && currentNode.type.name === node!.type.name) {
230
+ if (currentNode === node) {
231
+ foundPos = pos
232
+ foundNode = currentNode
233
+ return false
234
+ }
235
+ return true
236
+ })
237
+
238
+ if (foundPos !== -1 && foundNode !== null) {
239
+ return { pos: foundPos, node: foundNode }
240
+ }
241
+ }
242
+
243
+ // If we have a valid position, use findNodeAtPosition
244
+ if (hasValidPos) {
245
+ const nodeAtPos = findNodeAtPosition(editor, nodePos!)
246
+ if (nodeAtPos) {
247
+ return { pos: nodePos!, node: nodeAtPos }
248
+ }
249
+ }
250
+
251
+ return null
252
+ }
253
+
254
+ /**
255
+ * Determines whether the current selection contains a node whose type matches
256
+ * any of the provided node type names.
257
+ * @param editor Tiptap editor instance
258
+ * @param nodeTypeNames List of node type names to match against
259
+ * @param checkAncestorNodes Whether to check ancestor node types up the depth chain
260
+ */
261
+ export function isNodeTypeSelected(
262
+ editor: Editor | null,
263
+ nodeTypeNames: string[] = [],
264
+ checkAncestorNodes: boolean = false
265
+ ): boolean {
266
+ if (!editor || !editor.state.selection) return false
267
+
268
+ const { selection } = editor.state
269
+ if (selection.empty) return false
270
+
271
+ // Direct node selection check
272
+ if (selection instanceof NodeSelection) {
273
+ const selectedNode = selection.node
274
+ return selectedNode ? nodeTypeNames.includes(selectedNode.type.name) : false
275
+ }
276
+
277
+ // Depth-based ancestor node check
278
+ if (checkAncestorNodes) {
279
+ const { $from } = selection
280
+ for (let depth = $from.depth; depth > 0; depth--) {
281
+ const ancestorNode = $from.node(depth)
282
+ if (nodeTypeNames.includes(ancestorNode.type.name)) {
283
+ return true
284
+ }
285
+ }
286
+ }
287
+
288
+ return false
289
+ }
290
+
291
+ /**
292
+ * Check whether the current selection is fully within nodes
293
+ * whose type names are in the provided `types` list.
294
+ *
295
+ * - NodeSelection → checks the selected node.
296
+ * - Text/AllSelection → ensures all textblocks within [from, to) are allowed.
297
+ */
298
+ export function selectionWithinConvertibleTypes(editor: Editor, types: string[] = []): boolean {
299
+ if (!editor || types.length === 0) return false
300
+
301
+ const { state } = editor
302
+ const { selection } = state
303
+ const allowed = new Set(types)
304
+
305
+ if (selection instanceof NodeSelection) {
306
+ const nodeType = selection.node?.type?.name
307
+ return !!nodeType && allowed.has(nodeType)
308
+ }
309
+
310
+ if (selection instanceof TextSelection || selection instanceof AllSelection) {
311
+ let valid = true
312
+ state.doc.nodesBetween(selection.from, selection.to, (node) => {
313
+ if (node.isTextblock && !allowed.has(node.type.name)) {
314
+ valid = false
315
+ return false // stop early
316
+ }
317
+ return valid
318
+ })
319
+ return valid
320
+ }
321
+
322
+ return false
323
+ }
324
+
325
+ /**
326
+ * Handles image upload with progress tracking and abort capability
327
+ * @param file The file to upload
328
+ * @param onProgress Optional callback for tracking upload progress
329
+ * @param abortSignal Optional AbortSignal for cancelling the upload
330
+ * @returns Promise resolving to the URL of the uploaded image
331
+ */
332
+ export const handleImageUpload = async (
333
+ file: File,
334
+ onProgress?: (event: { progress: number }) => void,
335
+ abortSignal?: AbortSignal
336
+ ): Promise<string> => {
337
+ // Validate file
338
+ if (!file) {
339
+ throw new Error('No file provided')
340
+ }
341
+
342
+ if (file.size > MAX_FILE_SIZE) {
343
+ throw new Error(`File size exceeds maximum allowed (${MAX_FILE_SIZE / (1024 * 1024)}MB)`)
344
+ }
345
+
346
+ // For demo/testing: Simulate upload progress. In production, replace the following code
347
+ // with your own upload implementation.
348
+ for (let progress = 0; progress <= 100; progress += 10) {
349
+ if (abortSignal?.aborted) {
350
+ throw new Error('Upload cancelled')
351
+ }
352
+ await new Promise((resolve) => setTimeout(resolve, 500))
353
+ onProgress?.({ progress })
354
+ }
355
+
356
+ return '/images/tiptap-ui-placeholder-image.jpg'
357
+ }
358
+
359
+ type ProtocolOptions = {
360
+ /**
361
+ * The protocol scheme to be registered.
362
+ * @default '''
363
+ * @example 'ftp'
364
+ * @example 'git'
365
+ */
366
+ scheme: string
367
+
368
+ /**
369
+ * If enabled, it allows optional slashes after the protocol.
370
+ * @default false
371
+ * @example true
372
+ */
373
+ optionalSlashes?: boolean
374
+ }
375
+
376
+ type ProtocolConfig = Array<ProtocolOptions | string>
377
+
378
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: whitespace detection requires control chars
379
+ const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
380
+
381
+ export function isAllowedUri(uri: string | undefined, protocols?: ProtocolConfig) {
382
+ const allowedProtocols: string[] = [
383
+ 'http',
384
+ 'https',
385
+ 'ftp',
386
+ 'ftps',
387
+ 'mailto',
388
+ 'tel',
389
+ 'callto',
390
+ 'sms',
391
+ 'cid',
392
+ 'xmpp'
393
+ ]
394
+
395
+ if (protocols) {
396
+ protocols.forEach((protocol) => {
397
+ const nextProtocol = typeof protocol === 'string' ? protocol : protocol.scheme
398
+
399
+ if (nextProtocol) {
400
+ allowedProtocols.push(nextProtocol)
401
+ }
402
+ })
403
+ }
404
+
405
+ return (
406
+ !uri ||
407
+ uri.replace(ATTR_WHITESPACE, '').match(
408
+ new RegExp(
409
+ // eslint-disable-next-line no-useless-escape
410
+ `^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,
411
+ 'i'
412
+ )
413
+ )
414
+ )
415
+ }
416
+
417
+ export function sanitizeUrl(inputUrl: string, baseUrl: string, protocols?: ProtocolConfig): string {
418
+ try {
419
+ const url = new URL(inputUrl, baseUrl)
420
+
421
+ if (isAllowedUri(url.href, protocols)) {
422
+ return url.href
423
+ }
424
+ } catch {
425
+ // If URL creation fails, it's considered invalid
426
+ }
427
+ return '#'
428
+ }
429
+
430
+ /**
431
+ * Update a single attribute on multiple nodes.
432
+ *
433
+ * @param tr - The transaction to mutate
434
+ * @param targets - Array of { node, pos }
435
+ * @param attrName - Attribute key to update
436
+ * @param next - New value OR updater function receiving previous value
437
+ * Pass `undefined` to remove the attribute.
438
+ * @returns true if at least one node was updated, false otherwise
439
+ */
440
+ export function updateNodesAttr<A extends string = string, V = unknown>(
441
+ tr: Transaction,
442
+ targets: readonly NodeWithPos[],
443
+ attrName: A,
444
+ next: V | ((prev: V | undefined) => V | undefined)
445
+ ): boolean {
446
+ if (!targets.length) return false
447
+
448
+ let changed = false
449
+
450
+ for (const { pos } of targets) {
451
+ // Always re-read from the transaction's current doc
452
+ const currentNode = tr.doc.nodeAt(pos)
453
+ if (!currentNode) continue
454
+
455
+ const prevValue = (currentNode.attrs as Record<string, unknown>)[attrName] as V | undefined
456
+ const resolvedNext =
457
+ typeof next === 'function' ? (next as (p: V | undefined) => V | undefined)(prevValue) : next
458
+
459
+ if (prevValue === resolvedNext) continue
460
+
461
+ const nextAttrs: Record<string, unknown> = { ...currentNode.attrs }
462
+ if (resolvedNext === undefined) {
463
+ // Remove the key entirely instead of setting null
464
+ delete nextAttrs[attrName]
465
+ } else {
466
+ nextAttrs[attrName] = resolvedNext
467
+ }
468
+
469
+ tr.setNodeMarkup(pos, undefined, nextAttrs)
470
+ changed = true
471
+ }
472
+
473
+ return changed
474
+ }
475
+
476
+ /**
477
+ * Selects the entire content of the current block node if the selection is empty.
478
+ * If the selection is not empty, it does nothing.
479
+ * @param editor The Tiptap editor instance
480
+ */
481
+ export function selectCurrentBlockContent(editor: Editor) {
482
+ const { selection, doc } = editor.state
483
+
484
+ if (!selection.empty) return
485
+
486
+ const $pos = selection.$from
487
+ let blockNode = null
488
+ let blockPos = -1
489
+
490
+ for (let depth = $pos.depth; depth >= 0; depth--) {
491
+ const node = $pos.node(depth)
492
+ const pos = $pos.start(depth)
493
+
494
+ if (node.isBlock && node.textContent.trim()) {
495
+ blockNode = node
496
+ blockPos = pos
497
+ break
498
+ }
499
+ }
500
+
501
+ if (blockNode && blockPos >= 0) {
502
+ const from = blockPos
503
+ const to = blockPos + blockNode.nodeSize - 2 // -2 to exclude the closing tag
504
+
505
+ if (from < to) {
506
+ const $from = doc.resolve(from)
507
+ const $to = doc.resolve(to)
508
+ const newSelection = TextSelection.between($from, $to, 1)
509
+
510
+ if (newSelection && !selection.eq(newSelection)) {
511
+ editor.view.dispatch(editor.state.tr.setSelection(newSelection))
512
+ }
513
+ }
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Retrieves all nodes of specified types from the current selection.
519
+ * @param selection The current editor selection
520
+ * @param allowedNodeTypes An array of node type names to look for (e.g., ["image", "table"])
521
+ * @returns An array of objects containing the node and its position
522
+ */
523
+ export function getSelectedNodesOfType(
524
+ selection: Selection,
525
+ allowedNodeTypes: string[]
526
+ ): NodeWithPos[] {
527
+ const results: NodeWithPos[] = []
528
+ const allowed = new Set(allowedNodeTypes)
529
+
530
+ if (selection instanceof CellSelection) {
531
+ selection.forEachCell((node: PMNode, pos: number) => {
532
+ if (allowed.has(node.type.name)) {
533
+ results.push({ node, pos })
534
+ }
535
+ })
536
+ return results
537
+ }
538
+
539
+ if (selection instanceof NodeSelection) {
540
+ const { node, from: pos } = selection
541
+ if (node && allowed.has(node.type.name)) {
542
+ results.push({ node, pos })
543
+ }
544
+ return results
545
+ }
546
+
547
+ const { $anchor } = selection
548
+ const cell = cellAround($anchor)
549
+
550
+ if (cell) {
551
+ const cellNode = selection.$anchor.doc.nodeAt(cell.pos)
552
+ if (cellNode && allowed.has(cellNode.type.name)) {
553
+ results.push({ node: cellNode, pos: cell.pos })
554
+ return results
555
+ }
556
+ }
557
+
558
+ // Fallback: find parent nodes of allowed types
559
+ const parentNode = findParentNodeClosestToPos($anchor, (node) => allowed.has(node.type.name))
560
+
561
+ if (parentNode) {
562
+ results.push({ node: parentNode.node, pos: parentNode.pos })
563
+ }
564
+
565
+ return results
566
+ }
567
+
568
+ export function getSelectedBlockNodes(editor: Editor): PMNode[] {
569
+ const { doc } = editor.state
570
+ const { from, to } = editor.state.selection
571
+
572
+ const blocks: PMNode[] = []
573
+ const seen = new Set<number>()
574
+
575
+ doc.nodesBetween(from, to, (node, pos) => {
576
+ if (!node.isBlock) return
577
+
578
+ if (!seen.has(pos)) {
579
+ seen.add(pos)
580
+ blocks.push(node)
581
+ }
582
+
583
+ return false
584
+ })
585
+
586
+ return blocks
587
+ }
@@ -0,0 +1,91 @@
1
+ @keyframes fadeIn {
2
+ from {
3
+ opacity: 0;
4
+ }
5
+ to {
6
+ opacity: 1;
7
+ }
8
+ }
9
+
10
+ @keyframes fadeOut {
11
+ from {
12
+ opacity: 1;
13
+ }
14
+ to {
15
+ opacity: 0;
16
+ }
17
+ }
18
+
19
+ @keyframes zoomIn {
20
+ from {
21
+ transform: scale(0.95);
22
+ }
23
+ to {
24
+ transform: scale(1);
25
+ }
26
+ }
27
+
28
+ @keyframes zoomOut {
29
+ from {
30
+ transform: scale(1);
31
+ }
32
+ to {
33
+ transform: scale(0.95);
34
+ }
35
+ }
36
+
37
+ @keyframes zoom {
38
+ 0% {
39
+ opacity: 0;
40
+ transform: scale(0.95);
41
+ }
42
+ 100% {
43
+ opacity: 1;
44
+ transform: scale(1);
45
+ }
46
+ }
47
+
48
+ @keyframes slideFromTop {
49
+ from {
50
+ transform: translateY(-0.5rem);
51
+ }
52
+ to {
53
+ transform: translateY(0);
54
+ }
55
+ }
56
+
57
+ @keyframes slideFromRight {
58
+ from {
59
+ transform: translateX(0.5rem);
60
+ }
61
+ to {
62
+ transform: translateX(0);
63
+ }
64
+ }
65
+
66
+ @keyframes slideFromLeft {
67
+ from {
68
+ transform: translateX(-0.5rem);
69
+ }
70
+ to {
71
+ transform: translateX(0);
72
+ }
73
+ }
74
+
75
+ @keyframes slideFromBottom {
76
+ from {
77
+ transform: translateY(0.5rem);
78
+ }
79
+ to {
80
+ transform: translateY(0);
81
+ }
82
+ }
83
+
84
+ @keyframes spin {
85
+ from {
86
+ transform: rotate(0deg);
87
+ }
88
+ to {
89
+ transform: rotate(360deg);
90
+ }
91
+ }