@betterstart/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -354
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -260
  6. package/dist/index.js +4 -11373
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-G4KI4DVB.js +0 -179
  215. package/dist/chunk-G4KI4DVB.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,153 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback, useMemo } from 'react'
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
6
+ // --- Lib ---
7
+ import { parseShortcutKeys } from '../../lib/tiptap-utils'
8
+ import { Badge } from '../../tiptap-ui-primitive/badge'
9
+ // --- UI Primitives ---
10
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
11
+ import { Button } from '../../tiptap-ui-primitive/button'
12
+ // --- Tiptap UI ---
13
+ import type { UseColorHighlightConfig } from '.'
14
+ import { COLOR_HIGHLIGHT_SHORTCUT_KEY, useColorHighlight } from '.'
15
+
16
+ // --- Styles ---
17
+ import './color-highlight-button.scss'
18
+
19
+ export interface ColorHighlightButtonProps
20
+ extends Omit<ButtonProps, 'type'>,
21
+ UseColorHighlightConfig {
22
+ /**
23
+ * Optional text to display alongside the icon.
24
+ */
25
+ text?: string
26
+ /**
27
+ * Optional show shortcut keys in the button.
28
+ * @default false
29
+ */
30
+ showShortcut?: boolean
31
+ }
32
+
33
+ export function ColorHighlightShortcutBadge({
34
+ shortcutKeys = COLOR_HIGHLIGHT_SHORTCUT_KEY
35
+ }: {
36
+ shortcutKeys?: string
37
+ }) {
38
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
39
+ }
40
+
41
+ /**
42
+ * Button component for applying color highlights in a Tiptap editor.
43
+ *
44
+ * Supports two highlighting modes:
45
+ * - "mark": Uses the highlight mark extension (default)
46
+ * - "node": Uses the node background extension
47
+ *
48
+ * For custom button implementations, use the `useColorHighlight` hook instead.
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * // Mark-based highlighting (default)
53
+ * <ColorHighlightButton highlightColor="yellow" />
54
+ *
55
+ * // Node-based background coloring
56
+ * <ColorHighlightButton
57
+ * highlightColor="var(--tt-color-highlight-blue)"
58
+ * mode="node"
59
+ * />
60
+ *
61
+ * // With custom callback
62
+ * <ColorHighlightButton
63
+ * highlightColor="red"
64
+ * mode="mark"
65
+ * onApplied={({ color, mode }) => console.log(`Applied ${color} in ${mode} mode`)}
66
+ * />
67
+ * ```
68
+ */
69
+ export const ColorHighlightButton = forwardRef<HTMLButtonElement, ColorHighlightButtonProps>(
70
+ (
71
+ {
72
+ editor: providedEditor,
73
+ highlightColor,
74
+ text,
75
+ hideWhenUnavailable = false,
76
+ mode = 'mark',
77
+ onApplied,
78
+ showShortcut = false,
79
+ onClick,
80
+ children,
81
+ style,
82
+ useColorValue = false,
83
+ ...buttonProps
84
+ },
85
+ ref
86
+ ) => {
87
+ const { editor } = useTiptapEditor(providedEditor)
88
+ const { isVisible, canColorHighlight, isActive, handleColorHighlight, label, shortcutKeys } =
89
+ useColorHighlight({
90
+ editor,
91
+ highlightColor,
92
+ useColorValue,
93
+ label: text || `Toggle highlight (${highlightColor})`,
94
+ hideWhenUnavailable,
95
+ mode,
96
+ onApplied
97
+ })
98
+
99
+ const handleClick = useCallback(
100
+ (event: React.MouseEvent<HTMLButtonElement>) => {
101
+ onClick?.(event)
102
+ if (event.defaultPrevented) return
103
+ handleColorHighlight()
104
+ },
105
+ [handleColorHighlight, onClick]
106
+ )
107
+
108
+ const buttonStyle = useMemo(
109
+ () =>
110
+ ({
111
+ ...style,
112
+ '--highlight-color': highlightColor
113
+ }) as React.CSSProperties,
114
+ [highlightColor, style]
115
+ )
116
+
117
+ if (!isVisible) {
118
+ return null
119
+ }
120
+
121
+ return (
122
+ <Button
123
+ type="button"
124
+ variant="ghost"
125
+ data-active-state={isActive ? 'on' : 'off'}
126
+ role="button"
127
+ tabIndex={-1}
128
+ disabled={!canColorHighlight}
129
+ data-disabled={!canColorHighlight}
130
+ aria-label={label}
131
+ aria-pressed={isActive}
132
+ tooltip={label}
133
+ onClick={handleClick}
134
+ style={buttonStyle}
135
+ {...buttonProps}
136
+ ref={ref}
137
+ >
138
+ {children ?? (
139
+ <>
140
+ <span
141
+ className="tiptap-button-highlight"
142
+ style={{ '--highlight-color': highlightColor } as React.CSSProperties}
143
+ />
144
+ {text && <span className="tiptap-button-text">{text}</span>}
145
+ {showShortcut && <ColorHighlightShortcutBadge shortcutKeys={shortcutKeys} />}
146
+ </>
147
+ )}
148
+ </Button>
149
+ )
150
+ }
151
+ )
152
+
153
+ ColorHighlightButton.displayName = 'ColorHighlightButton'
@@ -0,0 +1,2 @@
1
+ export * from './color-highlight-button'
2
+ export * from './use-color-highlight'
@@ -0,0 +1,345 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+ import { useHotkeys } from 'react-hotkeys-hook'
6
+ import { useIsBreakpoint } from '../../hooks/use-is-breakpoint'
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+
10
+ // --- Lib ---
11
+ import { isExtensionAvailable, isMarkInSchema, isNodeTypeSelected } from '../../lib/tiptap-utils'
12
+
13
+ // --- Icons ---
14
+ import { HighlighterIcon } from '../../tiptap-icons/highlighter-icon'
15
+
16
+ export const COLOR_HIGHLIGHT_SHORTCUT_KEY = 'mod+shift+h'
17
+ export const HIGHLIGHT_COLORS = [
18
+ {
19
+ label: 'Default background',
20
+ value: 'var(--tt-bg-color)',
21
+ colorValue: '#ffffff',
22
+ border: 'var(--tt-bg-color-contrast)'
23
+ },
24
+ {
25
+ label: 'Gray background',
26
+ value: 'var(--tt-color-highlight-gray)',
27
+ colorValue: '#f8f8f7',
28
+ border: 'var(--tt-color-highlight-gray-contrast)'
29
+ },
30
+ {
31
+ label: 'Brown background',
32
+ value: 'var(--tt-color-highlight-brown)',
33
+ colorValue: '#f4eeee',
34
+ border: 'var(--tt-color-highlight-brown-contrast)'
35
+ },
36
+ {
37
+ label: 'Orange background',
38
+ value: 'var(--tt-color-highlight-orange)',
39
+ colorValue: '#fbecdd',
40
+ border: 'var(--tt-color-highlight-orange-contrast)'
41
+ },
42
+ {
43
+ label: 'Yellow background',
44
+ value: 'var(--tt-color-highlight-yellow)',
45
+ colorValue: '#fef9c3',
46
+ border: 'var(--tt-color-highlight-yellow-contrast)'
47
+ },
48
+ {
49
+ label: 'Green background',
50
+ value: 'var(--tt-color-highlight-green)',
51
+ colorValue: '#dcfce7',
52
+ border: 'var(--tt-color-highlight-green-contrast)'
53
+ },
54
+ {
55
+ label: 'Blue background',
56
+ value: 'var(--tt-color-highlight-blue)',
57
+ colorValue: '#e0f2fe',
58
+ border: 'var(--tt-color-highlight-blue-contrast)'
59
+ },
60
+ {
61
+ label: 'Purple background',
62
+ value: 'var(--tt-color-highlight-purple)',
63
+ colorValue: '#f3e8ff',
64
+ border: 'var(--tt-color-highlight-purple-contrast)'
65
+ },
66
+ {
67
+ label: 'Pink background',
68
+ value: 'var(--tt-color-highlight-pink)',
69
+ colorValue: '#fcf1f6',
70
+ border: 'var(--tt-color-highlight-pink-contrast)'
71
+ },
72
+ {
73
+ label: 'Red background',
74
+ value: 'var(--tt-color-highlight-red)',
75
+ colorValue: '#ffe4e6',
76
+ border: 'var(--tt-color-highlight-red-contrast)'
77
+ }
78
+ ]
79
+ export type HighlightColor = (typeof HIGHLIGHT_COLORS)[number]
80
+
81
+ export type HighlightMode = 'mark' | 'node'
82
+
83
+ /**
84
+ * Configuration for the color highlight functionality
85
+ */
86
+ export interface UseColorHighlightConfig {
87
+ /**
88
+ * The Tiptap editor instance.
89
+ */
90
+ editor?: Editor | null
91
+ /**
92
+ * The color to apply when toggling the highlight.
93
+ */
94
+ highlightColor?: string
95
+ /**
96
+ * Optional label to display alongside the icon.
97
+ */
98
+ label?: string
99
+ /**
100
+ * Whether the button should hide when the mark is not available.
101
+ * @default false
102
+ */
103
+ hideWhenUnavailable?: boolean
104
+ /**
105
+ * The highlighting mode to use.
106
+ * - "mark": Uses the highlight mark extension (default)
107
+ * - "node": Uses the node background extension
108
+ * @default "mark"
109
+ */
110
+ mode?: HighlightMode
111
+ /**
112
+ * When true, uses the actual color value (colorValue) instead of CSS variable (value).
113
+ * @default false
114
+ */
115
+ useColorValue?: boolean
116
+ /**
117
+ * Called when the highlight is applied.
118
+ */
119
+ onApplied?: ({
120
+ color,
121
+ label,
122
+ mode
123
+ }: {
124
+ color: string
125
+ label: string
126
+ mode: HighlightMode
127
+ }) => void
128
+ }
129
+
130
+ export function pickHighlightColorsByValue(values: string[]) {
131
+ const colorMap = new Map(HIGHLIGHT_COLORS.map((color) => [color.value, color]))
132
+ return values
133
+ .map((value) => colorMap.get(value))
134
+ .filter((color): color is (typeof HIGHLIGHT_COLORS)[number] => !!color)
135
+ }
136
+
137
+ /**
138
+ * Gets the appropriate color value based on configuration
139
+ */
140
+ export function getHighlightColorValue(color: string, useColorValue: boolean = false): string {
141
+ if (!useColorValue) return color
142
+
143
+ const colorItem = HIGHLIGHT_COLORS.find((c) => c.value === color || c.colorValue === color)
144
+ return colorItem?.colorValue || color
145
+ }
146
+
147
+ /**
148
+ * Checks if highlight can be applied based on the mode and current editor state
149
+ */
150
+ export function canColorHighlight(editor: Editor | null, mode: HighlightMode = 'mark'): boolean {
151
+ if (!editor || !editor.isEditable) return false
152
+
153
+ if (mode === 'mark') {
154
+ if (!isMarkInSchema('highlight', editor) || isNodeTypeSelected(editor, ['image'])) return false
155
+
156
+ return editor.can().setMark('highlight')
157
+ } else {
158
+ if (!isExtensionAvailable(editor, ['nodeBackground'])) return false
159
+
160
+ try {
161
+ return editor.can().toggleNodeBackgroundColor('test')
162
+ } catch {
163
+ return false
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Checks if highlight is currently active
170
+ */
171
+ export function isColorHighlightActive(
172
+ editor: Editor | null,
173
+ highlightColor?: string,
174
+ mode: HighlightMode = 'mark'
175
+ ): boolean {
176
+ if (!editor || !editor.isEditable) return false
177
+
178
+ if (mode === 'mark') {
179
+ return highlightColor
180
+ ? editor.isActive('highlight', { color: highlightColor })
181
+ : editor.isActive('highlight')
182
+ } else {
183
+ if (!highlightColor) return false
184
+
185
+ try {
186
+ const { state } = editor
187
+ const { selection } = state
188
+
189
+ const $pos = selection.$anchor
190
+ for (let depth = $pos.depth; depth >= 0; depth--) {
191
+ const node = $pos.node(depth)
192
+ if (node && node.attrs?.backgroundColor === highlightColor) {
193
+ return true
194
+ }
195
+ }
196
+ return false
197
+ } catch {
198
+ return false
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Removes highlight based on the mode
205
+ */
206
+ export function removeHighlight(editor: Editor | null, mode: HighlightMode = 'mark'): boolean {
207
+ if (!editor || !editor.isEditable) return false
208
+ if (!canColorHighlight(editor, mode)) return false
209
+
210
+ if (mode === 'mark') {
211
+ return editor.chain().focus().unsetMark('highlight').run()
212
+ } else {
213
+ return editor.chain().focus().unsetNodeBackgroundColor().run()
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Determines if the highlight button should be shown
219
+ */
220
+ export function shouldShowButton(props: {
221
+ editor: Editor | null
222
+ hideWhenUnavailable: boolean
223
+ mode: HighlightMode
224
+ }): boolean {
225
+ const { editor, hideWhenUnavailable, mode } = props
226
+
227
+ if (!editor || !editor.isEditable) return false
228
+
229
+ if (!hideWhenUnavailable) {
230
+ return true
231
+ }
232
+
233
+ // hideWhenUnavailable=true: check schema/extension availability
234
+ if (mode === 'mark') {
235
+ if (!isMarkInSchema('highlight', editor)) return false
236
+ } else {
237
+ if (!isExtensionAvailable(editor, ['nodeBackground'])) return false
238
+ }
239
+
240
+ if (!editor.isActive('code')) {
241
+ return canColorHighlight(editor, mode)
242
+ }
243
+
244
+ return true
245
+ }
246
+
247
+ export function useColorHighlight(config: UseColorHighlightConfig) {
248
+ const {
249
+ editor: providedEditor,
250
+ label,
251
+ highlightColor,
252
+ hideWhenUnavailable = false,
253
+ mode = 'mark',
254
+ useColorValue = false,
255
+ onApplied
256
+ } = config
257
+
258
+ const { editor } = useTiptapEditor(providedEditor)
259
+ const isMobile = useIsBreakpoint()
260
+ const [isVisible, setIsVisible] = useState<boolean>(true)
261
+ const canColorHighlightState = canColorHighlight(editor, mode)
262
+ const actualColor = highlightColor
263
+ ? getHighlightColorValue(highlightColor, useColorValue)
264
+ : highlightColor
265
+ const isActive = isColorHighlightActive(editor, actualColor, mode)
266
+
267
+ useEffect(() => {
268
+ if (!editor) return
269
+
270
+ const handleSelectionUpdate = () => {
271
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode }))
272
+ }
273
+
274
+ handleSelectionUpdate()
275
+
276
+ editor.on('selectionUpdate', handleSelectionUpdate)
277
+
278
+ return () => {
279
+ editor.off('selectionUpdate', handleSelectionUpdate)
280
+ }
281
+ }, [editor, hideWhenUnavailable, mode])
282
+
283
+ const handleColorHighlight = useCallback(() => {
284
+ if (!editor || !canColorHighlightState || !actualColor || !label) return false
285
+
286
+ if (mode === 'mark') {
287
+ if (editor.state.storedMarks) {
288
+ const highlightMarkType = editor.schema.marks.highlight
289
+ if (highlightMarkType) {
290
+ editor.view.dispatch(editor.state.tr.removeStoredMark(highlightMarkType))
291
+ }
292
+ }
293
+
294
+ setTimeout(() => {
295
+ const success = editor.chain().focus().toggleHighlight({ color: actualColor }).run()
296
+ if (success) {
297
+ onApplied?.({ color: actualColor, label, mode })
298
+ }
299
+ return success
300
+ }, 0)
301
+
302
+ return true
303
+ } else {
304
+ const success = editor.chain().focus().toggleNodeBackgroundColor(actualColor).run()
305
+
306
+ if (success) {
307
+ onApplied?.({ color: actualColor, label, mode })
308
+ }
309
+ return success
310
+ }
311
+ }, [canColorHighlightState, actualColor, editor, label, onApplied, mode])
312
+
313
+ const handleRemoveHighlight = useCallback(() => {
314
+ const success = removeHighlight(editor, mode)
315
+ if (success) {
316
+ onApplied?.({ color: '', label: 'Remove highlight', mode })
317
+ }
318
+ return success
319
+ }, [editor, onApplied, mode])
320
+
321
+ useHotkeys(
322
+ COLOR_HIGHLIGHT_SHORTCUT_KEY,
323
+ (event) => {
324
+ event.preventDefault()
325
+ handleColorHighlight()
326
+ },
327
+ {
328
+ enabled: isVisible && canColorHighlightState,
329
+ enableOnContentEditable: !isMobile,
330
+ enableOnFormTags: true
331
+ }
332
+ )
333
+
334
+ return {
335
+ isVisible,
336
+ isActive,
337
+ handleColorHighlight,
338
+ handleRemoveHighlight,
339
+ canColorHighlight: canColorHighlightState,
340
+ label: label || `Highlight`,
341
+ shortcutKeys: COLOR_HIGHLIGHT_SHORTCUT_KEY,
342
+ Icon: HighlighterIcon,
343
+ mode
344
+ }
345
+ }
@@ -0,0 +1,207 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { forwardRef, useMemo, useRef, useState } from 'react'
5
+ import { useIsBreakpoint } from '../../hooks/use-is-breakpoint'
6
+ // --- Hooks ---
7
+ import { useMenuNavigation } from '../../hooks/use-menu-navigation'
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+
10
+ // --- Icons ---
11
+ import { BanIcon } from '../../tiptap-icons/ban-icon'
12
+ import { HighlighterIcon } from '../../tiptap-icons/highlighter-icon'
13
+
14
+ // --- UI Primitives ---
15
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
16
+ import { Button, ButtonGroup } from '../../tiptap-ui-primitive/button'
17
+ import { Card, CardBody, CardItemGroup } from '../../tiptap-ui-primitive/card'
18
+ import { Popover, PopoverContent, PopoverTrigger } from '../../tiptap-ui-primitive/popover'
19
+ import { Separator } from '../../tiptap-ui-primitive/separator'
20
+
21
+ // --- Tiptap UI ---
22
+ import type { HighlightColor, UseColorHighlightConfig } from '../color-highlight-button'
23
+ import {
24
+ ColorHighlightButton,
25
+ pickHighlightColorsByValue,
26
+ useColorHighlight
27
+ } from '../color-highlight-button'
28
+
29
+ export interface ColorHighlightPopoverContentProps {
30
+ /**
31
+ * The Tiptap editor instance.
32
+ */
33
+ editor?: Editor | null
34
+ /**
35
+ * Optional colors to use in the highlight popover.
36
+ * If not provided, defaults to a predefined set of colors.
37
+ */
38
+ colors?: HighlightColor[]
39
+ /**
40
+ * When true, uses the actual color value (colorValue) instead of CSS variable (value).
41
+ * @default false
42
+ */
43
+ useColorValue?: boolean
44
+ }
45
+
46
+ export interface ColorHighlightPopoverProps
47
+ extends Omit<ButtonProps, 'type'>,
48
+ Pick<UseColorHighlightConfig, 'editor' | 'hideWhenUnavailable' | 'onApplied'> {
49
+ /**
50
+ * Optional colors to use in the highlight popover.
51
+ * If not provided, defaults to a predefined set of colors.
52
+ */
53
+ colors?: HighlightColor[]
54
+ /**
55
+ * When true, uses the actual color value (colorValue) instead of CSS variable (value).
56
+ * @default false
57
+ */
58
+ useColorValue?: boolean
59
+ }
60
+
61
+ export const ColorHighlightPopoverButton = forwardRef<HTMLButtonElement, ButtonProps>(
62
+ ({ className, children, ...props }, ref) => (
63
+ <Button
64
+ type="button"
65
+ className={className}
66
+ variant="ghost"
67
+ data-appearance="default"
68
+ role="button"
69
+ tabIndex={-1}
70
+ aria-label="Highlight text"
71
+ tooltip="Highlight"
72
+ ref={ref}
73
+ {...props}
74
+ >
75
+ {children ?? <HighlighterIcon className="tiptap-button-icon" />}
76
+ </Button>
77
+ )
78
+ )
79
+
80
+ ColorHighlightPopoverButton.displayName = 'ColorHighlightPopoverButton'
81
+
82
+ export function ColorHighlightPopoverContent({
83
+ editor,
84
+ colors = pickHighlightColorsByValue([
85
+ 'var(--tt-color-highlight-green)',
86
+ 'var(--tt-color-highlight-blue)',
87
+ 'var(--tt-color-highlight-red)',
88
+ 'var(--tt-color-highlight-purple)',
89
+ 'var(--tt-color-highlight-yellow)'
90
+ ]),
91
+ useColorValue = false
92
+ }: ColorHighlightPopoverContentProps) {
93
+ const { handleRemoveHighlight } = useColorHighlight({ editor })
94
+ const isMobile = useIsBreakpoint()
95
+ const containerRef = useRef<HTMLDivElement>(null)
96
+
97
+ const menuItems = useMemo(
98
+ () => [...colors, { label: 'Remove highlight', value: 'none' }],
99
+ [colors]
100
+ )
101
+
102
+ const { selectedIndex } = useMenuNavigation({
103
+ containerRef,
104
+ items: menuItems,
105
+ orientation: 'both',
106
+ onSelect: (item) => {
107
+ if (!containerRef.current) return false
108
+ const highlightedElement = containerRef.current.querySelector(
109
+ '[data-highlighted="true"]'
110
+ ) as HTMLElement
111
+ if (highlightedElement) highlightedElement.click()
112
+ if (item.value === 'none') handleRemoveHighlight()
113
+ return true
114
+ },
115
+ autoSelectFirstItem: false
116
+ })
117
+
118
+ return (
119
+ <Card ref={containerRef} tabIndex={0} style={isMobile ? { boxShadow: 'none', border: 0 } : {}}>
120
+ <CardBody style={isMobile ? { padding: 0 } : {}}>
121
+ <CardItemGroup orientation="horizontal">
122
+ <ButtonGroup orientation="horizontal">
123
+ {colors.map((color, index) => (
124
+ <ColorHighlightButton
125
+ key={color.value}
126
+ editor={editor}
127
+ highlightColor={useColorValue ? color.colorValue : color.value}
128
+ tooltip={color.label}
129
+ aria-label={`${color.label} highlight color`}
130
+ tabIndex={index === selectedIndex ? 0 : -1}
131
+ data-highlighted={selectedIndex === index}
132
+ useColorValue={useColorValue}
133
+ />
134
+ ))}
135
+ </ButtonGroup>
136
+ <Separator />
137
+ <ButtonGroup orientation="horizontal">
138
+ <Button
139
+ onClick={handleRemoveHighlight}
140
+ aria-label="Remove highlight"
141
+ tooltip="Remove highlight"
142
+ tabIndex={selectedIndex === colors.length ? 0 : -1}
143
+ type="button"
144
+ role="menuitem"
145
+ variant="ghost"
146
+ data-highlighted={selectedIndex === colors.length}
147
+ >
148
+ <BanIcon className="tiptap-button-icon" />
149
+ </Button>
150
+ </ButtonGroup>
151
+ </CardItemGroup>
152
+ </CardBody>
153
+ </Card>
154
+ )
155
+ }
156
+
157
+ export function ColorHighlightPopover({
158
+ editor: providedEditor,
159
+ colors = pickHighlightColorsByValue([
160
+ 'var(--tt-color-highlight-green)',
161
+ 'var(--tt-color-highlight-blue)',
162
+ 'var(--tt-color-highlight-red)',
163
+ 'var(--tt-color-highlight-purple)',
164
+ 'var(--tt-color-highlight-yellow)'
165
+ ]),
166
+ hideWhenUnavailable = false,
167
+ useColorValue = false,
168
+ onApplied,
169
+ ...props
170
+ }: ColorHighlightPopoverProps) {
171
+ const { editor } = useTiptapEditor(providedEditor)
172
+ const [isOpen, setIsOpen] = useState(false)
173
+ const { isVisible, canColorHighlight, isActive, label, Icon } = useColorHighlight({
174
+ editor,
175
+ hideWhenUnavailable,
176
+ onApplied
177
+ })
178
+
179
+ if (!isVisible) return null
180
+
181
+ return (
182
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
183
+ <PopoverTrigger asChild>
184
+ <ColorHighlightPopoverButton
185
+ disabled={!canColorHighlight}
186
+ data-active-state={isActive ? 'on' : 'off'}
187
+ data-disabled={!canColorHighlight}
188
+ aria-pressed={isActive}
189
+ aria-label={label}
190
+ tooltip={label}
191
+ {...props}
192
+ >
193
+ <Icon className="tiptap-button-icon" />
194
+ </ColorHighlightPopoverButton>
195
+ </PopoverTrigger>
196
+ <PopoverContent aria-label="Highlight colors">
197
+ <ColorHighlightPopoverContent
198
+ editor={editor}
199
+ colors={colors}
200
+ useColorValue={useColorValue}
201
+ />
202
+ </PopoverContent>
203
+ </Popover>
204
+ )
205
+ }
206
+
207
+ export default ColorHighlightPopover
@@ -0,0 +1 @@
1
+ export * from './color-highlight-popover'