@betterstart/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -354
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -260
  6. package/dist/index.js +4 -11373
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-G4KI4DVB.js +0 -179
  215. package/dist/chunk-G4KI4DVB.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,114 @@
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
+ // --- Tiptap UI ---
13
+ import type { UseImageUploadConfig } from '.'
14
+ import { IMAGE_UPLOAD_SHORTCUT_KEY, useImageUpload } from '.'
15
+
16
+ type IconProps = React.SVGProps<SVGSVGElement>
17
+ type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
18
+
19
+ export interface ImageUploadButtonProps extends Omit<ButtonProps, 'type'>, UseImageUploadConfig {
20
+ /**
21
+ * Optional text to display alongside the icon.
22
+ */
23
+ text?: string
24
+ /**
25
+ * Optional show shortcut keys in the button.
26
+ * @default false
27
+ */
28
+ showShortcut?: boolean
29
+ /**
30
+ * Optional custom icon component to render instead of the default.
31
+ */
32
+ icon?: React.MemoExoticComponent<IconComponent> | React.FC<IconProps>
33
+ }
34
+
35
+ export function ImageShortcutBadge({
36
+ shortcutKeys = IMAGE_UPLOAD_SHORTCUT_KEY
37
+ }: {
38
+ shortcutKeys?: string
39
+ }) {
40
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
41
+ }
42
+
43
+ /**
44
+ * Button component for uploading/inserting images in a Tiptap editor.
45
+ *
46
+ * For custom button implementations, use the `useImage` hook instead.
47
+ */
48
+ export const ImageUploadButton = forwardRef<HTMLButtonElement, ImageUploadButtonProps>(
49
+ (
50
+ {
51
+ editor: providedEditor,
52
+ text,
53
+ hideWhenUnavailable = false,
54
+ onInserted,
55
+ showShortcut = false,
56
+ onClick,
57
+ icon: CustomIcon,
58
+ children,
59
+ ...buttonProps
60
+ },
61
+ ref
62
+ ) => {
63
+ const { editor } = useTiptapEditor(providedEditor)
64
+ const { isVisible, canInsert, handleImage, label, isActive, shortcutKeys, Icon } =
65
+ useImageUpload({
66
+ editor,
67
+ hideWhenUnavailable,
68
+ onInserted
69
+ })
70
+
71
+ const handleClick = useCallback(
72
+ (event: React.MouseEvent<HTMLButtonElement>) => {
73
+ onClick?.(event)
74
+ if (event.defaultPrevented) return
75
+ handleImage()
76
+ },
77
+ [handleImage, onClick]
78
+ )
79
+
80
+ if (!isVisible) {
81
+ return null
82
+ }
83
+
84
+ const RenderIcon = CustomIcon ?? Icon
85
+
86
+ return (
87
+ <Button
88
+ type="button"
89
+ variant="ghost"
90
+ data-active-state={isActive ? 'on' : 'off'}
91
+ role="button"
92
+ tabIndex={-1}
93
+ disabled={!canInsert}
94
+ data-disabled={!canInsert}
95
+ aria-label={label}
96
+ aria-pressed={isActive}
97
+ tooltip={label}
98
+ onClick={handleClick}
99
+ {...buttonProps}
100
+ ref={ref}
101
+ >
102
+ {children ?? (
103
+ <>
104
+ <RenderIcon className="tiptap-button-icon" />
105
+ {text && <span className="tiptap-button-text">{text}</span>}
106
+ {showShortcut && <ImageShortcutBadge shortcutKeys={shortcutKeys} />}
107
+ </>
108
+ )}
109
+ </Button>
110
+ )
111
+ }
112
+ )
113
+
114
+ ImageUploadButton.displayName = 'ImageUploadButton'
@@ -0,0 +1,2 @@
1
+ export * from './image-upload-button'
2
+ export * from './use-image-upload'
@@ -0,0 +1,192 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+ import { useHotkeys } from 'react-hotkeys-hook'
6
+ import { useIsBreakpoint } from '../../hooks/use-is-breakpoint'
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+
10
+ // --- Lib ---
11
+ import { isExtensionAvailable } from '../../lib/tiptap-utils'
12
+
13
+ // --- Icons ---
14
+ import { ImagePlusIcon } from '../../tiptap-icons/image-plus-icon'
15
+
16
+ export const IMAGE_UPLOAD_SHORTCUT_KEY = 'mod+shift+i'
17
+
18
+ /**
19
+ * Configuration for the image upload functionality
20
+ */
21
+ export interface UseImageUploadConfig {
22
+ /**
23
+ * The Tiptap editor instance.
24
+ */
25
+ editor?: Editor | null
26
+ /**
27
+ * Whether the button should hide when insertion is not available.
28
+ * @default false
29
+ */
30
+ hideWhenUnavailable?: boolean
31
+ /**
32
+ * Callback function called after a successful image insertion.
33
+ */
34
+ onInserted?: () => void
35
+ }
36
+
37
+ /**
38
+ * Checks if image can be inserted in the current editor state
39
+ */
40
+ export function canInsertImage(editor: Editor | null): boolean {
41
+ if (!editor || !editor.isEditable) return false
42
+ if (!isExtensionAvailable(editor, 'imageUpload')) return false
43
+
44
+ return editor.can().insertContent({ type: 'imageUpload' })
45
+ }
46
+
47
+ /**
48
+ * Checks if image is currently active
49
+ */
50
+ export function isImageActive(editor: Editor | null): boolean {
51
+ if (!editor || !editor.isEditable) return false
52
+ return editor.isActive('imageUpload')
53
+ }
54
+
55
+ /**
56
+ * Inserts an image in the editor
57
+ */
58
+ export function insertImage(editor: Editor | null): boolean {
59
+ if (!editor || !editor.isEditable) return false
60
+ if (!canInsertImage(editor)) return false
61
+
62
+ try {
63
+ return editor
64
+ .chain()
65
+ .focus()
66
+ .insertContent({
67
+ type: 'imageUpload'
68
+ })
69
+ .run()
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Determines if the image button should be shown
77
+ */
78
+ export function shouldShowButton(props: {
79
+ editor: Editor | null
80
+ hideWhenUnavailable: boolean
81
+ }): boolean {
82
+ const { editor, hideWhenUnavailable } = props
83
+
84
+ if (!editor || !editor.isEditable) return false
85
+
86
+ if (!hideWhenUnavailable) {
87
+ return true
88
+ }
89
+
90
+ if (!isExtensionAvailable(editor, 'imageUpload')) return false
91
+
92
+ if (!editor.isActive('code')) {
93
+ return canInsertImage(editor)
94
+ }
95
+
96
+ return true
97
+ }
98
+
99
+ /**
100
+ * Custom hook that provides image functionality for Tiptap editor
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * // Simple usage - no params needed
105
+ * function MySimpleImageButton() {
106
+ * const { isVisible, handleImage } = useImage()
107
+ *
108
+ * if (!isVisible) return null
109
+ *
110
+ * return <button onClick={handleImage}>Add Image</button>
111
+ * }
112
+ *
113
+ * // Advanced usage with configuration
114
+ * function MyAdvancedImageButton() {
115
+ * const { isVisible, handleImage, label, isActive } = useImage({
116
+ * editor: myEditor,
117
+ * hideWhenUnavailable: true,
118
+ * onInserted: () => console.log('Image inserted!')
119
+ * })
120
+ *
121
+ * if (!isVisible) return null
122
+ *
123
+ * return (
124
+ * <MyButton
125
+ * onClick={handleImage}
126
+ * aria-pressed={isActive}
127
+ * aria-label={label}
128
+ * >
129
+ * Add Image
130
+ * </MyButton>
131
+ * )
132
+ * }
133
+ * ```
134
+ */
135
+ export function useImageUpload(config?: UseImageUploadConfig) {
136
+ const { editor: providedEditor, hideWhenUnavailable = false, onInserted } = config || {}
137
+
138
+ const { editor } = useTiptapEditor(providedEditor)
139
+ const isMobile = useIsBreakpoint()
140
+ const [isVisible, setIsVisible] = useState<boolean>(true)
141
+ const canInsert = canInsertImage(editor)
142
+ const isActive = isImageActive(editor)
143
+
144
+ useEffect(() => {
145
+ if (!editor) return
146
+
147
+ const handleSelectionUpdate = () => {
148
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
149
+ }
150
+
151
+ handleSelectionUpdate()
152
+
153
+ editor.on('selectionUpdate', handleSelectionUpdate)
154
+
155
+ return () => {
156
+ editor.off('selectionUpdate', handleSelectionUpdate)
157
+ }
158
+ }, [editor, hideWhenUnavailable])
159
+
160
+ const handleImage = useCallback(() => {
161
+ if (!editor) return false
162
+
163
+ const success = insertImage(editor)
164
+ if (success) {
165
+ onInserted?.()
166
+ }
167
+ return success
168
+ }, [editor, onInserted])
169
+
170
+ useHotkeys(
171
+ IMAGE_UPLOAD_SHORTCUT_KEY,
172
+ (event) => {
173
+ event.preventDefault()
174
+ handleImage()
175
+ },
176
+ {
177
+ enabled: isVisible && canInsert,
178
+ enableOnContentEditable: !isMobile,
179
+ enableOnFormTags: true
180
+ }
181
+ )
182
+
183
+ return {
184
+ isVisible,
185
+ isActive,
186
+ handleImage,
187
+ canInsert,
188
+ label: 'Add image',
189
+ shortcutKeys: IMAGE_UPLOAD_SHORTCUT_KEY,
190
+ Icon: ImagePlusIcon
191
+ }
192
+ }
@@ -0,0 +1,2 @@
1
+ export * from './link-popover'
2
+ export * from './use-link-popover'
@@ -0,0 +1,285 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { forwardRef, useCallback, useEffect, useState } from 'react'
5
+
6
+ // --- Hooks ---
7
+ import { useIsBreakpoint } from '../../hooks/use-is-breakpoint'
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+
10
+ // --- Icons ---
11
+ import { CornerDownLeftIcon } from '../../tiptap-icons/corner-down-left-icon'
12
+ import { ExternalLinkIcon } from '../../tiptap-icons/external-link-icon'
13
+ import { LinkIcon } from '../../tiptap-icons/link-icon'
14
+ import { TrashIcon } from '../../tiptap-icons/trash-icon'
15
+ // --- UI Primitives ---
16
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
17
+ import { Button, ButtonGroup } from '../../tiptap-ui-primitive/button'
18
+ import { Card, CardBody, CardItemGroup } from '../../tiptap-ui-primitive/card'
19
+ import { Input, InputGroup } from '../../tiptap-ui-primitive/input'
20
+ import { Popover, PopoverContent, PopoverTrigger } from '../../tiptap-ui-primitive/popover'
21
+ import { Separator } from '../../tiptap-ui-primitive/separator'
22
+ // --- Tiptap UI ---
23
+ import type { UseLinkPopoverConfig } from '.'
24
+ import { useLinkPopover } from '.'
25
+
26
+ export interface LinkMainProps {
27
+ /**
28
+ * The URL to set for the link.
29
+ */
30
+ url: string
31
+ /**
32
+ * Function to update the URL state.
33
+ */
34
+ setUrl: React.Dispatch<React.SetStateAction<string | null>>
35
+ /**
36
+ * Function to set the link in the editor.
37
+ */
38
+ setLink: () => void
39
+ /**
40
+ * Function to remove the link from the editor.
41
+ */
42
+ removeLink: () => void
43
+ /**
44
+ * Function to open the link.
45
+ */
46
+ openLink: () => void
47
+ /**
48
+ * Whether the link is currently active in the editor.
49
+ */
50
+ isActive: boolean
51
+ }
52
+
53
+ export interface LinkPopoverProps extends Omit<ButtonProps, 'type'>, UseLinkPopoverConfig {
54
+ /**
55
+ * Callback for when the popover opens or closes.
56
+ */
57
+ onOpenChange?: (isOpen: boolean) => void
58
+ /**
59
+ * Whether to automatically open the popover when a link is active.
60
+ * @default true
61
+ */
62
+ autoOpenOnLinkActive?: boolean
63
+ }
64
+
65
+ /**
66
+ * Link button component for triggering the link popover
67
+ */
68
+ export const LinkButton = forwardRef<HTMLButtonElement, ButtonProps>(
69
+ ({ className, children, ...props }, ref) => {
70
+ return (
71
+ <Button
72
+ type="button"
73
+ className={className}
74
+ variant="ghost"
75
+ role="button"
76
+ tabIndex={-1}
77
+ aria-label="Link"
78
+ tooltip="Link"
79
+ ref={ref}
80
+ {...props}
81
+ >
82
+ {children || <LinkIcon className="tiptap-button-icon" />}
83
+ </Button>
84
+ )
85
+ }
86
+ )
87
+
88
+ LinkButton.displayName = 'LinkButton'
89
+
90
+ /**
91
+ * Main content component for the link popover
92
+ */
93
+ const LinkMain: React.FC<LinkMainProps> = ({
94
+ url,
95
+ setUrl,
96
+ setLink,
97
+ removeLink,
98
+ openLink,
99
+ isActive
100
+ }) => {
101
+ const isMobile = useIsBreakpoint()
102
+
103
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
104
+ if (event.key === 'Enter') {
105
+ event.preventDefault()
106
+ setLink()
107
+ }
108
+ }
109
+
110
+ return (
111
+ <Card
112
+ style={{
113
+ ...(isMobile ? { boxShadow: 'none', border: 0 } : {})
114
+ }}
115
+ >
116
+ <CardBody
117
+ style={{
118
+ ...(isMobile ? { padding: 0 } : {})
119
+ }}
120
+ >
121
+ <CardItemGroup orientation="horizontal">
122
+ <InputGroup>
123
+ <Input
124
+ type="url"
125
+ placeholder="Paste a link..."
126
+ value={url}
127
+ onChange={(e) => setUrl(e.target.value)}
128
+ onKeyDown={handleKeyDown}
129
+ autoFocus
130
+ autoComplete="off"
131
+ autoCorrect="off"
132
+ autoCapitalize="off"
133
+ />
134
+ </InputGroup>
135
+
136
+ <ButtonGroup orientation="horizontal">
137
+ <Button
138
+ type="button"
139
+ onClick={setLink}
140
+ title="Apply link"
141
+ disabled={!url && !isActive}
142
+ variant="ghost"
143
+ >
144
+ <CornerDownLeftIcon className="tiptap-button-icon" />
145
+ </Button>
146
+ </ButtonGroup>
147
+
148
+ <Separator />
149
+
150
+ <ButtonGroup orientation="horizontal">
151
+ <Button
152
+ type="button"
153
+ onClick={openLink}
154
+ title="Open in new window"
155
+ disabled={!url && !isActive}
156
+ variant="ghost"
157
+ >
158
+ <ExternalLinkIcon className="tiptap-button-icon" />
159
+ </Button>
160
+
161
+ <Button
162
+ type="button"
163
+ onClick={removeLink}
164
+ title="Remove link"
165
+ disabled={!url && !isActive}
166
+ variant="ghost"
167
+ >
168
+ <TrashIcon className="tiptap-button-icon" />
169
+ </Button>
170
+ </ButtonGroup>
171
+ </CardItemGroup>
172
+ </CardBody>
173
+ </Card>
174
+ )
175
+ }
176
+
177
+ /**
178
+ * Link content component for standalone use
179
+ */
180
+ export const LinkContent: React.FC<{
181
+ editor?: Editor | null
182
+ }> = ({ editor }) => {
183
+ const linkPopover = useLinkPopover({
184
+ editor
185
+ })
186
+
187
+ return <LinkMain {...linkPopover} />
188
+ }
189
+
190
+ /**
191
+ * Link popover component for Tiptap editors.
192
+ *
193
+ * For custom popover implementations, use the `useLinkPopover` hook instead.
194
+ */
195
+ export const LinkPopover = forwardRef<HTMLButtonElement, LinkPopoverProps>(
196
+ (
197
+ {
198
+ editor: providedEditor,
199
+ hideWhenUnavailable = false,
200
+ onSetLink,
201
+ onOpenChange,
202
+ autoOpenOnLinkActive = true,
203
+ onClick,
204
+ children,
205
+ ...buttonProps
206
+ },
207
+ ref
208
+ ) => {
209
+ const { editor } = useTiptapEditor(providedEditor)
210
+ const [isOpen, setIsOpen] = useState(false)
211
+
212
+ const { isVisible, canSet, isActive, url, setUrl, setLink, removeLink, openLink, label, Icon } =
213
+ useLinkPopover({
214
+ editor,
215
+ hideWhenUnavailable,
216
+ onSetLink
217
+ })
218
+
219
+ const handleOnOpenChange = useCallback(
220
+ (nextIsOpen: boolean) => {
221
+ setIsOpen(nextIsOpen)
222
+ onOpenChange?.(nextIsOpen)
223
+ },
224
+ [onOpenChange]
225
+ )
226
+
227
+ const handleSetLink = useCallback(() => {
228
+ setLink()
229
+ setIsOpen(false)
230
+ }, [setLink])
231
+
232
+ const handleClick = useCallback(
233
+ (event: React.MouseEvent<HTMLButtonElement>) => {
234
+ onClick?.(event)
235
+ if (event.defaultPrevented) return
236
+ setIsOpen(!isOpen)
237
+ },
238
+ [onClick, isOpen]
239
+ )
240
+
241
+ useEffect(() => {
242
+ if (autoOpenOnLinkActive && isActive) {
243
+ setIsOpen(true)
244
+ }
245
+ }, [autoOpenOnLinkActive, isActive])
246
+
247
+ if (!isVisible) {
248
+ return null
249
+ }
250
+
251
+ return (
252
+ <Popover open={isOpen} onOpenChange={handleOnOpenChange}>
253
+ <PopoverTrigger asChild>
254
+ <LinkButton
255
+ disabled={!canSet}
256
+ data-active-state={isActive ? 'on' : 'off'}
257
+ data-disabled={!canSet}
258
+ aria-label={label}
259
+ aria-pressed={isActive}
260
+ onClick={handleClick}
261
+ {...buttonProps}
262
+ ref={ref}
263
+ >
264
+ {children ?? <Icon className="tiptap-button-icon" />}
265
+ </LinkButton>
266
+ </PopoverTrigger>
267
+
268
+ <PopoverContent>
269
+ <LinkMain
270
+ url={url}
271
+ setUrl={setUrl}
272
+ setLink={handleSetLink}
273
+ removeLink={removeLink}
274
+ openLink={openLink}
275
+ isActive={isActive}
276
+ />
277
+ </PopoverContent>
278
+ </Popover>
279
+ )
280
+ }
281
+ )
282
+
283
+ LinkPopover.displayName = 'LinkPopover'
284
+
285
+ export default LinkPopover