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,220 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useState } from "react"
4
+ import type { Editor } from "@tiptap/react"
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
8
+
9
+ // --- Icons ---
10
+ import { ListIcon } from "@/components/tiptap-icons/list-icon"
11
+ import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon"
12
+ import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon"
13
+
14
+ // --- Lib ---
15
+ import { isNodeInSchema } from "@/lib/tiptap-utils"
16
+
17
+ // --- Tiptap UI ---
18
+ import {
19
+ canToggleList,
20
+ isListActive,
21
+ listIcons,
22
+ type ListType,
23
+ } from "@/components/tiptap-ui/list-button"
24
+
25
+ /**
26
+ * Configuration for the list dropdown menu functionality
27
+ */
28
+ export interface UseListDropdownMenuConfig {
29
+ /**
30
+ * The Tiptap editor instance.
31
+ */
32
+ editor?: Editor | null
33
+ /**
34
+ * The list types to display in the dropdown.
35
+ * @default ["bulletList", "orderedList", "taskList"]
36
+ */
37
+ types?: ListType[]
38
+ /**
39
+ * Whether the dropdown should be hidden when no list types are available
40
+ * @default false
41
+ */
42
+ hideWhenUnavailable?: boolean
43
+ }
44
+
45
+ export interface ListOption {
46
+ label: string
47
+ type: ListType
48
+ icon: React.ElementType
49
+ }
50
+
51
+ export const listOptions: ListOption[] = [
52
+ {
53
+ label: "Bullet List",
54
+ type: "bulletList",
55
+ icon: ListIcon,
56
+ },
57
+ {
58
+ label: "Ordered List",
59
+ type: "orderedList",
60
+ icon: ListOrderedIcon,
61
+ },
62
+ {
63
+ label: "Task List",
64
+ type: "taskList",
65
+ icon: ListTodoIcon,
66
+ },
67
+ ]
68
+
69
+ export function canToggleAnyList(
70
+ editor: Editor | null,
71
+ listTypes: ListType[]
72
+ ): boolean {
73
+ if (!editor || !editor.isEditable) return false
74
+ return listTypes.some((type) => canToggleList(editor, type))
75
+ }
76
+
77
+ export function isAnyListActive(
78
+ editor: Editor | null,
79
+ listTypes: ListType[]
80
+ ): boolean {
81
+ if (!editor || !editor.isEditable) return false
82
+ return listTypes.some((type) => isListActive(editor, type))
83
+ }
84
+
85
+ export function getFilteredListOptions(
86
+ availableTypes: ListType[]
87
+ ): typeof listOptions {
88
+ return listOptions.filter(
89
+ (option) => !option.type || availableTypes.includes(option.type)
90
+ )
91
+ }
92
+
93
+ export function shouldShowListDropdown(params: {
94
+ editor: Editor | null
95
+ listTypes: ListType[]
96
+ hideWhenUnavailable: boolean
97
+ listInSchema: boolean
98
+ canToggleAny: boolean
99
+ }): boolean {
100
+ const { editor, hideWhenUnavailable, listInSchema, canToggleAny } = params
101
+
102
+ if (!editor) return false
103
+
104
+ if (!hideWhenUnavailable) {
105
+ return true
106
+ }
107
+
108
+ if (!listInSchema) return false
109
+
110
+ if (!editor.isActive("code")) {
111
+ return canToggleAny
112
+ }
113
+
114
+ return true
115
+ }
116
+
117
+ /**
118
+ * Gets the currently active list type from the available types
119
+ */
120
+ export function getActiveListType(
121
+ editor: Editor | null,
122
+ availableTypes: ListType[]
123
+ ): ListType | undefined {
124
+ if (!editor || !editor.isEditable) return undefined
125
+ return availableTypes.find((type) => isListActive(editor, type))
126
+ }
127
+
128
+ /**
129
+ * Custom hook that provides list dropdown menu functionality for Tiptap editor
130
+ *
131
+ * @example
132
+ * ```tsx
133
+ * // Simple usage
134
+ * function MyListDropdown() {
135
+ * const {
136
+ * isVisible,
137
+ * activeType,
138
+ * isAnyActive,
139
+ * canToggleAny,
140
+ * filteredLists,
141
+ * } = useListDropdownMenu()
142
+ *
143
+ * if (!isVisible) return null
144
+ *
145
+ * return (
146
+ * <DropdownMenu>
147
+ * // dropdown content
148
+ * </DropdownMenu>
149
+ * )
150
+ * }
151
+ *
152
+ * // Advanced usage with configuration
153
+ * function MyAdvancedListDropdown() {
154
+ * const {
155
+ * isVisible,
156
+ * activeType,
157
+ * } = useListDropdownMenu({
158
+ * editor: myEditor,
159
+ * types: ["bulletList", "orderedList"],
160
+ * hideWhenUnavailable: true,
161
+ * })
162
+ *
163
+ * // component implementation
164
+ * }
165
+ * ```
166
+ */
167
+ export function useListDropdownMenu(config?: UseListDropdownMenuConfig) {
168
+ const {
169
+ editor: providedEditor,
170
+ types = ["bulletList", "orderedList", "taskList"],
171
+ hideWhenUnavailable = false,
172
+ } = config || {}
173
+
174
+ const { editor } = useTiptapEditor(providedEditor)
175
+ const [isVisible, setIsVisible] = useState(true)
176
+
177
+ const listInSchema = types.some((type) => isNodeInSchema(type, editor))
178
+
179
+ const filteredLists = useMemo(() => getFilteredListOptions(types), [types])
180
+
181
+ const canToggleAny = canToggleAnyList(editor, types)
182
+ const isAnyActive = isAnyListActive(editor, types)
183
+ const activeType = getActiveListType(editor, types)
184
+ const activeList = filteredLists.find((option) => option.type === activeType)
185
+
186
+ useEffect(() => {
187
+ if (!editor) return
188
+
189
+ const handleSelectionUpdate = () => {
190
+ setIsVisible(
191
+ shouldShowListDropdown({
192
+ editor,
193
+ listTypes: types,
194
+ hideWhenUnavailable,
195
+ listInSchema,
196
+ canToggleAny,
197
+ })
198
+ )
199
+ }
200
+
201
+ handleSelectionUpdate()
202
+
203
+ editor.on("selectionUpdate", handleSelectionUpdate)
204
+
205
+ return () => {
206
+ editor.off("selectionUpdate", handleSelectionUpdate)
207
+ }
208
+ }, [canToggleAny, editor, hideWhenUnavailable, listInSchema, types])
209
+
210
+ return {
211
+ isVisible,
212
+ activeType,
213
+ isActive: isAnyActive,
214
+ canToggle: canToggleAny,
215
+ types,
216
+ filteredLists,
217
+ label: "List",
218
+ Icon: activeList ? listIcons[activeList.type] : ListIcon,
219
+ }
220
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./mark-button"
2
+ export * from "./use-mark"
@@ -0,0 +1,122 @@
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 { Mark, UseMarkConfig } from "@/components/tiptap-ui/mark-button"
13
+ import { MARK_SHORTCUT_KEYS, useMark } from "@/components/tiptap-ui/mark-button"
14
+
15
+ // --- UI Primitives ---
16
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
17
+ import { Button } from "@/components/tiptap-ui-primitive/button"
18
+ import { Badge } from "@/components/tiptap-ui-primitive/badge"
19
+
20
+ export interface MarkButtonProps
21
+ extends Omit<ButtonProps, "type">, UseMarkConfig {
22
+ /**
23
+ * Optional text to display alongside the icon.
24
+ */
25
+ text?: string
26
+ /**
27
+ * Optional show shortcut keys in the button.
28
+ * @default false
29
+ */
30
+ showShortcut?: boolean
31
+ }
32
+
33
+ export function MarkShortcutBadge({
34
+ type,
35
+ shortcutKeys = MARK_SHORTCUT_KEYS[type],
36
+ }: {
37
+ type: Mark
38
+ shortcutKeys?: string
39
+ }) {
40
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
41
+ }
42
+
43
+ /**
44
+ * Button component for toggling marks in a Tiptap editor.
45
+ *
46
+ * For custom button implementations, use the `useMark` hook instead.
47
+ */
48
+ export const MarkButton = forwardRef<HTMLButtonElement, MarkButtonProps>(
49
+ (
50
+ {
51
+ editor: providedEditor,
52
+ type,
53
+ text,
54
+ hideWhenUnavailable = false,
55
+ onToggled,
56
+ showShortcut = false,
57
+ onClick,
58
+ children,
59
+ ...buttonProps
60
+ },
61
+ ref
62
+ ) => {
63
+ const { editor } = useTiptapEditor(providedEditor)
64
+ const {
65
+ isVisible,
66
+ handleMark,
67
+ label,
68
+ canToggle,
69
+ isActive,
70
+ Icon,
71
+ shortcutKeys,
72
+ } = useMark({
73
+ editor,
74
+ type,
75
+ hideWhenUnavailable,
76
+ onToggled,
77
+ })
78
+
79
+ const handleClick = useCallback(
80
+ (event: React.MouseEvent<HTMLButtonElement>) => {
81
+ onClick?.(event)
82
+ if (event.defaultPrevented) return
83
+ handleMark()
84
+ },
85
+ [handleMark, onClick]
86
+ )
87
+
88
+ if (!isVisible) {
89
+ return null
90
+ }
91
+
92
+ return (
93
+ <Button
94
+ type="button"
95
+ disabled={!canToggle}
96
+ variant="ghost"
97
+ data-active-state={isActive ? "on" : "off"}
98
+ data-disabled={!canToggle}
99
+ role="button"
100
+ tabIndex={-1}
101
+ aria-label={label}
102
+ aria-pressed={isActive}
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
+ <MarkShortcutBadge type={type} shortcutKeys={shortcutKeys} />
114
+ )}
115
+ </>
116
+ )}
117
+ </Button>
118
+ )
119
+ }
120
+ )
121
+
122
+ MarkButton.displayName = "MarkButton"
@@ -0,0 +1,219 @@
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 { isMarkInSchema, isNodeTypeSelected } from "@/lib/tiptap-utils"
9
+
10
+ // --- Icons ---
11
+ import { BoldIcon } from "@/components/tiptap-icons/bold-icon"
12
+ import { Code2Icon } from "@/components/tiptap-icons/code2-icon"
13
+ import { ItalicIcon } from "@/components/tiptap-icons/italic-icon"
14
+ import { StrikeIcon } from "@/components/tiptap-icons/strike-icon"
15
+ import { SubscriptIcon } from "@/components/tiptap-icons/subscript-icon"
16
+ import { SuperscriptIcon } from "@/components/tiptap-icons/superscript-icon"
17
+ import { UnderlineIcon } from "@/components/tiptap-icons/underline-icon"
18
+
19
+ export type Mark =
20
+ | "bold"
21
+ | "italic"
22
+ | "strike"
23
+ | "code"
24
+ | "underline"
25
+ | "superscript"
26
+ | "subscript"
27
+
28
+ /**
29
+ * Configuration for the mark functionality
30
+ */
31
+ export interface UseMarkConfig {
32
+ /**
33
+ * The Tiptap editor instance.
34
+ */
35
+ editor?: Editor | null
36
+ /**
37
+ * The type of mark to toggle
38
+ */
39
+ type: Mark
40
+ /**
41
+ * Whether the button should hide when mark is not available.
42
+ * @default false
43
+ */
44
+ hideWhenUnavailable?: boolean
45
+ /**
46
+ * Callback function called after a successful mark toggle.
47
+ */
48
+ onToggled?: () => void
49
+ }
50
+
51
+ export const markIcons = {
52
+ bold: BoldIcon,
53
+ italic: ItalicIcon,
54
+ underline: UnderlineIcon,
55
+ strike: StrikeIcon,
56
+ code: Code2Icon,
57
+ superscript: SuperscriptIcon,
58
+ subscript: SubscriptIcon,
59
+ }
60
+
61
+ export const MARK_SHORTCUT_KEYS: Record<Mark, string> = {
62
+ bold: "mod+b",
63
+ italic: "mod+i",
64
+ underline: "mod+u",
65
+ strike: "mod+shift+s",
66
+ code: "mod+e",
67
+ superscript: "mod+.",
68
+ subscript: "mod+,",
69
+ }
70
+
71
+ /**
72
+ * Checks if a mark can be toggled in the current editor state
73
+ */
74
+ export function canToggleMark(editor: Editor | null, type: Mark): boolean {
75
+ if (!editor || !editor.isEditable) return false
76
+ if (!isMarkInSchema(type, editor) || isNodeTypeSelected(editor, ["image"]))
77
+ return false
78
+
79
+ return editor.can().toggleMark(type)
80
+ }
81
+
82
+ /**
83
+ * Checks if a mark is currently active
84
+ */
85
+ export function isMarkActive(editor: Editor | null, type: Mark): boolean {
86
+ if (!editor || !editor.isEditable) return false
87
+ return editor.isActive(type)
88
+ }
89
+
90
+ /**
91
+ * Toggles a mark in the editor
92
+ */
93
+ export function toggleMark(editor: Editor | null, type: Mark): boolean {
94
+ if (!editor || !editor.isEditable) return false
95
+ if (!canToggleMark(editor, type)) return false
96
+
97
+ return editor.chain().focus().toggleMark(type).run()
98
+ }
99
+
100
+ /**
101
+ * Determines if the mark button should be shown
102
+ */
103
+ export function shouldShowButton(props: {
104
+ editor: Editor | null
105
+ type: Mark
106
+ hideWhenUnavailable: boolean
107
+ }): boolean {
108
+ const { editor, type, hideWhenUnavailable } = props
109
+
110
+ if (!editor) return false
111
+
112
+ if (!hideWhenUnavailable) {
113
+ return true
114
+ }
115
+
116
+ if (!editor.isEditable) return false
117
+
118
+ if (!isMarkInSchema(type, editor)) return false
119
+
120
+ if (!editor.isActive("code")) {
121
+ return canToggleMark(editor, type)
122
+ }
123
+
124
+ return true
125
+ }
126
+
127
+ /**
128
+ * Gets the formatted mark name
129
+ */
130
+ export function getFormattedMarkName(type: Mark): string {
131
+ return type.charAt(0).toUpperCase() + type.slice(1)
132
+ }
133
+
134
+ /**
135
+ * Custom hook that provides mark functionality for Tiptap editor
136
+ *
137
+ * @example
138
+ * ```tsx
139
+ * // Simple usage
140
+ * function MySimpleBoldButton() {
141
+ * const { isVisible, handleMark } = useMark({ type: "bold" })
142
+ *
143
+ * if (!isVisible) return null
144
+ *
145
+ * return <button onClick={handleMark}>Bold</button>
146
+ * }
147
+ *
148
+ * // Advanced usage with configuration
149
+ * function MyAdvancedItalicButton() {
150
+ * const { isVisible, handleMark, label, isActive } = useMark({
151
+ * editor: myEditor,
152
+ * type: "italic",
153
+ * hideWhenUnavailable: true,
154
+ * onToggled: () => console.log('Mark toggled!')
155
+ * })
156
+ *
157
+ * if (!isVisible) return null
158
+ *
159
+ * return (
160
+ * <MyButton
161
+ * onClick={handleMark}
162
+ * aria-pressed={isActive}
163
+ * aria-label={label}
164
+ * >
165
+ * Italic
166
+ * </MyButton>
167
+ * )
168
+ * }
169
+ * ```
170
+ */
171
+ export function useMark(config: UseMarkConfig) {
172
+ const {
173
+ editor: providedEditor,
174
+ type,
175
+ hideWhenUnavailable = false,
176
+ onToggled,
177
+ } = config
178
+
179
+ const { editor } = useTiptapEditor(providedEditor)
180
+ const [isVisible, setIsVisible] = useState<boolean>(true)
181
+ const canToggle = canToggleMark(editor, type)
182
+ const isActive = isMarkActive(editor, type)
183
+
184
+ useEffect(() => {
185
+ if (!editor) return
186
+
187
+ const handleSelectionUpdate = () => {
188
+ setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
189
+ }
190
+
191
+ handleSelectionUpdate()
192
+
193
+ editor.on("selectionUpdate", handleSelectionUpdate)
194
+
195
+ return () => {
196
+ editor.off("selectionUpdate", handleSelectionUpdate)
197
+ }
198
+ }, [editor, type, hideWhenUnavailable])
199
+
200
+ const handleMark = useCallback(() => {
201
+ if (!editor) return false
202
+
203
+ const success = toggleMark(editor, type)
204
+ if (success) {
205
+ onToggled?.()
206
+ }
207
+ return success
208
+ }, [editor, type, onToggled])
209
+
210
+ return {
211
+ isVisible,
212
+ isActive,
213
+ handleMark,
214
+ canToggle,
215
+ label: getFormattedMarkName(type),
216
+ shortcutKeys: MARK_SHORTCUT_KEYS[type],
217
+ Icon: markIcons[type],
218
+ }
219
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./search-and-replace"
2
+ export * from "./use-search-and-replace"