@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,286 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
8
+ // --- Lib ---
9
+ import { isMarkInSchema, isNodeTypeSelected, sanitizeUrl } from '../../lib/tiptap-utils'
10
+ // --- Icons ---
11
+ import { LinkIcon } from '../../tiptap-icons/link-icon'
12
+
13
+ /**
14
+ * Configuration for the link popover functionality
15
+ */
16
+ export interface UseLinkPopoverConfig {
17
+ /**
18
+ * The Tiptap editor instance.
19
+ */
20
+ editor?: Editor | null
21
+ /**
22
+ * Whether to hide the link popover when not available.
23
+ * @default false
24
+ */
25
+ hideWhenUnavailable?: boolean
26
+ /**
27
+ * Callback function called when the link is set.
28
+ */
29
+ onSetLink?: () => void
30
+ }
31
+
32
+ /**
33
+ * Configuration for the link handler functionality
34
+ */
35
+ export interface LinkHandlerProps {
36
+ /**
37
+ * The Tiptap editor instance.
38
+ */
39
+ editor: Editor | null
40
+ /**
41
+ * Callback function called when the link is set.
42
+ */
43
+ onSetLink?: () => void
44
+ }
45
+
46
+ /**
47
+ * Checks if a link can be set in the current editor state
48
+ */
49
+ export function canSetLink(editor: Editor | null): boolean {
50
+ if (!editor || !editor.isEditable) return false
51
+
52
+ // The third argument 'true' checks whether the current selection is inside an image caption, and prevents setting a link there
53
+ // If the selection is inside an image caption, we can't set a link
54
+ if (isNodeTypeSelected(editor, ['image'], true)) return false
55
+ try {
56
+ return editor.can().setMark('link')
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Checks if a link is currently active in the editor
64
+ */
65
+ export function isLinkActive(editor: Editor | null): boolean {
66
+ if (!editor || !editor.isEditable) return false
67
+ return editor.isActive('link')
68
+ }
69
+
70
+ /**
71
+ * Determines if the link button should be shown
72
+ */
73
+ export function shouldShowLinkButton(props: {
74
+ editor: Editor | null
75
+ hideWhenUnavailable: boolean
76
+ }): boolean {
77
+ const { editor, hideWhenUnavailable } = props
78
+
79
+ if (!editor || !editor.isEditable) return false
80
+
81
+ const linkInSchema = isMarkInSchema('link', editor)
82
+
83
+ // If hideWhenUnavailable is false, always show the button (even if disabled)
84
+ if (!hideWhenUnavailable) {
85
+ return true
86
+ }
87
+
88
+ // hideWhenUnavailable is true: hide if link is not in schema
89
+ if (!linkInSchema) {
90
+ return false
91
+ }
92
+
93
+ // hideWhenUnavailable is true: hide if we can't set a link (unless in code block)
94
+ if (!editor.isActive('code')) {
95
+ return canSetLink(editor)
96
+ }
97
+
98
+ return true
99
+ }
100
+
101
+ /**
102
+ * Custom hook for handling link operations in a Tiptap editor
103
+ */
104
+ export function useLinkHandler(props: LinkHandlerProps) {
105
+ const { editor, onSetLink } = props
106
+ const [url, setUrl] = useState<string | null>(null)
107
+
108
+ useEffect(() => {
109
+ if (!editor) return
110
+
111
+ // Get URL immediately on mount
112
+ const { href } = editor.getAttributes('link')
113
+
114
+ if (isLinkActive(editor) && url === null) {
115
+ setUrl(href || '')
116
+ }
117
+ }, [editor, url])
118
+
119
+ useEffect(() => {
120
+ if (!editor) return
121
+
122
+ const updateLinkState = () => {
123
+ const { href } = editor.getAttributes('link')
124
+ setUrl(href || '')
125
+ }
126
+
127
+ editor.on('selectionUpdate', updateLinkState)
128
+ return () => {
129
+ editor.off('selectionUpdate', updateLinkState)
130
+ }
131
+ }, [editor])
132
+
133
+ const setLink = useCallback(() => {
134
+ if (!url || !editor) return
135
+
136
+ const { selection } = editor.state
137
+ const isEmpty = selection.empty
138
+
139
+ let chain = editor.chain().focus()
140
+
141
+ chain = chain.extendMarkRange('link').setLink({ href: url })
142
+
143
+ if (isEmpty) {
144
+ chain = chain.insertContent({ type: 'text', text: url })
145
+ }
146
+
147
+ chain.run()
148
+
149
+ setUrl(null)
150
+
151
+ onSetLink?.()
152
+ }, [editor, onSetLink, url])
153
+
154
+ const removeLink = useCallback(() => {
155
+ if (!editor) return
156
+ editor
157
+ .chain()
158
+ .focus()
159
+ .extendMarkRange('link')
160
+ .unsetLink()
161
+ .setMeta('preventAutolink', true)
162
+ .run()
163
+ setUrl('')
164
+ }, [editor])
165
+
166
+ const openLink = useCallback(
167
+ (target: string = '_blank', features: string = 'noopener,noreferrer') => {
168
+ if (!url) return
169
+
170
+ const safeUrl = sanitizeUrl(url, window.location.href)
171
+ if (safeUrl !== '#') {
172
+ window.open(safeUrl, target, features)
173
+ }
174
+ },
175
+ [url]
176
+ )
177
+
178
+ return {
179
+ url: url || '',
180
+ setUrl,
181
+ setLink,
182
+ removeLink,
183
+ openLink
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Custom hook for link popover state management
189
+ */
190
+ export function useLinkState(props: { editor: Editor | null; hideWhenUnavailable: boolean }) {
191
+ const { editor, hideWhenUnavailable = false } = props
192
+
193
+ const canSet = canSetLink(editor)
194
+ const isActive = isLinkActive(editor)
195
+
196
+ const [isVisible, setIsVisible] = useState(true)
197
+
198
+ useEffect(() => {
199
+ if (!editor) return
200
+
201
+ const handleSelectionUpdate = () => {
202
+ setIsVisible(
203
+ shouldShowLinkButton({
204
+ editor,
205
+ hideWhenUnavailable
206
+ })
207
+ )
208
+ }
209
+
210
+ handleSelectionUpdate()
211
+
212
+ editor.on('selectionUpdate', handleSelectionUpdate)
213
+
214
+ return () => {
215
+ editor.off('selectionUpdate', handleSelectionUpdate)
216
+ }
217
+ }, [editor, hideWhenUnavailable])
218
+
219
+ return {
220
+ isVisible,
221
+ canSet,
222
+ isActive
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Main hook that provides link popover functionality for Tiptap editor
228
+ *
229
+ * @example
230
+ * ```tsx
231
+ * // Simple usage
232
+ * function MyLinkButton() {
233
+ * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover()
234
+ *
235
+ * if (!isVisible) return null
236
+ *
237
+ * return <button disabled={!canSet}>Link</button>
238
+ * }
239
+ *
240
+ * // Advanced usage with configuration
241
+ * function MyAdvancedLinkButton() {
242
+ * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover({
243
+ * editor: myEditor,
244
+ * hideWhenUnavailable: true,
245
+ * onSetLink: () => console.log('Link set!')
246
+ * })
247
+ *
248
+ * if (!isVisible) return null
249
+ *
250
+ * return (
251
+ * <MyButton
252
+ * disabled={!canSet}
253
+ * aria-label={label}
254
+ * aria-pressed={isActive}
255
+ * >
256
+ * <Icon />
257
+ * {label}
258
+ * </MyButton>
259
+ * )
260
+ * }
261
+ * ```
262
+ */
263
+ export function useLinkPopover(config?: UseLinkPopoverConfig) {
264
+ const { editor: providedEditor, hideWhenUnavailable = false, onSetLink } = config || {}
265
+
266
+ const { editor } = useTiptapEditor(providedEditor)
267
+
268
+ const { isVisible, canSet, isActive } = useLinkState({
269
+ editor,
270
+ hideWhenUnavailable
271
+ })
272
+
273
+ const linkHandler = useLinkHandler({
274
+ editor,
275
+ onSetLink
276
+ })
277
+
278
+ return {
279
+ isVisible,
280
+ canSet,
281
+ isActive,
282
+ label: 'Link',
283
+ Icon: LinkIcon,
284
+ ...linkHandler
285
+ }
286
+ }
@@ -0,0 +1,2 @@
1
+ export * from './list-button'
2
+ export * from './use-list'
@@ -0,0 +1,108 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback } from 'react'
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
6
+ // --- Lib ---
7
+ import { parseShortcutKeys } from '../../lib/tiptap-utils'
8
+ import { Badge } from '../../tiptap-ui-primitive/badge'
9
+ // --- UI Primitives ---
10
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
11
+ import { Button } from '../../tiptap-ui-primitive/button'
12
+
13
+ // --- Tiptap UI ---
14
+ import type { ListType, UseListConfig } from '.'
15
+ import { LIST_SHORTCUT_KEYS, useList } from '.'
16
+
17
+ export interface ListButtonProps extends Omit<ButtonProps, 'type'>, UseListConfig {
18
+ /**
19
+ * Optional text to display alongside the icon.
20
+ */
21
+ text?: string
22
+ /**
23
+ * Optional show shortcut keys in the button.
24
+ * @default false
25
+ */
26
+ showShortcut?: boolean
27
+ }
28
+
29
+ export function ListShortcutBadge({
30
+ type,
31
+ shortcutKeys = LIST_SHORTCUT_KEYS[type]
32
+ }: {
33
+ type: ListType
34
+ shortcutKeys?: string
35
+ }) {
36
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
37
+ }
38
+
39
+ /**
40
+ * Button component for toggling lists in a Tiptap editor.
41
+ *
42
+ * For custom button implementations, use the `useList` hook instead.
43
+ */
44
+ export const ListButton = forwardRef<HTMLButtonElement, ListButtonProps>(
45
+ (
46
+ {
47
+ editor: providedEditor,
48
+ type,
49
+ text,
50
+ hideWhenUnavailable = false,
51
+ onToggled,
52
+ showShortcut = false,
53
+ onClick,
54
+ children,
55
+ ...buttonProps
56
+ },
57
+ ref
58
+ ) => {
59
+ const { editor } = useTiptapEditor(providedEditor)
60
+ const { isVisible, canToggle, isActive, handleToggle, label, shortcutKeys, Icon } = useList({
61
+ editor,
62
+ type,
63
+ hideWhenUnavailable,
64
+ onToggled
65
+ })
66
+
67
+ const handleClick = useCallback(
68
+ (event: React.MouseEvent<HTMLButtonElement>) => {
69
+ onClick?.(event)
70
+ if (event.defaultPrevented) return
71
+ handleToggle()
72
+ },
73
+ [handleToggle, onClick]
74
+ )
75
+
76
+ if (!isVisible) {
77
+ return null
78
+ }
79
+
80
+ return (
81
+ <Button
82
+ type="button"
83
+ variant="ghost"
84
+ data-active-state={isActive ? 'on' : 'off'}
85
+ role="button"
86
+ tabIndex={-1}
87
+ disabled={!canToggle}
88
+ data-disabled={!canToggle}
89
+ aria-label={label}
90
+ aria-pressed={isActive}
91
+ tooltip={label}
92
+ onClick={handleClick}
93
+ {...buttonProps}
94
+ ref={ref}
95
+ >
96
+ {children ?? (
97
+ <>
98
+ <Icon className="tiptap-button-icon" />
99
+ {text && <span className="tiptap-button-text">{text}</span>}
100
+ {showShortcut && <ListShortcutBadge type={type} shortcutKeys={shortcutKeys} />}
101
+ </>
102
+ )}
103
+ </Button>
104
+ )
105
+ }
106
+ )
107
+
108
+ ListButton.displayName = 'ListButton'
@@ -0,0 +1,329 @@
1
+ 'use client'
2
+
3
+ import { NodeSelection, TextSelection } from '@tiptap/pm/state'
4
+ import type { Editor } from '@tiptap/react'
5
+ import { useCallback, useEffect, useState } from 'react'
6
+
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+ // --- Lib ---
10
+ import {
11
+ findNodePosition,
12
+ getSelectedBlockNodes,
13
+ isNodeInSchema,
14
+ isNodeTypeSelected,
15
+ isValidPosition,
16
+ selectionWithinConvertibleTypes
17
+ } from '../../lib/tiptap-utils'
18
+ // --- Icons ---
19
+ import { ListIcon } from '../../tiptap-icons/list-icon'
20
+ import { ListOrderedIcon } from '../../tiptap-icons/list-ordered-icon'
21
+ import { ListTodoIcon } from '../../tiptap-icons/list-todo-icon'
22
+
23
+ export type ListType = 'bulletList' | 'orderedList' | 'taskList'
24
+
25
+ /**
26
+ * Configuration for the list functionality
27
+ */
28
+ export interface UseListConfig {
29
+ /**
30
+ * The Tiptap editor instance.
31
+ */
32
+ editor?: Editor | null
33
+ /**
34
+ * The type of list to toggle.
35
+ */
36
+ type: ListType
37
+ /**
38
+ * Whether the button should hide when list is not available.
39
+ * @default false
40
+ */
41
+ hideWhenUnavailable?: boolean
42
+ /**
43
+ * Callback function called after a successful toggle.
44
+ */
45
+ onToggled?: () => void
46
+ }
47
+
48
+ export const listIcons = {
49
+ bulletList: ListIcon,
50
+ orderedList: ListOrderedIcon,
51
+ taskList: ListTodoIcon
52
+ }
53
+
54
+ export const listLabels: Record<ListType, string> = {
55
+ bulletList: 'Bullet List',
56
+ orderedList: 'Ordered List',
57
+ taskList: 'Task List'
58
+ }
59
+
60
+ export const LIST_SHORTCUT_KEYS: Record<ListType, string> = {
61
+ bulletList: 'mod+shift+8',
62
+ orderedList: 'mod+shift+7',
63
+ taskList: 'mod+shift+9'
64
+ }
65
+
66
+ /**
67
+ * Checks if a list can be toggled in the current editor state
68
+ */
69
+ export function canToggleList(
70
+ editor: Editor | null,
71
+ type: ListType,
72
+ turnInto: boolean = true
73
+ ): boolean {
74
+ if (!editor || !editor.isEditable) return false
75
+ if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ['image'])) return false
76
+
77
+ if (!turnInto) {
78
+ switch (type) {
79
+ case 'bulletList':
80
+ return editor.can().toggleBulletList()
81
+ case 'orderedList':
82
+ return editor.can().toggleOrderedList()
83
+ case 'taskList':
84
+ return editor.can().toggleList('taskList', 'taskItem')
85
+ default:
86
+ return false
87
+ }
88
+ }
89
+
90
+ // Ensure selection is in nodes we're allowed to convert
91
+ if (
92
+ !selectionWithinConvertibleTypes(editor, [
93
+ 'paragraph',
94
+ 'heading',
95
+ 'bulletList',
96
+ 'orderedList',
97
+ 'taskList',
98
+ 'blockquote',
99
+ 'codeBlock'
100
+ ])
101
+ )
102
+ return false
103
+
104
+ // Either we can set list directly on the selection,
105
+ // or we can clear formatting/nodes to arrive at a list.
106
+ switch (type) {
107
+ case 'bulletList':
108
+ return editor.can().toggleBulletList() || editor.can().clearNodes()
109
+ case 'orderedList':
110
+ return editor.can().toggleOrderedList() || editor.can().clearNodes()
111
+ case 'taskList':
112
+ return editor.can().toggleList('taskList', 'taskItem') || editor.can().clearNodes()
113
+ default:
114
+ return false
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Checks if list is currently active
120
+ */
121
+ export function isListActive(editor: Editor | null, type: ListType): boolean {
122
+ if (!editor || !editor.isEditable) return false
123
+
124
+ switch (type) {
125
+ case 'bulletList':
126
+ return editor.isActive('bulletList')
127
+ case 'orderedList':
128
+ return editor.isActive('orderedList')
129
+ case 'taskList':
130
+ return editor.isActive('taskList')
131
+ default:
132
+ return false
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Toggles list in the editor
138
+ */
139
+ export function toggleList(editor: Editor | null, type: ListType): boolean {
140
+ if (!editor || !editor.isEditable) return false
141
+ if (!canToggleList(editor, type)) return false
142
+
143
+ try {
144
+ const view = editor.view
145
+ let state = view.state
146
+ let tr = state.tr
147
+
148
+ const blocks = getSelectedBlockNodes(editor)
149
+
150
+ // In case a selection contains multiple blocks, we only allow
151
+ // toggling to nide if there's exactly one block selected
152
+ // we also dont block the canToggle since it will fall back to the bottom logic
153
+ const isPossibleToTurnInto =
154
+ selectionWithinConvertibleTypes(editor, [
155
+ 'paragraph',
156
+ 'heading',
157
+ 'bulletList',
158
+ 'orderedList',
159
+ 'taskList',
160
+ 'blockquote',
161
+ 'codeBlock'
162
+ ]) && blocks.length === 1
163
+
164
+ // No selection, find the the cursor position
165
+ if (
166
+ (state.selection.empty || state.selection instanceof TextSelection) &&
167
+ isPossibleToTurnInto
168
+ ) {
169
+ const pos = findNodePosition({
170
+ editor,
171
+ node: state.selection.$anchor.node(1)
172
+ })?.pos
173
+ if (!isValidPosition(pos)) return false
174
+
175
+ tr = tr.setSelection(NodeSelection.create(state.doc, pos))
176
+ view.dispatch(tr)
177
+ state = view.state
178
+ }
179
+
180
+ const selection = state.selection
181
+
182
+ let chain = editor.chain().focus()
183
+
184
+ // Handle NodeSelection
185
+ if (selection instanceof NodeSelection) {
186
+ const firstChild = selection.node.firstChild?.firstChild
187
+ const lastChild = selection.node.lastChild?.lastChild
188
+
189
+ const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
190
+
191
+ const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
192
+
193
+ const resolvedFrom = state.doc.resolve(from)
194
+ const resolvedTo = state.doc.resolve(to)
195
+
196
+ chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
197
+ }
198
+
199
+ if (editor.isActive(type)) {
200
+ // Unwrap list
201
+ chain.liftListItem('listItem').lift('bulletList').lift('orderedList').lift('taskList').run()
202
+ } else {
203
+ // Wrap in specific list type
204
+ const toggleMap: Record<ListType, () => typeof chain> = {
205
+ bulletList: () => chain.toggleBulletList(),
206
+ orderedList: () => chain.toggleOrderedList(),
207
+ taskList: () => chain.toggleList('taskList', 'taskItem')
208
+ }
209
+
210
+ const toggle = toggleMap[type]
211
+ if (!toggle) return false
212
+
213
+ toggle().run()
214
+ }
215
+
216
+ editor.chain().focus().selectTextblockEnd().run()
217
+
218
+ return true
219
+ } catch {
220
+ return false
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Determines if the list button should be shown
226
+ */
227
+ export function shouldShowButton(props: {
228
+ editor: Editor | null
229
+ type: ListType
230
+ hideWhenUnavailable: boolean
231
+ }): boolean {
232
+ const { editor, type, hideWhenUnavailable } = props
233
+
234
+ if (!editor || !editor.isEditable) return false
235
+
236
+ if (!hideWhenUnavailable) {
237
+ return true
238
+ }
239
+
240
+ if (!isNodeInSchema(type, editor)) return false
241
+
242
+ if (!editor.isActive('code')) {
243
+ return canToggleList(editor, type)
244
+ }
245
+
246
+ return true
247
+ }
248
+
249
+ /**
250
+ * Custom hook that provides list functionality for Tiptap editor
251
+ *
252
+ * @example
253
+ * ```tsx
254
+ * // Simple usage
255
+ * function MySimpleListButton() {
256
+ * const { isVisible, handleToggle, isActive } = useList({ type: "bulletList" })
257
+ *
258
+ * if (!isVisible) return null
259
+ *
260
+ * return <button onClick={handleToggle}>Bullet List</button>
261
+ * }
262
+ *
263
+ * // Advanced usage with configuration
264
+ * function MyAdvancedListButton() {
265
+ * const { isVisible, handleToggle, label, isActive } = useList({
266
+ * type: "orderedList",
267
+ * editor: myEditor,
268
+ * hideWhenUnavailable: true,
269
+ * onToggled: () => console.log('List toggled!')
270
+ * })
271
+ *
272
+ * if (!isVisible) return null
273
+ *
274
+ * return (
275
+ * <MyButton
276
+ * onClick={handleToggle}
277
+ * aria-label={label}
278
+ * aria-pressed={isActive}
279
+ * >
280
+ * Toggle List
281
+ * </MyButton>
282
+ * )
283
+ * }
284
+ * ```
285
+ */
286
+ export function useList(config: UseListConfig) {
287
+ const { editor: providedEditor, type, hideWhenUnavailable = false, onToggled } = config
288
+
289
+ const { editor } = useTiptapEditor(providedEditor)
290
+ const [isVisible, setIsVisible] = useState<boolean>(true)
291
+ const canToggle = canToggleList(editor, type)
292
+ const isActive = isListActive(editor, type)
293
+
294
+ useEffect(() => {
295
+ if (!editor) return
296
+
297
+ const handleSelectionUpdate = () => {
298
+ setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
299
+ }
300
+
301
+ handleSelectionUpdate()
302
+
303
+ editor.on('selectionUpdate', handleSelectionUpdate)
304
+
305
+ return () => {
306
+ editor.off('selectionUpdate', handleSelectionUpdate)
307
+ }
308
+ }, [editor, type, hideWhenUnavailable])
309
+
310
+ const handleToggle = useCallback(() => {
311
+ if (!editor) return false
312
+
313
+ const success = toggleList(editor, type)
314
+ if (success) {
315
+ onToggled?.()
316
+ }
317
+ return success
318
+ }, [editor, type, onToggled])
319
+
320
+ return {
321
+ isVisible,
322
+ isActive,
323
+ handleToggle,
324
+ canToggle,
325
+ label: listLabels[type],
326
+ shortcutKeys: LIST_SHORTCUT_KEYS[type],
327
+ Icon: listIcons[type]
328
+ }
329
+ }