@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,130 @@
1
+ 'use client'
2
+ /* eslint-disable */
3
+ // @ts-nocheck
4
+ import { PopoverClose } from "@radix-ui/react-popover";
5
+ import { Trash2, X } from "lucide-react";
6
+
7
+ import React, { type FormEvent } from "react";
8
+
9
+ import { cn } from "@/lib/utils";
10
+ import { Button, type ButtonProps } from "components/ui/button";
11
+ import { Input } from "components/ui/input";
12
+ import { Label } from "components/ui/label";
13
+ import {
14
+ Tooltip,
15
+ TooltipContent,
16
+ TooltipTrigger,
17
+ } from "components/ui/tooltip";
18
+
19
+ import { getUrlFromString } from "@/lib/tiptap-utils";
20
+ import {
21
+ Popover,
22
+ PopoverContent,
23
+ PopoverTrigger,
24
+ } from "components/ui/popover";
25
+ import { useToolbar } from "./toolbar-provider";
26
+
27
+ const LinkToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
28
+ ({ className, ...props }, ref) => {
29
+ const { editor } = useToolbar();
30
+ const [link, setLink] = React.useState("");
31
+
32
+ const handleSubmit = (e: FormEvent) => {
33
+ e.preventDefault();
34
+ const url = getUrlFromString(link);
35
+ url && editor?.chain().focus().setLink({ href: url }).run();
36
+ };
37
+
38
+ React.useEffect(() => {
39
+ setLink(editor?.getAttributes("link").href ?? "");
40
+ }, [editor]);
41
+
42
+ return (
43
+ <Popover>
44
+ <Tooltip>
45
+ <TooltipTrigger asChild>
46
+ <PopoverTrigger
47
+ disabled={!editor?.can().chain().setLink({ href: "" }).run()}
48
+ asChild
49
+ >
50
+ <Button
51
+ variant="ghost"
52
+ size="sm"
53
+ type="button"
54
+ className={cn(
55
+ "h-8 w-max px-3 font-normal",
56
+ editor?.isActive("link") && "bg-accent",
57
+ className,
58
+ )}
59
+ ref={ref}
60
+ {...props}
61
+ >
62
+ <p className="mr-2 text-base">↗</p>
63
+ <p className={"underline decoration-gray-7 underline-offset-4"}>
64
+ Link
65
+ </p>
66
+ </Button>
67
+ </PopoverTrigger>
68
+ </TooltipTrigger>
69
+ <TooltipContent>
70
+ <span>Link</span>
71
+ </TooltipContent>
72
+ </Tooltip>
73
+
74
+ <PopoverContent
75
+ onCloseAutoFocus={(e) => {
76
+ e.preventDefault();
77
+ }}
78
+ asChild
79
+ className="relative px-3 py-2.5"
80
+ >
81
+ <div className="relative">
82
+ <PopoverClose className="absolute right-3 top-3">
83
+ <X className="h-4 w-4" />
84
+ </PopoverClose>
85
+ <form onSubmit={handleSubmit}>
86
+ <Label>Link</Label>
87
+ <p className="text-sm text-gray-11">
88
+ Attach a link to the selected text
89
+ </p>
90
+ <div className="mt-3 flex flex-col items-end justify-end gap-3">
91
+ <Input
92
+ value={link}
93
+ onChange={(e) => {
94
+ setLink(e.target.value);
95
+ }}
96
+ className="w-full"
97
+ placeholder="https://example.com"
98
+ />
99
+ <div className="flex items-center gap-3">
100
+ {editor?.getAttributes("link").href && (
101
+ <Button
102
+ type="reset"
103
+ size="sm"
104
+ className="h-8 text-gray-11"
105
+ variant="ghost"
106
+ onClick={() => {
107
+ editor?.chain().focus().unsetLink().run();
108
+ setLink("");
109
+ }}
110
+ >
111
+ <Trash2 className="mr-2 h-4 w-4" />
112
+ Remove
113
+ </Button>
114
+ )}
115
+ <Button size="sm" className="h-8">
116
+ {editor?.getAttributes("link").href ? "Update" : "Confirm"}
117
+ </Button>
118
+ </div>
119
+ </div>
120
+ </form>
121
+ </div>
122
+ </PopoverContent>
123
+ </Popover>
124
+ );
125
+ },
126
+ );
127
+
128
+ LinkToolbar.displayName = "LinkToolbar";
129
+
130
+ export { LinkToolbar };
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+ import { Button } from 'components/ui/button'
5
+ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from 'components/ui/drawer'
6
+ import { ChevronDown } from 'lucide-react'
7
+ import React, { useState } from 'react'
8
+
9
+ interface MobileToolbarGroupProps {
10
+ label: string
11
+ children: React.ReactNode
12
+ className?: string
13
+ }
14
+
15
+ export const MobileToolbarGroup = ({ label, children, className }: MobileToolbarGroupProps) => {
16
+ const [isOpen, setIsOpen] = useState(false)
17
+
18
+ const closeDrawer = () => setIsOpen(false)
19
+
20
+ return (
21
+ <Drawer open={isOpen} onOpenChange={setIsOpen}>
22
+ <DrawerTrigger asChild>
23
+ <Button
24
+ variant="ghost"
25
+ size="sm"
26
+ type="button"
27
+ className={cn('h-8 w-max gap-1 px-3 font-normal', className)}
28
+ >
29
+ {label}
30
+ <ChevronDown className="h-4 w-4" />
31
+ </Button>
32
+ </DrawerTrigger>
33
+ <DrawerContent>
34
+ <DrawerHeader>
35
+ <DrawerTitle className="text-start">{label}</DrawerTitle>
36
+ </DrawerHeader>
37
+ <div className="flex flex-col p-4">
38
+ {React.Children.map(children, (child) =>
39
+ React.isValidElement(child)
40
+ ? React.cloneElement(child, { closeDrawer } as {
41
+ closeDrawer: () => void
42
+ })
43
+ : child,
44
+ )}
45
+ </div>
46
+ </DrawerContent>
47
+ </Drawer>
48
+ )
49
+ }
50
+
51
+ export const MobileToolbarItem = ({
52
+ children,
53
+ active,
54
+ onClick,
55
+ closeDrawer,
56
+ ...props
57
+ }: React.ButtonHTMLAttributes<HTMLButtonElement> & {
58
+ active?: boolean
59
+ closeDrawer?: () => void
60
+ }) => (
61
+ <button
62
+ className={cn(
63
+ 'flex w-full items-center rounded-md px-4 py-2 text-sm transition-colors hover:bg-accent',
64
+ active && 'bg-accent',
65
+ )}
66
+ onClick={(e) => {
67
+ onClick?.(e)
68
+ setTimeout(() => {
69
+ closeDrawer?.()
70
+ }, 100)
71
+ }}
72
+ {...props}
73
+ >
74
+ {children}
75
+ </button>
76
+ )
@@ -0,0 +1,47 @@
1
+ 'use client'
2
+
3
+ import { ListOrdered } from 'lucide-react'
4
+ import React from 'react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button, type ButtonProps } from 'components/ui/button'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from 'components/ui/tooltip'
9
+ import { useToolbar } from './toolbar-provider'
10
+
11
+ const OrderedListToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
12
+ ({ className, onClick, children, ...props }, ref) => {
13
+ const { editor } = useToolbar()
14
+ return (
15
+ <Tooltip>
16
+ <TooltipTrigger asChild>
17
+ <Button
18
+ variant="ghost"
19
+ size="icon"
20
+ type="button"
21
+ className={cn(
22
+ 'h-8 w-8 p-0 sm:h-9 sm:w-9',
23
+ editor?.isActive('orderedList') && 'bg-accent',
24
+ className,
25
+ )}
26
+ onClick={(e) => {
27
+ editor?.chain().focus().toggleOrderedList().run()
28
+ onClick?.(e)
29
+ }}
30
+ disabled={!editor?.can().chain().focus().toggleOrderedList().run()}
31
+ ref={ref}
32
+ {...props}
33
+ >
34
+ {children ?? <ListOrdered className="h-4 w-4" />}
35
+ </Button>
36
+ </TooltipTrigger>
37
+ <TooltipContent>
38
+ <span>Ordered list</span>
39
+ </TooltipContent>
40
+ </Tooltip>
41
+ )
42
+ },
43
+ )
44
+
45
+ OrderedListToolbar.displayName = 'OrderedListToolbar'
46
+
47
+ export { OrderedListToolbar }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { Redo2 } from 'lucide-react'
4
+ import React from 'react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button, type ButtonProps } from 'components/ui/button'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from 'components/ui/tooltip'
9
+ import { useToolbar } from './toolbar-provider'
10
+
11
+ const RedoToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
12
+ ({ className, onClick, children, ...props }, ref) => {
13
+ const { editor } = useToolbar()
14
+
15
+ return (
16
+ <Tooltip>
17
+ <TooltipTrigger asChild>
18
+ <Button
19
+ variant="ghost"
20
+ size="icon"
21
+ type="button"
22
+ className={cn('h-8 w-8 p-0 sm:h-9 sm:w-9', className)}
23
+ onClick={(e) => {
24
+ editor?.chain().focus().redo().run()
25
+ onClick?.(e)
26
+ }}
27
+ disabled={!editor?.can().chain().focus().redo().run()}
28
+ ref={ref}
29
+ {...props}
30
+ >
31
+ {children ?? <Redo2 className="h-4 w-4" />}
32
+ </Button>
33
+ </TooltipTrigger>
34
+ <TooltipContent>
35
+ <span>Redo</span>
36
+ </TooltipContent>
37
+ </Tooltip>
38
+ )
39
+ },
40
+ )
41
+
42
+ RedoToolbar.displayName = 'RedoToolbar'
43
+
44
+ export { RedoToolbar }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { Strikethrough } from 'lucide-react'
4
+ import React from 'react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button, type ButtonProps } from 'components/ui/button'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from 'components/ui/tooltip'
9
+ import { useToolbar } from './toolbar-provider'
10
+
11
+ const StrikeThroughToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
12
+ ({ className, onClick, children, ...props }, ref) => {
13
+ const { editor } = useToolbar()
14
+ return (
15
+ <Tooltip>
16
+ <TooltipTrigger asChild>
17
+ <Button
18
+ variant="ghost"
19
+ size="icon"
20
+ type="button"
21
+ className={cn(
22
+ 'h-8 w-8 p-0 sm:h-9 sm:w-9',
23
+ editor?.isActive('strike') && 'bg-accent',
24
+ className,
25
+ )}
26
+ onClick={(e) => {
27
+ editor?.chain().focus().toggleStrike().run()
28
+ onClick?.(e)
29
+ }}
30
+ disabled={!editor?.can().chain().focus().toggleStrike().run()}
31
+ ref={ref}
32
+ {...props}
33
+ >
34
+ {children ?? <Strikethrough className="h-4 w-4" />}
35
+ </Button>
36
+ </TooltipTrigger>
37
+ <TooltipContent>
38
+ <span>Strikethrough</span>
39
+ <span className="ml-1 text-xs text-gray-11">(cmd + shift + x)</span>
40
+ </TooltipContent>
41
+ </Tooltip>
42
+ )
43
+ },
44
+ )
45
+
46
+ StrikeThroughToolbar.displayName = 'StrikeThroughToolbar'
47
+
48
+ export { StrikeThroughToolbar }
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import type { Editor } from '@tiptap/react'
4
+ import React from 'react'
5
+
6
+ export interface ToolbarContextProps {
7
+ editor: Editor | null
8
+ }
9
+
10
+ export const ToolbarContext = React.createContext<ToolbarContextProps | null>(null)
11
+
12
+ interface ToolbarProviderProps {
13
+ editor: Editor | null
14
+ children: React.ReactNode
15
+ }
16
+
17
+ export const ToolbarProvider = ({ editor, children }: ToolbarProviderProps) => {
18
+ return <ToolbarContext.Provider value={{ editor }}>{children}</ToolbarContext.Provider>
19
+ }
20
+
21
+ export const useToolbar = () => {
22
+ const context = React.useContext(ToolbarContext)
23
+
24
+ if (!context) {
25
+ throw new Error('useToolbar must be used within a ToolbarProvider')
26
+ }
27
+
28
+ return context
29
+ }
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { UnderlineIcon } from 'lucide-react'
4
+ import React from 'react'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { Button, type ButtonProps } from 'components/ui/button'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from 'components/ui/tooltip'
9
+ import { useToolbar } from './toolbar-provider'
10
+
11
+ const UnderlineToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
12
+ ({ className, onClick, children, ...props }, ref) => {
13
+ const { editor } = useToolbar()
14
+ return (
15
+ <Tooltip>
16
+ <TooltipTrigger asChild>
17
+ <Button
18
+ variant="ghost"
19
+ size="icon"
20
+ type="button"
21
+ className={cn(
22
+ 'h-8 w-8 p-0 sm:h-9 sm:w-9',
23
+ editor?.isActive('underline') && 'bg-accent',
24
+ className,
25
+ )}
26
+ onClick={(e) => {
27
+ editor?.chain().focus().toggleUnderline().run()
28
+ onClick?.(e)
29
+ }}
30
+ disabled={!editor?.can().chain().focus().toggleUnderline().run()}
31
+ ref={ref}
32
+ {...props}
33
+ >
34
+ {children ?? <UnderlineIcon className="h-4 w-4" />}
35
+ </Button>
36
+ </TooltipTrigger>
37
+ <TooltipContent>
38
+ <span>Underline</span>
39
+ <span className="ml-1 text-xs text-gray-11">(cmd + u)</span>
40
+ </TooltipContent>
41
+ </Tooltip>
42
+ )
43
+ },
44
+ )
45
+
46
+ UnderlineToolbar.displayName = 'UnderlineToolbar'
47
+
48
+ export { UnderlineToolbar }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+ import { Button, type ButtonProps } from 'components/ui/button'
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from 'components/ui/tooltip'
6
+ import { Undo2 } from 'lucide-react'
7
+ import React from 'react'
8
+ import { useToolbar } from './toolbar-provider'
9
+
10
+ const UndoToolbar = React.forwardRef<HTMLButtonElement, ButtonProps>(
11
+ ({ className, onClick, children, ...props }, ref) => {
12
+ const { editor } = useToolbar()
13
+
14
+ return (
15
+ <Tooltip>
16
+ <TooltipTrigger asChild>
17
+ <Button
18
+ variant="ghost"
19
+ size="icon"
20
+ type="button"
21
+ className={cn('h-8 w-8 p-0 sm:h-9 sm:w-9', className)}
22
+ onClick={(e) => {
23
+ editor?.chain().focus().undo().run()
24
+ onClick?.(e)
25
+ }}
26
+ disabled={!editor?.can().chain().focus().undo().run()}
27
+ ref={ref}
28
+ {...props}
29
+ >
30
+ {children ?? <Undo2 className="h-4 w-4" />}
31
+ </Button>
32
+ </TooltipTrigger>
33
+ <TooltipContent>
34
+ <span>Undo</span>
35
+ </TooltipContent>
36
+ </Tooltip>
37
+ )
38
+ },
39
+ )
40
+
41
+ UndoToolbar.displayName = 'UndoToolbar'
42
+
43
+ export { UndoToolbar }
@@ -0,0 +1,26 @@
1
+ 'use client'
2
+
3
+ import { Moon, Sun } from 'lucide-react'
4
+
5
+ import { Button } from '@/components/ui/button'
6
+ import { persistPreference } from '@/lib/preferences-storage'
7
+ import { applyThemeMode } from '@/lib/theme-utils'
8
+ import { usePreferencesStore } from '@/stores/preferences/preferences-provider'
9
+
10
+ export function ThemeSwitcher() {
11
+ const themeMode = usePreferencesStore((s) => s.themeMode)
12
+ const setThemeMode = usePreferencesStore((s) => s.setThemeMode)
13
+
14
+ const handleValueChange = async () => {
15
+ const newTheme = themeMode === 'dark' ? 'light' : 'dark'
16
+ applyThemeMode(newTheme)
17
+ setThemeMode(newTheme)
18
+ persistPreference('theme_mode', newTheme)
19
+ }
20
+
21
+ return (
22
+ <Button variant="ghost" onClick={handleValueChange}>
23
+ {themeMode === 'dark' ? <Sun /> : <Moon />}
24
+ </Button>
25
+ )
26
+ }
@@ -0,0 +1,24 @@
1
+ 'use client'
2
+
3
+ import { Button } from '@/components/ui/button'
4
+ import { navConfig } from '@/config/site'
5
+ import Link from 'next/link'
6
+ import { usePathname } from 'next/navigation'
7
+
8
+ export function MainNav() {
9
+ const pathname = usePathname()
10
+
11
+ return (
12
+ <div className="mr-4 hidden items-center space-x-6 md:flex">
13
+ <nav className="flex items-center gap-4 text-sm xl:gap-6">
14
+ {navConfig.mainNav.map((item, idx) => (
15
+ <Button key={idx} variant="ghost">
16
+ <Link key={item.path} href={item.path}>
17
+ {item.title}
18
+ </Link>
19
+ </Button>
20
+ ))}
21
+ </nav>
22
+ </div>
23
+ )
24
+ }
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import { navConfig } from '@/config/site'
4
+ import { Button } from 'components/ui/button'
5
+ import { Drawer, DrawerContent, DrawerTrigger } from 'components/ui/drawer'
6
+ import Link from 'next/link'
7
+ import * as React from 'react'
8
+
9
+ export default function MobileNav() {
10
+ const [open, setOpen] = React.useState(false)
11
+
12
+ return (
13
+ <Drawer open={open} onOpenChange={setOpen}>
14
+ <DrawerTrigger asChild>
15
+ <Button
16
+ variant="ghost"
17
+ className="mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
18
+ >
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ fill="none"
22
+ viewBox="0 0 24 24"
23
+ strokeWidth="1.5"
24
+ stroke="currentColor"
25
+ className="h-6 w-6"
26
+ >
27
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5" />
28
+ </svg>
29
+ <span className="sr-only">Toggle Menu</span>
30
+ </Button>
31
+ </DrawerTrigger>
32
+ <DrawerContent className="h-[50%]">
33
+ <nav className="flex flex-col space-y-3 p-4">
34
+ <Link href="/" onClick={() => setOpen(false)}>
35
+ Home
36
+ </Link>
37
+ {navConfig.mainNav.map((item) => (
38
+ <Link key={item.path} href={item.path} onClick={() => setOpen(false)}>
39
+ {item.title}
40
+ </Link>
41
+ ))}
42
+ </nav>
43
+ </DrawerContent>
44
+ </Drawer>
45
+ )
46
+ }
@@ -0,0 +1,38 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { Button } from 'components/ui/button'
3
+
4
+ export function OpenInV0Button({ name, className }: { name: string } & React.ComponentProps<typeof Button>) {
5
+ return (
6
+ <Button
7
+ aria-label="Open in v0"
8
+ className={cn(
9
+ 'h-7 gap-1 rounded-lg shadow-none bg-black px-3 text-xs text-white hover:bg-black hover:text-white dark:bg-white dark:text-black',
10
+ className,
11
+ )}
12
+ asChild
13
+ >
14
+ <a
15
+ href={`https://v0.dev/chat/api/open?url=${process.env.NEXT_PUBLIC_BASE_URL}/r/${name}.json`}
16
+ target="_blank"
17
+ rel="noreferrer"
18
+ >
19
+ Open in{' '}
20
+ <svg
21
+ viewBox="0 0 40 20"
22
+ fill="none"
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ className="h-5 w-5 text-current"
25
+ >
26
+ <path
27
+ d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
28
+ fill="currentColor"
29
+ ></path>
30
+ <path
31
+ d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
32
+ fill="currentColor"
33
+ ></path>
34
+ </svg>
35
+ </a>
36
+ </Button>
37
+ )
38
+ }
@@ -0,0 +1,30 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ function PageHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
4
+ return (
5
+ <section className={cn('border-grid border-b', className)} {...props}>
6
+ <div className="container-wrapper">
7
+ <div className="container flex flex-col items-start gap-1 py-8 md:py-10 lg:py-12">{children}</div>
8
+ </div>
9
+ </section>
10
+ )
11
+ }
12
+
13
+ function PageHeaderHeading({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
14
+ return (
15
+ <h1
16
+ className={cn('text-3xl font-bold leading-tight tracking-tighter md:text-4xl lg:leading-[1.1]', className)}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function PageHeaderDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
23
+ return <p className={cn('max-w-2xl text-balance text-lg font-light text-foreground', className)} {...props} />
24
+ }
25
+
26
+ function PageActions({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
27
+ return <div className={cn('flex w-full items-center justify-start gap-2 pt-2', className)} {...props} />
28
+ }
29
+
30
+ export { PageActions, PageHeader, PageHeaderDescription, PageHeaderHeading }
@@ -0,0 +1,41 @@
1
+ import { siteConfig } from '@/config/site'
2
+ import Link from 'next/link'
3
+
4
+ export function SiteFooter() {
5
+ return (
6
+ <footer className="border-t border-border/40 py-6 dark:border-border md:px-8 md:py-0">
7
+ <div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
8
+ <p className="text-balance text-center text-sm leading-loose text-muted-foreground md:text-left">
9
+ From{' '}
10
+ <a
11
+ href={siteConfig.authorUrl}
12
+ className="font-medium underline underline-offset-4"
13
+ rel="noreferrer"
14
+ target="_blank"
15
+ >
16
+ {siteConfig.author}
17
+ </a>{' '}
18
+ Inspired by{' '}
19
+ <a
20
+ className="font-medium underline underline-offset-4"
21
+ href="https://ui.shadcn.com"
22
+ rel="noreferrer"
23
+ target="_blank"
24
+ >
25
+ ShadCN UI Website
26
+ </a>{' '}
27
+ . The source code is available on{' '}
28
+ <Link
29
+ href={siteConfig.links.github}
30
+ rel="noreferrer"
31
+ target="_blank"
32
+ className="font-medium underline underline-offset-4"
33
+ >
34
+ GitHub
35
+ </Link>
36
+ .
37
+ </p>
38
+ </div>
39
+ </footer>
40
+ )
41
+ }