create-starlight-cms 0.9.0

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/THIRD_PARTY_NOTICES.md +4 -0
  4. package/bin/index.js +147 -0
  5. package/package.json +46 -0
  6. package/template/.nvmrc +1 -0
  7. package/template/LICENSE +21 -0
  8. package/template/README.md +68 -0
  9. package/template/THIRD_PARTY_NOTICES.md +12 -0
  10. package/template/_gitignore +14 -0
  11. package/template/astro.config.mjs +26 -0
  12. package/template/migrations/0001_schema.sql +73 -0
  13. package/template/migrations/0002_publish_delivery.sql +16 -0
  14. package/template/package-lock.json.template +9123 -0
  15. package/template/package.json +89 -0
  16. package/template/scripts/build-admin.mjs +38 -0
  17. package/template/scripts/check-linux-bindings.mjs +18 -0
  18. package/template/scripts/dev.mjs +94 -0
  19. package/template/src/admin/admin.scss +182 -0
  20. package/template/src/admin/app.ts +130 -0
  21. package/template/src/admin/client.tsx +684 -0
  22. package/template/src/admin/components/tiptap-extension/node-background-extension.ts +150 -0
  23. package/template/src/admin/components/tiptap-icons/align-center-icon.tsx +38 -0
  24. package/template/src/admin/components/tiptap-icons/align-justify-icon.tsx +38 -0
  25. package/template/src/admin/components/tiptap-icons/align-left-icon.tsx +38 -0
  26. package/template/src/admin/components/tiptap-icons/align-right-icon.tsx +38 -0
  27. package/template/src/admin/components/tiptap-icons/arrow-left-icon.tsx +24 -0
  28. package/template/src/admin/components/tiptap-icons/arrow-right-icon.tsx +26 -0
  29. package/template/src/admin/components/tiptap-icons/ban-icon.tsx +26 -0
  30. package/template/src/admin/components/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/template/src/admin/components/tiptap-icons/bold-icon.tsx +26 -0
  32. package/template/src/admin/components/tiptap-icons/case-sensitive-icon.tsx +32 -0
  33. package/template/src/admin/components/tiptap-icons/check-icon.tsx +26 -0
  34. package/template/src/admin/components/tiptap-icons/chevron-down-icon.tsx +26 -0
  35. package/template/src/admin/components/tiptap-icons/chevron-up-icon.tsx +22 -0
  36. package/template/src/admin/components/tiptap-icons/close-icon.tsx +24 -0
  37. package/template/src/admin/components/tiptap-icons/code-block-icon.tsx +38 -0
  38. package/template/src/admin/components/tiptap-icons/code2-icon.tsx +32 -0
  39. package/template/src/admin/components/tiptap-icons/corner-down-left-icon.tsx +26 -0
  40. package/template/src/admin/components/tiptap-icons/external-link-icon.tsx +28 -0
  41. package/template/src/admin/components/tiptap-icons/heading-five-icon.tsx +28 -0
  42. package/template/src/admin/components/tiptap-icons/heading-four-icon.tsx +28 -0
  43. package/template/src/admin/components/tiptap-icons/heading-icon.tsx +24 -0
  44. package/template/src/admin/components/tiptap-icons/heading-one-icon.tsx +28 -0
  45. package/template/src/admin/components/tiptap-icons/heading-six-icon.tsx +30 -0
  46. package/template/src/admin/components/tiptap-icons/heading-three-icon.tsx +36 -0
  47. package/template/src/admin/components/tiptap-icons/heading-two-icon.tsx +28 -0
  48. package/template/src/admin/components/tiptap-icons/highlighter-icon.tsx +26 -0
  49. package/template/src/admin/components/tiptap-icons/image-plus-icon.tsx +26 -0
  50. package/template/src/admin/components/tiptap-icons/italic-icon.tsx +24 -0
  51. package/template/src/admin/components/tiptap-icons/link-icon.tsx +28 -0
  52. package/template/src/admin/components/tiptap-icons/list-icon.tsx +56 -0
  53. package/template/src/admin/components/tiptap-icons/list-ordered-icon.tsx +56 -0
  54. package/template/src/admin/components/tiptap-icons/list-todo-icon.tsx +50 -0
  55. package/template/src/admin/components/tiptap-icons/moon-star-icon.tsx +30 -0
  56. package/template/src/admin/components/tiptap-icons/redo2-icon.tsx +26 -0
  57. package/template/src/admin/components/tiptap-icons/search-icon.tsx +26 -0
  58. package/template/src/admin/components/tiptap-icons/strike-icon.tsx +28 -0
  59. package/template/src/admin/components/tiptap-icons/subscript-icon.tsx +38 -0
  60. package/template/src/admin/components/tiptap-icons/sun-icon.tsx +58 -0
  61. package/template/src/admin/components/tiptap-icons/superscript-icon.tsx +38 -0
  62. package/template/src/admin/components/tiptap-icons/trash-icon.tsx +26 -0
  63. package/template/src/admin/components/tiptap-icons/underline-icon.tsx +26 -0
  64. package/template/src/admin/components/tiptap-icons/undo2-icon.tsx +26 -0
  65. package/template/src/admin/components/tiptap-icons/whole-word-icon.tsx +36 -0
  66. package/template/src/admin/components/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  67. package/template/src/admin/components/tiptap-node/code-block-node/code-block-node.scss +54 -0
  68. package/template/src/admin/components/tiptap-node/heading-node/heading-node.scss +45 -0
  69. package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +14 -0
  70. package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  71. package/template/src/admin/components/tiptap-node/image-node/image-node.scss +35 -0
  72. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +161 -0
  73. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  74. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.tsx +554 -0
  75. package/template/src/admin/components/tiptap-node/image-upload-node/index.tsx +1 -0
  76. package/template/src/admin/components/tiptap-node/list-node/list-node.scss +208 -0
  77. package/template/src/admin/components/tiptap-node/paragraph-node/paragraph-node.scss +217 -0
  78. package/template/src/admin/components/tiptap-templates/simple/NOTICE.md +14 -0
  79. package/template/src/admin/components/tiptap-templates/simple/data/content.json +477 -0
  80. package/template/src/admin/components/tiptap-templates/simple/simple-editor.scss +97 -0
  81. package/template/src/admin/components/tiptap-templates/simple/simple-editor.tsx +348 -0
  82. package/template/src/admin/components/tiptap-templates/simple/theme-toggle.tsx +44 -0
  83. package/template/src/admin/components/tiptap-ui/blockquote-button/blockquote-button.tsx +122 -0
  84. package/template/src/admin/components/tiptap-ui/blockquote-button/index.tsx +2 -0
  85. package/template/src/admin/components/tiptap-ui/blockquote-button/use-blockquote.ts +273 -0
  86. package/template/src/admin/components/tiptap-ui/code-block-button/code-block-button.tsx +122 -0
  87. package/template/src/admin/components/tiptap-ui/code-block-button/index.tsx +2 -0
  88. package/template/src/admin/components/tiptap-ui/code-block-button/use-code-block.ts +283 -0
  89. package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  90. package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +170 -0
  91. package/template/src/admin/components/tiptap-ui/color-highlight-button/index.tsx +2 -0
  92. package/template/src/admin/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +380 -0
  93. package/template/src/admin/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +231 -0
  94. package/template/src/admin/components/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  95. package/template/src/admin/components/tiptap-ui/heading-button/heading-button.tsx +124 -0
  96. package/template/src/admin/components/tiptap-ui/heading-button/index.tsx +2 -0
  97. package/template/src/admin/components/tiptap-ui/heading-button/use-heading.ts +348 -0
  98. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +128 -0
  99. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  100. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +132 -0
  101. package/template/src/admin/components/tiptap-ui/image-upload-button/image-upload-button.tsx +130 -0
  102. package/template/src/admin/components/tiptap-ui/image-upload-button/index.tsx +2 -0
  103. package/template/src/admin/components/tiptap-ui/image-upload-button/use-image-upload.ts +197 -0
  104. package/template/src/admin/components/tiptap-ui/link-popover/index.tsx +2 -0
  105. package/template/src/admin/components/tiptap-ui/link-popover/link-popover.scss +16 -0
  106. package/template/src/admin/components/tiptap-ui/link-popover/link-popover.tsx +315 -0
  107. package/template/src/admin/components/tiptap-ui/link-popover/use-link-popover.ts +297 -0
  108. package/template/src/admin/components/tiptap-ui/list-button/index.tsx +2 -0
  109. package/template/src/admin/components/tiptap-ui/list-button/list-button.tsx +120 -0
  110. package/template/src/admin/components/tiptap-ui/list-button/use-list.ts +353 -0
  111. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/index.tsx +2 -0
  112. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +126 -0
  113. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +220 -0
  114. package/template/src/admin/components/tiptap-ui/mark-button/index.tsx +2 -0
  115. package/template/src/admin/components/tiptap-ui/mark-button/mark-button.tsx +122 -0
  116. package/template/src/admin/components/tiptap-ui/mark-button/use-mark.ts +219 -0
  117. package/template/src/admin/components/tiptap-ui/search-and-replace/index.tsx +2 -0
  118. package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.scss +300 -0
  119. package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.tsx +718 -0
  120. package/template/src/admin/components/tiptap-ui/search-and-replace/use-search-and-replace.ts +528 -0
  121. package/template/src/admin/components/tiptap-ui/text-align-button/index.tsx +2 -0
  122. package/template/src/admin/components/tiptap-ui/text-align-button/text-align-button.tsx +144 -0
  123. package/template/src/admin/components/tiptap-ui/text-align-button/use-text-align.ts +237 -0
  124. package/template/src/admin/components/tiptap-ui/undo-redo-button/index.tsx +2 -0
  125. package/template/src/admin/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +125 -0
  126. package/template/src/admin/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +200 -0
  127. package/template/src/admin/components/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  128. package/template/src/admin/components/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  129. package/template/src/admin/components/tiptap-ui-primitive/badge/badge.scss +99 -0
  130. package/template/src/admin/components/tiptap-ui-primitive/badge/badge.tsx +44 -0
  131. package/template/src/admin/components/tiptap-ui-primitive/badge/index.tsx +1 -0
  132. package/template/src/admin/components/tiptap-ui-primitive/button/button-colors.scss +613 -0
  133. package/template/src/admin/components/tiptap-ui-primitive/button/button.scss +390 -0
  134. package/template/src/admin/components/tiptap-ui-primitive/button/button.tsx +158 -0
  135. package/template/src/admin/components/tiptap-ui-primitive/button/index.tsx +1 -0
  136. package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.scss +89 -0
  137. package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.tsx +72 -0
  138. package/template/src/admin/components/tiptap-ui-primitive/button-group/index.tsx +1 -0
  139. package/template/src/admin/components/tiptap-ui-primitive/card/card.scss +74 -0
  140. package/template/src/admin/components/tiptap-ui-primitive/card/card.tsx +79 -0
  141. package/template/src/admin/components/tiptap-ui-primitive/card/index.tsx +1 -0
  142. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +152 -0
  143. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +262 -0
  144. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  145. package/template/src/admin/components/tiptap-ui-primitive/input/index.tsx +1 -0
  146. package/template/src/admin/components/tiptap-ui-primitive/input/input.scss +41 -0
  147. package/template/src/admin/components/tiptap-ui-primitive/input/input.tsx +22 -0
  148. package/template/src/admin/components/tiptap-ui-primitive/input-group/index.tsx +1 -0
  149. package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.scss +225 -0
  150. package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.tsx +135 -0
  151. package/template/src/admin/components/tiptap-ui-primitive/popover/index.tsx +1 -0
  152. package/template/src/admin/components/tiptap-ui-primitive/popover/popover.scss +64 -0
  153. package/template/src/admin/components/tiptap-ui-primitive/popover/popover.tsx +35 -0
  154. package/template/src/admin/components/tiptap-ui-primitive/separator/index.tsx +1 -0
  155. package/template/src/admin/components/tiptap-ui-primitive/separator/separator.scss +23 -0
  156. package/template/src/admin/components/tiptap-ui-primitive/separator/separator.tsx +28 -0
  157. package/template/src/admin/components/tiptap-ui-primitive/spacer/index.tsx +1 -0
  158. package/template/src/admin/components/tiptap-ui-primitive/spacer/spacer.tsx +24 -0
  159. package/template/src/admin/components/tiptap-ui-primitive/switch/index.tsx +1 -0
  160. package/template/src/admin/components/tiptap-ui-primitive/switch/switch.scss +86 -0
  161. package/template/src/admin/components/tiptap-ui-primitive/switch/switch.tsx +29 -0
  162. package/template/src/admin/components/tiptap-ui-primitive/textarea/index.tsx +1 -0
  163. package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.scss +42 -0
  164. package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.tsx +16 -0
  165. package/template/src/admin/components/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  166. package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  167. package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.tsx +203 -0
  168. package/template/src/admin/components/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  169. package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  170. package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.tsx +241 -0
  171. package/template/src/admin/hooks/use-composed-ref.ts +46 -0
  172. package/template/src/admin/hooks/use-cursor-visibility.ts +69 -0
  173. package/template/src/admin/hooks/use-element-rect.ts +166 -0
  174. package/template/src/admin/hooks/use-is-breakpoint.ts +35 -0
  175. package/template/src/admin/hooks/use-menu-navigation.ts +204 -0
  176. package/template/src/admin/hooks/use-scrolling.ts +75 -0
  177. package/template/src/admin/hooks/use-throttled-callback.ts +48 -0
  178. package/template/src/admin/hooks/use-tiptap-editor.ts +69 -0
  179. package/template/src/admin/hooks/use-unmount.ts +21 -0
  180. package/template/src/admin/hooks/use-window-size.ts +93 -0
  181. package/template/src/admin/html.ts +17 -0
  182. package/template/src/admin/lib/tiptap-utils.ts +652 -0
  183. package/template/src/admin/navigation-tree.tsx +82 -0
  184. package/template/src/admin/preview.ts +73 -0
  185. package/template/src/admin/styles/_cms-theme.scss +58 -0
  186. package/template/src/admin/styles/_keyframe-animations.scss +91 -0
  187. package/template/src/admin/styles/_variables.scss +298 -0
  188. package/template/src/assets/favicon.svg +1 -0
  189. package/template/src/assets/logo.svg +1 -0
  190. package/template/src/astro-env.d.ts +1 -0
  191. package/template/src/content.config.ts +12 -0
  192. package/template/src/db/bootstrap.ts +36 -0
  193. package/template/src/db/client.ts +7 -0
  194. package/template/src/db/schema.ts +45 -0
  195. package/template/src/documents/service.ts +178 -0
  196. package/template/src/documents/validation.ts +9 -0
  197. package/template/src/env.ts +9 -0
  198. package/template/src/index.ts +10 -0
  199. package/template/src/locales.ts +10 -0
  200. package/template/src/media/service.ts +120 -0
  201. package/template/src/navigation/service.ts +171 -0
  202. package/template/src/pages/[locale]/cms-preview-shell.astro +12 -0
  203. package/template/src/pages/cms-preview-shell.astro +5 -0
  204. package/template/src/publish/deploy-hook.ts +42 -0
  205. package/template/src/publish/record.ts +20 -0
  206. package/template/src/publish/service.ts +135 -0
  207. package/template/src/scss.d.ts +5 -0
  208. package/template/src/site.config.ts +7 -0
  209. package/template/src/sql.d.ts +4 -0
  210. package/template/src/starlight/cms-origin.ts +4 -0
  211. package/template/src/starlight/loader.ts +21 -0
  212. package/template/src/starlight/preview-render.ts +162 -0
  213. package/template/src/starlight/preview-shell.astro +14 -0
  214. package/template/src/starlight/render.ts +59 -0
  215. package/template/src/starlight/schema.ts +34 -0
  216. package/template/src/starlight/sidebar.ts +35 -0
  217. package/template/src/starlight/snapshot.ts +47 -0
  218. package/template/src/starlight/source.ts +26 -0
  219. package/template/src/starlight/tiptap.ts +29 -0
  220. package/template/src/styles/starlight.css +12 -0
  221. package/template/test-files/content.test.template.ts +82 -0
  222. package/template/test-files/deploy-hook.test.template.ts +32 -0
  223. package/template/test-files/media.test.template.ts +39 -0
  224. package/template/test-files/publishing.integration.test.template.ts +156 -0
  225. package/template/tsconfig.admin.json +18 -0
  226. package/template/tsconfig.json +23 -0
  227. package/template/worker-configuration.d.ts +15389 -0
  228. package/template/wrangler.jsonc +26 -0
