@betterstart/cli 0.1.3 → 0.1.4

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 (234) hide show
  1. package/README.md +133 -0
  2. package/dist/cli.d.ts +1 -9
  3. package/dist/cli.js +13484 -367
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts +24 -266
  6. package/dist/index.js +4 -11378
  7. package/dist/index.js.map +1 -1
  8. package/package.json +29 -42
  9. package/templates/schema.json +959 -0
  10. package/templates/tiptap/hooks/use-composed-ref.ts +43 -0
  11. package/templates/tiptap/hooks/use-cursor-visibility.ts +68 -0
  12. package/templates/tiptap/hooks/use-element-rect.ts +166 -0
  13. package/templates/tiptap/hooks/use-is-breakpoint.ts +32 -0
  14. package/templates/tiptap/hooks/use-menu-navigation.ts +182 -0
  15. package/templates/tiptap/hooks/use-scrolling.ts +64 -0
  16. package/templates/tiptap/hooks/use-throttled-callback.ts +146 -0
  17. package/templates/tiptap/hooks/use-tiptap-editor.ts +46 -0
  18. package/templates/tiptap/hooks/use-unmount.ts +21 -0
  19. package/templates/tiptap/hooks/use-window-size.ts +87 -0
  20. package/templates/tiptap/lib/tiptap-utils.ts +587 -0
  21. package/templates/tiptap/styles/_keyframe-animations.scss +91 -0
  22. package/templates/tiptap/styles/_variables.scss +296 -0
  23. package/templates/tiptap/tiptap-extension/node-background-extension.ts +138 -0
  24. package/templates/tiptap/tiptap-icons/align-center-icon.tsx +38 -0
  25. package/templates/tiptap/tiptap-icons/align-justify-icon.tsx +38 -0
  26. package/templates/tiptap/tiptap-icons/align-left-icon.tsx +38 -0
  27. package/templates/tiptap/tiptap-icons/align-right-icon.tsx +38 -0
  28. package/templates/tiptap/tiptap-icons/arrow-left-icon.tsx +24 -0
  29. package/templates/tiptap/tiptap-icons/ban-icon.tsx +26 -0
  30. package/templates/tiptap/tiptap-icons/blockquote-icon.tsx +44 -0
  31. package/templates/tiptap/tiptap-icons/bold-icon.tsx +26 -0
  32. package/templates/tiptap/tiptap-icons/chevron-down-icon.tsx +26 -0
  33. package/templates/tiptap/tiptap-icons/close-icon.tsx +24 -0
  34. package/templates/tiptap/tiptap-icons/code-block-icon.tsx +38 -0
  35. package/templates/tiptap/tiptap-icons/code2-icon.tsx +32 -0
  36. package/templates/tiptap/tiptap-icons/corner-down-left-icon.tsx +26 -0
  37. package/templates/tiptap/tiptap-icons/external-link-icon.tsx +28 -0
  38. package/templates/tiptap/tiptap-icons/heading-five-icon.tsx +28 -0
  39. package/templates/tiptap/tiptap-icons/heading-four-icon.tsx +28 -0
  40. package/templates/tiptap/tiptap-icons/heading-icon.tsx +24 -0
  41. package/templates/tiptap/tiptap-icons/heading-one-icon.tsx +28 -0
  42. package/templates/tiptap/tiptap-icons/heading-six-icon.tsx +30 -0
  43. package/templates/tiptap/tiptap-icons/heading-three-icon.tsx +36 -0
  44. package/templates/tiptap/tiptap-icons/heading-two-icon.tsx +28 -0
  45. package/templates/tiptap/tiptap-icons/highlighter-icon.tsx +26 -0
  46. package/templates/tiptap/tiptap-icons/image-plus-icon.tsx +26 -0
  47. package/templates/tiptap/tiptap-icons/italic-icon.tsx +24 -0
  48. package/templates/tiptap/tiptap-icons/link-icon.tsx +28 -0
  49. package/templates/tiptap/tiptap-icons/list-icon.tsx +56 -0
  50. package/templates/tiptap/tiptap-icons/list-ordered-icon.tsx +56 -0
  51. package/templates/tiptap/tiptap-icons/list-todo-icon.tsx +50 -0
  52. package/templates/tiptap/tiptap-icons/moon-star-icon.tsx +30 -0
  53. package/templates/tiptap/tiptap-icons/redo2-icon.tsx +26 -0
  54. package/templates/tiptap/tiptap-icons/strike-icon.tsx +28 -0
  55. package/templates/tiptap/tiptap-icons/subscript-icon.tsx +38 -0
  56. package/templates/tiptap/tiptap-icons/sun-icon.tsx +58 -0
  57. package/templates/tiptap/tiptap-icons/superscript-icon.tsx +38 -0
  58. package/templates/tiptap/tiptap-icons/trash-icon.tsx +26 -0
  59. package/templates/tiptap/tiptap-icons/underline-icon.tsx +26 -0
  60. package/templates/tiptap/tiptap-icons/undo2-icon.tsx +26 -0
  61. package/templates/tiptap/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
  62. package/templates/tiptap/tiptap-node/code-block-node/code-block-node.scss +54 -0
  63. package/templates/tiptap/tiptap-node/heading-node/heading-node.scss +45 -0
  64. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +10 -0
  65. package/templates/tiptap/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
  66. package/templates/tiptap/tiptap-node/image-node/image-node.scss +35 -0
  67. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node-extension.ts +154 -0
  68. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
  69. package/templates/tiptap/tiptap-node/image-upload-node/image-upload-node.tsx +522 -0
  70. package/templates/tiptap/tiptap-node/image-upload-node/index.tsx +1 -0
  71. package/templates/tiptap/tiptap-node/list-node/list-node.scss +208 -0
  72. package/templates/tiptap/tiptap-node/paragraph-node/paragraph-node.scss +273 -0
  73. package/templates/tiptap/tiptap-ui/blockquote-button/blockquote-button.tsx +104 -0
  74. package/templates/tiptap/tiptap-ui/blockquote-button/index.tsx +2 -0
  75. package/templates/tiptap/tiptap-ui/blockquote-button/use-blockquote.ts +252 -0
  76. package/templates/tiptap/tiptap-ui/code-block-button/code-block-button.tsx +106 -0
  77. package/templates/tiptap/tiptap-ui/code-block-button/index.tsx +2 -0
  78. package/templates/tiptap/tiptap-ui/code-block-button/use-code-block.ts +261 -0
  79. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
  80. package/templates/tiptap/tiptap-ui/color-highlight-button/color-highlight-button.tsx +153 -0
  81. package/templates/tiptap/tiptap-ui/color-highlight-button/index.tsx +2 -0
  82. package/templates/tiptap/tiptap-ui/color-highlight-button/use-color-highlight.ts +345 -0
  83. package/templates/tiptap/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +207 -0
  84. package/templates/tiptap/tiptap-ui/color-highlight-popover/index.tsx +1 -0
  85. package/templates/tiptap/tiptap-ui/heading-button/heading-button.tsx +107 -0
  86. package/templates/tiptap/tiptap-ui/heading-button/index.tsx +2 -0
  87. package/templates/tiptap/tiptap-ui/heading-button/use-heading.ts +314 -0
  88. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +131 -0
  89. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
  90. package/templates/tiptap/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +130 -0
  91. package/templates/tiptap/tiptap-ui/image-upload-button/image-upload-button.tsx +114 -0
  92. package/templates/tiptap/tiptap-ui/image-upload-button/index.tsx +2 -0
  93. package/templates/tiptap/tiptap-ui/image-upload-button/use-image-upload.ts +192 -0
  94. package/templates/tiptap/tiptap-ui/link-popover/index.tsx +2 -0
  95. package/templates/tiptap/tiptap-ui/link-popover/link-popover.tsx +285 -0
  96. package/templates/tiptap/tiptap-ui/link-popover/use-link-popover.ts +286 -0
  97. package/templates/tiptap/tiptap-ui/list-button/index.tsx +2 -0
  98. package/templates/tiptap/tiptap-ui/list-button/list-button.tsx +108 -0
  99. package/templates/tiptap/tiptap-ui/list-button/use-list.ts +329 -0
  100. package/templates/tiptap/tiptap-ui/list-dropdown-menu/index.tsx +1 -0
  101. package/templates/tiptap/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +123 -0
  102. package/templates/tiptap/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +203 -0
  103. package/templates/tiptap/tiptap-ui/mark-button/index.tsx +2 -0
  104. package/templates/tiptap/tiptap-ui/mark-button/mark-button.tsx +107 -0
  105. package/templates/tiptap/tiptap-ui/mark-button/use-mark.ts +206 -0
  106. package/templates/tiptap/tiptap-ui/text-align-button/index.tsx +2 -0
  107. package/templates/tiptap/tiptap-ui/text-align-button/text-align-button.tsx +118 -0
  108. package/templates/tiptap/tiptap-ui/text-align-button/use-text-align.ts +212 -0
  109. package/templates/tiptap/tiptap-ui/undo-redo-button/index.tsx +2 -0
  110. package/templates/tiptap/tiptap-ui/undo-redo-button/undo-redo-button.tsx +105 -0
  111. package/templates/tiptap/tiptap-ui/undo-redo-button/use-undo-redo.ts +173 -0
  112. package/templates/tiptap/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
  113. package/templates/tiptap/tiptap-ui-primitive/badge/badge-group.scss +16 -0
  114. package/templates/tiptap/tiptap-ui-primitive/badge/badge.scss +99 -0
  115. package/templates/tiptap/tiptap-ui-primitive/badge/badge.tsx +46 -0
  116. package/templates/tiptap/tiptap-ui-primitive/badge/index.tsx +1 -0
  117. package/templates/tiptap/tiptap-ui-primitive/button/button-colors.scss +429 -0
  118. package/templates/tiptap/tiptap-ui-primitive/button/button-group.scss +22 -0
  119. package/templates/tiptap/tiptap-ui-primitive/button/button.scss +314 -0
  120. package/templates/tiptap/tiptap-ui-primitive/button/button.tsx +102 -0
  121. package/templates/tiptap/tiptap-ui-primitive/button/index.tsx +1 -0
  122. package/templates/tiptap/tiptap-ui-primitive/card/card.scss +77 -0
  123. package/templates/tiptap/tiptap-ui-primitive/card/card.tsx +59 -0
  124. package/templates/tiptap/tiptap-ui-primitive/card/index.tsx +1 -0
  125. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +63 -0
  126. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +95 -0
  127. package/templates/tiptap/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
  128. package/templates/tiptap/tiptap-ui-primitive/input/index.tsx +1 -0
  129. package/templates/tiptap/tiptap-ui-primitive/input/input.scss +45 -0
  130. package/templates/tiptap/tiptap-ui-primitive/input/input.tsx +18 -0
  131. package/templates/tiptap/tiptap-ui-primitive/popover/index.tsx +1 -0
  132. package/templates/tiptap/tiptap-ui-primitive/popover/popover.scss +63 -0
  133. package/templates/tiptap/tiptap-ui-primitive/popover/popover.tsx +33 -0
  134. package/templates/tiptap/tiptap-ui-primitive/separator/index.tsx +1 -0
  135. package/templates/tiptap/tiptap-ui-primitive/separator/separator.scss +23 -0
  136. package/templates/tiptap/tiptap-ui-primitive/separator/separator.tsx +33 -0
  137. package/templates/tiptap/tiptap-ui-primitive/spacer/index.tsx +1 -0
  138. package/templates/tiptap/tiptap-ui-primitive/spacer/spacer.tsx +21 -0
  139. package/templates/tiptap/tiptap-ui-primitive/toolbar/index.tsx +1 -0
  140. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
  141. package/templates/tiptap/tiptap-ui-primitive/toolbar/toolbar.tsx +113 -0
  142. package/templates/tiptap/tiptap-ui-primitive/tooltip/index.tsx +1 -0
  143. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
  144. package/templates/tiptap/tiptap-ui-primitive/tooltip/tooltip.tsx +223 -0
  145. package/templates/ui/accordion.tsx +52 -0
  146. package/templates/ui/alert-dialog.tsx +116 -0
  147. package/templates/ui/alert.tsx +48 -0
  148. package/templates/ui/aspect-ratio.tsx +7 -0
  149. package/templates/ui/avatar.tsx +46 -0
  150. package/templates/ui/badge.tsx +32 -0
  151. package/templates/ui/breadcrumb.tsx +98 -0
  152. package/templates/ui/button-group.tsx +77 -0
  153. package/templates/ui/button.tsx +48 -0
  154. package/templates/ui/calendar.tsx +176 -0
  155. package/templates/ui/card.tsx +54 -0
  156. package/templates/ui/carousel.tsx +234 -0
  157. package/templates/ui/chart.tsx +349 -0
  158. package/templates/ui/checkbox.tsx +27 -0
  159. package/templates/ui/collapsible.tsx +11 -0
  160. package/templates/ui/command.tsx +142 -0
  161. package/templates/ui/context-menu.tsx +188 -0
  162. package/templates/ui/curriculum-editor.tsx +601 -0
  163. package/templates/ui/date-picker.tsx +70 -0
  164. package/templates/ui/dialog.tsx +103 -0
  165. package/templates/ui/drawer.tsx +99 -0
  166. package/templates/ui/dropdown-menu.tsx +185 -0
  167. package/templates/ui/dynamic-list-field.tsx +95 -0
  168. package/templates/ui/empty.tsx +90 -0
  169. package/templates/ui/field.tsx +231 -0
  170. package/templates/ui/file-upload-example.tsx +113 -0
  171. package/templates/ui/form.tsx +172 -0
  172. package/templates/ui/hover-card.tsx +28 -0
  173. package/templates/ui/icon-picker.tsx +435 -0
  174. package/templates/ui/icons-data.ts +6 -0
  175. package/templates/ui/image-upload-field.tsx +360 -0
  176. package/templates/ui/input-group.tsx +160 -0
  177. package/templates/ui/input-otp.tsx +70 -0
  178. package/templates/ui/input.tsx +21 -0
  179. package/templates/ui/item.tsx +171 -0
  180. package/templates/ui/kbd.tsx +28 -0
  181. package/templates/ui/label.tsx +20 -0
  182. package/templates/ui/logo.tsx +113 -0
  183. package/templates/ui/markdown-editor.tsx +303 -0
  184. package/templates/ui/markdown-utils.ts +128 -0
  185. package/templates/ui/media-upload-field.tsx +255 -0
  186. package/templates/ui/menubar.tsx +230 -0
  187. package/templates/ui/navigation-menu.tsx +119 -0
  188. package/templates/ui/pagination.tsx +96 -0
  189. package/templates/ui/placeholder.tsx +25 -0
  190. package/templates/ui/popover.tsx +32 -0
  191. package/templates/ui/progress.tsx +24 -0
  192. package/templates/ui/radio-group.tsx +37 -0
  193. package/templates/ui/resizable.tsx +41 -0
  194. package/templates/ui/rich-text-editor.tsx +374 -0
  195. package/templates/ui/scroll-area.tsx +45 -0
  196. package/templates/ui/select.tsx +151 -0
  197. package/templates/ui/separator.tsx +25 -0
  198. package/templates/ui/sheet.tsx +120 -0
  199. package/templates/ui/sidebar.tsx +684 -0
  200. package/templates/ui/skeleton.tsx +7 -0
  201. package/templates/ui/slider.tsx +24 -0
  202. package/templates/ui/sonner.tsx +29 -0
  203. package/templates/ui/spinner.tsx +15 -0
  204. package/templates/ui/switch.tsx +28 -0
  205. package/templates/ui/table.tsx +93 -0
  206. package/templates/ui/tabs.tsx +54 -0
  207. package/templates/ui/textarea.tsx +20 -0
  208. package/templates/ui/toast.tsx +127 -0
  209. package/templates/ui/toggle-group.tsx +56 -0
  210. package/templates/ui/toggle.tsx +43 -0
  211. package/templates/ui/tooltip.tsx +31 -0
  212. package/templates/ui/use-mobile.tsx +19 -0
  213. package/templates/ui/video-upload-field.tsx +368 -0
  214. package/dist/chunk-EIH4RRIJ.js +0 -183
  215. package/dist/chunk-EIH4RRIJ.js.map +0 -1
  216. package/dist/chunk-NKRQYAS6.js +0 -260
  217. package/dist/chunk-NKRQYAS6.js.map +0 -1
  218. package/dist/chunk-QLVSHP7X.js +0 -235
  219. package/dist/chunk-QLVSHP7X.js.map +0 -1
  220. package/dist/chunk-WY6BC55D.js +0 -357
  221. package/dist/chunk-WY6BC55D.js.map +0 -1
  222. package/dist/config/index.d.ts +0 -93
  223. package/dist/config/index.js +0 -58
  224. package/dist/config/index.js.map +0 -1
  225. package/dist/core/index.d.ts +0 -415
  226. package/dist/core/index.js +0 -906
  227. package/dist/core/index.js.map +0 -1
  228. package/dist/import-resolver-BaZ-rzkH.d.ts +0 -123
  229. package/dist/logger-awLb347n.d.ts +0 -81
  230. package/dist/plugins/index.d.ts +0 -213
  231. package/dist/plugins/index.js +0 -365
  232. package/dist/plugins/index.js.map +0 -1
  233. package/dist/types-ByX_gl6y.d.ts +0 -232
  234. package/dist/types-eI549DEG.d.ts +0 -331
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useRef } from 'react'
4
+
5
+ // basically Exclude<React.ClassAttributes<T>["ref"], string>
6
+ type UserRef<T> = ((instance: T | null) => void) | React.RefObject<T | null> | null | undefined
7
+
8
+ const updateRef = <T>(ref: NonNullable<UserRef<T>>, value: T | null) => {
9
+ if (typeof ref === 'function') {
10
+ ref(value)
11
+ } else if (ref && typeof ref === 'object' && 'current' in ref) {
12
+ // Safe assignment without MutableRefObject
13
+ ;(ref as { current: T | null }).current = value
14
+ }
15
+ }
16
+
17
+ export const useComposedRef = <T extends HTMLElement>(
18
+ libRef: React.RefObject<T | null>,
19
+ userRef: UserRef<T>
20
+ ) => {
21
+ const prevUserRef = useRef<UserRef<T>>(null)
22
+
23
+ return useCallback(
24
+ (instance: T | null) => {
25
+ if (libRef && 'current' in libRef) {
26
+ ;(libRef as { current: T | null }).current = instance
27
+ }
28
+
29
+ if (prevUserRef.current) {
30
+ updateRef(prevUserRef.current, null)
31
+ }
32
+
33
+ prevUserRef.current = userRef
34
+
35
+ if (userRef) {
36
+ updateRef(userRef, instance)
37
+ }
38
+ },
39
+ [libRef, userRef]
40
+ )
41
+ }
42
+
43
+ export default useComposedRef
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useEffect } from 'react'
5
+ import { useBodyRect } from './use-element-rect'
6
+ import { useWindowSize } from './use-window-size'
7
+
8
+ export interface CursorVisibilityOptions {
9
+ /**
10
+ * The Tiptap editor instance
11
+ */
12
+ editor?: Editor | null
13
+ /**
14
+ * Reference to the toolbar element that may obscure the cursor
15
+ */
16
+ overlayHeight?: number
17
+ }
18
+
19
+ /**
20
+ * Custom hook that ensures the cursor remains visible when typing in a Tiptap editor.
21
+ * Automatically scrolls the window when the cursor would be hidden by the toolbar.
22
+ *
23
+ * @param options.editor The Tiptap editor instance
24
+ * @param options.overlayHeight Toolbar height to account for
25
+ * @returns The bounding rect of the body
26
+ */
27
+ export function useCursorVisibility({ editor, overlayHeight = 0 }: CursorVisibilityOptions) {
28
+ const { height: windowHeight } = useWindowSize()
29
+ const rect = useBodyRect({
30
+ enabled: true,
31
+ throttleMs: 100,
32
+ useResizeObserver: true
33
+ })
34
+
35
+ useEffect(() => {
36
+ const ensureCursorVisibility = () => {
37
+ if (!editor) return
38
+
39
+ const { state, view } = editor
40
+ if (!view.hasFocus()) return
41
+
42
+ // Get current cursor position coordinates
43
+ const { from } = state.selection
44
+ const cursorCoords = view.coordsAtPos(from)
45
+
46
+ if (windowHeight < rect.height && cursorCoords) {
47
+ const availableSpace = windowHeight - cursorCoords.top
48
+
49
+ // If the cursor is hidden behind the overlay or offscreen, scroll it into view
50
+ if (availableSpace < overlayHeight) {
51
+ const targetCursorY = Math.max(windowHeight / 2, overlayHeight)
52
+ const currentScrollY = window.scrollY
53
+ const cursorAbsoluteY = cursorCoords.top + currentScrollY
54
+ const newScrollY = cursorAbsoluteY - targetCursorY
55
+
56
+ window.scrollTo({
57
+ top: Math.max(0, newScrollY),
58
+ behavior: 'smooth'
59
+ })
60
+ }
61
+ }
62
+ }
63
+
64
+ ensureCursorVisibility()
65
+ }, [editor, overlayHeight, windowHeight, rect.height])
66
+
67
+ return rect
68
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import { useThrottledCallback } from './use-throttled-callback'
5
+
6
+ export type RectState = Omit<DOMRect, 'toJSON'>
7
+
8
+ export interface ElementRectOptions {
9
+ /**
10
+ * The element to track. Can be an Element, ref, or selector string.
11
+ * Defaults to document.body if not provided.
12
+ */
13
+ element?: Element | React.RefObject<Element> | string | null
14
+ /**
15
+ * Whether to enable rect tracking
16
+ */
17
+ enabled?: boolean
18
+ /**
19
+ * Throttle delay in milliseconds for rect updates
20
+ */
21
+ throttleMs?: number
22
+ /**
23
+ * Whether to use ResizeObserver for more accurate tracking
24
+ */
25
+ useResizeObserver?: boolean
26
+ }
27
+
28
+ const initialRect: RectState = {
29
+ x: 0,
30
+ y: 0,
31
+ width: 0,
32
+ height: 0,
33
+ top: 0,
34
+ right: 0,
35
+ bottom: 0,
36
+ left: 0
37
+ }
38
+
39
+ const isSSR = typeof window === 'undefined'
40
+ const hasResizeObserver = !isSSR && typeof ResizeObserver !== 'undefined'
41
+
42
+ /**
43
+ * Helper function to check if code is running on client side
44
+ */
45
+ const isClientSide = (): boolean => !isSSR
46
+
47
+ /**
48
+ * Custom hook that tracks an element's bounding rectangle and updates on resize, scroll, etc.
49
+ *
50
+ * @param options Configuration options for element rect tracking
51
+ * @returns The current bounding rectangle of the element
52
+ */
53
+ export function useElementRect({
54
+ element,
55
+ enabled = true,
56
+ throttleMs = 100,
57
+ useResizeObserver = true
58
+ }: ElementRectOptions = {}): RectState {
59
+ const [rect, setRect] = useState<RectState>(initialRect)
60
+
61
+ const getTargetElement = useCallback((): Element | null => {
62
+ if (!enabled || !isClientSide()) return null
63
+
64
+ if (!element) {
65
+ return document.body
66
+ }
67
+
68
+ if (typeof element === 'string') {
69
+ return document.querySelector(element)
70
+ }
71
+
72
+ if ('current' in element) {
73
+ return element.current
74
+ }
75
+
76
+ return element
77
+ }, [element, enabled])
78
+
79
+ const updateRect = useThrottledCallback(
80
+ () => {
81
+ if (!enabled || !isClientSide()) return
82
+
83
+ const targetElement = getTargetElement()
84
+ if (!targetElement) {
85
+ setRect(initialRect)
86
+ return
87
+ }
88
+
89
+ const newRect = targetElement.getBoundingClientRect()
90
+ setRect({
91
+ x: newRect.x,
92
+ y: newRect.y,
93
+ width: newRect.width,
94
+ height: newRect.height,
95
+ top: newRect.top,
96
+ right: newRect.right,
97
+ bottom: newRect.bottom,
98
+ left: newRect.left
99
+ })
100
+ },
101
+ throttleMs,
102
+ [enabled, getTargetElement],
103
+ { leading: true, trailing: true }
104
+ )
105
+
106
+ useEffect(() => {
107
+ if (!enabled || !isClientSide()) {
108
+ setRect(initialRect)
109
+ return
110
+ }
111
+
112
+ const targetElement = getTargetElement()
113
+ if (!targetElement) return
114
+
115
+ updateRect()
116
+
117
+ const cleanup: (() => void)[] = []
118
+
119
+ if (useResizeObserver && hasResizeObserver) {
120
+ const resizeObserver = new ResizeObserver(() => {
121
+ window.requestAnimationFrame(updateRect)
122
+ })
123
+ resizeObserver.observe(targetElement)
124
+ cleanup.push(() => resizeObserver.disconnect())
125
+ }
126
+
127
+ const handleUpdate = () => updateRect()
128
+
129
+ window.addEventListener('scroll', handleUpdate, true)
130
+ window.addEventListener('resize', handleUpdate, true)
131
+
132
+ cleanup.push(() => {
133
+ window.removeEventListener('scroll', handleUpdate)
134
+ window.removeEventListener('resize', handleUpdate)
135
+ })
136
+
137
+ return () => {
138
+ cleanup.forEach((fn) => {
139
+ fn()
140
+ })
141
+ setRect(initialRect)
142
+ }
143
+ }, [enabled, getTargetElement, updateRect, useResizeObserver])
144
+
145
+ return rect
146
+ }
147
+
148
+ /**
149
+ * Convenience hook for tracking document.body rect
150
+ */
151
+ export function useBodyRect(options: Omit<ElementRectOptions, 'element'> = {}): RectState {
152
+ return useElementRect({
153
+ ...options,
154
+ element: isClientSide() ? document.body : null
155
+ })
156
+ }
157
+
158
+ /**
159
+ * Convenience hook for tracking a ref element's rect
160
+ */
161
+ export function useRefRect<T extends Element>(
162
+ ref: React.RefObject<T>,
163
+ options: Omit<ElementRectOptions, 'element'> = {}
164
+ ): RectState {
165
+ return useElementRect({ ...options, element: ref })
166
+ }
@@ -0,0 +1,32 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ type BreakpointMode = 'min' | 'max'
6
+
7
+ /**
8
+ * Hook to detect whether the current viewport matches a given breakpoint rule.
9
+ * Example:
10
+ * useIsBreakpoint("max", 768) // true when width < 768
11
+ * useIsBreakpoint("min", 1024) // true when width >= 1024
12
+ */
13
+ export function useIsBreakpoint(mode: BreakpointMode = 'max', breakpoint = 768) {
14
+ const [matches, setMatches] = useState<boolean | undefined>(undefined)
15
+
16
+ useEffect(() => {
17
+ const query =
18
+ mode === 'min' ? `(min-width: ${breakpoint}px)` : `(max-width: ${breakpoint - 1}px)`
19
+
20
+ const mql = window.matchMedia(query)
21
+ const onChange = (e: MediaQueryListEvent) => setMatches(e.matches)
22
+
23
+ // Set initial value
24
+ setMatches(mql.matches)
25
+
26
+ // Add listener
27
+ mql.addEventListener('change', onChange)
28
+ return () => mql.removeEventListener('change', onChange)
29
+ }, [mode, breakpoint])
30
+
31
+ return !!matches
32
+ }
@@ -0,0 +1,182 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ type Orientation = 'horizontal' | 'vertical' | 'both'
7
+
8
+ interface MenuNavigationOptions<T> {
9
+ /**
10
+ * The Tiptap editor instance, if using with a Tiptap editor.
11
+ */
12
+ editor?: Editor | null
13
+ /**
14
+ * Reference to the container element for handling keyboard events.
15
+ */
16
+ containerRef?: React.RefObject<HTMLElement | null>
17
+ /**
18
+ * Search query that affects the selected item.
19
+ */
20
+ query?: string
21
+ /**
22
+ * Array of items to navigate through.
23
+ */
24
+ items: T[]
25
+ /**
26
+ * Callback fired when an item is selected.
27
+ */
28
+ onSelect?: (item: T) => void
29
+ /**
30
+ * Callback fired when the menu should close.
31
+ */
32
+ onClose?: () => void
33
+ /**
34
+ * The navigation orientation of the menu.
35
+ * @default "vertical"
36
+ */
37
+ orientation?: Orientation
38
+ /**
39
+ * Whether to automatically select the first item when the menu opens.
40
+ * @default true
41
+ */
42
+ autoSelectFirstItem?: boolean
43
+ }
44
+
45
+ /**
46
+ * Hook that implements keyboard navigation for dropdown menus and command palettes.
47
+ *
48
+ * Handles arrow keys, tab, home/end, enter for selection, and escape to close.
49
+ * Works with both Tiptap editors and regular DOM elements.
50
+ *
51
+ * @param options - Configuration options for the menu navigation
52
+ * @returns Object containing the selected index and a setter function
53
+ */
54
+ export function useMenuNavigation<T>({
55
+ editor,
56
+ containerRef,
57
+ query,
58
+ items,
59
+ onSelect,
60
+ onClose,
61
+ orientation = 'vertical',
62
+ autoSelectFirstItem = true
63
+ }: MenuNavigationOptions<T>) {
64
+ const [selectedIndex, setSelectedIndex] = useState<number>(autoSelectFirstItem ? 0 : -1)
65
+
66
+ useEffect(() => {
67
+ const handleKeyboardNavigation = (event: KeyboardEvent) => {
68
+ if (!items.length) return false
69
+
70
+ const moveNext = () =>
71
+ setSelectedIndex((currentIndex) => {
72
+ if (currentIndex === -1) return 0
73
+ return (currentIndex + 1) % items.length
74
+ })
75
+
76
+ const movePrev = () =>
77
+ setSelectedIndex((currentIndex) => {
78
+ if (currentIndex === -1) return items.length - 1
79
+ return (currentIndex - 1 + items.length) % items.length
80
+ })
81
+
82
+ switch (event.key) {
83
+ case 'ArrowUp': {
84
+ if (orientation === 'horizontal') return false
85
+ event.preventDefault()
86
+ movePrev()
87
+ return true
88
+ }
89
+
90
+ case 'ArrowDown': {
91
+ if (orientation === 'horizontal') return false
92
+ event.preventDefault()
93
+ moveNext()
94
+ return true
95
+ }
96
+
97
+ case 'ArrowLeft': {
98
+ if (orientation === 'vertical') return false
99
+ event.preventDefault()
100
+ movePrev()
101
+ return true
102
+ }
103
+
104
+ case 'ArrowRight': {
105
+ if (orientation === 'vertical') return false
106
+ event.preventDefault()
107
+ moveNext()
108
+ return true
109
+ }
110
+
111
+ case 'Tab': {
112
+ event.preventDefault()
113
+ if (event.shiftKey) {
114
+ movePrev()
115
+ } else {
116
+ moveNext()
117
+ }
118
+ return true
119
+ }
120
+
121
+ case 'Home': {
122
+ event.preventDefault()
123
+ setSelectedIndex(0)
124
+ return true
125
+ }
126
+
127
+ case 'End': {
128
+ event.preventDefault()
129
+ setSelectedIndex(items.length - 1)
130
+ return true
131
+ }
132
+
133
+ case 'Enter': {
134
+ if (event.isComposing) return false
135
+ event.preventDefault()
136
+ if (selectedIndex !== -1 && items[selectedIndex]) {
137
+ onSelect?.(items[selectedIndex])
138
+ }
139
+ return true
140
+ }
141
+
142
+ case 'Escape': {
143
+ event.preventDefault()
144
+ onClose?.()
145
+ return true
146
+ }
147
+
148
+ default:
149
+ return false
150
+ }
151
+ }
152
+
153
+ let targetElement: HTMLElement | null = null
154
+
155
+ if (editor) {
156
+ targetElement = editor.view.dom
157
+ } else if (containerRef?.current) {
158
+ targetElement = containerRef.current
159
+ }
160
+
161
+ if (targetElement) {
162
+ targetElement.addEventListener('keydown', handleKeyboardNavigation, true)
163
+
164
+ return () => {
165
+ targetElement?.removeEventListener('keydown', handleKeyboardNavigation, true)
166
+ }
167
+ }
168
+
169
+ return undefined
170
+ }, [editor, containerRef, items, selectedIndex, onSelect, onClose, orientation])
171
+
172
+ useEffect(() => {
173
+ if (query) {
174
+ setSelectedIndex(autoSelectFirstItem ? 0 : -1)
175
+ }
176
+ }, [query, autoSelectFirstItem])
177
+
178
+ return {
179
+ selectedIndex: items.length ? selectedIndex : undefined,
180
+ setSelectedIndex
181
+ }
182
+ }
@@ -0,0 +1,64 @@
1
+ import type { RefObject } from 'react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ type ScrollTarget = RefObject<HTMLElement> | Window | null | undefined
5
+ type EventTargetWithScroll = Window | HTMLElement | Document
6
+
7
+ interface UseScrollingOptions {
8
+ debounce?: number
9
+ fallbackToDocument?: boolean
10
+ }
11
+
12
+ export function useScrolling(target?: ScrollTarget, options: UseScrollingOptions = {}): boolean {
13
+ const { debounce = 150, fallbackToDocument = true } = options
14
+ const [isScrolling, setIsScrolling] = useState(false)
15
+
16
+ useEffect(() => {
17
+ // Resolve element or window
18
+ const element: EventTargetWithScroll =
19
+ target && typeof Window !== 'undefined' && target instanceof Window
20
+ ? target
21
+ : ((target as RefObject<HTMLElement>)?.current ?? window)
22
+
23
+ // Mobile: fallback to document when using window
24
+ const eventTarget: EventTargetWithScroll =
25
+ fallbackToDocument && element === window && typeof document !== 'undefined'
26
+ ? document
27
+ : element
28
+
29
+ const on = (el: EventTargetWithScroll, event: string, handler: EventListener) =>
30
+ el.addEventListener(event, handler, true)
31
+
32
+ const off = (el: EventTargetWithScroll, event: string, handler: EventListener) =>
33
+ el.removeEventListener(event, handler)
34
+
35
+ let timeout: ReturnType<typeof setTimeout>
36
+ const supportsScrollEnd = element === window && 'onscrollend' in window
37
+
38
+ const handleScroll: EventListener = () => {
39
+ if (!isScrolling) setIsScrolling(true)
40
+
41
+ if (!supportsScrollEnd) {
42
+ clearTimeout(timeout)
43
+ timeout = setTimeout(() => setIsScrolling(false), debounce)
44
+ }
45
+ }
46
+
47
+ const handleScrollEnd: EventListener = () => setIsScrolling(false)
48
+
49
+ on(eventTarget, 'scroll', handleScroll)
50
+ if (supportsScrollEnd) {
51
+ on(eventTarget, 'scrollend', handleScrollEnd)
52
+ }
53
+
54
+ return () => {
55
+ off(eventTarget, 'scroll', handleScroll)
56
+ if (supportsScrollEnd) {
57
+ off(eventTarget, 'scrollend', handleScrollEnd)
58
+ }
59
+ clearTimeout(timeout)
60
+ }
61
+ }, [target, debounce, fallbackToDocument, isScrolling])
62
+
63
+ return isScrolling
64
+ }
@@ -0,0 +1,146 @@
1
+ import { useMemo } from 'react'
2
+ import { useUnmount } from './use-unmount'
3
+
4
+ interface ThrottleSettings {
5
+ leading?: boolean | undefined
6
+ trailing?: boolean | undefined
7
+ }
8
+
9
+ const defaultOptions: ThrottleSettings = {
10
+ leading: false,
11
+ trailing: true
12
+ }
13
+
14
+ type ThrottledFunction<T extends (...args: any[]) => any> = {
15
+ (this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> | undefined
16
+ cancel: () => void
17
+ flush: () => void
18
+ }
19
+
20
+ function throttle<T extends (...args: any[]) => any>(
21
+ fn: T,
22
+ wait: number,
23
+ options: ThrottleSettings = {}
24
+ ): ThrottledFunction<T> {
25
+ let timeoutId: ReturnType<typeof setTimeout> | null = null
26
+ let lastArgs: Parameters<T> | null = null
27
+ let lastThis: ThisParameterType<T> | null = null
28
+ let lastCallTime: number | undefined
29
+ let result: ReturnType<T> | undefined
30
+
31
+ const leading = options.leading ?? true
32
+ const trailing = options.trailing ?? true
33
+
34
+ function invokeFunc(): ReturnType<T> | undefined {
35
+ const args = lastArgs!
36
+ const thisArg = lastThis!
37
+ lastArgs = lastThis = null
38
+ result = fn.apply(thisArg, args)
39
+ return result
40
+ }
41
+
42
+ function startTimer(pendingFunc: () => void, remainingWait: number) {
43
+ timeoutId = setTimeout(pendingFunc, remainingWait)
44
+ }
45
+
46
+ function remainingWait(time: number): number {
47
+ const timeSinceLastCall = time - (lastCallTime ?? 0)
48
+ return Math.max(0, wait - timeSinceLastCall)
49
+ }
50
+
51
+ function shouldInvoke(time: number): boolean {
52
+ if (lastCallTime === undefined) return true
53
+ const timeSinceLastCall = time - lastCallTime
54
+ return timeSinceLastCall >= wait || timeSinceLastCall < 0
55
+ }
56
+
57
+ function trailingEdge() {
58
+ timeoutId = null
59
+ if (trailing && lastArgs) {
60
+ return invokeFunc()
61
+ }
62
+ lastArgs = lastThis = null
63
+ return result
64
+ }
65
+
66
+ function timerExpired() {
67
+ const time = Date.now()
68
+ if (shouldInvoke(time)) {
69
+ return trailingEdge()
70
+ }
71
+ startTimer(timerExpired, remainingWait(time))
72
+ }
73
+
74
+ function leadingEdge(): ReturnType<T> | undefined {
75
+ startTimer(timerExpired, wait)
76
+ return leading ? invokeFunc() : result
77
+ }
78
+
79
+ const throttled = function (
80
+ this: ThisParameterType<T>,
81
+ ...args: Parameters<T>
82
+ ): ReturnType<T> | undefined {
83
+ const time = Date.now()
84
+ const isInvoking = shouldInvoke(time)
85
+
86
+ lastArgs = args
87
+ lastThis = this
88
+ lastCallTime = time
89
+
90
+ if (isInvoking) {
91
+ if (timeoutId === null) {
92
+ return leadingEdge()
93
+ }
94
+ // Handle invocations in a tight loop
95
+ if (trailing) {
96
+ startTimer(timerExpired, wait)
97
+ return invokeFunc()
98
+ }
99
+ }
100
+ if (timeoutId === null) {
101
+ startTimer(timerExpired, wait)
102
+ }
103
+ return result
104
+ } as ThrottledFunction<T>
105
+
106
+ throttled.cancel = () => {
107
+ if (timeoutId !== null) {
108
+ clearTimeout(timeoutId)
109
+ }
110
+ lastArgs = lastThis = null
111
+ lastCallTime = undefined
112
+ timeoutId = null
113
+ }
114
+
115
+ throttled.flush = () => {
116
+ if (timeoutId === null) return result
117
+ return trailingEdge()
118
+ }
119
+
120
+ return throttled
121
+ }
122
+
123
+ /**
124
+ * A hook that returns a throttled callback function.
125
+ *
126
+ * @param fn The function to throttle
127
+ * @param wait The time in ms to wait before calling the function
128
+ * @param dependencies The dependencies to watch for changes
129
+ * @param options The throttle options
130
+ */
131
+ export function useThrottledCallback<T extends (...args: any[]) => any>(
132
+ fn: T,
133
+ wait = 250,
134
+ dependencies: React.DependencyList = [],
135
+ options: ThrottleSettings = defaultOptions
136
+ ): ThrottledFunction<T> {
137
+ const handler = useMemo(() => throttle<T>(fn, wait, options), dependencies)
138
+
139
+ useUnmount(() => {
140
+ handler.cancel()
141
+ })
142
+
143
+ return handler
144
+ }
145
+
146
+ export default useThrottledCallback