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,315 @@
1
+ "use client"
2
+
3
+ import { forwardRef, useCallback, useEffect, useState } from "react"
4
+ import type { Editor } from "@tiptap/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 "@/components/tiptap-icons/corner-down-left-icon"
12
+ import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon"
13
+ import { LinkIcon } from "@/components/tiptap-icons/link-icon"
14
+ import { TrashIcon } from "@/components/tiptap-icons/trash-icon"
15
+
16
+ // --- Tiptap UI ---
17
+ import type { UseLinkPopoverConfig } from "@/components/tiptap-ui/link-popover"
18
+ import { useLinkPopover } from "@/components/tiptap-ui/link-popover"
19
+
20
+ // --- UI Primitives ---
21
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
22
+ import { Button } from "@/components/tiptap-ui-primitive/button"
23
+ import {
24
+ Popover,
25
+ PopoverContent,
26
+ PopoverTrigger,
27
+ } from "@/components/tiptap-ui-primitive/popover"
28
+ import { Separator } from "@/components/tiptap-ui-primitive/separator"
29
+ import {
30
+ Card,
31
+ CardBody,
32
+ CardItemGroup,
33
+ } from "@/components/tiptap-ui-primitive/card"
34
+ import { Input } from "@/components/tiptap-ui-primitive/input"
35
+ import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group"
36
+
37
+ import "./link-popover.scss"
38
+
39
+ export interface LinkMainProps {
40
+ /**
41
+ * The URL to set for the link.
42
+ */
43
+ url: string
44
+ /**
45
+ * Function to update the URL state.
46
+ */
47
+ setUrl: React.Dispatch<React.SetStateAction<string | null>>
48
+ /**
49
+ * Function to set the link in the editor.
50
+ */
51
+ setLink: () => void
52
+ /**
53
+ * Function to remove the link from the editor.
54
+ */
55
+ removeLink: () => void
56
+ /**
57
+ * Function to open the link.
58
+ */
59
+ openLink: () => void
60
+ /**
61
+ * Whether the link is currently active in the editor.
62
+ */
63
+ isActive: boolean
64
+ }
65
+
66
+ export interface LinkPopoverProps
67
+ extends Omit<ButtonProps, "type">, UseLinkPopoverConfig {
68
+ /**
69
+ * Callback for when the popover opens or closes.
70
+ */
71
+ onOpenChange?: (isOpen: boolean) => void
72
+ /**
73
+ * Whether to automatically open the popover when a link is active.
74
+ * @default true
75
+ */
76
+ autoOpenOnLinkActive?: boolean
77
+ }
78
+
79
+ /**
80
+ * Link button component for triggering the link popover
81
+ */
82
+ export const LinkButton = forwardRef<HTMLButtonElement, ButtonProps>(
83
+ ({ className, children, ...props }, ref) => {
84
+ return (
85
+ <Button
86
+ type="button"
87
+ className={className}
88
+ variant="ghost"
89
+ role="button"
90
+ tabIndex={-1}
91
+ aria-label="Link"
92
+ tooltip="Link"
93
+ ref={ref}
94
+ {...props}
95
+ >
96
+ {children || <LinkIcon className="tiptap-button-icon" />}
97
+ </Button>
98
+ )
99
+ }
100
+ )
101
+
102
+ LinkButton.displayName = "LinkButton"
103
+
104
+ /**
105
+ * Main content component for the link popover
106
+ */
107
+ const LinkMain: React.FC<LinkMainProps> = ({
108
+ url,
109
+ setUrl,
110
+ setLink,
111
+ removeLink,
112
+ openLink,
113
+ isActive,
114
+ }) => {
115
+ const isMobile = useIsBreakpoint()
116
+
117
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
118
+ if (event.key === "Enter") {
119
+ event.preventDefault()
120
+ setLink()
121
+ }
122
+ }
123
+
124
+ return (
125
+ <Card
126
+ style={{
127
+ ...(isMobile ? { boxShadow: "none", border: 0 } : {}),
128
+ }}
129
+ >
130
+ <CardBody
131
+ style={{
132
+ ...(isMobile ? { padding: 0 } : {}),
133
+ }}
134
+ >
135
+ <CardItemGroup orientation="horizontal">
136
+ <Input
137
+ type="url"
138
+ placeholder="Paste a link..."
139
+ value={url}
140
+ onChange={(e) => setUrl(e.target.value)}
141
+ onKeyDown={handleKeyDown}
142
+ autoFocus
143
+ autoComplete="off"
144
+ autoCorrect="off"
145
+ autoCapitalize="off"
146
+ className="tiptap-link-input"
147
+ />
148
+
149
+ <ButtonGroup>
150
+ <Button
151
+ type="button"
152
+ onClick={setLink}
153
+ title="Apply link"
154
+ disabled={!url && !isActive}
155
+ variant="ghost"
156
+ >
157
+ <CornerDownLeftIcon className="tiptap-button-icon" />
158
+ </Button>
159
+ </ButtonGroup>
160
+
161
+ <Separator />
162
+
163
+ <ButtonGroup>
164
+ <ButtonGroup>
165
+ <Button
166
+ type="button"
167
+ onClick={openLink}
168
+ title="Open in new window"
169
+ disabled={!url && !isActive}
170
+ variant="ghost"
171
+ >
172
+ <ExternalLinkIcon className="tiptap-button-icon" />
173
+ </Button>
174
+ </ButtonGroup>
175
+
176
+ <ButtonGroup>
177
+ <Button
178
+ type="button"
179
+ onClick={removeLink}
180
+ title="Remove link"
181
+ disabled={!url && !isActive}
182
+ variant="ghost"
183
+ >
184
+ <TrashIcon className="tiptap-button-icon" />
185
+ </Button>
186
+ </ButtonGroup>
187
+ </ButtonGroup>
188
+ </CardItemGroup>
189
+ </CardBody>
190
+ </Card>
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Link content component for standalone use
196
+ */
197
+ export const LinkContent: React.FC<{
198
+ editor?: Editor | null
199
+ }> = ({ editor }) => {
200
+ const linkPopover = useLinkPopover({
201
+ editor,
202
+ })
203
+
204
+ return <LinkMain {...linkPopover} />
205
+ }
206
+
207
+ /**
208
+ * Link popover component for Tiptap editors.
209
+ *
210
+ * For custom popover implementations, use the `useLinkPopover` hook instead.
211
+ */
212
+ export const LinkPopover = forwardRef<HTMLButtonElement, LinkPopoverProps>(
213
+ (
214
+ {
215
+ editor: providedEditor,
216
+ hideWhenUnavailable = false,
217
+ onSetLink,
218
+ onOpenChange,
219
+ autoOpenOnLinkActive = true,
220
+ onClick,
221
+ children,
222
+ ...buttonProps
223
+ },
224
+ ref
225
+ ) => {
226
+ const { editor } = useTiptapEditor(providedEditor)
227
+ const [isOpen, setIsOpen] = useState(false)
228
+
229
+ const {
230
+ isVisible,
231
+ canSet,
232
+ isActive,
233
+ url,
234
+ setUrl,
235
+ setLink,
236
+ removeLink,
237
+ openLink,
238
+ label,
239
+ Icon,
240
+ } = useLinkPopover({
241
+ editor,
242
+ hideWhenUnavailable,
243
+ onSetLink,
244
+ })
245
+
246
+ const shouldAutoOpen =
247
+ autoOpenOnLinkActive && !!editor?.isFocused && isActive
248
+
249
+ const handleOnOpenChange = useCallback(
250
+ (nextIsOpen: boolean) => {
251
+ setIsOpen(nextIsOpen)
252
+ onOpenChange?.(nextIsOpen)
253
+ },
254
+ [onOpenChange]
255
+ )
256
+
257
+ const handleSetLink = useCallback(() => {
258
+ setLink()
259
+ setIsOpen(false)
260
+ }, [setLink])
261
+
262
+ const handleClick = useCallback(
263
+ (event: React.MouseEvent<HTMLButtonElement>) => {
264
+ onClick?.(event)
265
+ if (event.defaultPrevented) return
266
+ setIsOpen(!isOpen)
267
+ },
268
+ [onClick, isOpen]
269
+ )
270
+
271
+ useEffect(() => {
272
+ if (shouldAutoOpen) {
273
+ setIsOpen(true)
274
+ }
275
+ }, [shouldAutoOpen])
276
+
277
+ if (!isVisible) {
278
+ return null
279
+ }
280
+
281
+ return (
282
+ <Popover open={isOpen} onOpenChange={handleOnOpenChange}>
283
+ <PopoverTrigger asChild>
284
+ <LinkButton
285
+ disabled={!canSet}
286
+ data-active-state={isActive ? "on" : "off"}
287
+ data-disabled={!canSet}
288
+ aria-label={label}
289
+ aria-pressed={isActive}
290
+ onClick={handleClick}
291
+ {...buttonProps}
292
+ ref={ref}
293
+ >
294
+ {children ?? <Icon className="tiptap-button-icon" />}
295
+ </LinkButton>
296
+ </PopoverTrigger>
297
+
298
+ <PopoverContent collisionPadding={4}>
299
+ <LinkMain
300
+ url={url}
301
+ setUrl={setUrl}
302
+ setLink={handleSetLink}
303
+ removeLink={removeLink}
304
+ openLink={openLink}
305
+ isActive={isActive}
306
+ />
307
+ </PopoverContent>
308
+ </Popover>
309
+ )
310
+ }
311
+ )
312
+
313
+ LinkPopover.displayName = "LinkPopover"
314
+
315
+ export default LinkPopover
@@ -0,0 +1,297 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+ import type { Editor } from "@tiptap/react"
3
+
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
6
+
7
+ // --- Icons ---
8
+ import { LinkIcon } from "@/components/tiptap-icons/link-icon"
9
+
10
+ // --- Lib ---
11
+ import {
12
+ isMarkInSchema,
13
+ isNodeTypeSelected,
14
+ sanitizeUrl,
15
+ } from "@/lib/tiptap-utils"
16
+
17
+ /**
18
+ * Configuration for the link popover functionality
19
+ */
20
+ export interface UseLinkPopoverConfig {
21
+ /**
22
+ * The Tiptap editor instance.
23
+ */
24
+ editor?: Editor | null
25
+ /**
26
+ * Whether to hide the link popover when not available.
27
+ * @default false
28
+ */
29
+ hideWhenUnavailable?: boolean
30
+ /**
31
+ * Callback function called when the link is set.
32
+ */
33
+ onSetLink?: () => void
34
+ }
35
+
36
+ /**
37
+ * Configuration for the link handler functionality
38
+ */
39
+ export interface LinkHandlerProps {
40
+ /**
41
+ * The Tiptap editor instance.
42
+ */
43
+ editor: Editor | null
44
+ /**
45
+ * Callback function called when the link is set.
46
+ */
47
+ onSetLink?: () => void
48
+ }
49
+
50
+ /**
51
+ * Checks if a link can be set in the current editor state
52
+ */
53
+ export function canSetLink(editor: Editor | null): boolean {
54
+ if (!editor || !editor.isEditable) return false
55
+
56
+ // The third argument 'true' checks whether the current selection is inside an image caption, and prevents setting a link there
57
+ // If the selection is inside an image caption, we can't set a link
58
+ if (isNodeTypeSelected(editor, ["image"], true)) return false
59
+ try {
60
+ return editor.can().setMark("link")
61
+ } catch {
62
+ return false
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Checks if a link is currently active in the editor
68
+ */
69
+ export function isLinkActive(editor: Editor | null): boolean {
70
+ if (!editor || !editor.isEditable) return false
71
+ return editor.isActive("link")
72
+ }
73
+
74
+ /**
75
+ * Determines if the link button should be shown
76
+ */
77
+ export function shouldShowLinkButton(props: {
78
+ editor: Editor | null
79
+ hideWhenUnavailable: boolean
80
+ }): boolean {
81
+ const { editor, hideWhenUnavailable } = props
82
+
83
+ if (!editor || !editor.isEditable) return false
84
+
85
+ const linkInSchema = isMarkInSchema("link", editor)
86
+
87
+ // If hideWhenUnavailable is false, always show the button (even if disabled)
88
+ if (!hideWhenUnavailable) {
89
+ return true
90
+ }
91
+
92
+ // hideWhenUnavailable is true: hide if link is not in schema
93
+ if (!linkInSchema) {
94
+ return false
95
+ }
96
+
97
+ // hideWhenUnavailable is true: hide if we can't set a link (unless in code block)
98
+ if (!editor.isActive("code")) {
99
+ return canSetLink(editor)
100
+ }
101
+
102
+ return true
103
+ }
104
+
105
+ /**
106
+ * Custom hook for handling link operations in a Tiptap editor
107
+ */
108
+ export function useLinkHandler(props: LinkHandlerProps) {
109
+ const { editor, onSetLink } = props
110
+ const [url, setUrl] = useState<string | null>(null)
111
+
112
+ useEffect(() => {
113
+ if (!editor) return
114
+
115
+ // Get URL immediately on mount
116
+ const { href } = editor.getAttributes("link")
117
+
118
+ if (isLinkActive(editor) && url === null) {
119
+ setUrl(href || "")
120
+ }
121
+ }, [editor, url])
122
+
123
+ useEffect(() => {
124
+ if (!editor) return
125
+
126
+ const updateLinkState = () => {
127
+ const { href } = editor.getAttributes("link")
128
+ setUrl(href || "")
129
+ }
130
+
131
+ editor.on("selectionUpdate", updateLinkState)
132
+ return () => {
133
+ editor.off("selectionUpdate", updateLinkState)
134
+ }
135
+ }, [editor])
136
+
137
+ const setLink = useCallback(() => {
138
+ if (!url || !editor) return
139
+
140
+ const { selection } = editor.state
141
+ const isEmpty = selection.empty
142
+
143
+ let chain = editor.chain().focus()
144
+
145
+ chain = chain.extendMarkRange("link").setLink({ href: url })
146
+
147
+ if (isEmpty) {
148
+ chain = chain.insertContent({ type: "text", text: url })
149
+ }
150
+
151
+ chain.run()
152
+
153
+ setUrl(null)
154
+
155
+ onSetLink?.()
156
+ }, [editor, onSetLink, url])
157
+
158
+ const removeLink = useCallback(() => {
159
+ if (!editor) return
160
+ editor
161
+ .chain()
162
+ .focus()
163
+ .extendMarkRange("link")
164
+ .unsetLink()
165
+ .setMeta("preventAutolink", true)
166
+ .run()
167
+ setUrl("")
168
+ }, [editor])
169
+
170
+ const openLink = useCallback(
171
+ (target: string = "_blank", features: string = "noopener,noreferrer") => {
172
+ if (!url) return
173
+
174
+ const safeUrl = sanitizeUrl(url, window.location.href)
175
+ if (safeUrl !== "#") {
176
+ window.open(safeUrl, target, features)
177
+ }
178
+ },
179
+ [url]
180
+ )
181
+
182
+ return {
183
+ url: url || "",
184
+ setUrl,
185
+ setLink,
186
+ removeLink,
187
+ openLink,
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Custom hook for link popover state management
193
+ */
194
+ export function useLinkState(props: {
195
+ editor: Editor | null
196
+ hideWhenUnavailable: boolean
197
+ }) {
198
+ const { editor, hideWhenUnavailable = false } = props
199
+
200
+ const canSet = canSetLink(editor)
201
+ const isActive = isLinkActive(editor)
202
+
203
+ const [isVisible, setIsVisible] = useState(true)
204
+
205
+ useEffect(() => {
206
+ if (!editor) return
207
+
208
+ const handleSelectionUpdate = () => {
209
+ setIsVisible(
210
+ shouldShowLinkButton({
211
+ editor,
212
+ hideWhenUnavailable,
213
+ })
214
+ )
215
+ }
216
+
217
+ handleSelectionUpdate()
218
+
219
+ editor.on("selectionUpdate", handleSelectionUpdate)
220
+
221
+ return () => {
222
+ editor.off("selectionUpdate", handleSelectionUpdate)
223
+ }
224
+ }, [editor, hideWhenUnavailable])
225
+
226
+ return {
227
+ isVisible,
228
+ canSet,
229
+ isActive,
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Main hook that provides link popover functionality for Tiptap editor
235
+ *
236
+ * @example
237
+ * ```tsx
238
+ * // Simple usage
239
+ * function MyLinkButton() {
240
+ * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover()
241
+ *
242
+ * if (!isVisible) return null
243
+ *
244
+ * return <button disabled={!canSet}>Link</button>
245
+ * }
246
+ *
247
+ * // Advanced usage with configuration
248
+ * function MyAdvancedLinkButton() {
249
+ * const { isVisible, canSet, isActive, Icon, label } = useLinkPopover({
250
+ * editor: myEditor,
251
+ * hideWhenUnavailable: true,
252
+ * onSetLink: () => console.log('Link set!')
253
+ * })
254
+ *
255
+ * if (!isVisible) return null
256
+ *
257
+ * return (
258
+ * <MyButton
259
+ * disabled={!canSet}
260
+ * aria-label={label}
261
+ * aria-pressed={isActive}
262
+ * >
263
+ * <Icon />
264
+ * {label}
265
+ * </MyButton>
266
+ * )
267
+ * }
268
+ * ```
269
+ */
270
+ export function useLinkPopover(config?: UseLinkPopoverConfig) {
271
+ const {
272
+ editor: providedEditor,
273
+ hideWhenUnavailable = false,
274
+ onSetLink,
275
+ } = config || {}
276
+
277
+ const { editor } = useTiptapEditor(providedEditor)
278
+
279
+ const { isVisible, canSet, isActive } = useLinkState({
280
+ editor,
281
+ hideWhenUnavailable,
282
+ })
283
+
284
+ const linkHandler = useLinkHandler({
285
+ editor,
286
+ onSetLink,
287
+ })
288
+
289
+ return {
290
+ isVisible,
291
+ canSet,
292
+ isActive,
293
+ label: "Link",
294
+ Icon: LinkIcon,
295
+ ...linkHandler,
296
+ }
297
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./list-button"
2
+ export * from "./use-list"