@open-cloud-initiative/editor-x 0.0.1

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 (190) hide show
  1. package/.devcontainer/Dockerfile +13 -0
  2. package/.devcontainer/devcontainer.json +52 -0
  3. package/.github/dependabot.yml +10 -0
  4. package/.github/workflows/pages.yml +42 -0
  5. package/.github/workflows/publish.yml +41 -0
  6. package/.vscode/settings.json +7 -0
  7. package/LICENSE +9 -0
  8. package/README.md +9 -0
  9. package/app/_component/editor.tsx +383 -0
  10. package/app/layout.tsx +46 -0
  11. package/app/page.tsx +11 -0
  12. package/app/r/registry.json/route.ts +22 -0
  13. package/components/editorx/editor.tsx +1794 -0
  14. package/components/editorx/extensions/floating-menu.tsx +376 -0
  15. package/components/editorx/extensions/floating-toolbar.tsx +97 -0
  16. package/components/editorx/extensions/image-placeholder.tsx +316 -0
  17. package/components/editorx/extensions/image.tsx +462 -0
  18. package/components/editorx/extensions/search-and-replace.tsx +438 -0
  19. package/components/editorx/rich-text-editor.tsx +383 -0
  20. package/components/editorx/tiptap.css +421 -0
  21. package/components/editorx/toolbars/alignment.tsx +126 -0
  22. package/components/editorx/toolbars/blockquote.tsx +47 -0
  23. package/components/editorx/toolbars/bold.tsx +48 -0
  24. package/components/editorx/toolbars/bullet-list.tsx +48 -0
  25. package/components/editorx/toolbars/code-block.tsx +47 -0
  26. package/components/editorx/toolbars/code.tsx +43 -0
  27. package/components/editorx/toolbars/color-and-highlight.tsx +215 -0
  28. package/components/editorx/toolbars/editor-toolbar.tsx +77 -0
  29. package/components/editorx/toolbars/hard-break.tsx +46 -0
  30. package/components/editorx/toolbars/headings.tsx +97 -0
  31. package/components/editorx/toolbars/horizontal-rule.tsx +42 -0
  32. package/components/editorx/toolbars/image-placeholder-toolbar.tsx +47 -0
  33. package/components/editorx/toolbars/italic.tsx +48 -0
  34. package/components/editorx/toolbars/link.tsx +130 -0
  35. package/components/editorx/toolbars/mobile-toolbar-group.tsx +76 -0
  36. package/components/editorx/toolbars/ordered-list.tsx +47 -0
  37. package/components/editorx/toolbars/redo.tsx +44 -0
  38. package/components/editorx/toolbars/strikethrough.tsx +48 -0
  39. package/components/editorx/toolbars/toolbar-provider.tsx +29 -0
  40. package/components/editorx/toolbars/underline.tsx +48 -0
  41. package/components/editorx/toolbars/undo.tsx +43 -0
  42. package/components/layout/theme-switcher.tsx +26 -0
  43. package/components/main-nav.tsx +24 -0
  44. package/components/mobile-nav.tsx +46 -0
  45. package/components/open-in-v0-button.tsx +38 -0
  46. package/components/page-header.tsx +30 -0
  47. package/components/site-footer.tsx +41 -0
  48. package/components/site-header.tsx +32 -0
  49. package/components/theme-provider.tsx +8 -0
  50. package/components/ui/button.tsx +57 -0
  51. package/components/ui/checkbox.tsx +30 -0
  52. package/components/ui/collapsible.tsx +11 -0
  53. package/components/ui/command.tsx +148 -0
  54. package/components/ui/dialog.tsx +122 -0
  55. package/components/ui/drawer.tsx +118 -0
  56. package/components/ui/dropdown-menu.tsx +201 -0
  57. package/components/ui/input.tsx +22 -0
  58. package/components/ui/label.tsx +26 -0
  59. package/components/ui/popover.tsx +33 -0
  60. package/components/ui/resizable.tsx +40 -0
  61. package/components/ui/scroll-area.tsx +42 -0
  62. package/components/ui/separator.tsx +31 -0
  63. package/components/ui/sheet.tsx +140 -0
  64. package/components/ui/sidebar.tsx +763 -0
  65. package/components/ui/skeleton.tsx +15 -0
  66. package/components/ui/spinner.tsx +29 -0
  67. package/components/ui/tabs.tsx +55 -0
  68. package/components/ui/toggle-group.tsx +61 -0
  69. package/components/ui/toggle.tsx +45 -0
  70. package/components/ui/tooltip.tsx +32 -0
  71. package/components.json +21 -0
  72. package/config/site.ts +15 -0
  73. package/eslint.config.mjs +20 -0
  74. package/hooks/use-character-limit.ts +28 -0
  75. package/hooks/use-copy-to-clipboard.ts +16 -0
  76. package/hooks/use-debounce.ts +17 -0
  77. package/hooks/use-image-upload.ts +97 -0
  78. package/hooks/use-media-querry.ts +18 -0
  79. package/hooks/use-mobile.tsx +19 -0
  80. package/images/editor.png +0 -0
  81. package/lib/content.ts +39 -0
  82. package/lib/cookie-client.ts +19 -0
  83. package/lib/localstorage-client.ts +19 -0
  84. package/lib/package.ts +144 -0
  85. package/lib/preferences-config.ts +72 -0
  86. package/lib/preferences-storage.ts +20 -0
  87. package/lib/theme-utils.ts +12 -0
  88. package/lib/theme.ts +50 -0
  89. package/lib/tiptap-utils.ts +45 -0
  90. package/lib/utils.ts +11 -0
  91. package/next-env.d.ts +6 -0
  92. package/next.config.mjs +11 -0
  93. package/package.json +92 -0
  94. package/postcss.config.mjs +8 -0
  95. package/prettier.config.mjs +15 -0
  96. package/public/android-chrome-192x192.png +0 -0
  97. package/public/android-chrome-512x512.png +0 -0
  98. package/public/apple-touch-icon.png +0 -0
  99. package/public/favicon-16x16.png +0 -0
  100. package/public/favicon-32x32.png +0 -0
  101. package/public/favicon.ico +0 -0
  102. package/public/file.svg +1 -0
  103. package/public/globe.svg +1 -0
  104. package/public/next.svg +1 -0
  105. package/public/og.webp +0 -0
  106. package/public/r/editor-x.json +85 -0
  107. package/public/r/registry.json +93 -0
  108. package/public/site.webmanifest +19 -0
  109. package/public/vercel.svg +1 -0
  110. package/public/window.svg +1 -0
  111. package/registry/editor/components/editor.tsx +1794 -0
  112. package/registry/editor/components/extensions/floating-menu.tsx +376 -0
  113. package/registry/editor/components/extensions/floating-toolbar.tsx +97 -0
  114. package/registry/editor/components/extensions/image-placeholder.tsx +316 -0
  115. package/registry/editor/components/extensions/image.tsx +462 -0
  116. package/registry/editor/components/extensions/search-and-replace.tsx +438 -0
  117. package/registry/editor/components/rich-text-editor.tsx +383 -0
  118. package/registry/editor/components/tiptap.css +421 -0
  119. package/registry/editor/components/toolbars/alignment.tsx +126 -0
  120. package/registry/editor/components/toolbars/blockquote.tsx +47 -0
  121. package/registry/editor/components/toolbars/bold.tsx +48 -0
  122. package/registry/editor/components/toolbars/bullet-list.tsx +48 -0
  123. package/registry/editor/components/toolbars/code-block.tsx +47 -0
  124. package/registry/editor/components/toolbars/code.tsx +43 -0
  125. package/registry/editor/components/toolbars/color-and-highlight.tsx +215 -0
  126. package/registry/editor/components/toolbars/editor-toolbar.tsx +77 -0
  127. package/registry/editor/components/toolbars/hard-break.tsx +46 -0
  128. package/registry/editor/components/toolbars/headings.tsx +97 -0
  129. package/registry/editor/components/toolbars/horizontal-rule.tsx +42 -0
  130. package/registry/editor/components/toolbars/image-placeholder-toolbar.tsx +47 -0
  131. package/registry/editor/components/toolbars/italic.tsx +48 -0
  132. package/registry/editor/components/toolbars/link.tsx +130 -0
  133. package/registry/editor/components/toolbars/mobile-toolbar-group.tsx +76 -0
  134. package/registry/editor/components/toolbars/ordered-list.tsx +47 -0
  135. package/registry/editor/components/toolbars/redo.tsx +44 -0
  136. package/registry/editor/components/toolbars/strikethrough.tsx +48 -0
  137. package/registry/editor/components/toolbars/toolbar-provider.tsx +29 -0
  138. package/registry/editor/components/toolbars/underline.tsx +48 -0
  139. package/registry/editor/components/toolbars/undo.tsx +43 -0
  140. package/registry/editor/components/ui/button.tsx +57 -0
  141. package/registry/editor/components/ui/checkbox.tsx +30 -0
  142. package/registry/editor/components/ui/collapsible.tsx +11 -0
  143. package/registry/editor/components/ui/command.tsx +148 -0
  144. package/registry/editor/components/ui/dialog.tsx +122 -0
  145. package/registry/editor/components/ui/drawer.tsx +118 -0
  146. package/registry/editor/components/ui/dropdown-menu.tsx +201 -0
  147. package/registry/editor/components/ui/input.tsx +22 -0
  148. package/registry/editor/components/ui/label.tsx +26 -0
  149. package/registry/editor/components/ui/popover.tsx +33 -0
  150. package/registry/editor/components/ui/resizable.tsx +40 -0
  151. package/registry/editor/components/ui/scroll-area.tsx +42 -0
  152. package/registry/editor/components/ui/separator.tsx +31 -0
  153. package/registry/editor/components/ui/sheet.tsx +140 -0
  154. package/registry/editor/components/ui/sidebar.tsx +763 -0
  155. package/registry/editor/components/ui/skeleton.tsx +15 -0
  156. package/registry/editor/components/ui/spinner.tsx +29 -0
  157. package/registry/editor/components/ui/tabs.tsx +55 -0
  158. package/registry/editor/components/ui/toggle-group.tsx +61 -0
  159. package/registry/editor/components/ui/toggle.tsx +45 -0
  160. package/registry/editor/components/ui/tooltip.tsx +32 -0
  161. package/registry/editor/hooks/use-character-limit.ts +28 -0
  162. package/registry/editor/hooks/use-copy-to-clipboard.ts +16 -0
  163. package/registry/editor/hooks/use-debounce.ts +17 -0
  164. package/registry/editor/hooks/use-image-upload.ts +97 -0
  165. package/registry/editor/hooks/use-media-querry.ts +18 -0
  166. package/registry/editor/hooks/use-mobile.tsx +19 -0
  167. package/registry/editor/lib/content.ts +39 -0
  168. package/registry/editor/lib/cookie-client.ts +19 -0
  169. package/registry/editor/lib/localstorage-client.ts +19 -0
  170. package/registry/editor/lib/package.ts +144 -0
  171. package/registry/editor/lib/preferences-config.ts +72 -0
  172. package/registry/editor/lib/preferences-storage.ts +20 -0
  173. package/registry/editor/lib/theme-utils.ts +12 -0
  174. package/registry/editor/lib/theme.ts +50 -0
  175. package/registry/editor/lib/tiptap-utils.ts +45 -0
  176. package/registry/editor/lib/utils.ts +11 -0
  177. package/registry/editor/page.tsx +9 -0
  178. package/registry.json +93 -0
  179. package/reset.d.ts +1 -0
  180. package/scripts/generate-theme-presets.ts +128 -0
  181. package/scripts/postCreateCommand.sh +0 -0
  182. package/scripts/theme-boot.tsx +105 -0
  183. package/server/server-actions.ts +27 -0
  184. package/stores/preferences/preferences-provider.tsx +55 -0
  185. package/stores/preferences/preferences-store.ts +23 -0
  186. package/styles/globals.css +288 -0
  187. package/styles/presets/brutalist.css +89 -0
  188. package/styles/presets/soft-pop.css +89 -0
  189. package/styles/presets/tangerine.css +89 -0
  190. package/tsconfig.json +50 -0
