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,718 @@
1
+ "use client"
2
+
3
+ import { forwardRef, useCallback, useEffect, useRef } from "react"
4
+ import { useHotkeys } from "react-hotkeys-hook"
5
+
6
+ // --- Hooks ---
7
+ import { useComposedRef } from "@/hooks/use-composed-ref"
8
+
9
+ // --- Icons ---
10
+ import { ArrowRightIcon } from "@/components/tiptap-icons/arrow-right-icon"
11
+ import { CaseSensitiveIcon } from "@/components/tiptap-icons/case-sensitive-icon"
12
+ import { ChevronDownIcon } from "@/components/tiptap-icons/chevron-down-icon"
13
+ import { ChevronUpIcon } from "@/components/tiptap-icons/chevron-up-icon"
14
+ import { CloseIcon } from "@/components/tiptap-icons/close-icon"
15
+ import { ExternalLinkIcon } from "@/components/tiptap-icons/external-link-icon"
16
+ import { SearchIcon } from "@/components/tiptap-icons/search-icon"
17
+ import { WholeWordIcon } from "@/components/tiptap-icons/whole-word-icon"
18
+
19
+ // --- Lib ---
20
+ import { cn } from "@/lib/tiptap-utils"
21
+
22
+ // --- Tiptap UI ---
23
+ import type { UseSearchAndReplaceConfig } from "@/components/tiptap-ui/search-and-replace"
24
+ import {
25
+ NEXT_RESULT_SHORTCUT_KEY,
26
+ PREVIOUS_RESULT_SHORTCUT_KEY,
27
+ SEARCH_AND_REPLACE_SHORTCUT_KEY,
28
+ useSearchAndReplace,
29
+ } from "@/components/tiptap-ui/search-and-replace"
30
+
31
+ // --- UI Primitives ---
32
+ import type { ButtonProps } from "@/components/tiptap-ui-primitive/button"
33
+ import { Button } from "@/components/tiptap-ui-primitive/button"
34
+ import { ButtonGroup } from "@/components/tiptap-ui-primitive/button-group"
35
+ import {
36
+ Card,
37
+ CardBody,
38
+ CardFooter,
39
+ CardHeader,
40
+ } from "@/components/tiptap-ui-primitive/card"
41
+ import {
42
+ InputGroup,
43
+ InputGroupAddon,
44
+ InputGroupInput,
45
+ } from "@/components/tiptap-ui-primitive/input-group"
46
+ import { Separator } from "@/components/tiptap-ui-primitive/separator"
47
+ import { Switch } from "@/components/tiptap-ui-primitive/switch"
48
+
49
+ // --- Styles ---
50
+ import "@/components/tiptap-ui/search-and-replace/search-and-replace.scss"
51
+
52
+ export interface SearchAndReplaceProps
53
+ extends
54
+ Omit<React.HTMLAttributes<HTMLDivElement>, "children">,
55
+ UseSearchAndReplaceConfig {
56
+ /**
57
+ * Whether the panel is active. While `false` the panel stays mounted but
58
+ * hidden: highlights are cleared and the entered terms are kept, so
59
+ * toggling back on restores the previous search. Unmounting the panel
60
+ * instead clears the search entirely.
61
+ * @default true
62
+ */
63
+ open?: boolean
64
+ /**
65
+ * Called when the panel requests to close (close button or Escape). The
66
+ * panel does not position or dismiss itself — rendering, placement and
67
+ * open state are owned by the consumer.
68
+ */
69
+ onClose?: () => void
70
+ /**
71
+ * Called when the built-in Mod+F shortcut fires while the panel is hidden.
72
+ * Wire it to your open state and the shortcut works out of the box;
73
+ * without it the shortcut only refocuses the search input while the panel
74
+ * is already open.
75
+ */
76
+ onOpen?: () => void
77
+ /**
78
+ * Whether the built-in Mod+F shortcut is bound. While bound (and `onOpen`
79
+ * is wired), it replaces the browser's find-in-page everywhere on the
80
+ * page — an app with its own search should never surface the native one.
81
+ * @default true
82
+ */
83
+ enableShortcut?: boolean
84
+ /**
85
+ * Whether the search input focuses itself when the panel becomes open.
86
+ * @default true
87
+ */
88
+ autoFocusSearch?: boolean
89
+ }
90
+
91
+ export type SearchAndReplaceContentProps = SearchAndReplaceProps
92
+
93
+ function isModKey(event: React.KeyboardEvent): boolean {
94
+ return event.metaKey || event.ctrlKey
95
+ }
96
+
97
+ const REGEX_DOCS_URL =
98
+ "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"
99
+
100
+ const REGEX_SEARCH_EXAMPLES = [
101
+ { label: "Any middle letter:", pattern: "c.t" },
102
+ { label: "Either term:", pattern: "cat|tiptap" },
103
+ { label: "Uppercase or lowercase C:", pattern: "[Cc]at" },
104
+ ]
105
+
106
+ const REGEX_REPLACE_EXAMPLES = [
107
+ { pattern: "TipTap|tiptap|TIPTAP", replacement: "Tiptap" },
108
+ { pattern: "(cat|tiptap)", replacement: '"$1"' },
109
+ ]
110
+
111
+ /**
112
+ * Example row for the regex explanation box. Deliberately local to this
113
+ * box — a small ghost-style button whose label mixes plain text with code
114
+ * chips is too specific to become a shared primitive.
115
+ */
116
+ function RegexExampleButton({
117
+ pattern,
118
+ onApply,
119
+ children,
120
+ }: {
121
+ pattern: string
122
+ onApply: () => void
123
+ children: React.ReactNode
124
+ }) {
125
+ return (
126
+ <ButtonGroup
127
+ className="tiptap-search-replace-regex-example-row"
128
+ orientation="horizontal"
129
+ >
130
+ <Button
131
+ type="button"
132
+ className="tiptap-search-replace-regex-example-button"
133
+ data-regex-example={pattern}
134
+ onClick={onApply}
135
+ variant="ghost"
136
+ size="small"
137
+ >
138
+ <span className="tiptap-button-text">{children}</span>
139
+ <ArrowRightIcon className="tiptap-button-icon" aria-hidden="true" />
140
+ </Button>
141
+ </ButtonGroup>
142
+ )
143
+ }
144
+
145
+ /**
146
+ * Search and replace trigger button for toolbars. Purely presentational —
147
+ * wire `onClick` to show the `SearchAndReplace` panel wherever your layout
148
+ * places it (the panel itself binds Mod+F via its `onOpen` prop).
149
+ */
150
+ export const SearchAndReplaceButton = forwardRef<
151
+ HTMLButtonElement,
152
+ ButtonProps
153
+ >(({ className, children, ...props }, ref) => {
154
+ return (
155
+ <Button
156
+ type="button"
157
+ className={className}
158
+ variant="ghost"
159
+ role="button"
160
+ tabIndex={-1}
161
+ aria-label="Search and replace"
162
+ tooltip="Search and replace"
163
+ shortcutKeys={SEARCH_AND_REPLACE_SHORTCUT_KEY}
164
+ ref={ref}
165
+ {...props}
166
+ >
167
+ {children || <SearchIcon className="tiptap-button-icon" />}
168
+ </Button>
169
+ )
170
+ })
171
+
172
+ SearchAndReplaceButton.displayName = "SearchAndReplaceButton"
173
+
174
+ /**
175
+ * Search and replace panel for Tiptap editors.
176
+ *
177
+ * Deliberately unpositioned: render it inside a popover, docked in a corner
178
+ * (Notion-style), inline in a collapsed mobile toolbar — the wrapper is up to
179
+ * you. The panel only manages the search itself.
180
+ *
181
+ * For fully custom UIs, use the `useSearchAndReplace` hook instead.
182
+ */
183
+ export const SearchAndReplace = forwardRef<
184
+ HTMLDivElement,
185
+ SearchAndReplaceProps
186
+ >(
187
+ (
188
+ {
189
+ editor: providedEditor,
190
+ hideWhenUnavailable = false,
191
+ scrollIntoViewOptions,
192
+ open = true,
193
+ onClose,
194
+ onOpen,
195
+ enableShortcut = true,
196
+ autoFocusSearch = true,
197
+ className,
198
+ ...divProps
199
+ },
200
+ ref
201
+ ) => {
202
+ const searchAndReplace = useSearchAndReplace({
203
+ editor: providedEditor,
204
+ hideWhenUnavailable,
205
+ scrollIntoViewOptions,
206
+ })
207
+
208
+ const {
209
+ isVisible,
210
+ resultCountLabel,
211
+ searchTerm,
212
+ replaceTerm,
213
+ caseSensitive,
214
+ wholeWord,
215
+ useRegex,
216
+ canSearch,
217
+ canNavigate,
218
+ canReplace,
219
+ canReplaceAll,
220
+ setSearchTerm,
221
+ setReplaceTerm,
222
+ toggleCaseSensitive,
223
+ toggleWholeWord,
224
+ toggleUseRegex,
225
+ goToNext,
226
+ goToPrevious,
227
+ replaceCurrent,
228
+ replaceAll,
229
+ applySearch,
230
+ suspendSearch,
231
+ } = searchAndReplace
232
+
233
+ const searchInputRef = useRef<HTMLInputElement>(null)
234
+ const panelRef = useRef<HTMLDivElement>(null)
235
+ const composedPanelRef = useComposedRef(panelRef, ref)
236
+
237
+ const focusSearchInput = useCallback(() => {
238
+ const input = searchInputRef.current
239
+ if (!input) return
240
+ input.focus()
241
+ input.select()
242
+ }, [])
243
+
244
+ // Example buttons fill the inputs so the pattern can be tested right
245
+ // away; the extension debounce then applies the search.
246
+ const applyRegexExample = useCallback(
247
+ (pattern: string, replacement?: string) => {
248
+ setSearchTerm(pattern)
249
+ if (replacement !== undefined) {
250
+ setReplaceTerm(replacement)
251
+ }
252
+ focusSearchInput()
253
+ },
254
+ [setSearchTerm, setReplaceTerm, focusSearchInput]
255
+ )
256
+
257
+ // Built-in Mod+F: with a custom search present, the browser's
258
+ // find-in-page is replaced everywhere on the page, no matter where
259
+ // focus sits. Opens the panel via `onOpen` when hidden, refocuses the
260
+ // search input while already open. Only an unwired `onOpen` falls back
261
+ // to the native find.
262
+ useHotkeys(
263
+ SEARCH_AND_REPLACE_SHORTCUT_KEY,
264
+ (event) => {
265
+ if (open) {
266
+ event.preventDefault()
267
+ focusSearchInput()
268
+ return
269
+ }
270
+
271
+ if (!onOpen) return
272
+
273
+ event.preventDefault()
274
+ onOpen()
275
+ },
276
+ {
277
+ enabled: enableShortcut && isVisible && canSearch,
278
+ enableOnContentEditable: true,
279
+ enableOnFormTags: true,
280
+ },
281
+ [open, onOpen, focusSearchInput]
282
+ )
283
+
284
+ // While search mode is on, plain arrow keys navigate results even when
285
+ // focus sits outside the panel (e.g. on the page body right after
286
+ // load). Form fields and the editor itself are excluded by default, so
287
+ // the caret and other inputs keep their arrows; inside the panel the
288
+ // panel's own key handler takes over.
289
+ useHotkeys(
290
+ "up,down",
291
+ (event) => {
292
+ const target = event.target instanceof Node ? event.target : null
293
+ if (panelRef.current?.contains(target)) return
294
+
295
+ event.preventDefault()
296
+ if (event.key === "ArrowDown") {
297
+ goToNext()
298
+ } else {
299
+ goToPrevious()
300
+ }
301
+ },
302
+ { enabled: open && canNavigate },
303
+ [goToNext, goToPrevious]
304
+ )
305
+
306
+ // Re-apply the stored terms when the panel becomes open and clear
307
+ // highlights (keeping the values) when it hides. Refs keep the effect
308
+ // from re-firing on every keystroke.
309
+ const applySearchRef = useRef(applySearch)
310
+ const suspendSearchRef = useRef(suspendSearch)
311
+ useEffect(() => {
312
+ applySearchRef.current = applySearch
313
+ suspendSearchRef.current = suspendSearch
314
+ })
315
+
316
+ const previousOpenRef = useRef(false)
317
+ useEffect(() => {
318
+ if (previousOpenRef.current === open) return
319
+ previousOpenRef.current = open
320
+
321
+ if (open) {
322
+ applySearchRef.current()
323
+ } else {
324
+ suspendSearchRef.current()
325
+ }
326
+ }, [open])
327
+
328
+ // Focus the search input once per open, waiting until the editor is
329
+ // ready — on a freshly loaded page the input is still disabled for a
330
+ // moment, and focusing a disabled input is a no-op.
331
+ const focusedForOpenRef = useRef(false)
332
+ useEffect(() => {
333
+ if (!open) {
334
+ focusedForOpenRef.current = false
335
+ return
336
+ }
337
+ if (focusedForOpenRef.current || !autoFocusSearch || !canSearch) return
338
+
339
+ focusedForOpenRef.current = true
340
+ focusSearchInput()
341
+ }, [open, canSearch, autoFocusSearch, focusSearchInput])
342
+
343
+ const handlePanelKeyDown = useCallback(
344
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
345
+ if (event.key === "Escape") {
346
+ if (onClose) {
347
+ event.preventDefault()
348
+ event.stopPropagation()
349
+ onClose()
350
+ }
351
+ return
352
+ }
353
+
354
+ // Arrow up/down step through the results from anywhere inside the
355
+ // panel — inputs, nav buttons, options — like Notion's find bar.
356
+ // Handled at the panel so a focused button navigates too (and the
357
+ // page doesn't scroll instead).
358
+ if (
359
+ (event.key === "ArrowDown" || event.key === "ArrowUp") &&
360
+ !event.shiftKey &&
361
+ !event.altKey &&
362
+ !isModKey(event) &&
363
+ !event.nativeEvent.isComposing &&
364
+ canNavigate
365
+ ) {
366
+ event.preventDefault()
367
+ if (event.key === "ArrowDown") {
368
+ goToNext()
369
+ } else {
370
+ goToPrevious()
371
+ }
372
+ return
373
+ }
374
+
375
+ // Navigation shortcuts are scoped to the open panel on purpose, so
376
+ // they never compete with shortcuts elsewhere in the app (e.g. the
377
+ // image download button also uses Mod+Shift+D).
378
+ if (isModKey(event) && event.shiftKey && !event.altKey) {
379
+ const key = event.key.toLowerCase()
380
+
381
+ if (key === "f" && canNavigate) {
382
+ event.preventDefault()
383
+ event.stopPropagation()
384
+ goToNext()
385
+ return
386
+ }
387
+
388
+ if (key === "d" && canNavigate) {
389
+ event.preventDefault()
390
+ event.stopPropagation()
391
+ goToPrevious()
392
+ }
393
+ }
394
+ },
395
+ [onClose, canNavigate, goToNext, goToPrevious]
396
+ )
397
+
398
+ const handleSearchKeyDown = useCallback(
399
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
400
+ if (
401
+ event.key === "Enter" &&
402
+ !event.shiftKey &&
403
+ !isModKey(event) &&
404
+ !event.nativeEvent.isComposing &&
405
+ canNavigate
406
+ ) {
407
+ event.preventDefault()
408
+ goToNext()
409
+ }
410
+ },
411
+ [canNavigate, goToNext]
412
+ )
413
+
414
+ const handleReplaceKeyDown = useCallback(
415
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
416
+ if (
417
+ event.key === "Enter" &&
418
+ !event.shiftKey &&
419
+ !isModKey(event) &&
420
+ !event.nativeEvent.isComposing &&
421
+ canReplace
422
+ ) {
423
+ event.preventDefault()
424
+ replaceCurrent()
425
+ }
426
+ },
427
+ [canReplace, replaceCurrent]
428
+ )
429
+
430
+ if (!isVisible) {
431
+ return null
432
+ }
433
+
434
+ return (
435
+ <Card
436
+ className={cn("tiptap-search-replace", className)}
437
+ data-open={open ? "true" : "false"}
438
+ role="dialog"
439
+ aria-label="Search and replace"
440
+ onKeyDown={handlePanelKeyDown}
441
+ ref={composedPanelRef}
442
+ {...divProps}
443
+ >
444
+ <CardHeader className="tiptap-search-replace-header">
445
+ <span className="tiptap-search-replace-count" aria-live="polite">
446
+ {resultCountLabel}
447
+ </span>
448
+
449
+ <div className="tiptap-search-replace-nav-group">
450
+ <ButtonGroup className="button-group" orientation="horizontal">
451
+ <ButtonGroup>
452
+ <Button
453
+ type="button"
454
+ data-search-replace-action="prev"
455
+ variant="ghost"
456
+ size="small"
457
+ disabled={!canNavigate}
458
+ aria-label="Previous result"
459
+ tooltip="Previous result"
460
+ shortcutKeys={PREVIOUS_RESULT_SHORTCUT_KEY}
461
+ onClick={goToPrevious}
462
+ >
463
+ <ChevronUpIcon className="tiptap-button-icon" />
464
+ </Button>
465
+ </ButtonGroup>
466
+ <ButtonGroup>
467
+ <Button
468
+ type="button"
469
+ data-search-replace-action="next"
470
+ variant="ghost"
471
+ size="small"
472
+ disabled={!canNavigate}
473
+ aria-label="Next result"
474
+ tooltip="Next result"
475
+ shortcutKeys={NEXT_RESULT_SHORTCUT_KEY}
476
+ onClick={goToNext}
477
+ >
478
+ <ChevronDownIcon className="tiptap-button-icon" />
479
+ </Button>
480
+ </ButtonGroup>
481
+ </ButtonGroup>
482
+
483
+ <ButtonGroup className="button-group" orientation="horizontal">
484
+ <Button
485
+ type="button"
486
+ data-search-replace-action="close"
487
+ variant="ghost"
488
+ size="small"
489
+ aria-label="Close"
490
+ tooltip="Close"
491
+ shortcutKeys="esc"
492
+ onClick={onClose}
493
+ >
494
+ <CloseIcon className="tiptap-button-icon" />
495
+ </Button>
496
+ </ButtonGroup>
497
+ </div>
498
+ </CardHeader>
499
+
500
+ <CardBody className="tiptap-search-replace-body">
501
+ <div className="tiptap-search-replace-query-controls">
502
+ <div className="tiptap-search-replace-inputs">
503
+ <InputGroup className="tiptap-search-replace-input-group">
504
+ <InputGroupInput
505
+ data-field="search-query"
506
+ placeholder="Search"
507
+ aria-label="Search"
508
+ value={searchTerm}
509
+ autoFocus={open && autoFocusSearch}
510
+ autoComplete="off"
511
+ autoCorrect="off"
512
+ autoCapitalize="off"
513
+ spellCheck={false}
514
+ disabled={!canSearch}
515
+ onChange={(event) => setSearchTerm(event.target.value)}
516
+ onKeyDown={handleSearchKeyDown}
517
+ ref={searchInputRef}
518
+ />
519
+ <InputGroupAddon
520
+ align="inline-end"
521
+ className="tiptap-search-replace-keyboard-hint-addon"
522
+ >
523
+ <kbd
524
+ className="tiptap-search-replace-keyboard-hint"
525
+ aria-hidden="true"
526
+ >
527
+
528
+ </kbd>
529
+ </InputGroupAddon>
530
+ </InputGroup>
531
+
532
+ <InputGroup className="tiptap-search-replace-input-group">
533
+ <InputGroupInput
534
+ data-field="replace-query"
535
+ placeholder="Replace"
536
+ aria-label="Replace"
537
+ value={replaceTerm}
538
+ autoComplete="off"
539
+ autoCorrect="off"
540
+ autoCapitalize="off"
541
+ spellCheck={false}
542
+ disabled={!canSearch}
543
+ onChange={(event) => setReplaceTerm(event.target.value)}
544
+ onKeyDown={handleReplaceKeyDown}
545
+ />
546
+ <InputGroupAddon
547
+ align="inline-end"
548
+ className="tiptap-search-replace-keyboard-hint-addon"
549
+ >
550
+ <kbd
551
+ className="tiptap-search-replace-keyboard-hint"
552
+ aria-hidden="true"
553
+ >
554
+
555
+ </kbd>
556
+ </InputGroupAddon>
557
+ </InputGroup>
558
+ </div>
559
+
560
+ <div className="tiptap-search-replace-options">
561
+ <Button
562
+ type="button"
563
+ data-option="match-case"
564
+ variant="check"
565
+ aria-checked={caseSensitive}
566
+ disabled={!canSearch}
567
+ onClick={toggleCaseSensitive}
568
+ >
569
+ <CaseSensitiveIcon
570
+ className="tiptap-button-icon"
571
+ aria-hidden="true"
572
+ />
573
+ <span className="tiptap-button-text">Match case</span>
574
+ </Button>
575
+
576
+ <Button
577
+ type="button"
578
+ data-option="whole-words"
579
+ variant="check"
580
+ aria-checked={wholeWord}
581
+ disabled={!canSearch}
582
+ onClick={toggleWholeWord}
583
+ >
584
+ <WholeWordIcon
585
+ className="tiptap-button-icon"
586
+ aria-hidden="true"
587
+ />
588
+ <span className="tiptap-button-text">Whole words</span>
589
+ </Button>
590
+ </div>
591
+ </div>
592
+
593
+ <Separator orientation="horizontal" />
594
+
595
+ <div className="tiptap-search-replace-regex-toggle">
596
+ <label
597
+ data-option="use-regex"
598
+ className="tiptap-search-replace-regex-toggle-row"
599
+ >
600
+ <span className="tiptap-search-replace-regex-toggle-label">
601
+ Use regular expression
602
+ </span>
603
+ <Switch
604
+ checked={useRegex}
605
+ onCheckedChange={toggleUseRegex}
606
+ disabled={!canSearch}
607
+ />
608
+ </label>
609
+
610
+ {useRegex && (
611
+ <div className="tiptap-search-replace-regex-help">
612
+ <div className="tiptap-search-replace-regex-help-section">
613
+ <span className="tiptap-search-replace-regex-help-label">
614
+ Try a search pattern
615
+ </span>
616
+
617
+ <ButtonGroup
618
+ orientation="vertical"
619
+ className="tiptap-search-replace-regex-example-list"
620
+ >
621
+ {REGEX_SEARCH_EXAMPLES.map(({ label, pattern }) => (
622
+ <RegexExampleButton
623
+ key={pattern}
624
+ pattern={pattern}
625
+ onApply={() => applyRegexExample(pattern)}
626
+ >
627
+ {label} <code>{pattern}</code>
628
+ </RegexExampleButton>
629
+ ))}
630
+ </ButtonGroup>
631
+ </div>
632
+
633
+ <Separator
634
+ orientation="horizontal"
635
+ className="tiptap-search-replace-regex-help-separator"
636
+ />
637
+
638
+ <div className="tiptap-search-replace-regex-help-section">
639
+ <span className="tiptap-search-replace-regex-help-label">
640
+ Try search and replace
641
+ </span>
642
+
643
+ <ButtonGroup
644
+ orientation="vertical"
645
+ className="tiptap-search-replace-regex-example-list"
646
+ >
647
+ {REGEX_REPLACE_EXAMPLES.map(({ pattern, replacement }) => (
648
+ <RegexExampleButton
649
+ key={pattern}
650
+ pattern={pattern}
651
+ onApply={() => applyRegexExample(pattern, replacement)}
652
+ >
653
+ <code>{pattern}</code> to <code>{replacement}</code>
654
+ </RegexExampleButton>
655
+ ))}
656
+ </ButtonGroup>
657
+
658
+ <a
659
+ className="tiptap-search-replace-regex-docs-link"
660
+ data-search-replace-action="regex-docs"
661
+ href={REGEX_DOCS_URL}
662
+ target="_blank"
663
+ rel="noopener noreferrer"
664
+ >
665
+ Learn about regular expressions
666
+ <ExternalLinkIcon
667
+ className="tiptap-search-replace-regex-docs-link-icon"
668
+ aria-hidden="true"
669
+ />
670
+ </a>
671
+ </div>
672
+ </div>
673
+ )}
674
+ </div>
675
+
676
+ <Separator orientation="horizontal" />
677
+ </CardBody>
678
+
679
+ <CardFooter className="tiptap-search-replace-actions">
680
+ <ButtonGroup className="button-group" orientation="horizontal">
681
+ <ButtonGroup>
682
+ <Button
683
+ type="button"
684
+ data-search-replace-action="replace"
685
+ disabled={!canReplace}
686
+ aria-label="Replace current result"
687
+ onClick={replaceCurrent}
688
+ >
689
+ Replace
690
+ </Button>
691
+ </ButtonGroup>
692
+ <ButtonGroup>
693
+ <Button
694
+ type="button"
695
+ data-search-replace-action="replace-all"
696
+ disabled={!canReplaceAll}
697
+ aria-label="Replace all results"
698
+ onClick={replaceAll}
699
+ >
700
+ Replace all
701
+ </Button>
702
+ </ButtonGroup>
703
+ </ButtonGroup>
704
+ </CardFooter>
705
+ </Card>
706
+ )
707
+ }
708
+ )
709
+
710
+ SearchAndReplace.displayName = "SearchAndReplace"
711
+
712
+ /**
713
+ * Alias of `SearchAndReplace` for contexts where the panel is embedded as
714
+ * plain content (e.g. inside a collapsed mobile toolbar).
715
+ */
716
+ export const SearchAndReplaceContent = SearchAndReplace
717
+
718
+ export default SearchAndReplace