@betterstart/cli 0.1.3 → 0.1.5

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 +13585 -367
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -266
  6. package/dist/index.js +4 -11378
  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 +687 -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-EIH4RRIJ.js +0 -183
  215. package/dist/chunk-EIH4RRIJ.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,107 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback } from 'react'
4
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
5
+ // --- Lib ---
6
+ import { parseShortcutKeys } from '../../lib/tiptap-utils'
7
+ import { Badge } from '../../tiptap-ui-primitive/badge'
8
+
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 { Level, UseHeadingConfig } from '.'
14
+ import { HEADING_SHORTCUT_KEYS, useHeading } from '.'
15
+
16
+ export interface HeadingButtonProps extends Omit<ButtonProps, 'type'>, UseHeadingConfig {
17
+ /**
18
+ * Optional text to display alongside the icon.
19
+ */
20
+ text?: string
21
+ /**
22
+ * Optional show shortcut keys in the button.
23
+ * @default false
24
+ */
25
+ showShortcut?: boolean
26
+ }
27
+
28
+ export function HeadingShortcutBadge({
29
+ level,
30
+ shortcutKeys = HEADING_SHORTCUT_KEYS[level]
31
+ }: {
32
+ level: Level
33
+ shortcutKeys?: string
34
+ }) {
35
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
36
+ }
37
+
38
+ /**
39
+ * Button component for toggling heading in a Tiptap editor.
40
+ *
41
+ * For custom button implementations, use the `useHeading` hook instead.
42
+ */
43
+ export const HeadingButton = forwardRef<HTMLButtonElement, HeadingButtonProps>(
44
+ (
45
+ {
46
+ editor: providedEditor,
47
+ level,
48
+ text,
49
+ hideWhenUnavailable = false,
50
+ onToggled,
51
+ showShortcut = false,
52
+ onClick,
53
+ children,
54
+ ...buttonProps
55
+ },
56
+ ref
57
+ ) => {
58
+ const { editor } = useTiptapEditor(providedEditor)
59
+ const { isVisible, canToggle, isActive, handleToggle, label, Icon, shortcutKeys } = useHeading({
60
+ editor,
61
+ level,
62
+ hideWhenUnavailable,
63
+ onToggled
64
+ })
65
+
66
+ const handleClick = useCallback(
67
+ (event: React.MouseEvent<HTMLButtonElement>) => {
68
+ onClick?.(event)
69
+ if (event.defaultPrevented) return
70
+ handleToggle()
71
+ },
72
+ [handleToggle, onClick]
73
+ )
74
+
75
+ if (!isVisible) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <Button
81
+ type="button"
82
+ variant="ghost"
83
+ data-active-state={isActive ? 'on' : 'off'}
84
+ role="button"
85
+ tabIndex={-1}
86
+ disabled={!canToggle}
87
+ data-disabled={!canToggle}
88
+ aria-label={label}
89
+ aria-pressed={isActive}
90
+ tooltip={label}
91
+ onClick={handleClick}
92
+ {...buttonProps}
93
+ ref={ref}
94
+ >
95
+ {children ?? (
96
+ <>
97
+ <Icon className="tiptap-button-icon" />
98
+ {text && <span className="tiptap-button-text">{text}</span>}
99
+ {showShortcut && <HeadingShortcutBadge level={level} shortcutKeys={shortcutKeys} />}
100
+ </>
101
+ )}
102
+ </Button>
103
+ )
104
+ }
105
+ )
106
+
107
+ HeadingButton.displayName = 'HeadingButton'
@@ -0,0 +1,2 @@
1
+ export * from './heading-button'
2
+ export * from './use-heading'
@@ -0,0 +1,314 @@
1
+ 'use client'
2
+
3
+ import { NodeSelection, TextSelection } from '@tiptap/pm/state'
4
+ import type { Editor } from '@tiptap/react'
5
+ import { useCallback, useEffect, useState } from 'react'
6
+
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
9
+
10
+ // --- Lib ---
11
+ import {
12
+ findNodePosition,
13
+ getSelectedBlockNodes,
14
+ isNodeInSchema,
15
+ isNodeTypeSelected,
16
+ isValidPosition,
17
+ selectionWithinConvertibleTypes
18
+ } from '../../lib/tiptap-utils'
19
+ import { HeadingFiveIcon } from '../../tiptap-icons/heading-five-icon'
20
+ import { HeadingFourIcon } from '../../tiptap-icons/heading-four-icon'
21
+ // --- Icons ---
22
+ import { HeadingOneIcon } from '../../tiptap-icons/heading-one-icon'
23
+ import { HeadingSixIcon } from '../../tiptap-icons/heading-six-icon'
24
+ import { HeadingThreeIcon } from '../../tiptap-icons/heading-three-icon'
25
+ import { HeadingTwoIcon } from '../../tiptap-icons/heading-two-icon'
26
+
27
+ export type Level = 1 | 2 | 3 | 4 | 5 | 6
28
+
29
+ /**
30
+ * Configuration for the heading functionality
31
+ */
32
+ export interface UseHeadingConfig {
33
+ /**
34
+ * The Tiptap editor instance.
35
+ */
36
+ editor?: Editor | null
37
+ /**
38
+ * The heading level.
39
+ */
40
+ level: Level
41
+ /**
42
+ * Whether the button should hide when heading is not available.
43
+ * @default false
44
+ */
45
+ hideWhenUnavailable?: boolean
46
+ /**
47
+ * Callback function called after a successful heading toggle.
48
+ */
49
+ onToggled?: () => void
50
+ }
51
+
52
+ export const headingIcons = {
53
+ 1: HeadingOneIcon,
54
+ 2: HeadingTwoIcon,
55
+ 3: HeadingThreeIcon,
56
+ 4: HeadingFourIcon,
57
+ 5: HeadingFiveIcon,
58
+ 6: HeadingSixIcon
59
+ }
60
+
61
+ export const HEADING_SHORTCUT_KEYS: Record<Level, string> = {
62
+ 1: 'ctrl+alt+1',
63
+ 2: 'ctrl+alt+2',
64
+ 3: 'ctrl+alt+3',
65
+ 4: 'ctrl+alt+4',
66
+ 5: 'ctrl+alt+5',
67
+ 6: 'ctrl+alt+6'
68
+ }
69
+
70
+ /**
71
+ * Checks if heading can be toggled in the current editor state
72
+ */
73
+ export function canToggle(editor: Editor | null, level?: Level, turnInto: boolean = true): boolean {
74
+ if (!editor || !editor.isEditable) return false
75
+ if (!isNodeInSchema('heading', editor) || isNodeTypeSelected(editor, ['image'])) return false
76
+
77
+ if (!turnInto) {
78
+ return level ? editor.can().setNode('heading', { level }) : editor.can().setNode('heading')
79
+ }
80
+
81
+ // Ensure selection is in nodes we're allowed to convert
82
+ if (
83
+ !selectionWithinConvertibleTypes(editor, [
84
+ 'paragraph',
85
+ 'heading',
86
+ 'bulletList',
87
+ 'orderedList',
88
+ 'taskList',
89
+ 'blockquote',
90
+ 'codeBlock'
91
+ ])
92
+ )
93
+ return false
94
+
95
+ // Either we can set heading directly on the selection,
96
+ // or we can clear formatting/nodes to arrive at a heading.
97
+ return level
98
+ ? editor.can().setNode('heading', { level }) || editor.can().clearNodes()
99
+ : editor.can().setNode('heading') || editor.can().clearNodes()
100
+ }
101
+
102
+ /**
103
+ * Checks if heading is currently active
104
+ */
105
+ export function isHeadingActive(editor: Editor | null, level?: Level | Level[]): boolean {
106
+ if (!editor || !editor.isEditable) return false
107
+
108
+ if (Array.isArray(level)) {
109
+ return level.some((l) => editor.isActive('heading', { level: l }))
110
+ }
111
+
112
+ return level ? editor.isActive('heading', { level }) : editor.isActive('heading')
113
+ }
114
+
115
+ /**
116
+ * Toggles heading in the editor
117
+ */
118
+ export function toggleHeading(editor: Editor | null, level: Level | Level[]): boolean {
119
+ if (!editor || !editor.isEditable) return false
120
+
121
+ const levels = Array.isArray(level) ? level : [level]
122
+ const toggleLevel = levels.find((l) => canToggle(editor, l))
123
+
124
+ if (!toggleLevel) return false
125
+
126
+ try {
127
+ const view = editor.view
128
+ let state = view.state
129
+ let tr = state.tr
130
+
131
+ const blocks = getSelectedBlockNodes(editor)
132
+
133
+ // In case a selection contains multiple blocks, we only allow
134
+ // toggling to nide if there's exactly one block selected
135
+ // we also dont block the canToggle since it will fall back to the bottom logic
136
+ const isPossibleToTurnInto =
137
+ selectionWithinConvertibleTypes(editor, [
138
+ 'paragraph',
139
+ 'heading',
140
+ 'bulletList',
141
+ 'orderedList',
142
+ 'taskList',
143
+ 'blockquote',
144
+ 'codeBlock'
145
+ ]) && blocks.length === 1
146
+
147
+ // No selection, find the the cursor position
148
+ if (
149
+ (state.selection.empty || state.selection instanceof TextSelection) &&
150
+ isPossibleToTurnInto
151
+ ) {
152
+ const pos = findNodePosition({
153
+ editor,
154
+ node: state.selection.$anchor.node(1)
155
+ })?.pos
156
+ if (!isValidPosition(pos)) return false
157
+
158
+ tr = tr.setSelection(NodeSelection.create(state.doc, pos))
159
+ view.dispatch(tr)
160
+ state = view.state
161
+ }
162
+
163
+ const selection = state.selection
164
+ let chain = editor.chain().focus()
165
+
166
+ // Handle NodeSelection
167
+ if (selection instanceof NodeSelection) {
168
+ const firstChild = selection.node.firstChild?.firstChild
169
+ const lastChild = selection.node.lastChild?.lastChild
170
+
171
+ const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
172
+
173
+ const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
174
+
175
+ const resolvedFrom = state.doc.resolve(from)
176
+ const resolvedTo = state.doc.resolve(to)
177
+
178
+ chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
179
+ }
180
+
181
+ const isActive = levels.some((l) => editor.isActive('heading', { level: l }))
182
+
183
+ const toggle = isActive
184
+ ? chain.setNode('paragraph')
185
+ : chain.setNode('heading', { level: toggleLevel })
186
+
187
+ toggle.run()
188
+
189
+ editor.chain().focus().selectTextblockEnd().run()
190
+
191
+ return true
192
+ } catch {
193
+ return false
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Determines if the heading button should be shown
199
+ */
200
+ export function shouldShowButton(props: {
201
+ editor: Editor | null
202
+ level?: Level | Level[]
203
+ hideWhenUnavailable: boolean
204
+ }): boolean {
205
+ const { editor, level, hideWhenUnavailable } = props
206
+
207
+ if (!editor || !editor.isEditable) return false
208
+
209
+ if (!hideWhenUnavailable) {
210
+ return true
211
+ }
212
+
213
+ if (!isNodeInSchema('heading', editor)) return false
214
+
215
+ if (!editor.isActive('code')) {
216
+ if (Array.isArray(level)) {
217
+ return level.some((l) => canToggle(editor, l))
218
+ }
219
+ return canToggle(editor, level)
220
+ }
221
+
222
+ return true
223
+ }
224
+
225
+ /**
226
+ * Custom hook that provides heading functionality for Tiptap editor
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * // Simple usage
231
+ * function MySimpleHeadingButton() {
232
+ * const { isVisible, isActive, handleToggle, Icon } = useHeading({ level: 1 })
233
+ *
234
+ * if (!isVisible) return null
235
+ *
236
+ * return (
237
+ * <button
238
+ * onClick={handleToggle}
239
+ * aria-pressed={isActive}
240
+ * >
241
+ * <Icon />
242
+ * Heading 1
243
+ * </button>
244
+ * )
245
+ * }
246
+ *
247
+ * // Advanced usage with configuration
248
+ * function MyAdvancedHeadingButton() {
249
+ * const { isVisible, isActive, handleToggle, label, Icon } = useHeading({
250
+ * level: 2,
251
+ * editor: myEditor,
252
+ * hideWhenUnavailable: true,
253
+ * onToggled: (isActive) => console.log('Heading toggled:', isActive)
254
+ * })
255
+ *
256
+ * if (!isVisible) return null
257
+ *
258
+ * return (
259
+ * <MyButton
260
+ * onClick={handleToggle}
261
+ * aria-label={label}
262
+ * aria-pressed={isActive}
263
+ * >
264
+ * <Icon />
265
+ * Toggle Heading 2
266
+ * </MyButton>
267
+ * )
268
+ * }
269
+ * ```
270
+ */
271
+ export function useHeading(config: UseHeadingConfig) {
272
+ const { editor: providedEditor, level, hideWhenUnavailable = false, onToggled } = config
273
+
274
+ const { editor } = useTiptapEditor(providedEditor)
275
+ const [isVisible, setIsVisible] = useState<boolean>(true)
276
+ const canToggleState = canToggle(editor, level)
277
+ const isActive = isHeadingActive(editor, level)
278
+
279
+ useEffect(() => {
280
+ if (!editor) return
281
+
282
+ const handleSelectionUpdate = () => {
283
+ setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable }))
284
+ }
285
+
286
+ handleSelectionUpdate()
287
+
288
+ editor.on('selectionUpdate', handleSelectionUpdate)
289
+
290
+ return () => {
291
+ editor.off('selectionUpdate', handleSelectionUpdate)
292
+ }
293
+ }, [editor, level, hideWhenUnavailable])
294
+
295
+ const handleToggle = useCallback(() => {
296
+ if (!editor) return false
297
+
298
+ const success = toggleHeading(editor, level)
299
+ if (success) {
300
+ onToggled?.()
301
+ }
302
+ return success
303
+ }, [editor, level, onToggled])
304
+
305
+ return {
306
+ isVisible,
307
+ isActive,
308
+ handleToggle,
309
+ canToggle: canToggleState,
310
+ label: `Heading ${level}`,
311
+ shortcutKeys: HEADING_SHORTCUT_KEYS[level],
312
+ Icon: headingIcons[level]
313
+ }
314
+ }
@@ -0,0 +1,131 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback, useState } from 'react'
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
6
+ // --- Icons ---
7
+ import { ChevronDownIcon } from '../../tiptap-icons/chevron-down-icon'
8
+ // --- UI Primitives ---
9
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
10
+ import { Button, ButtonGroup } from '../../tiptap-ui-primitive/button'
11
+ import { Card, CardBody } from '../../tiptap-ui-primitive/card'
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuTrigger
17
+ } from '../../tiptap-ui-primitive/dropdown-menu'
18
+ // --- Tiptap UI ---
19
+ import { HeadingButton } from '../heading-button'
20
+ import type { UseHeadingDropdownMenuConfig } from '.'
21
+ import { useHeadingDropdownMenu } from '.'
22
+
23
+ export interface HeadingDropdownMenuProps
24
+ extends Omit<ButtonProps, 'type'>,
25
+ UseHeadingDropdownMenuConfig {
26
+ /**
27
+ * Whether to render the dropdown menu in a portal
28
+ * @default false
29
+ */
30
+ portal?: boolean
31
+ /**
32
+ * Callback for when the dropdown opens or closes
33
+ */
34
+ onOpenChange?: (isOpen: boolean) => void
35
+ }
36
+
37
+ /**
38
+ * Dropdown menu component for selecting heading levels in a Tiptap editor.
39
+ *
40
+ * For custom dropdown implementations, use the `useHeadingDropdownMenu` hook instead.
41
+ */
42
+ export const HeadingDropdownMenu = forwardRef<HTMLButtonElement, HeadingDropdownMenuProps>(
43
+ (
44
+ {
45
+ editor: providedEditor,
46
+ levels = [2, 3, 4, 5, 6],
47
+ hideWhenUnavailable = false,
48
+ portal = false,
49
+ onOpenChange,
50
+ children,
51
+ ...buttonProps
52
+ },
53
+ ref
54
+ ) => {
55
+ const { editor } = useTiptapEditor(providedEditor)
56
+ const [isOpen, setIsOpen] = useState<boolean>(false)
57
+ const { isVisible, isActive, canToggle, Icon } = useHeadingDropdownMenu({
58
+ editor,
59
+ levels,
60
+ hideWhenUnavailable
61
+ })
62
+
63
+ const handleOpenChange = useCallback(
64
+ (open: boolean) => {
65
+ if (!editor || !canToggle) return
66
+ setIsOpen(open)
67
+ onOpenChange?.(open)
68
+ },
69
+ [canToggle, editor, onOpenChange]
70
+ )
71
+
72
+ if (!isVisible) {
73
+ return null
74
+ }
75
+
76
+ return (
77
+ <DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
78
+ <DropdownMenuTrigger asChild>
79
+ <Button
80
+ type="button"
81
+ variant="ghost"
82
+ data-active-state={isActive ? 'on' : 'off'}
83
+ role="button"
84
+ tabIndex={-1}
85
+ disabled={!canToggle}
86
+ data-disabled={!canToggle}
87
+ aria-label="Format text as heading"
88
+ aria-pressed={isActive}
89
+ tooltip="Heading"
90
+ {...buttonProps}
91
+ ref={ref}
92
+ onPointerDown={(e) => e.preventDefault()}
93
+ onClick={() => handleOpenChange(!isOpen)}
94
+ >
95
+ {children ? (
96
+ children
97
+ ) : (
98
+ <>
99
+ <Icon className="tiptap-button-icon" />
100
+ <ChevronDownIcon className="tiptap-button-dropdown-small" />
101
+ </>
102
+ )}
103
+ </Button>
104
+ </DropdownMenuTrigger>
105
+
106
+ <DropdownMenuContent align="start" portal={portal}>
107
+ <Card>
108
+ <CardBody>
109
+ <ButtonGroup>
110
+ {levels.map((level) => (
111
+ <DropdownMenuItem key={`heading-${level}`} asChild>
112
+ <HeadingButton
113
+ editor={editor}
114
+ level={level}
115
+ text={`Heading ${level}`}
116
+ showTooltip={false}
117
+ />
118
+ </DropdownMenuItem>
119
+ ))}
120
+ </ButtonGroup>
121
+ </CardBody>
122
+ </Card>
123
+ </DropdownMenuContent>
124
+ </DropdownMenu>
125
+ )
126
+ }
127
+ )
128
+
129
+ HeadingDropdownMenu.displayName = 'HeadingDropdownMenu'
130
+
131
+ export default HeadingDropdownMenu
@@ -0,0 +1,2 @@
1
+ export * from './heading-dropdown-menu'
2
+ export * from './use-heading-dropdown-menu'
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
8
+
9
+ // --- Icons ---
10
+ import { HeadingIcon } from '../../tiptap-icons/heading-icon'
11
+
12
+ // --- Tiptap UI ---
13
+ import {
14
+ canToggle,
15
+ headingIcons,
16
+ isHeadingActive,
17
+ type Level,
18
+ shouldShowButton
19
+ } from '../heading-button'
20
+
21
+ /**
22
+ * Configuration for the heading dropdown menu functionality
23
+ */
24
+ export interface UseHeadingDropdownMenuConfig {
25
+ /**
26
+ * The Tiptap editor instance.
27
+ */
28
+ editor?: Editor | null
29
+ /**
30
+ * Available heading levels to show in the dropdown
31
+ * @default [1, 2, 3, 4, 5, 6]
32
+ */
33
+ levels?: Level[]
34
+ /**
35
+ * Whether the dropdown should hide when headings are not available.
36
+ * @default false
37
+ */
38
+ hideWhenUnavailable?: boolean
39
+ }
40
+
41
+ /**
42
+ * Gets the currently active heading level from the available levels
43
+ */
44
+ export function getActiveHeadingLevel(
45
+ editor: Editor | null,
46
+ levels: Level[] = [1, 2, 3, 4, 5, 6]
47
+ ): Level | undefined {
48
+ if (!editor || !editor.isEditable) return undefined
49
+ return levels.find((level) => isHeadingActive(editor, level))
50
+ }
51
+
52
+ /**
53
+ * Custom hook that provides heading dropdown menu functionality for Tiptap editor
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * // Simple usage
58
+ * function MyHeadingDropdown() {
59
+ * const {
60
+ * isVisible,
61
+ * activeLevel,
62
+ * isAnyHeadingActive,
63
+ * canToggle,
64
+ * levels,
65
+ * } = useHeadingDropdownMenu()
66
+ *
67
+ * if (!isVisible) return null
68
+ *
69
+ * return (
70
+ * <DropdownMenu>
71
+ * // dropdown content
72
+ * </DropdownMenu>
73
+ * )
74
+ * }
75
+ *
76
+ * // Advanced usage with configuration
77
+ * function MyAdvancedHeadingDropdown() {
78
+ * const {
79
+ * isVisible,
80
+ * activeLevel,
81
+ * } = useHeadingDropdownMenu({
82
+ * editor: myEditor,
83
+ * levels: [1, 2, 3],
84
+ * hideWhenUnavailable: true,
85
+ * })
86
+ *
87
+ * // component implementation
88
+ * }
89
+ * ```
90
+ */
91
+ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) {
92
+ const {
93
+ editor: providedEditor,
94
+ levels = [1, 2, 3, 4, 5, 6],
95
+ hideWhenUnavailable = false
96
+ } = config || {}
97
+
98
+ const { editor } = useTiptapEditor(providedEditor)
99
+ const [isVisible, setIsVisible] = useState(true)
100
+
101
+ const activeLevel = getActiveHeadingLevel(editor, levels)
102
+ const isActive = isHeadingActive(editor)
103
+ const canToggleState = canToggle(editor)
104
+
105
+ useEffect(() => {
106
+ if (!editor) return
107
+
108
+ const handleSelectionUpdate = () => {
109
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, level: levels }))
110
+ }
111
+
112
+ handleSelectionUpdate()
113
+
114
+ editor.on('selectionUpdate', handleSelectionUpdate)
115
+
116
+ return () => {
117
+ editor.off('selectionUpdate', handleSelectionUpdate)
118
+ }
119
+ }, [editor, hideWhenUnavailable, levels])
120
+
121
+ return {
122
+ isVisible,
123
+ activeLevel,
124
+ isActive,
125
+ canToggle: canToggleState,
126
+ levels,
127
+ label: 'Heading',
128
+ Icon: activeLevel ? headingIcons[activeLevel] : HeadingIcon
129
+ }
130
+ }