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,49 @@
1
+ .tiptap-button-highlight {
2
+ position: relative;
3
+ width: 1.25rem;
4
+ height: 1.25rem;
5
+ margin: 0 -0.175rem;
6
+ border-radius: var(--tt-radius-xl);
7
+ background-color: var(--highlight-color);
8
+ transition: transform 0.2s ease;
9
+
10
+ &::after {
11
+ content: "";
12
+ position: absolute;
13
+ width: 100%;
14
+ height: 100%;
15
+ left: 0;
16
+ top: 0;
17
+ border-radius: inherit;
18
+ box-sizing: border-box;
19
+ border: 1px solid var(--highlight-color);
20
+ filter: brightness(95%);
21
+ mix-blend-mode: multiply;
22
+
23
+ .dark & {
24
+ filter: brightness(140%);
25
+ mix-blend-mode: lighten;
26
+ }
27
+ }
28
+ }
29
+
30
+ .tiptap-button {
31
+ &[data-active-state="on"] {
32
+ .tiptap-button-highlight {
33
+ &::after {
34
+ filter: brightness(80%);
35
+ }
36
+ }
37
+ }
38
+
39
+ .dark & {
40
+ &[data-active-state="on"] {
41
+ .tiptap-button-highlight {
42
+ &::after {
43
+ // Andere Eigenschaft für .dark Kontext
44
+ filter: brightness(180%);
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,170 @@
1
+ import { forwardRef, useCallback, useMemo } 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 { UseColorHighlightConfig } from "@/components/tiptap-ui/color-highlight-button"
11
+ import {
12
+ COLOR_HIGHLIGHT_SHORTCUT_KEY,
13
+ useColorHighlight,
14
+ } from "@/components/tiptap-ui/color-highlight-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
+ // --- Styles ---
22
+ import "@/components/tiptap-ui/color-highlight-button/color-highlight-button.scss"
23
+
24
+ export interface ColorHighlightButtonProps
25
+ extends Omit<ButtonProps, "type">, UseColorHighlightConfig {
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
+
37
+ export function ColorHighlightShortcutBadge({
38
+ shortcutKeys = COLOR_HIGHLIGHT_SHORTCUT_KEY,
39
+ }: {
40
+ shortcutKeys?: string
41
+ }) {
42
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
43
+ }
44
+
45
+ /**
46
+ * Button component for applying color highlights in a Tiptap editor.
47
+ *
48
+ * Supports two highlighting modes:
49
+ * - "mark": Uses the highlight mark extension (default)
50
+ * - "node": Uses the node background extension
51
+ *
52
+ * For custom button implementations, use the `useColorHighlight` hook instead.
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * // Mark-based highlighting (default)
57
+ * <ColorHighlightButton highlightColor="yellow" />
58
+ *
59
+ * // Node-based background coloring
60
+ * <ColorHighlightButton
61
+ * highlightColor="var(--tt-color-highlight-blue)"
62
+ * mode="node"
63
+ * />
64
+ *
65
+ * // With custom callback
66
+ * <ColorHighlightButton
67
+ * highlightColor="red"
68
+ * mode="mark"
69
+ * onApplied={({ color, mode }) => console.log(`Applied ${color} in ${mode} mode`)}
70
+ * />
71
+ * ```
72
+ */
73
+ export const ColorHighlightButton = forwardRef<
74
+ HTMLButtonElement,
75
+ ColorHighlightButtonProps
76
+ >(
77
+ (
78
+ {
79
+ editor: providedEditor,
80
+ highlightColor,
81
+ text,
82
+ hideWhenUnavailable = false,
83
+ mode = "mark",
84
+ onApplied,
85
+ showShortcut = false,
86
+ onClick,
87
+ children,
88
+ style,
89
+ useColorValue = false,
90
+ ...buttonProps
91
+ },
92
+ ref
93
+ ) => {
94
+ const { editor } = useTiptapEditor(providedEditor)
95
+ const {
96
+ isVisible,
97
+ canColorHighlight,
98
+ isActive,
99
+ handleColorHighlight,
100
+ label,
101
+ shortcutKeys,
102
+ } = useColorHighlight({
103
+ editor,
104
+ highlightColor,
105
+ useColorValue,
106
+ label: text || `Toggle highlight (${highlightColor})`,
107
+ hideWhenUnavailable,
108
+ mode,
109
+ onApplied,
110
+ })
111
+
112
+ const handleClick = useCallback(
113
+ (event: React.MouseEvent<HTMLButtonElement>) => {
114
+ onClick?.(event)
115
+ if (event.defaultPrevented) return
116
+ handleColorHighlight()
117
+ },
118
+ [handleColorHighlight, onClick]
119
+ )
120
+
121
+ const buttonStyle = useMemo(
122
+ () =>
123
+ ({
124
+ ...style,
125
+ "--highlight-color": highlightColor,
126
+ }) as React.CSSProperties,
127
+ [highlightColor, style]
128
+ )
129
+
130
+ if (!isVisible) {
131
+ return null
132
+ }
133
+
134
+ return (
135
+ <Button
136
+ type="button"
137
+ variant="ghost"
138
+ data-active-state={isActive ? "on" : "off"}
139
+ role="button"
140
+ tabIndex={-1}
141
+ disabled={!canColorHighlight}
142
+ data-disabled={!canColorHighlight}
143
+ aria-label={label}
144
+ aria-pressed={isActive}
145
+ tooltip={label}
146
+ onClick={handleClick}
147
+ style={buttonStyle}
148
+ {...buttonProps}
149
+ ref={ref}
150
+ >
151
+ {children ?? (
152
+ <>
153
+ <span
154
+ className="tiptap-button-highlight"
155
+ style={
156
+ { "--highlight-color": highlightColor } as React.CSSProperties
157
+ }
158
+ />
159
+ {text && <span className="tiptap-button-text">{text}</span>}
160
+ {showShortcut && (
161
+ <ColorHighlightShortcutBadge shortcutKeys={shortcutKeys} />
162
+ )}
163
+ </>
164
+ )}
165
+ </Button>
166
+ )
167
+ }
168
+ )
169
+
170
+ ColorHighlightButton.displayName = "ColorHighlightButton"
@@ -0,0 +1,2 @@
1
+ export * from "./color-highlight-button"
2
+ export * from "./use-color-highlight"
@@ -0,0 +1,380 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useState } from "react"
4
+ import { type Editor } from "@tiptap/react"
5
+ import { useHotkeys } from "react-hotkeys-hook"
6
+
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
9
+ import { useIsBreakpoint } from "@/hooks/use-is-breakpoint"
10
+
11
+ // --- Lib ---
12
+ import {
13
+ isMarkInSchema,
14
+ isNodeTypeSelected,
15
+ isExtensionAvailable,
16
+ } from "@/lib/tiptap-utils"
17
+
18
+ // --- Icons ---
19
+ import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon"
20
+
21
+ export const COLOR_HIGHLIGHT_SHORTCUT_KEY = "mod+shift+h"
22
+ export const HIGHLIGHT_COLORS = [
23
+ {
24
+ label: "Default background",
25
+ value: "var(--tt-bg-color)",
26
+ colorValue: "#ffffff",
27
+ border: "var(--tt-bg-color-contrast)",
28
+ },
29
+ {
30
+ label: "Gray background",
31
+ value: "var(--tt-color-highlight-gray)",
32
+ colorValue: "#f8f8f7",
33
+ border: "var(--tt-color-highlight-gray-contrast)",
34
+ },
35
+ {
36
+ label: "Brown background",
37
+ value: "var(--tt-color-highlight-brown)",
38
+ colorValue: "#f4eeee",
39
+ border: "var(--tt-color-highlight-brown-contrast)",
40
+ },
41
+ {
42
+ label: "Orange background",
43
+ value: "var(--tt-color-highlight-orange)",
44
+ colorValue: "#fbecdd",
45
+ border: "var(--tt-color-highlight-orange-contrast)",
46
+ },
47
+ {
48
+ label: "Yellow background",
49
+ value: "var(--tt-color-highlight-yellow)",
50
+ colorValue: "#fef9c3",
51
+ border: "var(--tt-color-highlight-yellow-contrast)",
52
+ },
53
+ {
54
+ label: "Green background",
55
+ value: "var(--tt-color-highlight-green)",
56
+ colorValue: "#dcfce7",
57
+ border: "var(--tt-color-highlight-green-contrast)",
58
+ },
59
+ {
60
+ label: "Blue background",
61
+ value: "var(--tt-color-highlight-blue)",
62
+ colorValue: "#e0f2fe",
63
+ border: "var(--tt-color-highlight-blue-contrast)",
64
+ },
65
+ {
66
+ label: "Purple background",
67
+ value: "var(--tt-color-highlight-purple)",
68
+ colorValue: "#f3e8ff",
69
+ border: "var(--tt-color-highlight-purple-contrast)",
70
+ },
71
+ {
72
+ label: "Pink background",
73
+ value: "var(--tt-color-highlight-pink)",
74
+ colorValue: "#fcf1f6",
75
+ border: "var(--tt-color-highlight-pink-contrast)",
76
+ },
77
+ {
78
+ label: "Red background",
79
+ value: "var(--tt-color-highlight-red)",
80
+ colorValue: "#ffe4e6",
81
+ border: "var(--tt-color-highlight-red-contrast)",
82
+ },
83
+ ]
84
+ export type HighlightColor = (typeof HIGHLIGHT_COLORS)[number]
85
+
86
+ export type HighlightMode = "mark" | "node"
87
+
88
+ /**
89
+ * Configuration for the color highlight functionality
90
+ */
91
+ export interface UseColorHighlightConfig {
92
+ /**
93
+ * The Tiptap editor instance.
94
+ */
95
+ editor?: Editor | null
96
+ /**
97
+ * The color to apply when toggling the highlight.
98
+ */
99
+ highlightColor?: string
100
+ /**
101
+ * Optional label to display alongside the icon.
102
+ */
103
+ label?: string
104
+ /**
105
+ * Whether the button should hide when the mark is not available.
106
+ * @default false
107
+ */
108
+ hideWhenUnavailable?: boolean
109
+ /**
110
+ * The highlighting mode to use.
111
+ * - "mark": Uses the highlight mark extension (default)
112
+ * - "node": Uses the node background extension
113
+ * @default "mark"
114
+ */
115
+ mode?: HighlightMode
116
+ /**
117
+ * When true, uses the actual color value (colorValue) instead of CSS variable (value).
118
+ * @default false
119
+ */
120
+ useColorValue?: boolean
121
+ /**
122
+ * Called when the highlight is applied.
123
+ */
124
+ onApplied?: ({
125
+ color,
126
+ label,
127
+ mode,
128
+ }: {
129
+ color: string
130
+ label: string
131
+ mode: HighlightMode
132
+ }) => void
133
+ }
134
+
135
+ export function pickHighlightColorsByValue(values: string[]) {
136
+ const colorMap = new Map(
137
+ HIGHLIGHT_COLORS.map((color) => [color.value, color])
138
+ )
139
+ return values
140
+ .map((value) => colorMap.get(value))
141
+ .filter((color): color is (typeof HIGHLIGHT_COLORS)[number] => !!color)
142
+ }
143
+
144
+ /**
145
+ * Gets the appropriate color value based on configuration
146
+ */
147
+ export function getHighlightColorValue(
148
+ color: string,
149
+ useColorValue: boolean = false
150
+ ): string {
151
+ if (!useColorValue) return color
152
+
153
+ const colorItem = HIGHLIGHT_COLORS.find(
154
+ (c) => c.value === color || c.colorValue === color
155
+ )
156
+ return colorItem?.colorValue || color
157
+ }
158
+
159
+ /**
160
+ * Checks if highlight can be applied based on the mode and current editor state
161
+ */
162
+ export function canColorHighlight(
163
+ editor: Editor | null,
164
+ mode: HighlightMode = "mark"
165
+ ): boolean {
166
+ if (!editor || !editor.isEditable) return false
167
+
168
+ if (mode === "mark") {
169
+ if (
170
+ !isMarkInSchema("highlight", editor) ||
171
+ isNodeTypeSelected(editor, ["image"])
172
+ )
173
+ return false
174
+
175
+ return editor.can().setMark("highlight")
176
+ } else {
177
+ if (!isExtensionAvailable(editor, ["nodeBackground"])) return false
178
+
179
+ try {
180
+ return editor.can().toggleNodeBackgroundColor("test")
181
+ } catch {
182
+ return false
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Checks if highlight is currently active
189
+ */
190
+ export function isColorHighlightActive(
191
+ editor: Editor | null,
192
+ highlightColor?: string,
193
+ mode: HighlightMode = "mark"
194
+ ): boolean {
195
+ if (!editor || !editor.isEditable) return false
196
+
197
+ if (mode === "mark") {
198
+ return highlightColor
199
+ ? editor.isActive("highlight", { color: highlightColor })
200
+ : editor.isActive("highlight")
201
+ } else {
202
+ if (!highlightColor) return false
203
+
204
+ try {
205
+ const { state } = editor
206
+ const { selection } = state
207
+
208
+ const $pos = selection.$anchor
209
+ for (let depth = $pos.depth; depth >= 0; depth--) {
210
+ const node = $pos.node(depth)
211
+ if (node && node.attrs?.backgroundColor === highlightColor) {
212
+ return true
213
+ }
214
+ }
215
+ return false
216
+ } catch {
217
+ return false
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Removes highlight based on the mode
224
+ */
225
+ export function removeHighlight(
226
+ editor: Editor | null,
227
+ mode: HighlightMode = "mark"
228
+ ): boolean {
229
+ if (!editor || !editor.isEditable) return false
230
+ if (!canColorHighlight(editor, mode)) return false
231
+
232
+ if (mode === "mark") {
233
+ return editor.chain().focus().unsetMark("highlight").run()
234
+ } else {
235
+ return editor.chain().focus().unsetNodeBackgroundColor().run()
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Determines if the highlight button should be shown
241
+ */
242
+ export function shouldShowButton(props: {
243
+ editor: Editor | null
244
+ hideWhenUnavailable: boolean
245
+ mode: HighlightMode
246
+ }): boolean {
247
+ const { editor, hideWhenUnavailable, mode } = props
248
+
249
+ if (!editor) return false
250
+
251
+ if (!hideWhenUnavailable) {
252
+ return true
253
+ }
254
+
255
+ if (!editor.isEditable) return false
256
+
257
+ // hideWhenUnavailable=true: check schema/extension availability
258
+ if (mode === "mark") {
259
+ if (!isMarkInSchema("highlight", editor)) return false
260
+ } else {
261
+ if (!isExtensionAvailable(editor, ["nodeBackground"])) return false
262
+ }
263
+
264
+ if (!editor.isActive("code")) {
265
+ return canColorHighlight(editor, mode)
266
+ }
267
+
268
+ return true
269
+ }
270
+
271
+ export function useColorHighlight(config: UseColorHighlightConfig) {
272
+ const {
273
+ editor: providedEditor,
274
+ label,
275
+ highlightColor,
276
+ hideWhenUnavailable = false,
277
+ mode = "mark",
278
+ useColorValue = false,
279
+ onApplied,
280
+ } = config
281
+
282
+ const { editor } = useTiptapEditor(providedEditor)
283
+ const isMobile = useIsBreakpoint()
284
+ const [isVisible, setIsVisible] = useState<boolean>(true)
285
+ const canColorHighlightState = canColorHighlight(editor, mode)
286
+ const actualColor = highlightColor
287
+ ? getHighlightColorValue(highlightColor, useColorValue)
288
+ : highlightColor
289
+ const isActive = isColorHighlightActive(editor, actualColor, mode)
290
+
291
+ useEffect(() => {
292
+ if (!editor) return
293
+
294
+ const handleSelectionUpdate = () => {
295
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode }))
296
+ }
297
+
298
+ handleSelectionUpdate()
299
+
300
+ editor.on("selectionUpdate", handleSelectionUpdate)
301
+
302
+ return () => {
303
+ editor.off("selectionUpdate", handleSelectionUpdate)
304
+ }
305
+ }, [editor, hideWhenUnavailable, mode])
306
+
307
+ const handleColorHighlight = useCallback(() => {
308
+ if (!editor || !canColorHighlightState || !actualColor || !label)
309
+ return false
310
+
311
+ if (mode === "mark") {
312
+ if (editor.state.storedMarks) {
313
+ const highlightMarkType = editor.schema.marks.highlight
314
+ if (highlightMarkType) {
315
+ editor.view.dispatch(
316
+ editor.state.tr.removeStoredMark(highlightMarkType)
317
+ )
318
+ }
319
+ }
320
+
321
+ setTimeout(() => {
322
+ const success = editor
323
+ .chain()
324
+ .focus()
325
+ .toggleHighlight({ color: actualColor })
326
+ .run()
327
+ if (success) {
328
+ onApplied?.({ color: actualColor, label, mode })
329
+ }
330
+ return success
331
+ }, 0)
332
+
333
+ return true
334
+ } else {
335
+ const success = editor
336
+ .chain()
337
+ .focus()
338
+ .toggleNodeBackgroundColor(actualColor)
339
+ .run()
340
+
341
+ if (success) {
342
+ onApplied?.({ color: actualColor, label, mode })
343
+ }
344
+ return success
345
+ }
346
+ }, [canColorHighlightState, actualColor, editor, label, onApplied, mode])
347
+
348
+ const handleRemoveHighlight = useCallback(() => {
349
+ const success = removeHighlight(editor, mode)
350
+ if (success) {
351
+ onApplied?.({ color: "", label: "Remove highlight", mode })
352
+ }
353
+ return success
354
+ }, [editor, onApplied, mode])
355
+
356
+ useHotkeys(
357
+ COLOR_HIGHLIGHT_SHORTCUT_KEY,
358
+ (event) => {
359
+ event.preventDefault()
360
+ handleColorHighlight()
361
+ },
362
+ {
363
+ enabled: isVisible && canColorHighlightState,
364
+ enableOnContentEditable: !isMobile,
365
+ enableOnFormTags: true,
366
+ }
367
+ )
368
+
369
+ return {
370
+ isVisible,
371
+ isActive,
372
+ handleColorHighlight,
373
+ handleRemoveHighlight,
374
+ canColorHighlight: canColorHighlightState,
375
+ label: label || `Highlight`,
376
+ shortcutKeys: COLOR_HIGHLIGHT_SHORTCUT_KEY,
377
+ Icon: HighlighterIcon,
378
+ mode,
379
+ }
380
+ }