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,120 @@
1
+ import { forwardRef, useCallback } from "react"
2
+
3
+ // --- Lib ---
4
+ import { parseShortcutKeys } from "@/lib/tiptap-utils"
5
+
6
+ // --- Hooks ---
7
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
8
+
9
+ // --- UI Primitives ---
10
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
11
+ import { Button } from "@/components/tiptap-ui-primitive/button"
12
+ import { Badge } from "@/components/tiptap-ui-primitive/badge"
13
+
14
+ // --- Tiptap UI ---
15
+ import type { ListType, UseListConfig } from "@/components/tiptap-ui/list-button"
16
+ import { LIST_SHORTCUT_KEYS, useList } from "@/components/tiptap-ui/list-button"
17
+
18
+ export interface ListButtonProps
19
+ extends Omit<ButtonProps, "type">, UseListConfig {
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
+
31
+ export function ListShortcutBadge({
32
+ type,
33
+ shortcutKeys = LIST_SHORTCUT_KEYS[type],
34
+ }: {
35
+ type: ListType
36
+ shortcutKeys?: string
37
+ }) {
38
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
39
+ }
40
+
41
+ /**
42
+ * Button component for toggling lists in a Tiptap editor.
43
+ *
44
+ * For custom button implementations, use the `useList` hook instead.
45
+ */
46
+ export const ListButton = forwardRef<HTMLButtonElement, ListButtonProps>(
47
+ (
48
+ {
49
+ editor: providedEditor,
50
+ type,
51
+ text,
52
+ hideWhenUnavailable = false,
53
+ onToggled,
54
+ showShortcut = false,
55
+ onClick,
56
+ children,
57
+ ...buttonProps
58
+ },
59
+ ref
60
+ ) => {
61
+ const { editor } = useTiptapEditor(providedEditor)
62
+ const {
63
+ isVisible,
64
+ canToggle,
65
+ isActive,
66
+ handleToggle,
67
+ label,
68
+ shortcutKeys,
69
+ Icon,
70
+ } = useList({
71
+ editor,
72
+ type,
73
+ hideWhenUnavailable,
74
+ onToggled,
75
+ })
76
+
77
+ const handleClick = useCallback(
78
+ (event: React.MouseEvent<HTMLButtonElement>) => {
79
+ onClick?.(event)
80
+ if (event.defaultPrevented) return
81
+ handleToggle()
82
+ },
83
+ [handleToggle, onClick]
84
+ )
85
+
86
+ if (!isVisible) {
87
+ return null
88
+ }
89
+
90
+ return (
91
+ <Button
92
+ type="button"
93
+ variant="ghost"
94
+ data-active-state={isActive ? "on" : "off"}
95
+ role="button"
96
+ tabIndex={-1}
97
+ disabled={!canToggle}
98
+ data-disabled={!canToggle}
99
+ aria-label={label}
100
+ aria-pressed={isActive}
101
+ tooltip={label}
102
+ onClick={handleClick}
103
+ {...buttonProps}
104
+ ref={ref}
105
+ >
106
+ {children ?? (
107
+ <>
108
+ <Icon className="tiptap-button-icon" />
109
+ {text && <span className="tiptap-button-text">{text}</span>}
110
+ {showShortcut && (
111
+ <ListShortcutBadge type={type} shortcutKeys={shortcutKeys} />
112
+ )}
113
+ </>
114
+ )}
115
+ </Button>
116
+ )
117
+ }
118
+ )
119
+
120
+ ListButton.displayName = "ListButton"
@@ -0,0 +1,353 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useState } from "react"
4
+ import { type Editor } from "@tiptap/react"
5
+ import { NodeSelection, TextSelection } from "@tiptap/pm/state"
6
+
7
+ // --- Hooks ---
8
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
9
+
10
+ // --- Icons ---
11
+ import { ListIcon } from "@/components/tiptap-icons/list-icon"
12
+ import { ListOrderedIcon } from "@/components/tiptap-icons/list-ordered-icon"
13
+ import { ListTodoIcon } from "@/components/tiptap-icons/list-todo-icon"
14
+
15
+ // --- Lib ---
16
+ import {
17
+ findNodePosition,
18
+ getSelectedBlockNodes,
19
+ isNodeInSchema,
20
+ isNodeTypeSelected,
21
+ isValidPosition,
22
+ selectionWithinConvertibleTypes,
23
+ } from "@/lib/tiptap-utils"
24
+
25
+ export type ListType = "bulletList" | "orderedList" | "taskList"
26
+
27
+ /**
28
+ * Configuration for the list functionality
29
+ */
30
+ export interface UseListConfig {
31
+ /**
32
+ * The Tiptap editor instance.
33
+ */
34
+ editor?: Editor | null
35
+ /**
36
+ * The type of list to toggle.
37
+ */
38
+ type: ListType
39
+ /**
40
+ * Whether the button should hide when list is not available.
41
+ * @default false
42
+ */
43
+ hideWhenUnavailable?: boolean
44
+ /**
45
+ * Callback function called after a successful toggle.
46
+ */
47
+ onToggled?: () => void
48
+ }
49
+
50
+ export const listIcons = {
51
+ bulletList: ListIcon,
52
+ orderedList: ListOrderedIcon,
53
+ taskList: ListTodoIcon,
54
+ }
55
+
56
+ export const listLabels: Record<ListType, string> = {
57
+ bulletList: "Bullet List",
58
+ orderedList: "Ordered List",
59
+ taskList: "Task List",
60
+ }
61
+
62
+ export const LIST_SHORTCUT_KEYS: Record<ListType, string> = {
63
+ bulletList: "mod+shift+8",
64
+ orderedList: "mod+shift+7",
65
+ taskList: "mod+shift+9",
66
+ }
67
+
68
+ /**
69
+ * Checks if a list can be toggled in the current editor state
70
+ */
71
+ export function canToggleList(
72
+ editor: Editor | null,
73
+ type: ListType,
74
+ turnInto: boolean = true
75
+ ): boolean {
76
+ if (!editor || !editor.isEditable) return false
77
+ if (!isNodeInSchema(type, editor) || isNodeTypeSelected(editor, ["image"]))
78
+ return false
79
+
80
+ if (!turnInto) {
81
+ switch (type) {
82
+ case "bulletList":
83
+ return editor.can().toggleBulletList()
84
+ case "orderedList":
85
+ return editor.can().toggleOrderedList()
86
+ case "taskList":
87
+ return editor.can().toggleList("taskList", "taskItem")
88
+ default:
89
+ return false
90
+ }
91
+ }
92
+
93
+ // Ensure selection is in nodes we're allowed to convert
94
+ if (
95
+ !selectionWithinConvertibleTypes(editor, [
96
+ "paragraph",
97
+ "heading",
98
+ "bulletList",
99
+ "orderedList",
100
+ "taskList",
101
+ "blockquote",
102
+ "codeBlock",
103
+ ])
104
+ )
105
+ return false
106
+
107
+ // Either we can set list directly on the selection,
108
+ // or we can clear formatting/nodes to arrive at a list.
109
+ switch (type) {
110
+ case "bulletList":
111
+ return editor.can().toggleBulletList() || editor.can().clearNodes()
112
+ case "orderedList":
113
+ return editor.can().toggleOrderedList() || editor.can().clearNodes()
114
+ case "taskList":
115
+ return (
116
+ editor.can().toggleList("taskList", "taskItem") ||
117
+ editor.can().clearNodes()
118
+ )
119
+ default:
120
+ return false
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Checks if list is currently active
126
+ */
127
+ export function isListActive(editor: Editor | null, type: ListType): boolean {
128
+ if (!editor || !editor.isEditable) return false
129
+
130
+ switch (type) {
131
+ case "bulletList":
132
+ return editor.isActive("bulletList")
133
+ case "orderedList":
134
+ return editor.isActive("orderedList")
135
+ case "taskList":
136
+ return editor.isActive("taskList")
137
+ default:
138
+ return false
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Toggles list in the editor
144
+ */
145
+ export function toggleList(editor: Editor | null, type: ListType): boolean {
146
+ if (!editor || !editor.isEditable) return false
147
+ if (!canToggleList(editor, type)) return false
148
+
149
+ try {
150
+ const view = editor.view
151
+ let state = view.state
152
+ let tr = state.tr
153
+
154
+ const blocks = getSelectedBlockNodes(editor)
155
+
156
+ // In case a selection contains multiple blocks, we only allow
157
+ // toggling to node if there's exactly one block selected
158
+ // we also dont block the canToggle since it will fall back to the bottom logic
159
+ const isPossibleToTurnInto =
160
+ selectionWithinConvertibleTypes(editor, [
161
+ "paragraph",
162
+ "heading",
163
+ "bulletList",
164
+ "orderedList",
165
+ "taskList",
166
+ "blockquote",
167
+ "codeBlock",
168
+ ]) && blocks.length === 1
169
+
170
+ // No selection, find the the cursor position
171
+ if (
172
+ (state.selection.empty || state.selection instanceof TextSelection) &&
173
+ isPossibleToTurnInto
174
+ ) {
175
+ const pos = findNodePosition({
176
+ editor,
177
+ node: state.selection.$anchor.node(1),
178
+ })?.pos
179
+ if (!isValidPosition(pos)) return false
180
+
181
+ tr = tr.setSelection(NodeSelection.create(state.doc, pos))
182
+ view.dispatch(tr)
183
+ state = view.state
184
+ }
185
+
186
+ const selection = state.selection
187
+
188
+ let chain = editor.chain().focus()
189
+
190
+ // Handle NodeSelection
191
+ if (selection instanceof NodeSelection) {
192
+ const firstChild = selection.node.firstChild?.firstChild
193
+ const lastChild = selection.node.lastChild?.lastChild
194
+
195
+ const from = firstChild
196
+ ? selection.from + firstChild.nodeSize
197
+ : selection.from + 1
198
+
199
+ const to = lastChild
200
+ ? selection.to - lastChild.nodeSize
201
+ : selection.to - 1
202
+
203
+ const resolvedFrom = state.doc.resolve(from)
204
+ const resolvedTo = state.doc.resolve(to)
205
+
206
+ chain = chain
207
+ .setTextSelection(TextSelection.between(resolvedFrom, resolvedTo))
208
+ .clearNodes()
209
+ }
210
+
211
+ if (editor.isActive(type)) {
212
+ // Unwrap list
213
+ chain
214
+ .liftListItem("listItem")
215
+ .lift("bulletList")
216
+ .lift("orderedList")
217
+ .lift("taskList")
218
+ .run()
219
+ } else {
220
+ // Wrap in specific list type
221
+ const toggleMap: Record<ListType, () => typeof chain> = {
222
+ bulletList: () => chain.toggleBulletList(),
223
+ orderedList: () => chain.toggleOrderedList(),
224
+ taskList: () => chain.toggleList("taskList", "taskItem"),
225
+ }
226
+
227
+ const toggle = toggleMap[type]
228
+ if (!toggle) return false
229
+
230
+ toggle().run()
231
+ }
232
+
233
+ editor.chain().focus().selectTextblockEnd().run()
234
+
235
+ return true
236
+ } catch {
237
+ return false
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Determines if the list button should be shown
243
+ */
244
+ export function shouldShowButton(props: {
245
+ editor: Editor | null
246
+ type: ListType
247
+ hideWhenUnavailable: boolean
248
+ }): boolean {
249
+ const { editor, type, hideWhenUnavailable } = props
250
+
251
+ if (!editor) return false
252
+
253
+ if (!hideWhenUnavailable) {
254
+ return true
255
+ }
256
+
257
+ if (!editor.isEditable) return false
258
+
259
+ if (!isNodeInSchema(type, editor)) return false
260
+
261
+ if (!editor.isActive("code")) {
262
+ return canToggleList(editor, type)
263
+ }
264
+
265
+ return true
266
+ }
267
+
268
+ /**
269
+ * Custom hook that provides list functionality for Tiptap editor
270
+ *
271
+ * @example
272
+ * ```tsx
273
+ * // Simple usage
274
+ * function MySimpleListButton() {
275
+ * const { isVisible, handleToggle, isActive } = useList({ type: "bulletList" })
276
+ *
277
+ * if (!isVisible) return null
278
+ *
279
+ * return <button onClick={handleToggle}>Bullet List</button>
280
+ * }
281
+ *
282
+ * // Advanced usage with configuration
283
+ * function MyAdvancedListButton() {
284
+ * const { isVisible, handleToggle, label, isActive } = useList({
285
+ * type: "orderedList",
286
+ * editor: myEditor,
287
+ * hideWhenUnavailable: true,
288
+ * onToggled: () => console.log('List toggled!')
289
+ * })
290
+ *
291
+ * if (!isVisible) return null
292
+ *
293
+ * return (
294
+ * <MyButton
295
+ * onClick={handleToggle}
296
+ * aria-label={label}
297
+ * aria-pressed={isActive}
298
+ * >
299
+ * Toggle List
300
+ * </MyButton>
301
+ * )
302
+ * }
303
+ * ```
304
+ */
305
+ export function useList(config: UseListConfig) {
306
+ const {
307
+ editor: providedEditor,
308
+ type,
309
+ hideWhenUnavailable = false,
310
+ onToggled,
311
+ } = config
312
+
313
+ const { editor } = useTiptapEditor(providedEditor)
314
+ const [isVisible, setIsVisible] = useState<boolean>(true)
315
+ const canToggle = canToggleList(editor, type)
316
+ const isActive = isListActive(editor, type)
317
+
318
+ useEffect(() => {
319
+ if (!editor) return
320
+
321
+ const handleSelectionUpdate = () => {
322
+ setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable }))
323
+ }
324
+
325
+ handleSelectionUpdate()
326
+
327
+ editor.on("selectionUpdate", handleSelectionUpdate)
328
+
329
+ return () => {
330
+ editor.off("selectionUpdate", handleSelectionUpdate)
331
+ }
332
+ }, [editor, type, hideWhenUnavailable])
333
+
334
+ const handleToggle = useCallback(() => {
335
+ if (!editor) return false
336
+
337
+ const success = toggleList(editor, type)
338
+ if (success) {
339
+ onToggled?.()
340
+ }
341
+ return success
342
+ }, [editor, type, onToggled])
343
+
344
+ return {
345
+ isVisible,
346
+ isActive,
347
+ handleToggle,
348
+ canToggle,
349
+ label: listLabels[type],
350
+ shortcutKeys: LIST_SHORTCUT_KEYS[type],
351
+ Icon: listIcons[type],
352
+ }
353
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./list-dropdown-menu"
2
+ export * from "./use-list-dropdown-menu"
@@ -0,0 +1,126 @@
1
+ import { forwardRef, useCallback, useState, type ForwardedRef } from "react"
2
+ import { type Editor } from "@tiptap/react"
3
+
4
+ // --- Hooks ---
5
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
6
+
7
+ // --- Icons ---
8
+ import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon"
9
+
10
+ // --- Tiptap UI ---
11
+ import { ListButton, type ListType } from "@/components/tiptap-ui/list-button"
12
+
13
+ import { useListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu"
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 {
19
+ DropdownMenu,
20
+ DropdownMenuTrigger,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuGroup,
24
+ } from "@/components/tiptap-ui-primitive/dropdown-menu"
25
+
26
+ export interface ListDropdownMenuProps extends Omit<ButtonProps, "type"> {
27
+ /**
28
+ * The Tiptap editor instance.
29
+ */
30
+ editor?: Editor
31
+ /**
32
+ * The list types to display in the dropdown.
33
+ */
34
+ types?: ListType[]
35
+ /**
36
+ * Whether the dropdown should be hidden when no list types are available
37
+ * @default false
38
+ */
39
+ hideWhenUnavailable?: boolean
40
+ /**
41
+ * Callback for when the dropdown opens or closes
42
+ */
43
+ onOpenChange?: (isOpen: boolean) => void
44
+ /**
45
+ * Whether the dropdown should use a modal
46
+ */
47
+ modal?: boolean
48
+ }
49
+
50
+ function ListDropdownMenuImpl(
51
+ {
52
+ editor: providedEditor,
53
+ types = ["bulletList", "orderedList", "taskList"],
54
+ hideWhenUnavailable = false,
55
+ onOpenChange,
56
+ modal = true,
57
+ ...props
58
+ }: ListDropdownMenuProps,
59
+ ref: ForwardedRef<HTMLButtonElement>
60
+ ) {
61
+ const { editor } = useTiptapEditor(providedEditor)
62
+ const [isOpen, setIsOpen] = useState(false)
63
+
64
+ const { filteredLists, canToggle, isActive, isVisible, Icon } =
65
+ useListDropdownMenu({
66
+ editor,
67
+ types,
68
+ hideWhenUnavailable,
69
+ })
70
+
71
+ const handleOnOpenChange = useCallback(
72
+ (open: boolean) => {
73
+ setIsOpen(open)
74
+ onOpenChange?.(open)
75
+ },
76
+ [onOpenChange]
77
+ )
78
+
79
+ if (!isVisible) {
80
+ return null
81
+ }
82
+
83
+ return (
84
+ <DropdownMenu modal={modal} open={isOpen} onOpenChange={handleOnOpenChange}>
85
+ <DropdownMenuTrigger asChild>
86
+ <Button
87
+ type="button"
88
+ variant="ghost"
89
+ data-active-state={isActive ? "on" : "off"}
90
+ role="button"
91
+ tabIndex={-1}
92
+ disabled={!canToggle}
93
+ data-disabled={!canToggle}
94
+ aria-label="List options"
95
+ tooltip="List"
96
+ {...props}
97
+ ref={ref}
98
+ >
99
+ <Icon className="tiptap-button-icon" />
100
+ <ChevronDownIcon className="tiptap-button-dropdown-small" />
101
+ </Button>
102
+ </DropdownMenuTrigger>
103
+
104
+ <DropdownMenuContent align="start">
105
+ <DropdownMenuGroup>
106
+ {filteredLists.map((option) => (
107
+ <DropdownMenuItem key={option.type} asChild>
108
+ <ListButton
109
+ editor={editor}
110
+ type={option.type}
111
+ text={option.label}
112
+ showTooltip={false}
113
+ />
114
+ </DropdownMenuItem>
115
+ ))}
116
+ </DropdownMenuGroup>
117
+ </DropdownMenuContent>
118
+ </DropdownMenu>
119
+ )
120
+ }
121
+
122
+ export const ListDropdownMenu = forwardRef(ListDropdownMenuImpl)
123
+
124
+ ListDropdownMenu.displayName = "ListDropdownMenu"
125
+
126
+ export default ListDropdownMenu