create-starlight-cms 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/THIRD_PARTY_NOTICES.md +4 -0
  4. package/bin/index.js +147 -0
  5. package/package.json +46 -0
  6. package/template/.nvmrc +1 -0
  7. package/template/LICENSE +21 -0
  8. package/template/README.md +68 -0
  9. package/template/THIRD_PARTY_NOTICES.md +12 -0
  10. package/template/_gitignore +14 -0
  11. package/template/astro.config.mjs +26 -0
  12. package/template/migrations/0001_schema.sql +73 -0
  13. package/template/migrations/0002_publish_delivery.sql +16 -0
  14. package/template/package-lock.json.template +9123 -0
  15. package/template/package.json +89 -0
  16. package/template/scripts/build-admin.mjs +38 -0
  17. package/template/scripts/check-linux-bindings.mjs +18 -0
  18. package/template/scripts/dev.mjs +94 -0
  19. package/template/src/admin/admin.scss +182 -0
  20. package/template/src/admin/app.ts +130 -0
  21. package/template/src/admin/client.tsx +684 -0
  22. package/template/src/admin/components/tiptap-extension/node-background-extension.ts +150 -0
  23. package/template/src/admin/components/tiptap-icons/align-center-icon.tsx +38 -0
  24. package/template/src/admin/components/tiptap-icons/align-justify-icon.tsx +38 -0
  25. package/template/src/admin/components/tiptap-icons/align-left-icon.tsx +38 -0
  26. package/template/src/admin/components/tiptap-icons/align-right-icon.tsx +38 -0
  27. package/template/src/admin/components/tiptap-icons/arrow-left-icon.tsx +24 -0
  28. package/template/src/admin/components/tiptap-icons/arrow-right-icon.tsx +26 -0
  29. package/template/src/admin/components/tiptap-icons/ban-icon.tsx +26 -0
  30. package/template/src/admin/components/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/template/src/admin/components/tiptap-icons/bold-icon.tsx +26 -0
  32. package/template/src/admin/components/tiptap-icons/case-sensitive-icon.tsx +32 -0
  33. package/template/src/admin/components/tiptap-icons/check-icon.tsx +26 -0
  34. package/template/src/admin/components/tiptap-icons/chevron-down-icon.tsx +26 -0
  35. package/template/src/admin/components/tiptap-icons/chevron-up-icon.tsx +22 -0
  36. package/template/src/admin/components/tiptap-icons/close-icon.tsx +24 -0
  37. package/template/src/admin/components/tiptap-icons/code-block-icon.tsx +38 -0
  38. package/template/src/admin/components/tiptap-icons/code2-icon.tsx +32 -0
  39. package/template/src/admin/components/tiptap-icons/corner-down-left-icon.tsx +26 -0
  40. package/template/src/admin/components/tiptap-icons/external-link-icon.tsx +28 -0
  41. package/template/src/admin/components/tiptap-icons/heading-five-icon.tsx +28 -0
  42. package/template/src/admin/components/tiptap-icons/heading-four-icon.tsx +28 -0
  43. package/template/src/admin/components/tiptap-icons/heading-icon.tsx +24 -0
  44. package/template/src/admin/components/tiptap-icons/heading-one-icon.tsx +28 -0
  45. package/template/src/admin/components/tiptap-icons/heading-six-icon.tsx +30 -0
  46. package/template/src/admin/components/tiptap-icons/heading-three-icon.tsx +36 -0
  47. package/template/src/admin/components/tiptap-icons/heading-two-icon.tsx +28 -0
  48. package/template/src/admin/components/tiptap-icons/highlighter-icon.tsx +26 -0
  49. package/template/src/admin/components/tiptap-icons/image-plus-icon.tsx +26 -0
  50. package/template/src/admin/components/tiptap-icons/italic-icon.tsx +24 -0
  51. package/template/src/admin/components/tiptap-icons/link-icon.tsx +28 -0
  52. package/template/src/admin/components/tiptap-icons/list-icon.tsx +56 -0
  53. package/template/src/admin/components/tiptap-icons/list-ordered-icon.tsx +56 -0
  54. package/template/src/admin/components/tiptap-icons/list-todo-icon.tsx +50 -0
  55. package/template/src/admin/components/tiptap-icons/moon-star-icon.tsx +30 -0
  56. package/template/src/admin/components/tiptap-icons/redo2-icon.tsx +26 -0
  57. package/template/src/admin/components/tiptap-icons/search-icon.tsx +26 -0
  58. package/template/src/admin/components/tiptap-icons/strike-icon.tsx +28 -0
  59. package/template/src/admin/components/tiptap-icons/subscript-icon.tsx +38 -0
  60. package/template/src/admin/components/tiptap-icons/sun-icon.tsx +58 -0
  61. package/template/src/admin/components/tiptap-icons/superscript-icon.tsx +38 -0
  62. package/template/src/admin/components/tiptap-icons/trash-icon.tsx +26 -0
  63. package/template/src/admin/components/tiptap-icons/underline-icon.tsx +26 -0
  64. package/template/src/admin/components/tiptap-icons/undo2-icon.tsx +26 -0
  65. package/template/src/admin/components/tiptap-icons/whole-word-icon.tsx +36 -0
  66. package/template/src/admin/components/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  67. package/template/src/admin/components/tiptap-node/code-block-node/code-block-node.scss +54 -0
  68. package/template/src/admin/components/tiptap-node/heading-node/heading-node.scss +45 -0
  69. package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +14 -0
  70. package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  71. package/template/src/admin/components/tiptap-node/image-node/image-node.scss +35 -0
  72. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +161 -0
  73. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  74. package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.tsx +554 -0
  75. package/template/src/admin/components/tiptap-node/image-upload-node/index.tsx +1 -0
  76. package/template/src/admin/components/tiptap-node/list-node/list-node.scss +208 -0
  77. package/template/src/admin/components/tiptap-node/paragraph-node/paragraph-node.scss +217 -0
  78. package/template/src/admin/components/tiptap-templates/simple/NOTICE.md +14 -0
  79. package/template/src/admin/components/tiptap-templates/simple/data/content.json +477 -0
  80. package/template/src/admin/components/tiptap-templates/simple/simple-editor.scss +97 -0
  81. package/template/src/admin/components/tiptap-templates/simple/simple-editor.tsx +348 -0
  82. package/template/src/admin/components/tiptap-templates/simple/theme-toggle.tsx +44 -0
  83. package/template/src/admin/components/tiptap-ui/blockquote-button/blockquote-button.tsx +122 -0
  84. package/template/src/admin/components/tiptap-ui/blockquote-button/index.tsx +2 -0
  85. package/template/src/admin/components/tiptap-ui/blockquote-button/use-blockquote.ts +273 -0
  86. package/template/src/admin/components/tiptap-ui/code-block-button/code-block-button.tsx +122 -0
  87. package/template/src/admin/components/tiptap-ui/code-block-button/index.tsx +2 -0
  88. package/template/src/admin/components/tiptap-ui/code-block-button/use-code-block.ts +283 -0
  89. package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  90. package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +170 -0
  91. package/template/src/admin/components/tiptap-ui/color-highlight-button/index.tsx +2 -0
  92. package/template/src/admin/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +380 -0
  93. package/template/src/admin/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +231 -0
  94. package/template/src/admin/components/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  95. package/template/src/admin/components/tiptap-ui/heading-button/heading-button.tsx +124 -0
  96. package/template/src/admin/components/tiptap-ui/heading-button/index.tsx +2 -0
  97. package/template/src/admin/components/tiptap-ui/heading-button/use-heading.ts +348 -0
  98. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +128 -0
  99. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  100. package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +132 -0
  101. package/template/src/admin/components/tiptap-ui/image-upload-button/image-upload-button.tsx +130 -0
  102. package/template/src/admin/components/tiptap-ui/image-upload-button/index.tsx +2 -0
  103. package/template/src/admin/components/tiptap-ui/image-upload-button/use-image-upload.ts +197 -0
  104. package/template/src/admin/components/tiptap-ui/link-popover/index.tsx +2 -0
  105. package/template/src/admin/components/tiptap-ui/link-popover/link-popover.scss +16 -0
  106. package/template/src/admin/components/tiptap-ui/link-popover/link-popover.tsx +315 -0
  107. package/template/src/admin/components/tiptap-ui/link-popover/use-link-popover.ts +297 -0
  108. package/template/src/admin/components/tiptap-ui/list-button/index.tsx +2 -0
  109. package/template/src/admin/components/tiptap-ui/list-button/list-button.tsx +120 -0
  110. package/template/src/admin/components/tiptap-ui/list-button/use-list.ts +353 -0
  111. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/index.tsx +2 -0
  112. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +126 -0
  113. package/template/src/admin/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +220 -0
  114. package/template/src/admin/components/tiptap-ui/mark-button/index.tsx +2 -0
  115. package/template/src/admin/components/tiptap-ui/mark-button/mark-button.tsx +122 -0
  116. package/template/src/admin/components/tiptap-ui/mark-button/use-mark.ts +219 -0
  117. package/template/src/admin/components/tiptap-ui/search-and-replace/index.tsx +2 -0
  118. package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.scss +300 -0
  119. package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.tsx +718 -0
  120. package/template/src/admin/components/tiptap-ui/search-and-replace/use-search-and-replace.ts +528 -0
  121. package/template/src/admin/components/tiptap-ui/text-align-button/index.tsx +2 -0
  122. package/template/src/admin/components/tiptap-ui/text-align-button/text-align-button.tsx +144 -0
  123. package/template/src/admin/components/tiptap-ui/text-align-button/use-text-align.ts +237 -0
  124. package/template/src/admin/components/tiptap-ui/undo-redo-button/index.tsx +2 -0
  125. package/template/src/admin/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +125 -0
  126. package/template/src/admin/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +200 -0
  127. package/template/src/admin/components/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  128. package/template/src/admin/components/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  129. package/template/src/admin/components/tiptap-ui-primitive/badge/badge.scss +99 -0
  130. package/template/src/admin/components/tiptap-ui-primitive/badge/badge.tsx +44 -0
  131. package/template/src/admin/components/tiptap-ui-primitive/badge/index.tsx +1 -0
  132. package/template/src/admin/components/tiptap-ui-primitive/button/button-colors.scss +613 -0
  133. package/template/src/admin/components/tiptap-ui-primitive/button/button.scss +390 -0
  134. package/template/src/admin/components/tiptap-ui-primitive/button/button.tsx +158 -0
  135. package/template/src/admin/components/tiptap-ui-primitive/button/index.tsx +1 -0
  136. package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.scss +89 -0
  137. package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.tsx +72 -0
  138. package/template/src/admin/components/tiptap-ui-primitive/button-group/index.tsx +1 -0
  139. package/template/src/admin/components/tiptap-ui-primitive/card/card.scss +74 -0
  140. package/template/src/admin/components/tiptap-ui-primitive/card/card.tsx +79 -0
  141. package/template/src/admin/components/tiptap-ui-primitive/card/index.tsx +1 -0
  142. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +152 -0
  143. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +262 -0
  144. package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  145. package/template/src/admin/components/tiptap-ui-primitive/input/index.tsx +1 -0
  146. package/template/src/admin/components/tiptap-ui-primitive/input/input.scss +41 -0
  147. package/template/src/admin/components/tiptap-ui-primitive/input/input.tsx +22 -0
  148. package/template/src/admin/components/tiptap-ui-primitive/input-group/index.tsx +1 -0
  149. package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.scss +225 -0
  150. package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.tsx +135 -0
  151. package/template/src/admin/components/tiptap-ui-primitive/popover/index.tsx +1 -0
  152. package/template/src/admin/components/tiptap-ui-primitive/popover/popover.scss +64 -0
  153. package/template/src/admin/components/tiptap-ui-primitive/popover/popover.tsx +35 -0
  154. package/template/src/admin/components/tiptap-ui-primitive/separator/index.tsx +1 -0
  155. package/template/src/admin/components/tiptap-ui-primitive/separator/separator.scss +23 -0
  156. package/template/src/admin/components/tiptap-ui-primitive/separator/separator.tsx +28 -0
  157. package/template/src/admin/components/tiptap-ui-primitive/spacer/index.tsx +1 -0
  158. package/template/src/admin/components/tiptap-ui-primitive/spacer/spacer.tsx +24 -0
  159. package/template/src/admin/components/tiptap-ui-primitive/switch/index.tsx +1 -0
  160. package/template/src/admin/components/tiptap-ui-primitive/switch/switch.scss +86 -0
  161. package/template/src/admin/components/tiptap-ui-primitive/switch/switch.tsx +29 -0
  162. package/template/src/admin/components/tiptap-ui-primitive/textarea/index.tsx +1 -0
  163. package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.scss +42 -0
  164. package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.tsx +16 -0
  165. package/template/src/admin/components/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  166. package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  167. package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.tsx +203 -0
  168. package/template/src/admin/components/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  169. package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  170. package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.tsx +241 -0
  171. package/template/src/admin/hooks/use-composed-ref.ts +46 -0
  172. package/template/src/admin/hooks/use-cursor-visibility.ts +69 -0
  173. package/template/src/admin/hooks/use-element-rect.ts +166 -0
  174. package/template/src/admin/hooks/use-is-breakpoint.ts +35 -0
  175. package/template/src/admin/hooks/use-menu-navigation.ts +204 -0
  176. package/template/src/admin/hooks/use-scrolling.ts +75 -0
  177. package/template/src/admin/hooks/use-throttled-callback.ts +48 -0
  178. package/template/src/admin/hooks/use-tiptap-editor.ts +69 -0
  179. package/template/src/admin/hooks/use-unmount.ts +21 -0
  180. package/template/src/admin/hooks/use-window-size.ts +93 -0
  181. package/template/src/admin/html.ts +17 -0
  182. package/template/src/admin/lib/tiptap-utils.ts +652 -0
  183. package/template/src/admin/navigation-tree.tsx +82 -0
  184. package/template/src/admin/preview.ts +73 -0
  185. package/template/src/admin/styles/_cms-theme.scss +58 -0
  186. package/template/src/admin/styles/_keyframe-animations.scss +91 -0
  187. package/template/src/admin/styles/_variables.scss +298 -0
  188. package/template/src/assets/favicon.svg +1 -0
  189. package/template/src/assets/logo.svg +1 -0
  190. package/template/src/astro-env.d.ts +1 -0
  191. package/template/src/content.config.ts +12 -0
  192. package/template/src/db/bootstrap.ts +36 -0
  193. package/template/src/db/client.ts +7 -0
  194. package/template/src/db/schema.ts +45 -0
  195. package/template/src/documents/service.ts +178 -0
  196. package/template/src/documents/validation.ts +9 -0
  197. package/template/src/env.ts +9 -0
  198. package/template/src/index.ts +10 -0
  199. package/template/src/locales.ts +10 -0
  200. package/template/src/media/service.ts +120 -0
  201. package/template/src/navigation/service.ts +171 -0
  202. package/template/src/pages/[locale]/cms-preview-shell.astro +12 -0
  203. package/template/src/pages/cms-preview-shell.astro +5 -0
  204. package/template/src/publish/deploy-hook.ts +42 -0
  205. package/template/src/publish/record.ts +20 -0
  206. package/template/src/publish/service.ts +135 -0
  207. package/template/src/scss.d.ts +5 -0
  208. package/template/src/site.config.ts +7 -0
  209. package/template/src/sql.d.ts +4 -0
  210. package/template/src/starlight/cms-origin.ts +4 -0
  211. package/template/src/starlight/loader.ts +21 -0
  212. package/template/src/starlight/preview-render.ts +162 -0
  213. package/template/src/starlight/preview-shell.astro +14 -0
  214. package/template/src/starlight/render.ts +59 -0
  215. package/template/src/starlight/schema.ts +34 -0
  216. package/template/src/starlight/sidebar.ts +35 -0
  217. package/template/src/starlight/snapshot.ts +47 -0
  218. package/template/src/starlight/source.ts +26 -0
  219. package/template/src/starlight/tiptap.ts +29 -0
  220. package/template/src/styles/starlight.css +12 -0
  221. package/template/test-files/content.test.template.ts +82 -0
  222. package/template/test-files/deploy-hook.test.template.ts +32 -0
  223. package/template/test-files/media.test.template.ts +39 -0
  224. package/template/test-files/publishing.integration.test.template.ts +156 -0
  225. package/template/tsconfig.admin.json +18 -0
  226. package/template/tsconfig.json +23 -0
  227. package/template/worker-configuration.d.ts +15389 -0
  228. package/template/wrangler.jsonc +26 -0