@@ -0,0 +1,376 @@
1
+ 'use client'
2
+
3
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
4
+ import { cn } from '@/lib/utils'
5
+ import type { Editor } from '@tiptap/core'
6
+ import { FloatingMenu } from '@tiptap/react/menus'
7
+ import { useDebounce } from 'hooks/use-debounce'
8
+ import {
9
+ AlignCenter,
10
+ AlignLeft,
11
+ AlignRight,
12
+ ChevronRight,
13
+ Code2,
14
+ CodeSquare,
15
+ Heading1,
16
+ Heading2,
17
+ Heading3,
18
+ ImageIcon,
19
+ List,
20
+ ListOrdered,
21
+ Minus,
22
+ Quote,
23
+ TextQuote,
24
+ } from 'lucide-react'
25
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
26
+
27
+ interface CommandItemType {
28
+ title: string
29
+ description: string
30
+ icon: React.ComponentType<{ className?: string }>
31
+ keywords: string
32
+ command: (editor: Editor) => void
33
+ group: string
34
+ }
35
+
36
+ type CommandGroupType = {
37
+ group: string
38
+ items: Omit<CommandItemType, 'group'>[]
39
+ }
40
+
41
+ const groups: CommandGroupType[] = [
42
+ {
43
+ group: 'Basic blocks',
44
+ items: [
45
+ {
46
+ title: 'Text',
47
+ description: 'Just start writing with plain text',
48
+ icon: ChevronRight,
49
+ keywords: 'paragraph text',
50
+ command: (editor) => editor.chain().focus().clearNodes().run(),
51
+ },
52
+ {
53
+ title: 'Heading 1',
54
+ description: 'Large section heading',
55
+ icon: Heading1,
56
+ keywords: 'h1 title header',
57
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
58
+ },
59
+ {
60
+ title: 'Heading 2',
61
+ description: 'Medium section heading',
62
+ icon: Heading2,
63
+ keywords: 'h2 subtitle',
64
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
65
+ },
66
+ {
67
+ title: 'Heading 3',
68
+ description: 'Small section heading',
69
+ icon: Heading3,
70
+ keywords: 'h3 subheader',
71
+ command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
72
+ },
73
+ {
74
+ title: 'Bullet List',
75
+ description: 'Create a simple bullet list',
76
+ icon: List,
77
+ keywords: 'unordered ul bullets',
78
+ command: (editor) => editor.chain().focus().toggleBulletList().run(),
79
+ },
80
+ {
81
+ title: 'Numbered List',
82
+ description: 'Create a ordered list',
83
+ icon: ListOrdered,
84
+ keywords: 'numbered ol',
85
+ command: (editor) => editor.chain().focus().toggleOrderedList().run(),
86
+ },
87
+ {
88
+ title: 'Code Block',
89
+ description: 'Capture code snippets',
90
+ icon: Code2,
91
+ keywords: 'code snippet pre',
92
+ command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
93
+ },
94
+ {
95
+ title: 'Image',
96
+ description: 'Insert an image',
97
+ icon: ImageIcon,
98
+ keywords: 'image picture photo',
99
+ command: (editor) => editor.chain().focus().insertImagePlaceholder().run(),
100
+ },
101
+ {
102
+ title: 'Horizontal Rule',
103
+ description: 'Add a horizontal divider',
104
+ icon: Minus,
105
+ keywords: 'horizontal rule divider',
106
+ command: (editor) => editor.chain().focus().setHorizontalRule().run(),
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ group: 'Inline',
112
+ items: [
113
+ {
114
+ title: 'Quote',
115
+ description: 'Capture a quotation',
116
+ icon: Quote,
117
+ keywords: 'blockquote cite',
118
+ command: (editor) => editor.chain().focus().toggleBlockquote().run(),
119
+ },
120
+ {
121
+ title: 'Code',
122
+ description: 'Inline code snippet',
123
+ icon: CodeSquare,
124
+ keywords: 'code inline',
125
+ command: (editor) => editor.chain().focus().toggleCode().run(),
126
+ },
127
+ {
128
+ title: 'Blockquote',
129
+ description: 'Block quote',
130
+ icon: TextQuote,
131
+ keywords: 'blockquote quote',
132
+ command: (editor) => editor.chain().focus().toggleBlockquote().run(),
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ group: 'Alignment',
138
+ items: [
139
+ {
140
+ title: 'Align Left',
141
+ description: 'Align text to the left',
142
+ icon: AlignLeft,
143
+ keywords: 'align left',
144
+ command: (editor) => editor.chain().focus().setTextAlign('left').run(),
145
+ },
146
+ {
147
+ title: 'Align Center',
148
+ description: 'Center align text',
149
+ icon: AlignCenter,
150
+ keywords: 'align center',
151
+ command: (editor) => editor.chain().focus().setTextAlign('center').run(),
152
+ },
153
+ {
154
+ title: 'Align Right',
155
+ description: 'Align text to the right',
156
+ icon: AlignRight,
157
+ keywords: 'align right',
158
+ command: (editor) => editor.chain().focus().setTextAlign('right').run(),
159
+ },
160
+ ],
161
+ },
162
+ ]
163
+
164
+ export function TipTapFloatingMenu({ editor }: { editor: Editor }) {
165
+ const [isOpen, setIsOpen] = useState(false)
166
+ const [search, setSearch] = useState('')
167
+ const debouncedSearch = useDebounce(search, 300)
168
+ const commandRef = useRef<HTMLDivElement>(null)
169
+ const [selectedIndex, setSelectedIndex] = useState(-1)
170
+ const itemRefs = useRef<(HTMLDivElement | null)[]>([])
171
+
172
+ const filteredGroups = useMemo(
173
+ () =>
174
+ groups
175
+ .map((group) => ({
176
+ ...group,
177
+ items: group.items.filter(
178
+ (item) =>
179
+ item.title.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
180
+ item.description.toLowerCase().includes(debouncedSearch.toLowerCase()) ||
181
+ item.keywords.toLowerCase().includes(debouncedSearch.toLowerCase()),
182
+ ),
183
+ }))
184
+ .filter((group) => group.items.length > 0),
185
+ [debouncedSearch],
186
+ )
187
+
188
+ const flatFilteredItems = useMemo(() => filteredGroups.flatMap((g) => g.items), [filteredGroups])
189
+
190
+ const executeCommand = useCallback(
191
+ (commandFn: (editor: Editor) => void) => {
192
+ if (!editor) return
193
+
194
+ try {
195
+ const { from } = editor.state.selection
196
+ const slashCommandLength = search.length + 1
197
+
198
+ editor
199
+ .chain()
200
+ .focus()
201
+ .deleteRange({
202
+ from: Math.max(0, from - slashCommandLength),
203
+ to: from,
204
+ })
205
+ .run()
206
+
207
+ commandFn(editor)
208
+ } catch (error) {
209
+ console.error('Error executing command:', error)
210
+ } finally {
211
+ setIsOpen(false)
212
+ setSearch('')
213
+ setSelectedIndex(-1)
214
+ }
215
+ },
216
+ [editor, search],
217
+ )
218
+
219
+ const handleKeyDown = useCallback(
220
+ (e: KeyboardEvent) => {
221
+ if (!isOpen || !editor) return
222
+
223
+ const preventDefault = () => {
224
+ e.preventDefault()
225
+ e.stopImmediatePropagation()
226
+ }
227
+
228
+ switch (e.key) {
229
+ case 'ArrowDown':
230
+ preventDefault()
231
+ setSelectedIndex((prev) => {
232
+ if (prev === -1) return 0
233
+ return prev < flatFilteredItems.length - 1 ? prev + 1 : 0
234
+ })
235
+ break
236
+
237
+ case 'ArrowUp':
238
+ preventDefault()
239
+ setSelectedIndex((prev) => {
240
+ if (prev === -1) return flatFilteredItems.length - 1
241
+ return prev > 0 ? prev - 1 : flatFilteredItems.length - 1
242
+ })
243
+ break
244
+
245
+ case 'Enter':
246
+ preventDefault()
247
+ const targetIndex = selectedIndex === -1 ? 0 : selectedIndex
248
+ if (flatFilteredItems[targetIndex]) {
249
+ executeCommand(flatFilteredItems[targetIndex].command)
250
+ }
251
+ break
252
+
253
+ case 'Escape':
254
+ preventDefault()
255
+ setIsOpen(false)
256
+ setSelectedIndex(-1)
257
+ break
258
+ }
259
+ },
260
+ [isOpen, selectedIndex, flatFilteredItems, executeCommand, editor],
261
+ )
262
+
263
+ useEffect(() => {
264
+ if (!editor?.options.element) return
265
+
266
+ const editorElement = editor.options.element as HTMLElement
267
+ const handleEditorKeyDown = (e: Event) => handleKeyDown(e as KeyboardEvent)
268
+
269
+ editorElement.addEventListener('keydown', handleEditorKeyDown)
270
+ return () => editorElement.removeEventListener('keydown', handleEditorKeyDown)
271
+ }, [handleKeyDown, editor])
272
+
273
+ // Add new effect for resetting selectedIndex
274
+ useEffect(() => {
275
+ setSelectedIndex(-1)
276
+ }, [search])
277
+
278
+ useEffect(() => {
279
+ if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
280
+ itemRefs.current[selectedIndex]?.focus()
281
+ }
282
+ }, [selectedIndex])
283
+
284
+ return (
285
+ <FloatingMenu
286
+ editor={editor}
287
+ shouldShow={({ state, editor }) => {
288
+ if (!(editor && editor.isFocused)) return false
289
+
290
+ const { $from } = state.selection
291
+ const currentLineText = $from.parent.textBetween(0, $from.parentOffset, '\n', ' ')
292
+
293
+ const isSlashCommand =
294
+ currentLineText.startsWith('/') &&
295
+ $from.parent.type.name !== 'codeBlock' &&
296
+ $from.parentOffset === currentLineText.length
297
+
298
+ if (!isSlashCommand) {
299
+ if (isOpen) setIsOpen(false)
300
+ return false
301
+ }
302
+
303
+ const query = currentLineText.slice(1).trim()
304
+ if (query !== search) setSearch(query)
305
+ if (!isOpen) setIsOpen(true)
306
+
307
+ return true
308
+ }}
309
+ // appendTo={() => document.body}
310
+ options={
311
+ {
312
+ // placement: 'bottom-start',
313
+ // onHide: () => {
314
+ // setIsOpen(false)
315
+ // setSelectedIndex(-1)
316
+ // },
317
+ }
318
+ }
319
+ >
320
+ <Command role="listbox" ref={commandRef} className="rounded-lg border shadow-md md:min-w-[450px]">
321
+ <CommandList>
322
+ <CommandInput placeholder="Type a command or search..." />
323
+ <CommandEmpty className="py-3 text-center text-sm text-muted-foreground">
324
+ No results found
325
+ </CommandEmpty>
326
+
327
+ {filteredGroups.map((group, groupIndex) => (
328
+ <CommandGroup
329
+ key={`${group.group}-${groupIndex}`}
330
+ heading={
331
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
332
+ {group.group}
333
+ </div>
334
+ }
335
+ >
336
+ {group.items.map((item, itemIndex) => {
337
+ const flatIndex =
338
+ filteredGroups.slice(0, groupIndex).reduce((acc, g) => acc + g.items.length, 0) +
339
+ itemIndex
340
+
341
+ return (
342
+ <CommandItem
343
+ role="option"
344
+ key={`${group.group}-${item.title}-${itemIndex}`}
345
+ value={`${group.group}-${item.title}`}
346
+ onSelect={() => executeCommand(item.command)}
347
+ className={cn(
348
+ 'gap-3 aria-selected:bg-accent/50',
349
+ flatIndex === selectedIndex ? 'bg-accent/50' : '',
350
+ )}
351
+ aria-selected={flatIndex === selectedIndex}
352
+ ref={(el) => {
353
+ itemRefs.current[flatIndex] = el
354
+ }}
355
+ tabIndex={flatIndex === selectedIndex ? 0 : -1}
356
+ >
357
+ <div className="flex h-9 w-9 items-center justify-center rounded-md border bg-background">
358
+ <item.icon className="h-4 w-4" />
359
+ </div>
360
+ <div className="flex flex-1 flex-col">
361
+ <span className="text-sm font-medium">{item.title}</span>
362
+ <span className="text-xs text-muted-foreground">{item.description}</span>
363
+ </div>
364
+ <kbd className="ml-auto flex h-5 items-center rounded bg-muted px-1.5 text-xs text-muted-foreground">
365
+
366
+ </kbd>
367
+ </CommandItem>
368
+ )
369
+ })}
370
+ </CommandGroup>
371
+ ))}
372
+ </CommandList>
373
+ </Command>
374
+ </FloatingMenu>
375
+ )
376
+ }
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import { BubbleMenu } from '@tiptap/react/menus'
5
+ import { ScrollArea, ScrollBar } from 'components/ui/scroll-area'
6
+ import { Separator } from 'components/ui/separator'
7
+ import { TooltipProvider } from 'components/ui/tooltip'
8
+ import { useMediaQuery } from 'hooks/use-media-querry'
9
+ import { useEffect } from 'react'
10
+ import { AlignmentTooolbar } from '../toolbars/alignment'
11
+ import { BlockquoteToolbar } from '../toolbars/blockquote'
12
+ import { BoldToolbar } from '../toolbars/bold'
13
+ import { BulletListToolbar } from '../toolbars/bullet-list'
14
+ import { ColorHighlightToolbar } from '../toolbars/color-and-highlight'
15
+ import { HeadingsToolbar } from '../toolbars/headings'
16
+ import { ImagePlaceholderToolbar } from '../toolbars/image-placeholder-toolbar'
17
+ import { ItalicToolbar } from '../toolbars/italic'
18
+ import { LinkToolbar } from '../toolbars/link'
19
+ import { OrderedListToolbar } from '../toolbars/ordered-list'
20
+ import { ToolbarProvider } from '../toolbars/toolbar-provider'
21
+ import { UnderlineToolbar } from '../toolbars/underline'
22
+
23
+ export function FloatingToolbar({ editor }: { editor: Editor | null }) {
24
+ const isMobile = useMediaQuery('(max-width: 640px)')
25
+
26
+ // Prevent default context menu on mobile
27
+ useEffect(() => {
28
+ if (!editor?.options.element || !isMobile) return
29
+
30
+ const handleContextMenu = (e: Event) => {
31
+ e.preventDefault()
32
+ }
33
+
34
+ const el = editor.options.element as HTMLElement
35
+ el.addEventListener('contextmenu', handleContextMenu)
36
+
37
+ return () => el.removeEventListener('contextmenu', handleContextMenu)
38
+ }, [editor, isMobile])
39
+
40
+ if (!editor) return null
41
+
42
+ if (isMobile) {
43
+ return (
44
+ <TooltipProvider>
45
+ <BubbleMenu
46
+ options={{
47
+ placement: 'bottom',
48
+ offset: {
49
+ mainAxis: 10,
50
+ crossAxis: 0,
51
+ },
52
+ }}
53
+ resizeDelay={100}
54
+ shouldShow={() => {
55
+ // Show toolbar when editor is focused and has selection
56
+ return editor.isEditable && editor.isFocused
57
+ }}
58
+ editor={editor}
59
+ className="w-full min-w-full mx-0 shadow-sm border rounded-sm bg-background"
60
+ >
61
+ <ToolbarProvider editor={editor}>
62
+ <ScrollArea className="h-fit py-0.5 w-full">
63
+ <div className="flex items-center px-2 gap-0.5">
64
+ <div className="flex items-center gap-0.5 p-1">
65
+ {/* Primary formatting */}
66
+ <BoldToolbar />
67
+ <ItalicToolbar />
68
+ <UnderlineToolbar />
69
+ <Separator orientation="vertical" className="h-6 mx-1" />
70
+
71
+ {/* Structure controls */}
72
+ <HeadingsToolbar />
73
+ <BulletListToolbar />
74
+ <OrderedListToolbar />
75
+ <Separator orientation="vertical" className="h-6 mx-1" />
76
+
77
+ {/* Rich formatting */}
78
+ <ColorHighlightToolbar />
79
+ <LinkToolbar />
80
+ <ImagePlaceholderToolbar />
81
+ <Separator orientation="vertical" className="h-6 mx-1" />
82
+
83
+ {/* Additional controls */}
84
+ <AlignmentTooolbar />
85
+ <BlockquoteToolbar />
86
+ </div>
87
+ </div>
88
+ <ScrollBar className="h-0.5" orientation="horizontal" />
89
+ </ScrollArea>
90
+ </ToolbarProvider>
91
+ </BubbleMenu>
92
+ </TooltipProvider>
93
+ )
94
+ }
95
+
96
+ return null
97
+ }