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,348 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import { EditorContent, EditorContext, useEditor, type Editor, type Extensions } from "@tiptap/react"
5
+ import type { JSONContent } from "@tiptap/core"
6
+
7
+ // --- Tiptap Core Extensions ---
8
+ import { StarterKit } from "@tiptap/starter-kit"
9
+ import { Image } from "@tiptap/extension-image"
10
+ import { TaskItem, TaskList } from "@tiptap/extension-list"
11
+ import { TextAlign } from "@tiptap/extension-text-align"
12
+ import { Typography } from "@tiptap/extension-typography"
13
+ import { Highlight } from "@tiptap/extension-highlight"
14
+ import { Subscript } from "@tiptap/extension-subscript"
15
+ import { Superscript } from "@tiptap/extension-superscript"
16
+ import { FindAndReplace } from "@tiptap/extension-find-and-replace"
17
+ import { Selection } from "@tiptap/extensions"
18
+
19
+ // --- UI Primitives ---
20
+ import { Button } from "@/components/tiptap-ui-primitive/button"
21
+ import { Spacer } from "@/components/tiptap-ui-primitive/spacer"
22
+ import {
23
+ Toolbar,
24
+ ToolbarGroup,
25
+ ToolbarSeparator,
26
+ } from "@/components/tiptap-ui-primitive/toolbar"
27
+
28
+ // --- Tiptap Node ---
29
+ import { ImageUploadNode } from "@/components/tiptap-node/image-upload-node/image-upload-node-extension"
30
+ import { HorizontalRule } from "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension"
31
+ import "@/components/tiptap-node/blockquote-node/blockquote-node.scss"
32
+ import "@/components/tiptap-node/code-block-node/code-block-node.scss"
33
+ import "@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss"
34
+ import "@/components/tiptap-node/list-node/list-node.scss"
35
+ import "@/components/tiptap-node/image-node/image-node.scss"
36
+ import "@/components/tiptap-node/heading-node/heading-node.scss"
37
+ import "@/components/tiptap-node/paragraph-node/paragraph-node.scss"
38
+
39
+ // --- Tiptap UI ---
40
+ import { HeadingDropdownMenu } from "@/components/tiptap-ui/heading-dropdown-menu"
41
+ import { ImageUploadButton } from "@/components/tiptap-ui/image-upload-button"
42
+ import { ListDropdownMenu } from "@/components/tiptap-ui/list-dropdown-menu"
43
+ import { BlockquoteButton } from "@/components/tiptap-ui/blockquote-button"
44
+ import { CodeBlockButton } from "@/components/tiptap-ui/code-block-button"
45
+ import {
46
+ ColorHighlightPopover,
47
+ ColorHighlightPopoverContent,
48
+ ColorHighlightPopoverButton,
49
+ } from "@/components/tiptap-ui/color-highlight-popover"
50
+ import {
51
+ LinkPopover,
52
+ LinkContent,
53
+ LinkButton,
54
+ } from "@/components/tiptap-ui/link-popover"
55
+ import { MarkButton } from "@/components/tiptap-ui/mark-button"
56
+ import { TextAlignButton } from "@/components/tiptap-ui/text-align-button"
57
+ import { UndoRedoButton } from "@/components/tiptap-ui/undo-redo-button"
58
+ import {
59
+ SearchAndReplace,
60
+ SearchAndReplaceButton,
61
+ } from "@/components/tiptap-ui/search-and-replace"
62
+
63
+ // --- Icons ---
64
+ import { ArrowLeftIcon } from "@/components/tiptap-icons/arrow-left-icon"
65
+ import { HighlighterIcon } from "@/components/tiptap-icons/highlighter-icon"
66
+ import { LinkIcon } from "@/components/tiptap-icons/link-icon"
67
+
68
+ // --- Hooks ---
69
+ import { useIsBreakpoint } from "@/hooks/use-is-breakpoint"
70
+ import { useWindowSize } from "@/hooks/use-window-size"
71
+ import { useCursorVisibility } from "@/hooks/use-cursor-visibility"
72
+
73
+ // --- Components ---
74
+
75
+ // --- Lib ---
76
+ import { MAX_FILE_SIZE } from "@/lib/tiptap-utils"
77
+
78
+ // --- Styles ---
79
+ import "@/components/tiptap-templates/simple/simple-editor.scss"
80
+
81
+ const SEARCH_AND_REPLACE_SCROLL_OPTIONS: ScrollIntoViewOptions = {
82
+ block: "center",
83
+ }
84
+
85
+ const MainToolbarContent = ({
86
+ onHighlighterClick,
87
+ onLinkClick,
88
+ onSearchAndReplaceClick,
89
+ isSearchAndReplaceOpen,
90
+ searchAndReplaceButtonRef,
91
+ isMobile,
92
+ }: {
93
+ onHighlighterClick: () => void
94
+ onLinkClick: () => void
95
+ onSearchAndReplaceClick: () => void
96
+ isSearchAndReplaceOpen: boolean
97
+ searchAndReplaceButtonRef: React.RefObject<HTMLButtonElement>
98
+ isMobile: boolean
99
+ }) => {
100
+ return (
101
+ <>
102
+ <Spacer />
103
+
104
+ <ToolbarGroup>
105
+ <UndoRedoButton action="undo" />
106
+ <UndoRedoButton action="redo" />
107
+ </ToolbarGroup>
108
+
109
+ <ToolbarSeparator />
110
+
111
+ <ToolbarGroup>
112
+ <HeadingDropdownMenu modal={false} levels={[1, 2, 3, 4]} />
113
+ <ListDropdownMenu
114
+ modal={false}
115
+ types={["bulletList", "orderedList", "taskList"]}
116
+ />
117
+ <BlockquoteButton />
118
+ <CodeBlockButton />
119
+ </ToolbarGroup>
120
+
121
+ <ToolbarSeparator />
122
+
123
+ <ToolbarGroup>
124
+ <MarkButton type="bold" />
125
+ <MarkButton type="italic" />
126
+ <MarkButton type="strike" />
127
+ <MarkButton type="code" />
128
+ <MarkButton type="underline" />
129
+ {!isMobile ? (
130
+ <ColorHighlightPopover />
131
+ ) : (
132
+ <ColorHighlightPopoverButton onClick={onHighlighterClick} />
133
+ )}
134
+ {!isMobile ? <LinkPopover /> : <LinkButton onClick={onLinkClick} />}
135
+ </ToolbarGroup>
136
+
137
+ <ToolbarSeparator />
138
+
139
+ <ToolbarGroup>
140
+ <MarkButton type="superscript" />
141
+ <MarkButton type="subscript" />
142
+ </ToolbarGroup>
143
+
144
+ <ToolbarSeparator />
145
+
146
+ <ToolbarGroup>
147
+ <TextAlignButton align="left" />
148
+ <TextAlignButton align="center" />
149
+ <TextAlignButton align="right" />
150
+ <TextAlignButton align="justify" />
151
+ </ToolbarGroup>
152
+
153
+ <ToolbarSeparator />
154
+
155
+ <ToolbarGroup>
156
+ <ImageUploadButton text="Add" />
157
+ </ToolbarGroup>
158
+
159
+ <Spacer />
160
+
161
+ {isMobile && <ToolbarSeparator />}
162
+
163
+ <ToolbarGroup>
164
+ <SearchAndReplaceButton
165
+ ref={searchAndReplaceButtonRef}
166
+ aria-expanded={isSearchAndReplaceOpen}
167
+ data-active-state={isSearchAndReplaceOpen ? "on" : "off"}
168
+ onClick={onSearchAndReplaceClick}
169
+ />
170
+ </ToolbarGroup>
171
+ </>
172
+ )
173
+ }
174
+
175
+ const MobileToolbarContent = ({
176
+ type,
177
+ onBack,
178
+ }: {
179
+ type: "highlighter" | "link"
180
+ onBack: () => void
181
+ }) => (
182
+ <>
183
+ <ToolbarGroup>
184
+ <Button variant="ghost" onClick={onBack}>
185
+ <ArrowLeftIcon className="tiptap-button-icon" />
186
+ {type === "highlighter" ? (
187
+ <HighlighterIcon className="tiptap-button-icon" />
188
+ ) : (
189
+ <LinkIcon className="tiptap-button-icon" />
190
+ )}
191
+ </Button>
192
+ </ToolbarGroup>
193
+
194
+ <ToolbarSeparator />
195
+
196
+ {type === "highlighter" ? (
197
+ <ColorHighlightPopoverContent />
198
+ ) : (
199
+ <LinkContent />
200
+ )}
201
+ </>
202
+ )
203
+
204
+ export function SimpleEditor({ content, extensions = [], onEditorReady, onUpdate, uploadImage, editable = true }: {
205
+ content: JSONContent
206
+ extensions?: Extensions
207
+ onEditorReady?: (editor: Editor) => void
208
+ onUpdate?: (editor: Editor) => void
209
+ uploadImage?: (file: File) => Promise<string>
210
+ editable?: boolean
211
+ }) {
212
+ const isMobile = useIsBreakpoint()
213
+ const { height } = useWindowSize()
214
+ const [mobileView, setMobileView] = useState<"main" | "highlighter" | "link">(
215
+ "main"
216
+ )
217
+ const [isSearchAndReplaceOpen, setIsSearchAndReplaceOpen] = useState(false)
218
+ const toolbarRef = useRef<HTMLDivElement>(null)
219
+ const searchAndReplaceButtonRef = useRef<HTMLButtonElement>(null)
220
+
221
+ const editor = useEditor({
222
+ immediatelyRender: false,
223
+ editable,
224
+ editorProps: {
225
+ attributes: {
226
+ autocomplete: "off",
227
+ autocorrect: "off",
228
+ autocapitalize: "off",
229
+ "aria-label": editable ? "Main content area, start typing to enter text." : "Document preview content.",
230
+ class: "simple-editor",
231
+ },
232
+ },
233
+ extensions: [
234
+ StarterKit.configure({
235
+ horizontalRule: false,
236
+ link: {
237
+ openOnClick: false,
238
+ enableClickSelection: true,
239
+ },
240
+ }),
241
+ HorizontalRule,
242
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
243
+ TaskList,
244
+ TaskItem.configure({ nested: true }),
245
+ Highlight.configure({ multicolor: true }),
246
+ Image,
247
+ Typography,
248
+ Superscript,
249
+ Subscript,
250
+ Selection,
251
+ FindAndReplace.configure({
252
+ searchDebounceMs: 500,
253
+ injectCSS: false,
254
+ }),
255
+ ImageUploadNode.configure({
256
+ accept: "image/*",
257
+ maxSize: MAX_FILE_SIZE,
258
+ limit: 3,
259
+ upload: async (file) => {
260
+ if (!uploadImage) throw new Error("Image upload is not configured")
261
+ return uploadImage(file)
262
+ },
263
+ onError: (error) => console.error("Upload failed:", error),
264
+ }),
265
+ ...extensions,
266
+ ],
267
+ content,
268
+ onCreate: ({ editor }) => onEditorReady?.(editor),
269
+ onUpdate: ({ editor }) => onUpdate?.(editor),
270
+ })
271
+
272
+ const rect = useCursorVisibility({
273
+ editor,
274
+ overlayHeight: toolbarRef.current?.getBoundingClientRect().height ?? 0,
275
+ })
276
+
277
+ useEffect(() => {
278
+ if (!isMobile && mobileView !== "main") {
279
+ setMobileView("main")
280
+ }
281
+ }, [isMobile, mobileView])
282
+
283
+ const openSearchAndReplace = useCallback(() => {
284
+ setMobileView("main")
285
+ setIsSearchAndReplaceOpen(true)
286
+ }, [])
287
+
288
+ const closeSearchAndReplace = useCallback(() => {
289
+ setIsSearchAndReplaceOpen(false)
290
+ searchAndReplaceButtonRef.current?.focus()
291
+ }, [])
292
+
293
+ const toggleSearchAndReplace = useCallback(() => {
294
+ if (isSearchAndReplaceOpen) {
295
+ closeSearchAndReplace()
296
+ return
297
+ }
298
+
299
+ openSearchAndReplace()
300
+ }, [closeSearchAndReplace, isSearchAndReplaceOpen, openSearchAndReplace])
301
+
302
+ return (
303
+ <div className="simple-editor-wrapper">
304
+ <EditorContext.Provider value={{ editor }}>
305
+ {editable && <Toolbar
306
+ ref={toolbarRef}
307
+ style={{
308
+ ...(isMobile
309
+ ? {
310
+ bottom: `calc(100% - ${height - rect.y}px)`,
311
+ }
312
+ : {}),
313
+ }}
314
+ >
315
+ {mobileView === "main" ? (
316
+ <MainToolbarContent
317
+ onHighlighterClick={() => setMobileView("highlighter")}
318
+ onLinkClick={() => setMobileView("link")}
319
+ onSearchAndReplaceClick={toggleSearchAndReplace}
320
+ isSearchAndReplaceOpen={isSearchAndReplaceOpen}
321
+ searchAndReplaceButtonRef={searchAndReplaceButtonRef}
322
+ isMobile={isMobile}
323
+ />
324
+ ) : (
325
+ <MobileToolbarContent
326
+ type={mobileView === "highlighter" ? "highlighter" : "link"}
327
+ onBack={() => setMobileView("main")}
328
+ />
329
+ )}
330
+ </Toolbar>}
331
+
332
+ {editable && <SearchAndReplace
333
+ className="simple-editor-search-and-replace"
334
+ open={isSearchAndReplaceOpen}
335
+ onOpen={openSearchAndReplace}
336
+ onClose={closeSearchAndReplace}
337
+ scrollIntoViewOptions={SEARCH_AND_REPLACE_SCROLL_OPTIONS}
338
+ />}
339
+
340
+ <EditorContent
341
+ editor={editor}
342
+ role="presentation"
343
+ className="simple-editor-content"
344
+ />
345
+ </EditorContext.Provider>
346
+ </div>
347
+ )
348
+ }
@@ -0,0 +1,44 @@
1
+ import { Button } from "@/components/tiptap-ui-primitive/button"
2
+
3
+ // --- Icons ---
4
+ import { MoonStarIcon } from "@/components/tiptap-icons/moon-star-icon"
5
+ import { SunIcon } from "@/components/tiptap-icons/sun-icon"
6
+ import { useEffect, useState } from "react"
7
+
8
+ export function ThemeToggle() {
9
+ const [isDarkMode, setIsDarkMode] = useState<boolean>(false)
10
+
11
+ useEffect(() => {
12
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
13
+ const handleChange = () => setIsDarkMode(mediaQuery.matches)
14
+ mediaQuery.addEventListener("change", handleChange)
15
+ return () => mediaQuery.removeEventListener("change", handleChange)
16
+ }, [])
17
+
18
+ useEffect(() => {
19
+ const initialDarkMode =
20
+ !!document.querySelector('meta[name="color-scheme"][content="dark"]') ||
21
+ window.matchMedia("(prefers-color-scheme: dark)").matches
22
+ setIsDarkMode(initialDarkMode)
23
+ }, [])
24
+
25
+ useEffect(() => {
26
+ document.documentElement.classList.toggle("dark", isDarkMode)
27
+ }, [isDarkMode])
28
+
29
+ const toggleDarkMode = () => setIsDarkMode((isDark) => !isDark)
30
+
31
+ return (
32
+ <Button
33
+ onClick={toggleDarkMode}
34
+ aria-label={`Switch to ${isDarkMode ? "light" : "dark"} mode`}
35
+ variant="ghost"
36
+ >
37
+ {isDarkMode ? (
38
+ <MoonStarIcon className="tiptap-button-icon" />
39
+ ) : (
40
+ <SunIcon className="tiptap-button-icon" />
41
+ )}
42
+ </Button>
43
+ )
44
+ }
@@ -0,0 +1,122 @@
1
+ import { forwardRef, useCallback } from "react"
2
+
3
+ // --- Tiptap UI ---
4
+ import type { UseBlockquoteConfig } from "@/components/tiptap-ui/blockquote-button"
5
+ import {
6
+ BLOCKQUOTE_SHORTCUT_KEY,
7
+ useBlockquote,
8
+ } from "@/components/tiptap-ui/blockquote-button"
9
+
10
+ // --- Hooks ---
11
+ import { useTiptapEditor } from "@/hooks/use-tiptap-editor"
12
+
13
+ // --- Lib ---
14
+ import { parseShortcutKeys } from "@/lib/tiptap-utils"
15
+
16
+ // --- UI Primitives ---
17
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
18
+ import { Button } from "@/components/tiptap-ui-primitive/button"
19
+ import { Badge } from "@/components/tiptap-ui-primitive/badge"
20
+
21
+ export interface BlockquoteButtonProps
22
+ extends Omit<ButtonProps, "type">, UseBlockquoteConfig {
23
+ /**
24
+ * Optional text to display alongside the icon.
25
+ */
26
+ text?: string
27
+ /**
28
+ * Optional show shortcut keys in the button.
29
+ * @default false
30
+ */
31
+ showShortcut?: boolean
32
+ }
33
+
34
+ export function BlockquoteShortcutBadge({
35
+ shortcutKeys = BLOCKQUOTE_SHORTCUT_KEY,
36
+ }: {
37
+ shortcutKeys?: string
38
+ }) {
39
+ return <Badge>{parseShortcutKeys({ shortcutKeys })}</Badge>
40
+ }
41
+
42
+ /**
43
+ * Button component for toggling blockquote in a Tiptap editor.
44
+ *
45
+ * For custom button implementations, use the `useBlockquote` hook instead.
46
+ */
47
+ export const BlockquoteButton = forwardRef<
48
+ HTMLButtonElement,
49
+ BlockquoteButtonProps
50
+ >(
51
+ (
52
+ {
53
+ editor: providedEditor,
54
+ text,
55
+ hideWhenUnavailable = false,
56
+ onToggled,
57
+ showShortcut = false,
58
+ onClick,
59
+ children,
60
+ ...buttonProps
61
+ },
62
+ ref
63
+ ) => {
64
+ const { editor } = useTiptapEditor(providedEditor)
65
+ const {
66
+ isVisible,
67
+ canToggle,
68
+ isActive,
69
+ handleToggle,
70
+ label,
71
+ shortcutKeys,
72
+ Icon,
73
+ } = useBlockquote({
74
+ editor,
75
+ hideWhenUnavailable,
76
+ onToggled,
77
+ })
78
+
79
+ const handleClick = useCallback(
80
+ (event: React.MouseEvent<HTMLButtonElement>) => {
81
+ onClick?.(event)
82
+ if (event.defaultPrevented) return
83
+ handleToggle()
84
+ },
85
+ [handleToggle, onClick]
86
+ )
87
+
88
+ if (!isVisible) {
89
+ return null
90
+ }
91
+
92
+ return (
93
+ <Button
94
+ type="button"
95
+ variant="ghost"
96
+ data-active-state={isActive ? "on" : "off"}
97
+ role="button"
98
+ tabIndex={-1}
99
+ disabled={!canToggle}
100
+ data-disabled={!canToggle}
101
+ aria-label={label}
102
+ aria-pressed={isActive}
103
+ tooltip="Blockquote"
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
+ <BlockquoteShortcutBadge shortcutKeys={shortcutKeys} />
114
+ )}
115
+ </>
116
+ )}
117
+ </Button>
118
+ )
119
+ }
120
+ )
121
+
122
+ BlockquoteButton.displayName = "BlockquoteButton"
@@ -0,0 +1,2 @@
1
+ export * from "./blockquote-button"
2
+ export * from "./use-blockquote"