@@ -0,0 +1,237 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+ import type { ChainedCommands } from "@tiptap/react"
3
+ import { type Editor } from "@tiptap/react"
4
+
5
+ // --- Hooks ---
6
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
7
+
8
+ // --- Lib ---
9
+ import {
10
+ isExtensionAvailable,
11
+ isNodeTypeSelected,
12
+ } from "@/lib/tiptap-utils"
13
+
14
+ // --- Icons ---
15
+ import { AlignCenterIcon } from "@/components/tiptap-icons/align-center-icon"
16
+ import { AlignJustifyIcon } from "@/components/tiptap-icons/align-justify-icon"
17
+ import { AlignLeftIcon } from "@/components/tiptap-icons/align-left-icon"
18
+ import { AlignRightIcon } from "@/components/tiptap-icons/align-right-icon"
19
+
20
+ export type TextAlign = "left" | "center" | "right" | "justify"
21
+
22
+ /**
23
+ * Configuration for the text align functionality
24
+ */
25
+ export interface UseTextAlignConfig {
26
+ /**
27
+ * The Tiptap editor instance.
28
+ */
29
+ editor?: Editor | null
30
+ /**
31
+ * The text alignment to apply.
32
+ */
33
+ align: TextAlign
34
+ /**
35
+ * Whether the button should hide when alignment is not available.
36
+ * @default false
37
+ */
38
+ hideWhenUnavailable?: boolean
39
+ /**
40
+ * Callback function called after a successful alignment change.
41
+ */
42
+ onAligned?: () => void
43
+ }
44
+
45
+ export const TEXT_ALIGN_SHORTCUT_KEYS: Record<TextAlign, string> = {
46
+ left: "mod+shift+l",
47
+ center: "mod+shift+e",
48
+ right: "mod+shift+r",
49
+ justify: "mod+shift+j",
50
+ }
51
+
52
+ export const textAlignIcons = {
53
+ left: AlignLeftIcon,
54
+ center: AlignCenterIcon,
55
+ right: AlignRightIcon,
56
+ justify: AlignJustifyIcon,
57
+ }
58
+
59
+ export const textAlignLabels: Record<TextAlign, string> = {
60
+ left: "Align left",
61
+ center: "Align center",
62
+ right: "Align right",
63
+ justify: "Align justify",
64
+ }
65
+
66
+ /**
67
+ * Checks if text alignment can be performed in the current editor state
68
+ */
69
+ export function canSetTextAlign(
70
+ editor: Editor | null,
71
+ align: TextAlign
72
+ ): boolean {
73
+ if (!editor || editor.isDestroyed || !editor.isEditable) return false
74
+ if (
75
+ !isExtensionAvailable(editor, "textAlign") ||
76
+ isNodeTypeSelected(editor, ["image", "horizontalRule"])
77
+ )
78
+ return false
79
+
80
+ try {
81
+ return editor.can().setTextAlign(align)
82
+ } catch {
83
+ return false
84
+ }
85
+ }
86
+
87
+ export function hasSetTextAlign(
88
+ commands: ChainedCommands
89
+ ): commands is ChainedCommands & {
90
+ setTextAlign: (align: TextAlign) => ChainedCommands
91
+ } {
92
+ return "setTextAlign" in commands
93
+ }
94
+
95
+ /**
96
+ * Checks if the text alignment is currently active
97
+ */
98
+ export function isTextAlignActive(
99
+ editor: Editor | null,
100
+ align: TextAlign
101
+ ): boolean {
102
+ if (!editor || !editor.isEditable) return false
103
+ return editor.isActive({ textAlign: align })
104
+ }
105
+
106
+ /**
107
+ * Sets text alignment in the editor
108
+ */
109
+ export function setTextAlign(editor: Editor | null, align: TextAlign): boolean {
110
+ if (!editor || editor.isDestroyed || !editor.isEditable) return false
111
+ if (!canSetTextAlign(editor, align)) return false
112
+
113
+ try {
114
+ const chain = editor.chain().focus()
115
+ if (hasSetTextAlign(chain)) {
116
+ return chain.setTextAlign(align).run()
117
+ }
118
+ } catch {
119
+ return false
120
+ }
121
+
122
+ return false
123
+ }
124
+
125
+ /**
126
+ * Determines if the text align button should be shown
127
+ */
128
+ export function shouldShowButton(props: {
129
+ editor: Editor | null
130
+ hideWhenUnavailable: boolean
131
+ align: TextAlign
132
+ }): boolean {
133
+ const { editor, hideWhenUnavailable, align } = props
134
+
135
+ if (!editor) return false
136
+
137
+ if (!hideWhenUnavailable) {
138
+ return true
139
+ }
140
+
141
+ if (!editor.isEditable) return false
142
+
143
+ if (!isExtensionAvailable(editor, "textAlign")) return false
144
+
145
+ if (!editor.isActive("code")) {
146
+ return canSetTextAlign(editor, align)
147
+ }
148
+
149
+ return true
150
+ }
151
+
152
+ /**
153
+ * Custom hook that provides text align functionality for Tiptap editor
154
+ *
155
+ * @example
156
+ * ```tsx
157
+ * // Simple usage
158
+ * function MySimpleAlignButton() {
159
+ * const { isVisible, handleTextAlign } = useTextAlign({ align: "center" })
160
+ *
161
+ * if (!isVisible) return null
162
+ *
163
+ * return <button onClick={handleTextAlign}>Align Center</button>
164
+ * }
165
+ *
166
+ * // Advanced usage with configuration
167
+ * function MyAdvancedAlignButton() {
168
+ * const { isVisible, handleTextAlign, label, isActive } = useTextAlign({
169
+ * editor: myEditor,
170
+ * align: "right",
171
+ * hideWhenUnavailable: true,
172
+ * onAligned: () => console.log('Text aligned!')
173
+ * })
174
+ *
175
+ * if (!isVisible) return null
176
+ *
177
+ * return (
178
+ * <MyButton
179
+ * onClick={handleTextAlign}
180
+ * aria-pressed={isActive}
181
+ * aria-label={label}
182
+ * >
183
+ * Align Right
184
+ * </MyButton>
185
+ * )
186
+ * }
187
+ * ```
188
+ */
189
+ export function useTextAlign(config: UseTextAlignConfig) {
190
+ const {
191
+ editor: providedEditor,
192
+ align,
193
+ hideWhenUnavailable = false,
194
+ onAligned,
195
+ } = config
196
+
197
+ const { editor } = useTiptapEditor(providedEditor)
198
+ const [isVisible, setIsVisible] = useState<boolean>(true)
199
+ const canAlign = canSetTextAlign(editor, align)
200
+ const isActive = isTextAlignActive(editor, align)
201
+
202
+ useEffect(() => {
203
+ if (!editor) return
204
+
205
+ const handleSelectionUpdate = () => {
206
+ setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable }))
207
+ }
208
+
209
+ handleSelectionUpdate()
210
+
211
+ editor.on("selectionUpdate", handleSelectionUpdate)
212
+
213
+ return () => {
214
+ editor.off("selectionUpdate", handleSelectionUpdate)
215
+ }
216
+ }, [editor, hideWhenUnavailable, align])
217
+
218
+ const handleTextAlign = useCallback(() => {
219
+ if (!editor) return false
220
+
221
+ const success = setTextAlign(editor, align)
222
+ if (success) {
223
+ onAligned?.()
224
+ }
225
+ return success
226
+ }, [editor, align, onAligned])
227
+
228
+ return {
229
+ isVisible,
230
+ isActive,
231
+ handleTextAlign,
232
+ canAlign,
233
+ label: textAlignLabels[align],
234
+ shortcutKeys: TEXT_ALIGN_SHORTCUT_KEYS[align],
235
+ Icon: textAlignIcons[align],
236
+ }
237
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./undo-redo-button"
2
+ export * from "./use-undo-redo"
@@ -0,0 +1,125 @@
1
+ "use client"
2
+
3
+ import { forwardRef, useCallback } from "react"
4
+
5
+ // --- Lib ---
6
+ import { parseShortcutKeys } from "@/lib/tiptap-utils"
7
+
8
+ // --- Hooks ---
9
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
10
+
11
+ // --- Tiptap UI ---
12
+ import type {
13
+ UndoRedoAction,
14
+ UseUndoRedoConfig,
15
+ } from "@/components/tiptap-ui/undo-redo-button"
16
+ import {
17
+ UNDO_REDO_SHORTCUT_KEYS,
18
+ useUndoRedo,
19
+ } from "@/components/tiptap-ui/undo-redo-button"
20
+
21
+ // --- UI Primitives ---
22
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
23
+ import { Button } from "@/components/tiptap-ui-primitive/button"
24
+ import { Badge } from "@/components/tiptap-ui-primitive/badge"
25
+
26
+ export interface UndoRedoButtonProps
27
+ extends Omit<ButtonProps, "type">, UseUndoRedoConfig {
28
+ /**
29
+ * Optional text to display alongside the icon.
30
+ */
31
+ text?: string
32
+ /**
33
+ * Optional show shortcut keys in the button.
34
+ * @default false
35
+ */
36
+ showShortcut?: boolean
37
+ }
38
+
39
+ export function HistoryShortcutBadge({
40
+ action,
41
+ shortcutKeys = UNDO_REDO_SHORTCUT_KEYS[action],
42
+ }: {
43
+ action: UndoRedoAction
44
+ shortcutKeys?: string
45
+ }) {
46
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
47
+ }
48
+
49
+ /**
50
+ * Button component for triggering undo/redo actions in a Tiptap editor.
51
+ *
52
+ * For custom button implementations, use the `useHistory` hook instead.
53
+ */
54
+ export const UndoRedoButton = forwardRef<
55
+ HTMLButtonElement,
56
+ UndoRedoButtonProps
57
+ >(
58
+ (
59
+ {
60
+ editor: providedEditor,
61
+ action,
62
+ text,
63
+ hideWhenUnavailable = false,
64
+ onExecuted,
65
+ showShortcut = false,
66
+ onClick,
67
+ children,
68
+ ...buttonProps
69
+ },
70
+ ref
71
+ ) => {
72
+ const { editor } = useTiptapEditor(providedEditor)
73
+ const { isVisible, handleAction, label, canExecute, Icon, shortcutKeys } =
74
+ useUndoRedo({
75
+ editor,
76
+ action,
77
+ hideWhenUnavailable,
78
+ onExecuted,
79
+ })
80
+
81
+ const handleClick = useCallback(
82
+ (event: React.MouseEvent<HTMLButtonElement>) => {
83
+ onClick?.(event)
84
+ if (event.defaultPrevented) return
85
+ handleAction()
86
+ },
87
+ [handleAction, onClick]
88
+ )
89
+
90
+ if (!isVisible) {
91
+ return null
92
+ }
93
+
94
+ return (
95
+ <Button
96
+ type="button"
97
+ disabled={!canExecute}
98
+ variant="ghost"
99
+ data-disabled={!canExecute}
100
+ role="button"
101
+ tabIndex={-1}
102
+ aria-label={label}
103
+ tooltip={label}
104
+ onClick={handleClick}
105
+ {...buttonProps}
106
+ ref={ref}
107
+ >
108
+ {children ?? (
109
+ <>
110
+ <Icon className="tiptap-button-icon" />
111
+ {text && <span className="tiptap-button-text">{text}</span>}
112
+ {showShortcut && (
113
+ <HistoryShortcutBadge
114
+ action={action}
115
+ shortcutKeys={shortcutKeys}
116
+ />
117
+ )}
118
+ </>
119
+ )}
120
+ </Button>
121
+ )
122
+ }
123
+ )
124
+
125
+ UndoRedoButton.displayName = "UndoRedoButton"
@@ -0,0 +1,200 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+ import { type Editor } from "@tiptap/react"
3
+
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
6
+
7
+ // --- Lib ---
8
+ import { isNodeTypeSelected } from "@/lib/tiptap-utils"
9
+
10
+ // --- Icons ---
11
+ import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon"
12
+ import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon"
13
+
14
+ export type UndoRedoAction = "undo" | "redo"
15
+
16
+ /**
17
+ * Configuration for the history functionality
18
+ */
19
+ export interface UseUndoRedoConfig {
20
+ /**
21
+ * The Tiptap editor instance.
22
+ */
23
+ editor?: Editor | null
24
+ /**
25
+ * The history action to perform (undo or redo).
26
+ */
27
+ action: UndoRedoAction
28
+ /**
29
+ * Whether the button should hide when action is not available.
30
+ * @default false
31
+ */
32
+ hideWhenUnavailable?: boolean
33
+ /**
34
+ * Callback function called after a successful action execution.
35
+ */
36
+ onExecuted?: () => void
37
+ }
38
+
39
+ export const UNDO_REDO_SHORTCUT_KEYS: Record<UndoRedoAction, string> = {
40
+ undo: "mod+z",
41
+ redo: "mod+shift+z",
42
+ }
43
+
44
+ export const historyActionLabels: Record<UndoRedoAction, string> = {
45
+ undo: "Undo",
46
+ redo: "Redo",
47
+ }
48
+
49
+ export const historyIcons = {
50
+ undo: Undo2Icon,
51
+ redo: Redo2Icon,
52
+ }
53
+
54
+ /**
55
+ * Checks if a history action can be executed
56
+ */
57
+ export function canExecuteUndoRedoAction(
58
+ editor: Editor | null,
59
+ action: UndoRedoAction
60
+ ): boolean {
61
+ if (!editor || editor.isDestroyed || !editor.isEditable) return false
62
+
63
+ try {
64
+ if (isNodeTypeSelected(editor, ["image"])) return false
65
+
66
+ const commands = editor.can()
67
+ const command = action === "undo" ? commands.undo : commands.redo
68
+
69
+ return typeof command === "function" ? command() : false
70
+ } catch {
71
+ return false
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Executes a history action on the editor
77
+ */
78
+ export function executeUndoRedoAction(
79
+ editor: Editor | null,
80
+ action: UndoRedoAction
81
+ ): boolean {
82
+ if (!editor || editor.isDestroyed || !editor.isEditable) return false
83
+ if (!canExecuteUndoRedoAction(editor, action)) return false
84
+
85
+ try {
86
+ const chain = editor.chain().focus()
87
+ return action === "undo" ? chain.undo().run() : chain.redo().run()
88
+ } catch {
89
+ return false
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Determines if the history button should be shown
95
+ */
96
+ export function shouldShowButton(props: {
97
+ editor: Editor | null
98
+ hideWhenUnavailable: boolean
99
+ action: UndoRedoAction
100
+ }): boolean {
101
+ const { editor, hideWhenUnavailable, action } = props
102
+
103
+ if (!editor) return false
104
+
105
+ if (!hideWhenUnavailable) {
106
+ return true
107
+ }
108
+
109
+ if (!editor.isEditable) return false
110
+
111
+ if (!editor.isActive("code")) {
112
+ return canExecuteUndoRedoAction(editor, action)
113
+ }
114
+
115
+ return true
116
+ }
117
+
118
+ /**
119
+ * Custom hook that provides history functionality for Tiptap editor
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * // Simple usage
124
+ * function MySimpleUndoButton() {
125
+ * const { isVisible, handleAction } = useHistory({ action: "undo" })
126
+ *
127
+ * if (!isVisible) return null
128
+ *
129
+ * return <button onClick={handleAction}>Undo</button>
130
+ * }
131
+ *
132
+ * // Advanced usage with configuration
133
+ * function MyAdvancedRedoButton() {
134
+ * const { isVisible, handleAction, label } = useHistory({
135
+ * editor: myEditor,
136
+ * action: "redo",
137
+ * hideWhenUnavailable: true,
138
+ * onExecuted: () => console.log('Action executed!')
139
+ * })
140
+ *
141
+ * if (!isVisible) return null
142
+ *
143
+ * return (
144
+ * <MyButton
145
+ * onClick={handleAction}
146
+ * aria-label={label}
147
+ * >
148
+ * Redo
149
+ * </MyButton>
150
+ * )
151
+ * }
152
+ * ```
153
+ */
154
+ export function useUndoRedo(config: UseUndoRedoConfig) {
155
+ const {
156
+ editor: providedEditor,
157
+ action,
158
+ hideWhenUnavailable = false,
159
+ onExecuted,
160
+ } = config
161
+
162
+ const { editor } = useTiptapEditor(providedEditor)
163
+ const [isVisible, setIsVisible] = useState<boolean>(true)
164
+ const canExecute = canExecuteUndoRedoAction(editor, action)
165
+
166
+ useEffect(() => {
167
+ if (!editor) return
168
+
169
+ const handleUpdate = () => {
170
+ setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action }))
171
+ }
172
+
173
+ handleUpdate()
174
+
175
+ editor.on("transaction", handleUpdate)
176
+
177
+ return () => {
178
+ editor.off("transaction", handleUpdate)
179
+ }
180
+ }, [editor, hideWhenUnavailable, action])
181
+
182
+ const handleAction = useCallback(() => {
183
+ if (!editor) return false
184
+
185
+ const success = executeUndoRedoAction(editor, action)
186
+ if (success) {
187
+ onExecuted?.()
188
+ }
189
+ return success
190
+ }, [editor, action, onExecuted])
191
+
192
+ return {
193
+ isVisible,
194
+ handleAction,
195
+ canExecute,
196
+ label: historyActionLabels[action],
197
+ shortcutKeys: UNDO_REDO_SHORTCUT_KEYS[action],
198
+ Icon: historyIcons[action],
199
+ }
200
+ }