@@ -0,0 +1,128 @@
1
+ import { forwardRef, useCallback, useState } from "react"
2
+
3
+ // --- Icons ---
4
+ import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon"
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
8
+
9
+ // --- Tiptap UI ---
10
+ import { HeadingButton } from "@/components/tiptap-ui/heading-button"
11
+ import type { UseHeadingDropdownMenuConfig } from "@/components/tiptap-ui/heading-dropdown-menu"
12
+ import { useHeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu"
13
+
14
+ // --- UI Primitives ---
15
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
16
+ import { Button } from "@/components/tiptap-ui-primitive/button"
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuTrigger,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuGroup,
23
+ } from "@/components/tiptap-ui-primitive/dropdown-menu"
24
+
25
+ export interface HeadingDropdownMenuProps
26
+ extends Omit<ButtonProps, "type">, UseHeadingDropdownMenuConfig {
27
+ /**
28
+ * Callback for when the dropdown opens or closes
29
+ */
30
+ onOpenChange?: (isOpen: boolean) => void
31
+ /**
32
+ * Whether the dropdown should use a modal
33
+ */
34
+ modal?: boolean
35
+ }
36
+
37
+ /**
38
+ * Dropdown menu component for selecting heading levels in a Tiptap editor.
39
+ *
40
+ * For custom dropdown implementations, use the `useHeadingDropdownMenu` hook instead.
41
+ */
42
+ export const HeadingDropdownMenu = forwardRef<
43
+ HTMLButtonElement,
44
+ HeadingDropdownMenuProps
45
+ >(
46
+ (
47
+ {
48
+ editor: providedEditor,
49
+ levels = [1, 2, 3, 4, 5, 6],
50
+ hideWhenUnavailable = false,
51
+ onOpenChange,
52
+ children,
53
+ modal = true,
54
+ ...buttonProps
55
+ },
56
+ ref
57
+ ) => {
58
+ const { editor } = useTiptapEditor(providedEditor)
59
+ const [isOpen, setIsOpen] = useState<boolean>(false)
60
+ const { isVisible, isActive, canToggle, Icon } = useHeadingDropdownMenu({
61
+ editor,
62
+ levels,
63
+ hideWhenUnavailable,
64
+ })
65
+
66
+ const handleOpenChange = useCallback(
67
+ (open: boolean) => {
68
+ if (!editor || !canToggle) return
69
+ setIsOpen(open)
70
+ onOpenChange?.(open)
71
+ },
72
+ [canToggle, editor, onOpenChange]
73
+ )
74
+
75
+ if (!isVisible) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <DropdownMenu modal={modal} open={isOpen} onOpenChange={handleOpenChange}>
81
+ <DropdownMenuTrigger asChild>
82
+ <Button
83
+ type="button"
84
+ variant="ghost"
85
+ data-active-state={isActive ? "on" : "off"}
86
+ role="button"
87
+ tabIndex={-1}
88
+ disabled={!canToggle}
89
+ data-disabled={!canToggle}
90
+ aria-label="Format text as heading"
91
+ aria-pressed={isActive}
92
+ tooltip="Heading"
93
+ {...buttonProps}
94
+ ref={ref}
95
+ >
96
+ {children ? (
97
+ children
98
+ ) : (
99
+ <>
100
+ <Icon className="tiptap-button-icon" />
101
+ <ChevronDownIcon className="tiptap-button-dropdown-small" />
102
+ </>
103
+ )}
104
+ </Button>
105
+ </DropdownMenuTrigger>
106
+
107
+ <DropdownMenuContent align="start">
108
+ <DropdownMenuGroup>
109
+ {levels.map((level) => (
110
+ <DropdownMenuItem key={`heading-${level}`} asChild>
111
+ <HeadingButton
112
+ editor={editor}
113
+ level={level}
114
+ text={`Heading ${level}`}
115
+ showTooltip={false}
116
+ />
117
+ </DropdownMenuItem>
118
+ ))}
119
+ </DropdownMenuGroup>
120
+ </DropdownMenuContent>
121
+ </DropdownMenu>
122
+ )
123
+ }
124
+ )
125
+
126
+ HeadingDropdownMenu.displayName = "HeadingDropdownMenu"
127
+
128
+ export default HeadingDropdownMenu
@@ -0,0 +1,2 @@
1
+ export * from "./heading-dropdown-menu"
2
+ export * from "./use-heading-dropdown-menu"
@@ -0,0 +1,132 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import type { Editor } from "@tiptap/react"
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
8
+
9
+ // --- Icons ---
10
+ import { HeadingIcon } from "@/components/tiptap-icons/heading-icon"
11
+
12
+ // --- Tiptap UI ---
13
+ import {
14
+ headingIcons,
15
+ type Level,
16
+ isHeadingActive,
17
+ canToggle,
18
+ shouldShowButton,
19
+ } from "@/components/tiptap-ui/heading-button"
20
+
21
+ /**
22
+ * Configuration for the heading dropdown menu functionality
23
+ */
24
+ export interface UseHeadingDropdownMenuConfig {
25
+ /**
26
+ * The Tiptap editor instance.
27
+ */
28
+ editor?: Editor | null
29
+ /**
30
+ * Available heading levels to show in the dropdown
31
+ * @default [1, 2, 3, 4, 5, 6]
32
+ */
33
+ levels?: Level[]
34
+ /**
35
+ * Whether the dropdown should hide when headings are not available.
36
+ * @default false
37
+ */
38
+ hideWhenUnavailable?: boolean
39
+ }
40
+
41
+ /**
42
+ * Gets the currently active heading level from the available levels
43
+ */
44
+ export function getActiveHeadingLevel(
45
+ editor: Editor | null,
46
+ levels: Level[] = [1, 2, 3, 4, 5, 6]
47
+ ): Level | undefined {
48
+ if (!editor || !editor.isEditable) return undefined
49
+ return levels.find((level) => isHeadingActive(editor, level))
50
+ }
51
+
52
+ /**
53
+ * Custom hook that provides heading dropdown menu functionality for Tiptap editor
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * // Simple usage
58
+ * function MyHeadingDropdown() {
59
+ * const {
60
+ * isVisible,
61
+ * activeLevel,
62
+ * isAnyHeadingActive,
63
+ * canToggle,
64
+ * levels,
65
+ * } = useHeadingDropdownMenu()
66
+ *
67
+ * if (!isVisible) return null
68
+ *
69
+ * return (
70
+ * <DropdownMenu>
71
+ * // dropdown content
72
+ * </DropdownMenu>
73
+ * )
74
+ * }
75
+ *
76
+ * // Advanced usage with configuration
77
+ * function MyAdvancedHeadingDropdown() {
78
+ * const {
79
+ * isVisible,
80
+ * activeLevel,
81
+ * } = useHeadingDropdownMenu({
82
+ * editor: myEditor,
83
+ * levels: [1, 2, 3],
84
+ * hideWhenUnavailable: true,
85
+ * })
86
+ *
87
+ * // component implementation
88
+ * }
89
+ * ```
90
+ */
91
+ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) {
92
+ const {
93
+ editor: providedEditor,
94
+ levels = [1, 2, 3, 4, 5, 6],
95
+ hideWhenUnavailable = false,
96
+ } = config || {}
97
+
98
+ const { editor } = useTiptapEditor(providedEditor)
99
+ const [isVisible, setIsVisible] = useState(true)
100
+
101
+ const activeLevel = getActiveHeadingLevel(editor, levels)
102
+ const isActive = isHeadingActive(editor)
103
+ const canToggleState = canToggle(editor)
104
+
105
+ useEffect(() => {
106
+ if (!editor) return
107
+
108
+ const handleSelectionUpdate = () => {
109
+ setIsVisible(
110
+ shouldShowButton({ editor, hideWhenUnavailable, level: levels })
111
+ )
112
+ }
113
+
114
+ handleSelectionUpdate()
115
+
116
+ editor.on("selectionUpdate", handleSelectionUpdate)
117
+
118
+ return () => {
119
+ editor.off("selectionUpdate", handleSelectionUpdate)
120
+ }
121
+ }, [editor, hideWhenUnavailable, levels])
122
+
123
+ return {
124
+ isVisible,
125
+ activeLevel,
126
+ isActive,
127
+ canToggle: canToggleState,
128
+ levels,
129
+ label: "Heading",
130
+ Icon: activeLevel ? headingIcons[activeLevel] : HeadingIcon,
131
+ }
132
+ }
@@ -0,0 +1,130 @@
1
+ import { forwardRef, useCallback } from "react"
2
+
3
+ // --- Lib ---
4
+ import { parseShortcutKeys } from "@/lib/tiptap-utils"
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
8
+
9
+ // --- Tiptap UI ---
10
+ import type { UseImageUploadConfig } from "@/components/tiptap-ui/image-upload-button"
11
+ import {
12
+ IMAGE_UPLOAD_SHORTCUT_KEY,
13
+ useImageUpload,
14
+ } from "@/components/tiptap-ui/image-upload-button"
15
+
16
+ // --- UI Primitives ---
17
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
18
+ import { Button } from "@/components/tiptap-ui-primitive/button"
19
+ import { Badge } from "@/components/tiptap-ui-primitive/badge"
20
+
21
+ type IconProps = React.SVGProps<SVGSVGElement>
22
+ type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
23
+
24
+ export interface ImageUploadButtonProps
25
+ extends Omit<ButtonProps, "type">, UseImageUploadConfig {
26
+ /**
27
+ * Optional text to display alongside the icon.
28
+ */
29
+ text?: string
30
+ /**
31
+ * Optional show shortcut keys in the button.
32
+ * @default false
33
+ */
34
+ showShortcut?: boolean
35
+ /**
36
+ * Optional custom icon component to render instead of the default.
37
+ */
38
+ icon?: React.MemoExoticComponent<IconComponent> | React.FC<IconProps>
39
+ }
40
+
41
+ export function ImageShortcutBadge({
42
+ shortcutKeys = IMAGE_UPLOAD_SHORTCUT_KEY,
43
+ }: {
44
+ shortcutKeys?: string
45
+ }) {
46
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
47
+ }
48
+
49
+ /**
50
+ * Button component for uploading/inserting images in a Tiptap editor.
51
+ *
52
+ * For custom button implementations, use the `useImage` hook instead.
53
+ */
54
+ export const ImageUploadButton = forwardRef<
55
+ HTMLButtonElement,
56
+ ImageUploadButtonProps
57
+ >(
58
+ (
59
+ {
60
+ editor: providedEditor,
61
+ text,
62
+ hideWhenUnavailable = false,
63
+ onInserted,
64
+ showShortcut = false,
65
+ onClick,
66
+ icon: CustomIcon,
67
+ children,
68
+ ...buttonProps
69
+ },
70
+ ref
71
+ ) => {
72
+ const { editor } = useTiptapEditor(providedEditor)
73
+ const {
74
+ isVisible,
75
+ canInsert,
76
+ handleImage,
77
+ label,
78
+ isActive,
79
+ shortcutKeys,
80
+ Icon,
81
+ } = useImageUpload({
82
+ editor,
83
+ hideWhenUnavailable,
84
+ onInserted,
85
+ })
86
+
87
+ const handleClick = useCallback(
88
+ (event: React.MouseEvent<HTMLButtonElement>) => {
89
+ onClick?.(event)
90
+ if (event.defaultPrevented) return
91
+ handleImage()
92
+ },
93
+ [handleImage, onClick]
94
+ )
95
+
96
+ if (!isVisible) {
97
+ return null
98
+ }
99
+
100
+ const RenderIcon = CustomIcon ?? Icon
101
+
102
+ return (
103
+ <Button
104
+ type="button"
105
+ variant="ghost"
106
+ data-active-state={isActive ? "on" : "off"}
107
+ role="button"
108
+ tabIndex={-1}
109
+ disabled={!canInsert}
110
+ data-disabled={!canInsert}
111
+ aria-label={label}
112
+ aria-pressed={isActive}
113
+ tooltip={label}
114
+ onClick={handleClick}
115
+ {...buttonProps}
116
+ ref={ref}
117
+ >
118
+ {children ?? (
119
+ <>
120
+ <RenderIcon className="tiptap-button-icon" />
121
+ {text && <span className="tiptap-button-text">{text}</span>}
122
+ {showShortcut && <ImageShortcutBadge shortcutKeys={shortcutKeys} />}
123
+ </>
124
+ )}
125
+ </Button>
126
+ )
127
+ }
128
+ )
129
+
130
+ ImageUploadButton.displayName = "ImageUploadButton"
@@ -0,0 +1,2 @@
1
+ export * from "./image-upload-button"
2
+ export * from "./use-image-upload"
@@ -0,0 +1,197 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useState } from "react"
4
+ import { useHotkeys } from "react-hotkeys-hook"
5
+ import { type Editor } from "@tiptap/react"
6
+
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
9
+ import { useIsBreakpoint } from "@/hooks/use-is-breakpoint"
10
+
11
+ // --- Lib ---
12
+ import { isExtensionAvailable } from "@/lib/tiptap-utils"
13
+
14
+ // --- Icons ---
15
+ import { ImagePlusIcon } from "@/components/tiptap-icons/image-plus-icon"
16
+
17
+ export const IMAGE_UPLOAD_SHORTCUT_KEY = "mod+shift+i"
18
+
19
+ /**
20
+ * Configuration for the image upload functionality
21
+ */
22
+ export interface UseImageUploadConfig {
23
+ /**
24
+ * The Tiptap editor instance.
25
+ */
26
+ editor?: Editor | null
27
+ /**
28
+ * Whether the button should hide when insertion is not available.
29
+ * @default false
30
+ */
31
+ hideWhenUnavailable?: boolean
32
+ /**
33
+ * Callback function called after a successful image insertion.
34
+ */
35
+ onInserted?: () => void
36
+ }
37
+
38
+ /**
39
+ * Checks if image can be inserted in the current editor state
40
+ */
41
+ export function canInsertImage(editor: Editor | null): boolean {
42
+ if (!editor || !editor.isEditable) return false
43
+ if (!isExtensionAvailable(editor, "imageUpload")) return false
44
+
45
+ return editor.can().insertContent({ type: "imageUpload" })
46
+ }
47
+
48
+ /**
49
+ * Checks if image is currently active
50
+ */
51
+ export function isImageActive(editor: Editor | null): boolean {
52
+ if (!editor || !editor.isEditable) return false
53
+ return editor.isActive("imageUpload")
54
+ }
55
+
56
+ /**
57
+ * Inserts an image in the editor
58
+ */
59
+ export function insertImage(editor: Editor | null): boolean {
60
+ if (!editor || !editor.isEditable) return false
61
+ if (!canInsertImage(editor)) return false
62
+
63
+ try {
64
+ return editor
65
+ .chain()
66
+ .focus()
67
+ .insertContent({
68
+ type: "imageUpload",
69
+ })
70
+ .run()
71
+ } catch {
72
+ return false
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Determines if the image button should be shown
78
+ */
79
+ export function shouldShowButton(props: {
80
+ editor: Editor | null
81
+ hideWhenUnavailable: boolean
82
+ }): boolean {
83
+ const { editor, hideWhenUnavailable } = props
84
+
85
+ if (!editor || !editor.isEditable) return false
86
+
87
+ if (!hideWhenUnavailable) {
88
+ return true
89
+ }
90
+
91
+ if (!isExtensionAvailable(editor, "imageUpload")) return false
92
+
93
+ if (!editor.isActive("code")) {
94
+ return canInsertImage(editor)
95
+ }
96
+
97
+ return true
98
+ }
99
+
100
+ /**
101
+ * Custom hook that provides image functionality for Tiptap editor
102
+ *
103
+ * @example
104
+ * ```tsx
105
+ * // Simple usage - no params needed
106
+ * function MySimpleImageButton() {
107
+ * const { isVisible, handleImage } = useImage()
108
+ *
109
+ * if (!isVisible) return null
110
+ *
111
+ * return <button onClick={handleImage}>Add Image</button>
112
+ * }
113
+ *
114
+ * // Advanced usage with configuration
115
+ * function MyAdvancedImageButton() {
116
+ * const { isVisible, handleImage, label, isActive } = useImage({
117
+ * editor: myEditor,
118
+ * hideWhenUnavailable: true,
119
+ * onInserted: () => console.log('Image inserted!')
120
+ * })
121
+ *
122
+ * if (!isVisible) return null
123
+ *
124
+ * return (
125
+ * <MyButton
126
+ * onClick={handleImage}
127
+ * aria-pressed={isActive}
128
+ * aria-label={label}
129
+ * >
130
+ * Add Image
131
+ * </MyButton>
132
+ * )
133
+ * }
134
+ * ```
135
+ */
136
+ export function useImageUpload(config?: UseImageUploadConfig) {
137
+ const {
138
+ editor: providedEditor,
139
+ hideWhenUnavailable = false,
140
+ onInserted,
141
+ } = config || {}
142
+
143
+ const { editor } = useTiptapEditor(providedEditor)
144
+ const isMobile = useIsBreakpoint()
145
+ const [isVisible, setIsVisible] = useState<boolean>(true)
146
+ const canInsert = canInsertImage(editor)
147
+ const isActive = isImageActive(editor)
148
+
149
+ useEffect(() => {
150
+ if (!editor) return
151
+
152
+ const handleSelectionUpdate = () => {
153
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
154
+ }
155
+
156
+ handleSelectionUpdate()
157
+
158
+ editor.on("selectionUpdate", handleSelectionUpdate)
159
+
160
+ return () => {
161
+ editor.off("selectionUpdate", handleSelectionUpdate)
162
+ }
163
+ }, [editor, hideWhenUnavailable])
164
+
165
+ const handleImage = useCallback(() => {
166
+ if (!editor) return false
167
+
168
+ const success = insertImage(editor)
169
+ if (success) {
170
+ onInserted?.()
171
+ }
172
+ return success
173
+ }, [editor, onInserted])
174
+
175
+ useHotkeys(
176
+ IMAGE_UPLOAD_SHORTCUT_KEY,
177
+ (event) => {
178
+ event.preventDefault()
179
+ handleImage()
180
+ },
181
+ {
182
+ enabled: isVisible && canInsert,
183
+ enableOnContentEditable: !isMobile,
184
+ enableOnFormTags: true,
185
+ }
186
+ )
187
+
188
+ return {
189
+ isVisible,
190
+ isActive,
191
+ handleImage,
192
+ canInsert,
193
+ label: "Add image",
194
+ shortcutKeys: IMAGE_UPLOAD_SHORTCUT_KEY,
195
+ Icon: ImagePlusIcon,
196
+ }
197
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./link-popover"
2
+ export * from "./use-link-popover"
@@ -0,0 +1,16 @@
1
+ .tiptap-link-input {
2
+ font-size: 0.875rem;
3
+ border: none;
4
+
5
+ /* Clamp the input */
6
+ min-width: 12rem;
7
+ padding-right: 0;
8
+
9
+ text-overflow: ellipsis;
10
+ white-space: nowrap;
11
+
12
+ &:focus {
13
+ text-overflow: clip;
14
+ overflow: visible;
15
+ }
16
+ }