@betterstart/cli 0.1.3 → 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 -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 +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-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,252 @@
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
+ // --- UI Utils ---
10
+ import {
11
+ findNodePosition,
12
+ getSelectedBlockNodes,
13
+ isNodeInSchema,
14
+ isNodeTypeSelected,
15
+ isValidPosition,
16
+ selectionWithinConvertibleTypes
17
+ } from '../../lib/tiptap-utils'
18
+ // --- Icons ---
19
+ import { BlockquoteIcon } from '../../tiptap-icons/blockquote-icon'
20
+
21
+ export const BLOCKQUOTE_SHORTCUT_KEY = 'mod+shift+b'
22
+
23
+ /**
24
+ * Configuration for the blockquote functionality
25
+ */
26
+ export interface UseBlockquoteConfig {
27
+ /**
28
+ * The Tiptap editor instance.
29
+ */
30
+ editor?: Editor | null
31
+ /**
32
+ * Whether the button should hide when blockquote is not available.
33
+ * @default false
34
+ */
35
+ hideWhenUnavailable?: boolean
36
+ /**
37
+ * Callback function called after a successful toggle.
38
+ */
39
+ onToggled?: () => void
40
+ }
41
+
42
+ /**
43
+ * Checks if blockquote can be toggled in the current editor state
44
+ */
45
+ export function canToggleBlockquote(editor: Editor | null, turnInto: boolean = true): boolean {
46
+ if (!editor || !editor.isEditable) return false
47
+ if (!isNodeInSchema('blockquote', editor) || isNodeTypeSelected(editor, ['image'])) return false
48
+
49
+ if (!turnInto) {
50
+ return editor.can().toggleWrap('blockquote')
51
+ }
52
+
53
+ // Ensure selection is in nodes we're allowed to convert
54
+ if (
55
+ !selectionWithinConvertibleTypes(editor, [
56
+ 'paragraph',
57
+ 'heading',
58
+ 'bulletList',
59
+ 'orderedList',
60
+ 'taskList',
61
+ 'blockquote',
62
+ 'codeBlock'
63
+ ])
64
+ )
65
+ return false
66
+
67
+ // Either we can wrap in blockquote directly on the selection,
68
+ // or we can clear formatting/nodes to arrive at a blockquote.
69
+ return editor.can().toggleWrap('blockquote') || editor.can().clearNodes()
70
+ }
71
+
72
+ /**
73
+ * Toggles blockquote formatting for a specific node or the current selection
74
+ */
75
+ export function toggleBlockquote(editor: Editor | null): boolean {
76
+ if (!editor || !editor.isEditable) return false
77
+ if (!canToggleBlockquote(editor)) return false
78
+
79
+ try {
80
+ const view = editor.view
81
+ let state = view.state
82
+ let tr = state.tr
83
+
84
+ const blocks = getSelectedBlockNodes(editor)
85
+
86
+ // In case a selection contains multiple blocks, we only allow
87
+ // toggling to nide if there's exactly one block selected
88
+ // we also dont block the canToggle since it will fall back to the bottom logic
89
+ const isPossibleToTurnInto =
90
+ selectionWithinConvertibleTypes(editor, [
91
+ 'paragraph',
92
+ 'heading',
93
+ 'bulletList',
94
+ 'orderedList',
95
+ 'taskList',
96
+ 'blockquote',
97
+ 'codeBlock'
98
+ ]) && blocks.length === 1
99
+
100
+ // No selection, find the the cursor position
101
+ if (
102
+ (state.selection.empty || state.selection instanceof TextSelection) &&
103
+ isPossibleToTurnInto
104
+ ) {
105
+ const pos = findNodePosition({
106
+ editor,
107
+ node: state.selection.$anchor.node(1)
108
+ })?.pos
109
+ if (!isValidPosition(pos)) return false
110
+
111
+ tr = tr.setSelection(NodeSelection.create(state.doc, pos))
112
+ view.dispatch(tr)
113
+ state = view.state
114
+ }
115
+
116
+ const selection = state.selection
117
+
118
+ let chain = editor.chain().focus()
119
+
120
+ // Handle NodeSelection
121
+ if (selection instanceof NodeSelection) {
122
+ const firstChild = selection.node.firstChild?.firstChild
123
+ const lastChild = selection.node.lastChild?.lastChild
124
+
125
+ const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
126
+
127
+ const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
128
+
129
+ const resolvedFrom = state.doc.resolve(from)
130
+ const resolvedTo = state.doc.resolve(to)
131
+
132
+ chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
133
+ }
134
+
135
+ const toggle = editor.isActive('blockquote')
136
+ ? chain.lift('blockquote')
137
+ : chain.wrapIn('blockquote')
138
+
139
+ toggle.run()
140
+
141
+ editor.chain().focus().selectTextblockEnd().run()
142
+
143
+ return true
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Determines if the blockquote button should be shown
151
+ */
152
+ export function shouldShowButton(props: {
153
+ editor: Editor | null
154
+ hideWhenUnavailable: boolean
155
+ }): boolean {
156
+ const { editor, hideWhenUnavailable } = props
157
+
158
+ if (!editor || !editor.isEditable) return false
159
+
160
+ if (!hideWhenUnavailable) {
161
+ return true
162
+ }
163
+
164
+ if (!isNodeInSchema('blockquote', editor)) return false
165
+
166
+ if (!editor.isActive('code')) {
167
+ return canToggleBlockquote(editor)
168
+ }
169
+
170
+ return true
171
+ }
172
+
173
+ /**
174
+ * Custom hook that provides blockquote functionality for Tiptap editor
175
+ *
176
+ * @example
177
+ * ```tsx
178
+ * // Simple usage - no params needed
179
+ * function MySimpleBlockquoteButton() {
180
+ * const { isVisible, handleToggle, isActive } = useBlockquote()
181
+ *
182
+ * if (!isVisible) return null
183
+ *
184
+ * return <button onClick={handleToggle}>Blockquote</button>
185
+ * }
186
+ *
187
+ * // Advanced usage with configuration
188
+ * function MyAdvancedBlockquoteButton() {
189
+ * const { isVisible, handleToggle, label, isActive } = useBlockquote({
190
+ * editor: myEditor,
191
+ * hideWhenUnavailable: true,
192
+ * onToggled: () => console.log('Blockquote toggled!')
193
+ * })
194
+ *
195
+ * if (!isVisible) return null
196
+ *
197
+ * return (
198
+ * <MyButton
199
+ * onClick={handleToggle}
200
+ * aria-label={label}
201
+ * aria-pressed={isActive}
202
+ * >
203
+ * Toggle Blockquote
204
+ * </MyButton>
205
+ * )
206
+ * }
207
+ * ```
208
+ */
209
+ export function useBlockquote(config?: UseBlockquoteConfig) {
210
+ const { editor: providedEditor, hideWhenUnavailable = false, onToggled } = config || {}
211
+
212
+ const { editor } = useTiptapEditor(providedEditor)
213
+ const [isVisible, setIsVisible] = useState<boolean>(true)
214
+ const canToggle = canToggleBlockquote(editor)
215
+ const isActive = editor?.isActive('blockquote') || false
216
+
217
+ useEffect(() => {
218
+ if (!editor) return
219
+
220
+ const handleSelectionUpdate = () => {
221
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
222
+ }
223
+
224
+ handleSelectionUpdate()
225
+
226
+ editor.on('selectionUpdate', handleSelectionUpdate)
227
+
228
+ return () => {
229
+ editor.off('selectionUpdate', handleSelectionUpdate)
230
+ }
231
+ }, [editor, hideWhenUnavailable])
232
+
233
+ const handleToggle = useCallback(() => {
234
+ if (!editor) return false
235
+
236
+ const success = toggleBlockquote(editor)
237
+ if (success) {
238
+ onToggled?.()
239
+ }
240
+ return success
241
+ }, [editor, onToggled])
242
+
243
+ return {
244
+ isVisible,
245
+ isActive,
246
+ handleToggle,
247
+ canToggle,
248
+ label: 'Blockquote',
249
+ shortcutKeys: BLOCKQUOTE_SHORTCUT_KEY,
250
+ Icon: BlockquoteIcon
251
+ }
252
+ }
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback } from 'react'
4
+
5
+ // --- Hooks ---
6
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
7
+
8
+ // --- Lib ---
9
+ import { parseShortcutKeys } from '../../lib/tiptap-utils'
10
+ import { Badge } from '../../tiptap-ui-primitive/badge'
11
+ // --- UI Primitives ---
12
+ import type { ButtonProps } from '../../tiptap-ui-primitive/button'
13
+ import { Button } from '../../tiptap-ui-primitive/button'
14
+ // --- Tiptap UI ---
15
+ import type { UseCodeBlockConfig } from '.'
16
+ import { CODE_BLOCK_SHORTCUT_KEY, useCodeBlock } from '.'
17
+
18
+ export interface CodeBlockButtonProps extends Omit<ButtonProps, 'type'>, UseCodeBlockConfig {
19
+ /**
20
+ * Optional text to display alongside the icon.
21
+ */
22
+ text?: string
23
+ /**
24
+ * Optional show shortcut keys in the button.
25
+ * @default false
26
+ */
27
+ showShortcut?: boolean
28
+ }
29
+
30
+ export function CodeBlockShortcutBadge({
31
+ shortcutKeys = CODE_BLOCK_SHORTCUT_KEY
32
+ }: {
33
+ shortcutKeys?: string
34
+ }) {
35
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
36
+ }
37
+
38
+ /**
39
+ * Button component for toggling code block in a Tiptap editor.
40
+ *
41
+ * For custom button implementations, use the `useCodeBlock` hook instead.
42
+ */
43
+ export const CodeBlockButton = forwardRef<HTMLButtonElement, CodeBlockButtonProps>(
44
+ (
45
+ {
46
+ editor: providedEditor,
47
+ text,
48
+ hideWhenUnavailable = false,
49
+ onToggled,
50
+ showShortcut = false,
51
+ onClick,
52
+ children,
53
+ ...buttonProps
54
+ },
55
+ ref
56
+ ) => {
57
+ const { editor } = useTiptapEditor(providedEditor)
58
+ const { isVisible, canToggle, isActive, handleToggle, label, shortcutKeys, Icon } =
59
+ useCodeBlock({
60
+ editor,
61
+ hideWhenUnavailable,
62
+ onToggled
63
+ })
64
+
65
+ const handleClick = useCallback(
66
+ (event: React.MouseEvent<HTMLButtonElement>) => {
67
+ onClick?.(event)
68
+ if (event.defaultPrevented) return
69
+ handleToggle()
70
+ },
71
+ [handleToggle, onClick]
72
+ )
73
+
74
+ if (!isVisible) {
75
+ return null
76
+ }
77
+
78
+ return (
79
+ <Button
80
+ type="button"
81
+ variant="ghost"
82
+ data-active-state={isActive ? 'on' : 'off'}
83
+ role="button"
84
+ disabled={!canToggle}
85
+ data-disabled={!canToggle}
86
+ tabIndex={-1}
87
+ aria-label={label}
88
+ aria-pressed={isActive}
89
+ tooltip="Code Block"
90
+ onClick={handleClick}
91
+ {...buttonProps}
92
+ ref={ref}
93
+ >
94
+ {children ?? (
95
+ <>
96
+ <Icon className="tiptap-button-icon" />
97
+ {text && <span className="tiptap-button-text">{text}</span>}
98
+ {showShortcut && <CodeBlockShortcutBadge shortcutKeys={shortcutKeys} />}
99
+ </>
100
+ )}
101
+ </Button>
102
+ )
103
+ }
104
+ )
105
+
106
+ CodeBlockButton.displayName = 'CodeBlockButton'
@@ -0,0 +1,2 @@
1
+ export * from './code-block-button'
2
+ export * from './use-code-block'
@@ -0,0 +1,261 @@
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
+
20
+ // --- Icons ---
21
+ import { CodeBlockIcon } from '../../tiptap-icons/code-block-icon'
22
+
23
+ export const CODE_BLOCK_SHORTCUT_KEY = 'mod+alt+c'
24
+
25
+ /**
26
+ * Configuration for the code block functionality
27
+ */
28
+ export interface UseCodeBlockConfig {
29
+ /**
30
+ * The Tiptap editor instance.
31
+ */
32
+ editor?: Editor | null
33
+ /**
34
+ * Whether the button should hide when code block is not available.
35
+ * @default false
36
+ */
37
+ hideWhenUnavailable?: boolean
38
+ /**
39
+ * Callback function called after a successful code block toggle.
40
+ */
41
+ onToggled?: () => void
42
+ }
43
+
44
+ /**
45
+ * Checks if code block can be toggled in the current editor state
46
+ */
47
+ export function canToggle(editor: Editor | null, turnInto: boolean = true): boolean {
48
+ if (!editor || !editor.isEditable) return false
49
+ if (!isNodeInSchema('codeBlock', editor) || isNodeTypeSelected(editor, ['image'])) return false
50
+
51
+ if (!turnInto) {
52
+ return editor.can().toggleNode('codeBlock', 'paragraph')
53
+ }
54
+
55
+ // Ensure selection is in nodes we're allowed to convert
56
+ if (
57
+ !selectionWithinConvertibleTypes(editor, [
58
+ 'paragraph',
59
+ 'heading',
60
+ 'bulletList',
61
+ 'orderedList',
62
+ 'taskList',
63
+ 'blockquote',
64
+ 'codeBlock'
65
+ ])
66
+ )
67
+ return false
68
+
69
+ // Either we can toggle code block directly on the selection,
70
+ // or we can clear formatting/nodes to arrive at a code block.
71
+ return editor.can().toggleNode('codeBlock', 'paragraph') || editor.can().clearNodes()
72
+ }
73
+
74
+ /**
75
+ * Toggles code block in the editor
76
+ */
77
+ export function toggleCodeBlock(editor: Editor | null): boolean {
78
+ if (!editor || !editor.isEditable) return false
79
+ if (!canToggle(editor)) return false
80
+
81
+ try {
82
+ const view = editor.view
83
+ let state = view.state
84
+ let tr = state.tr
85
+
86
+ const blocks = getSelectedBlockNodes(editor)
87
+
88
+ // In case a selection contains multiple blocks, we only allow
89
+ // toggling to nide if there's exactly one block selected
90
+ // we also dont block the canToggle since it will fall back to the bottom logic
91
+ const isPossibleToTurnInto =
92
+ selectionWithinConvertibleTypes(editor, [
93
+ 'paragraph',
94
+ 'heading',
95
+ 'bulletList',
96
+ 'orderedList',
97
+ 'taskList',
98
+ 'blockquote',
99
+ 'codeBlock'
100
+ ]) && blocks.length === 1
101
+
102
+ // No selection, find the the cursor position
103
+ if (
104
+ (state.selection.empty || state.selection instanceof TextSelection) &&
105
+ isPossibleToTurnInto
106
+ ) {
107
+ const pos = findNodePosition({
108
+ editor,
109
+ node: state.selection.$anchor.node(1)
110
+ })?.pos
111
+ if (!isValidPosition(pos)) return false
112
+
113
+ tr = tr.setSelection(NodeSelection.create(state.doc, pos))
114
+ view.dispatch(tr)
115
+ state = view.state
116
+ }
117
+
118
+ const selection = state.selection
119
+
120
+ let chain = editor.chain().focus()
121
+
122
+ // Handle NodeSelection
123
+ if (selection instanceof NodeSelection) {
124
+ const firstChild = selection.node.firstChild?.firstChild
125
+ const lastChild = selection.node.lastChild?.lastChild
126
+
127
+ const from = firstChild ? selection.from + firstChild.nodeSize : selection.from + 1
128
+
129
+ const to = lastChild ? selection.to - lastChild.nodeSize : selection.to - 1
130
+
131
+ const resolvedFrom = state.doc.resolve(from)
132
+ const resolvedTo = state.doc.resolve(to)
133
+
134
+ chain = chain.setTextSelection(TextSelection.between(resolvedFrom, resolvedTo)).clearNodes()
135
+ }
136
+
137
+ const toggle = editor.isActive('codeBlock')
138
+ ? chain.setNode('paragraph')
139
+ : chain.toggleNode('codeBlock', 'paragraph')
140
+
141
+ toggle.run()
142
+
143
+ editor.chain().focus().selectTextblockEnd().run()
144
+
145
+ return true
146
+ } catch {
147
+ return false
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Determines if the code block button should be shown
153
+ */
154
+ export function shouldShowButton(props: {
155
+ editor: Editor | null
156
+ hideWhenUnavailable: boolean
157
+ }): boolean {
158
+ const { editor, hideWhenUnavailable } = props
159
+
160
+ if (!editor || !editor.isEditable) return false
161
+
162
+ if (!hideWhenUnavailable) {
163
+ return true
164
+ }
165
+
166
+ if (!isNodeInSchema('codeBlock', editor)) return false
167
+
168
+ if (!editor.isActive('code')) {
169
+ return canToggle(editor)
170
+ }
171
+
172
+ return true
173
+ }
174
+
175
+ /**
176
+ * Custom hook that provides code block functionality for Tiptap editor
177
+ *
178
+ * @example
179
+ * ```tsx
180
+ * // Simple usage - no params needed
181
+ * function MySimpleCodeBlockButton() {
182
+ * const { isVisible, isActive, handleToggle } = useCodeBlock()
183
+ *
184
+ * if (!isVisible) return null
185
+ *
186
+ * return (
187
+ * <button
188
+ * onClick={handleToggle}
189
+ * aria-pressed={isActive}
190
+ * >
191
+ * Code Block
192
+ * </button>
193
+ * )
194
+ * }
195
+ *
196
+ * // Advanced usage with configuration
197
+ * function MyAdvancedCodeBlockButton() {
198
+ * const { isVisible, isActive, handleToggle, label } = useCodeBlock({
199
+ * editor: myEditor,
200
+ * hideWhenUnavailable: true,
201
+ * onToggled: (isActive) => console.log('Code block toggled:', isActive)
202
+ * })
203
+ *
204
+ * if (!isVisible) return null
205
+ *
206
+ * return (
207
+ * <MyButton
208
+ * onClick={handleToggle}
209
+ * aria-label={label}
210
+ * aria-pressed={isActive}
211
+ * >
212
+ * Toggle Code Block
213
+ * </MyButton>
214
+ * )
215
+ * }
216
+ * ```
217
+ */
218
+ export function useCodeBlock(config?: UseCodeBlockConfig) {
219
+ const { editor: providedEditor, hideWhenUnavailable = false, onToggled } = config || {}
220
+
221
+ const { editor } = useTiptapEditor(providedEditor)
222
+ const [isVisible, setIsVisible] = useState<boolean>(true)
223
+ const canToggleState = canToggle(editor)
224
+ const isActive = editor?.isActive('codeBlock') || false
225
+
226
+ useEffect(() => {
227
+ if (!editor) return
228
+
229
+ const handleSelectionUpdate = () => {
230
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable }))
231
+ }
232
+
233
+ handleSelectionUpdate()
234
+
235
+ editor.on('selectionUpdate', handleSelectionUpdate)
236
+
237
+ return () => {
238
+ editor.off('selectionUpdate', handleSelectionUpdate)
239
+ }
240
+ }, [editor, hideWhenUnavailable])
241
+
242
+ const handleToggle = useCallback(() => {
243
+ if (!editor) return false
244
+
245
+ const success = toggleCodeBlock(editor)
246
+ if (success) {
247
+ onToggled?.()
248
+ }
249
+ return success
250
+ }, [editor, onToggled])
251
+
252
+ return {
253
+ isVisible,
254
+ isActive,
255
+ handleToggle,
256
+ canToggle: canToggleState,
257
+ label: 'Code Block',
258
+ shortcutKeys: CODE_BLOCK_SHORTCUT_KEY,
259
+ Icon: CodeBlockIcon
260
+ }
261
+ }
@@ -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
+ }