@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,118 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback } 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 { TextAlign, UseTextAlignConfig } from '.'
14
+ import { TEXT_ALIGN_SHORTCUT_KEYS, useTextAlign } from '.'
15
+
16
+ type IconProps = React.SVGProps<SVGSVGElement>
17
+ type IconComponent = ({ className, ...props }: IconProps) => React.ReactElement
18
+
19
+ export interface TextAlignButtonProps extends Omit<ButtonProps, 'type'>, UseTextAlignConfig {
20
+ /**
21
+ * Optional text to display alongside the icon.
22
+ */
23
+ text?: string
24
+ /**
25
+ * Optional show shortcut keys in the button.
26
+ * @default false
27
+ */
28
+ showShortcut?: boolean
29
+ /**
30
+ * Optional custom icon component to render instead of the default.
31
+ */
32
+ icon?: React.MemoExoticComponent<IconComponent> | React.FC<IconProps>
33
+ }
34
+
35
+ export function TextAlignShortcutBadge({
36
+ align,
37
+ shortcutKeys = TEXT_ALIGN_SHORTCUT_KEYS[align]
38
+ }: {
39
+ align: TextAlign
40
+ shortcutKeys?: string
41
+ }) {
42
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
43
+ }
44
+
45
+ /**
46
+ * Button component for setting text alignment in a Tiptap editor.
47
+ *
48
+ * For custom button implementations, use the `useTextAlign` hook instead.
49
+ */
50
+ export const TextAlignButton = forwardRef<HTMLButtonElement, TextAlignButtonProps>(
51
+ (
52
+ {
53
+ editor: providedEditor,
54
+ align,
55
+ text,
56
+ hideWhenUnavailable = false,
57
+ onAligned,
58
+ showShortcut = false,
59
+ onClick,
60
+ icon: CustomIcon,
61
+ children,
62
+ ...buttonProps
63
+ },
64
+ ref
65
+ ) => {
66
+ const { editor } = useTiptapEditor(providedEditor)
67
+ const { isVisible, handleTextAlign, label, canAlign, isActive, Icon, shortcutKeys } =
68
+ useTextAlign({
69
+ editor,
70
+ align,
71
+ hideWhenUnavailable,
72
+ onAligned
73
+ })
74
+
75
+ const handleClick = useCallback(
76
+ (event: React.MouseEvent<HTMLButtonElement>) => {
77
+ onClick?.(event)
78
+ if (event.defaultPrevented) return
79
+ handleTextAlign()
80
+ },
81
+ [handleTextAlign, onClick]
82
+ )
83
+
84
+ if (!isVisible) {
85
+ return null
86
+ }
87
+
88
+ const RenderIcon = CustomIcon ?? Icon
89
+
90
+ return (
91
+ <Button
92
+ type="button"
93
+ disabled={!canAlign}
94
+ variant="ghost"
95
+ data-active-state={isActive ? 'on' : 'off'}
96
+ data-disabled={!canAlign}
97
+ role="button"
98
+ tabIndex={-1}
99
+ aria-label={label}
100
+ aria-pressed={isActive}
101
+ tooltip={label}
102
+ onClick={handleClick}
103
+ {...buttonProps}
104
+ ref={ref}
105
+ >
106
+ {children ?? (
107
+ <>
108
+ <RenderIcon className="tiptap-button-icon" />
109
+ {text && <span className="tiptap-button-text">{text}</span>}
110
+ {showShortcut && <TextAlignShortcutBadge align={align} shortcutKeys={shortcutKeys} />}
111
+ </>
112
+ )}
113
+ </Button>
114
+ )
115
+ }
116
+ )
117
+
118
+ TextAlignButton.displayName = 'TextAlignButton'
@@ -0,0 +1,212 @@
1
+ 'use client'
2
+
3
+ import type { ChainedCommands, Editor } from '@tiptap/react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
8
+
9
+ // --- Lib ---
10
+ import { isExtensionAvailable, isNodeTypeSelected } from '../../lib/tiptap-utils'
11
+
12
+ // --- Icons ---
13
+ import { AlignCenterIcon } from '../../tiptap-icons/align-center-icon'
14
+ import { AlignJustifyIcon } from '../../tiptap-icons/align-justify-icon'
15
+ import { AlignLeftIcon } from '../../tiptap-icons/align-left-icon'
16
+ import { AlignRightIcon } from '../../tiptap-icons/align-right-icon'
17
+
18
+ export type TextAlign = 'left' | 'center' | 'right' | 'justify'
19
+
20
+ /**
21
+ * Configuration for the text align functionality
22
+ */
23
+ export interface UseTextAlignConfig {
24
+ /**
25
+ * The Tiptap editor instance.
26
+ */
27
+ editor?: Editor | null
28
+ /**
29
+ * The text alignment to apply.
30
+ */
31
+ align: TextAlign
32
+ /**
33
+ * Whether the button should hide when alignment is not available.
34
+ * @default false
35
+ */
36
+ hideWhenUnavailable?: boolean
37
+ /**
38
+ * Callback function called after a successful alignment change.
39
+ */
40
+ onAligned?: () => void
41
+ }
42
+
43
+ export const TEXT_ALIGN_SHORTCUT_KEYS: Record<TextAlign, string> = {
44
+ left: 'mod+shift+l',
45
+ center: 'mod+shift+e',
46
+ right: 'mod+shift+r',
47
+ justify: 'mod+shift+j'
48
+ }
49
+
50
+ export const textAlignIcons = {
51
+ left: AlignLeftIcon,
52
+ center: AlignCenterIcon,
53
+ right: AlignRightIcon,
54
+ justify: AlignJustifyIcon
55
+ }
56
+
57
+ export const textAlignLabels: Record<TextAlign, string> = {
58
+ left: 'Align left',
59
+ center: 'Align center',
60
+ right: 'Align right',
61
+ justify: 'Align justify'
62
+ }
63
+
64
+ /**
65
+ * Checks if text alignment can be performed in the current editor state
66
+ */
67
+ export function canSetTextAlign(editor: Editor | null, align: TextAlign): boolean {
68
+ if (!editor || !editor.isEditable) return false
69
+ if (
70
+ !isExtensionAvailable(editor, 'textAlign') ||
71
+ isNodeTypeSelected(editor, ['image', 'horizontalRule'])
72
+ )
73
+ return false
74
+
75
+ return editor.can().setTextAlign(align)
76
+ }
77
+
78
+ export function hasSetTextAlign(commands: ChainedCommands): commands is ChainedCommands & {
79
+ setTextAlign: (align: TextAlign) => ChainedCommands
80
+ } {
81
+ return 'setTextAlign' in commands
82
+ }
83
+
84
+ /**
85
+ * Checks if the text alignment is currently active
86
+ */
87
+ export function isTextAlignActive(editor: Editor | null, align: TextAlign): boolean {
88
+ if (!editor || !editor.isEditable) return false
89
+ return editor.isActive({ textAlign: align })
90
+ }
91
+
92
+ /**
93
+ * Sets text alignment in the editor
94
+ */
95
+ export function setTextAlign(editor: Editor | null, align: TextAlign): boolean {
96
+ if (!editor || !editor.isEditable) return false
97
+ if (!canSetTextAlign(editor, align)) return false
98
+
99
+ const chain = editor.chain().focus()
100
+ if (hasSetTextAlign(chain)) {
101
+ return chain.setTextAlign(align).run()
102
+ }
103
+
104
+ return false
105
+ }
106
+
107
+ /**
108
+ * Determines if the text align button should be shown
109
+ */
110
+ export function shouldShowButton(props: {
111
+ editor: Editor | null
112
+ hideWhenUnavailable: boolean
113
+ align: TextAlign
114
+ }): boolean {
115
+ const { editor, hideWhenUnavailable, align } = props
116
+
117
+ if (!editor || !editor.isEditable) return false
118
+
119
+ if (!hideWhenUnavailable) {
120
+ return true
121
+ }
122
+
123
+ if (!isExtensionAvailable(editor, 'textAlign')) return false
124
+
125
+ if (!editor.isActive('code')) {
126
+ return canSetTextAlign(editor, align)
127
+ }
128
+
129
+ return true
130
+ }
131
+
132
+ /**
133
+ * Custom hook that provides text align functionality for Tiptap editor
134
+ *
135
+ * @example
136
+ * ```tsx
137
+ * // Simple usage
138
+ * function MySimpleAlignButton() {
139
+ * const { isVisible, handleTextAlign } = useTextAlign({ align: "center" })
140
+ *
141
+ * if (!isVisible) return null
142
+ *
143
+ * return <button onClick={handleTextAlign}>Align Center</button>
144
+ * }
145
+ *
146
+ * // Advanced usage with configuration
147
+ * function MyAdvancedAlignButton() {
148
+ * const { isVisible, handleTextAlign, label, isActive } = useTextAlign({
149
+ * editor: myEditor,
150
+ * align: "right",
151
+ * hideWhenUnavailable: true,
152
+ * onAligned: () => console.log('Text aligned!')
153
+ * })
154
+ *
155
+ * if (!isVisible) return null
156
+ *
157
+ * return (
158
+ * <MyButton
159
+ * onClick={handleTextAlign}
160
+ * aria-pressed={isActive}
161
+ * aria-label={label}
162
+ * >
163
+ * Align Right
164
+ * </MyButton>
165
+ * )
166
+ * }
167
+ * ```
168
+ */
169
+ export function useTextAlign(config: UseTextAlignConfig) {
170
+ const { editor: providedEditor, align, hideWhenUnavailable = false, onAligned } = config
171
+
172
+ const { editor } = useTiptapEditor(providedEditor)
173
+ const [isVisible, setIsVisible] = useState<boolean>(true)
174
+ const canAlign = canSetTextAlign(editor, align)
175
+ const isActive = isTextAlignActive(editor, align)
176
+
177
+ useEffect(() => {
178
+ if (!editor) return
179
+
180
+ const handleSelectionUpdate = () => {
181
+ setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable }))
182
+ }
183
+
184
+ handleSelectionUpdate()
185
+
186
+ editor.on('selectionUpdate', handleSelectionUpdate)
187
+
188
+ return () => {
189
+ editor.off('selectionUpdate', handleSelectionUpdate)
190
+ }
191
+ }, [editor, hideWhenUnavailable, align])
192
+
193
+ const handleTextAlign = useCallback(() => {
194
+ if (!editor) return false
195
+
196
+ const success = setTextAlign(editor, align)
197
+ if (success) {
198
+ onAligned?.()
199
+ }
200
+ return success
201
+ }, [editor, align, onAligned])
202
+
203
+ return {
204
+ isVisible,
205
+ isActive,
206
+ handleTextAlign,
207
+ canAlign,
208
+ label: textAlignLabels[align],
209
+ shortcutKeys: TEXT_ALIGN_SHORTCUT_KEYS[align],
210
+ Icon: textAlignIcons[align]
211
+ }
212
+ }
@@ -0,0 +1,2 @@
1
+ export * from './undo-redo-button'
2
+ export * from './use-undo-redo'
@@ -0,0 +1,105 @@
1
+ 'use client'
2
+
3
+ import { forwardRef, useCallback } 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 { UndoRedoAction, UseUndoRedoConfig } from '.'
14
+ import { UNDO_REDO_SHORTCUT_KEYS, useUndoRedo } from '.'
15
+
16
+ export interface UndoRedoButtonProps extends Omit<ButtonProps, 'type'>, UseUndoRedoConfig {
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 HistoryShortcutBadge({
29
+ action,
30
+ shortcutKeys = UNDO_REDO_SHORTCUT_KEYS[action]
31
+ }: {
32
+ action: UndoRedoAction
33
+ shortcutKeys?: string
34
+ }) {
35
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
36
+ }
37
+
38
+ /**
39
+ * Button component for triggering undo/redo actions in a Tiptap editor.
40
+ *
41
+ * For custom button implementations, use the `useHistory` hook instead.
42
+ */
43
+ export const UndoRedoButton = forwardRef<HTMLButtonElement, UndoRedoButtonProps>(
44
+ (
45
+ {
46
+ editor: providedEditor,
47
+ action,
48
+ text,
49
+ hideWhenUnavailable = false,
50
+ onExecuted,
51
+ showShortcut = false,
52
+ onClick,
53
+ children,
54
+ ...buttonProps
55
+ },
56
+ ref
57
+ ) => {
58
+ const { editor } = useTiptapEditor(providedEditor)
59
+ const { isVisible, handleAction, label, canExecute, Icon, shortcutKeys } = useUndoRedo({
60
+ editor,
61
+ action,
62
+ hideWhenUnavailable,
63
+ onExecuted
64
+ })
65
+
66
+ const handleClick = useCallback(
67
+ (event: React.MouseEvent<HTMLButtonElement>) => {
68
+ onClick?.(event)
69
+ if (event.defaultPrevented) return
70
+ handleAction()
71
+ },
72
+ [handleAction, onClick]
73
+ )
74
+
75
+ if (!isVisible) {
76
+ return null
77
+ }
78
+
79
+ return (
80
+ <Button
81
+ type="button"
82
+ disabled={!canExecute}
83
+ variant="ghost"
84
+ data-disabled={!canExecute}
85
+ role="button"
86
+ tabIndex={-1}
87
+ aria-label={label}
88
+ tooltip={label}
89
+ onClick={handleClick}
90
+ {...buttonProps}
91
+ ref={ref}
92
+ >
93
+ {children ?? (
94
+ <>
95
+ <Icon className="tiptap-button-icon" />
96
+ {text && <span className="tiptap-button-text">{text}</span>}
97
+ {showShortcut && <HistoryShortcutBadge action={action} shortcutKeys={shortcutKeys} />}
98
+ </>
99
+ )}
100
+ </Button>
101
+ )
102
+ }
103
+ )
104
+
105
+ UndoRedoButton.displayName = 'UndoRedoButton'
@@ -0,0 +1,173 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useCallback, useEffect, useState } from 'react'
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from '../../hooks/use-tiptap-editor'
8
+
9
+ // --- Lib ---
10
+ import { isNodeTypeSelected } from '../../lib/tiptap-utils'
11
+
12
+ // --- Icons ---
13
+ import { Redo2Icon } from '../../tiptap-icons/redo2-icon'
14
+ import { Undo2Icon } from '../../tiptap-icons/undo2-icon'
15
+
16
+ export type UndoRedoAction = 'undo' | 'redo'
17
+
18
+ /**
19
+ * Configuration for the history functionality
20
+ */
21
+ export interface UseUndoRedoConfig {
22
+ /**
23
+ * The Tiptap editor instance.
24
+ */
25
+ editor?: Editor | null
26
+ /**
27
+ * The history action to perform (undo or redo).
28
+ */
29
+ action: UndoRedoAction
30
+ /**
31
+ * Whether the button should hide when action is not available.
32
+ * @default false
33
+ */
34
+ hideWhenUnavailable?: boolean
35
+ /**
36
+ * Callback function called after a successful action execution.
37
+ */
38
+ onExecuted?: () => void
39
+ }
40
+
41
+ export const UNDO_REDO_SHORTCUT_KEYS: Record<UndoRedoAction, string> = {
42
+ undo: 'mod+z',
43
+ redo: 'mod+shift+z'
44
+ }
45
+
46
+ export const historyActionLabels: Record<UndoRedoAction, string> = {
47
+ undo: 'Undo',
48
+ redo: 'Redo'
49
+ }
50
+
51
+ export const historyIcons = {
52
+ undo: Undo2Icon,
53
+ redo: Redo2Icon
54
+ }
55
+
56
+ /**
57
+ * Checks if a history action can be executed
58
+ */
59
+ export function canExecuteUndoRedoAction(editor: Editor | null, action: UndoRedoAction): boolean {
60
+ if (!editor || !editor.isEditable) return false
61
+ if (isNodeTypeSelected(editor, ['image'])) return false
62
+
63
+ return action === 'undo' ? editor.can().undo() : editor.can().redo()
64
+ }
65
+
66
+ /**
67
+ * Executes a history action on the editor
68
+ */
69
+ export function executeUndoRedoAction(editor: Editor | null, action: UndoRedoAction): boolean {
70
+ if (!editor || !editor.isEditable) return false
71
+ if (!canExecuteUndoRedoAction(editor, action)) return false
72
+
73
+ const chain = editor.chain().focus()
74
+ return action === 'undo' ? chain.undo().run() : chain.redo().run()
75
+ }
76
+
77
+ /**
78
+ * Determines if the history button should be shown
79
+ */
80
+ export function shouldShowButton(props: {
81
+ editor: Editor | null
82
+ hideWhenUnavailable: boolean
83
+ action: UndoRedoAction
84
+ }): boolean {
85
+ const { editor, hideWhenUnavailable, action } = props
86
+
87
+ if (!editor || !editor.isEditable) return false
88
+
89
+ if (hideWhenUnavailable && !editor.isActive('code')) {
90
+ return canExecuteUndoRedoAction(editor, action)
91
+ }
92
+
93
+ return true
94
+ }
95
+
96
+ /**
97
+ * Custom hook that provides history functionality for Tiptap editor
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // Simple usage
102
+ * function MySimpleUndoButton() {
103
+ * const { isVisible, handleAction } = useHistory({ action: "undo" })
104
+ *
105
+ * if (!isVisible) return null
106
+ *
107
+ * return <button onClick={handleAction}>Undo</button>
108
+ * }
109
+ *
110
+ * // Advanced usage with configuration
111
+ * function MyAdvancedRedoButton() {
112
+ * const { isVisible, handleAction, label } = useHistory({
113
+ * editor: myEditor,
114
+ * action: "redo",
115
+ * hideWhenUnavailable: true,
116
+ * onExecuted: () => console.log('Action executed!')
117
+ * })
118
+ *
119
+ * if (!isVisible) return null
120
+ *
121
+ * return (
122
+ * <MyButton
123
+ * onClick={handleAction}
124
+ * aria-label={label}
125
+ * >
126
+ * Redo
127
+ * </MyButton>
128
+ * )
129
+ * }
130
+ * ```
131
+ */
132
+ export function useUndoRedo(config: UseUndoRedoConfig) {
133
+ const { editor: providedEditor, action, hideWhenUnavailable = false, onExecuted } = config
134
+
135
+ const { editor } = useTiptapEditor(providedEditor)
136
+ const [isVisible, setIsVisible] = useState<boolean>(true)
137
+ const canExecute = canExecuteUndoRedoAction(editor, action)
138
+
139
+ useEffect(() => {
140
+ if (!editor) return
141
+
142
+ const handleUpdate = () => {
143
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action }))
144
+ }
145
+
146
+ handleUpdate()
147
+
148
+ editor.on('transaction', handleUpdate)
149
+
150
+ return () => {
151
+ editor.off('transaction', handleUpdate)
152
+ }
153
+ }, [editor, hideWhenUnavailable, action])
154
+
155
+ const handleAction = useCallback(() => {
156
+ if (!editor) return false
157
+
158
+ const success = executeUndoRedoAction(editor, action)
159
+ if (success) {
160
+ onExecuted?.()
161
+ }
162
+ return success
163
+ }, [editor, action, onExecuted])
164
+
165
+ return {
166
+ isVisible,
167
+ handleAction,
168
+ canExecute,
169
+ label: historyActionLabels[action],
170
+ shortcutKeys: UNDO_REDO_SHORTCUT_KEYS[action],
171
+ Icon: historyIcons[action]
172
+ }
173
+ }