@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,15 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn("animate-pulse rounded-md bg-primary/10", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Skeleton }
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { type VariantProps, cva } from "class-variance-authority";
3
+ import { type LucideProps, Loader2Icon } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const spinnerVariants = cva("animate-spin text-muted-foreground", {
7
+ defaultVariants: {
8
+ size: "default",
9
+ },
10
+ variants: {
11
+ size: {
12
+ default: "size-4",
13
+ icon: "size-10",
14
+ lg: "size-6",
15
+ sm: "size-2",
16
+ },
17
+ },
18
+ });
19
+
20
+ export const Spinner = ({
21
+ className,
22
+ size,
23
+ ...props
24
+ }: Partial<LucideProps & VariantProps<typeof spinnerVariants>>) => (
25
+ <Loader2Icon
26
+ className={cn(spinnerVariants({ size }), className)}
27
+ {...props}
28
+ />
29
+ );
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TabsPrimitive from "@radix-ui/react-tabs"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const Tabs = TabsPrimitive.Root
9
+
10
+ const TabsList = React.forwardRef<
11
+ React.ElementRef<typeof TabsPrimitive.List>,
12
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13
+ >(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.List
15
+ ref={ref}
16
+ className={cn(
17
+ "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ))
23
+ TabsList.displayName = TabsPrimitive.List.displayName
24
+
25
+ const TabsTrigger = React.forwardRef<
26
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28
+ >(({ className, ...props }, ref) => (
29
+ <TabsPrimitive.Trigger
30
+ ref={ref}
31
+ className={cn(
32
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ ))
38
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39
+
40
+ const TabsContent = React.forwardRef<
41
+ React.ElementRef<typeof TabsPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43
+ >(({ className, ...props }, ref) => (
44
+ <TabsPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ))
53
+ TabsContent.displayName = TabsPrimitive.Content.displayName
54
+
55
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,61 @@
1
+ "use client"
2
+
3
+ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
4
+ import { type VariantProps } from "class-variance-authority"
5
+ import * as React from "react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { toggleVariants } from "components/ui/toggle"
9
+
10
+ const ToggleGroupContext = React.createContext<
11
+ VariantProps<typeof toggleVariants>
12
+ >({
13
+ size: "default",
14
+ variant: "default",
15
+ })
16
+
17
+ const ToggleGroup = React.forwardRef<
18
+ React.ElementRef<typeof ToggleGroupPrimitive.Root>,
19
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
20
+ VariantProps<typeof toggleVariants>
21
+ >(({ className, variant, size, children, ...props }, ref) => (
22
+ <ToggleGroupPrimitive.Root
23
+ ref={ref}
24
+ className={cn("flex items-center justify-center gap-1", className)}
25
+ {...props}
26
+ >
27
+ <ToggleGroupContext.Provider value={{ variant, size }}>
28
+ {children}
29
+ </ToggleGroupContext.Provider>
30
+ </ToggleGroupPrimitive.Root>
31
+ ))
32
+
33
+ ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34
+
35
+ const ToggleGroupItem = React.forwardRef<
36
+ React.ElementRef<typeof ToggleGroupPrimitive.Item>,
37
+ React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
38
+ VariantProps<typeof toggleVariants>
39
+ >(({ className, children, variant, size, ...props }, ref) => {
40
+ const context = React.useContext(ToggleGroupContext)
41
+
42
+ return (
43
+ <ToggleGroupPrimitive.Item
44
+ ref={ref}
45
+ className={cn(
46
+ toggleVariants({
47
+ variant: context.variant || variant,
48
+ size: context.size || size,
49
+ }),
50
+ className
51
+ )}
52
+ {...props}
53
+ >
54
+ {children}
55
+ </ToggleGroupPrimitive.Item>
56
+ )
57
+ })
58
+
59
+ ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60
+
61
+ export { ToggleGroup, ToggleGroupItem }
@@ -0,0 +1,45 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TogglePrimitive from "@radix-ui/react-toggle"
5
+ import { cva, type VariantProps } from "class-variance-authority"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const toggleVariants = cva(
10
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-transparent",
15
+ outline:
16
+ "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17
+ },
18
+ size: {
19
+ default: "h-9 px-2 min-w-9",
20
+ sm: "h-8 px-1.5 min-w-8",
21
+ lg: "h-10 px-2.5 min-w-10",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ size: "default",
27
+ },
28
+ }
29
+ )
30
+
31
+ const Toggle = React.forwardRef<
32
+ React.ElementRef<typeof TogglePrimitive.Root>,
33
+ React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
34
+ VariantProps<typeof toggleVariants>
35
+ >(({ className, variant, size, ...props }, ref) => (
36
+ <TogglePrimitive.Root
37
+ ref={ref}
38
+ className={cn(toggleVariants({ variant, size, className }))}
39
+ {...props}
40
+ />
41
+ ))
42
+
43
+ Toggle.displayName = TogglePrimitive.Root.displayName
44
+
45
+ export { Toggle, toggleVariants }
@@ -0,0 +1,32 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ const TooltipProvider = TooltipPrimitive.Provider
9
+
10
+ const Tooltip = TooltipPrimitive.Root
11
+
12
+ const TooltipTrigger = TooltipPrimitive.Trigger
13
+
14
+ const TooltipContent = React.forwardRef<
15
+ React.ElementRef<typeof TooltipPrimitive.Content>,
16
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
+ >(({ className, sideOffset = 4, ...props }, ref) => (
18
+ <TooltipPrimitive.Portal>
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ))
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
+
32
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "stone",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
package/config/site.ts ADDED
@@ -0,0 +1,15 @@
1
+ export const siteConfig = {
2
+ name: 'Editor X',
3
+ url: 'https://open-cloud-initiative.github.io/editor-x/',
4
+ author: 'Sebastian Döll (@katallaxie)',
5
+ authorUrl: 'https://github.com/katallaxie',
6
+ links: {
7
+ github: 'https://github.com/open-cloud-initiative/editor-x',
8
+ },
9
+ }
10
+
11
+ export type SiteConfig = typeof siteConfig
12
+
13
+ export const navConfig = {
14
+ mainNav: [{ title: siteConfig.name, path: '/' }],
15
+ }
@@ -0,0 +1,20 @@
1
+ import nextVitals from 'eslint-config-next/core-web-vitals'
2
+ import prettier from 'eslint-config-prettier/flat'
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
4
+ import { defineConfig, globalIgnores } from 'eslint/config'
5
+
6
+ const eslintConfig = defineConfig([
7
+ ...nextVitals,
8
+ prettier,
9
+ eslintPluginPrettierRecommended,
10
+ // Override default ignores of eslint-config-next.
11
+ globalIgnores([
12
+ // Default ignores of eslint-config-next:
13
+ '.next/**',
14
+ 'out/**',
15
+ 'build/**',
16
+ 'next-env.d.ts',
17
+ ]),
18
+ ])
19
+
20
+ export default eslintConfig
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { ChangeEvent, useState } from "react";
4
+
5
+ type UseCharacterLimitProps = {
6
+ maxLength: number;
7
+ initialValue?: string;
8
+ };
9
+
10
+ export function useCharacterLimit({ maxLength, initialValue = "" }: UseCharacterLimitProps) {
11
+ const [value, setValue] = useState(initialValue);
12
+ const [characterCount, setCharacterCount] = useState(initialValue.length);
13
+
14
+ const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
15
+ const newValue = e.target.value;
16
+ if (newValue.length <= maxLength) {
17
+ setValue(newValue);
18
+ setCharacterCount(newValue.length);
19
+ }
20
+ };
21
+
22
+ return {
23
+ value,
24
+ characterCount,
25
+ handleChange,
26
+ maxLength,
27
+ };
28
+ }
@@ -0,0 +1,16 @@
1
+ "use client"
2
+ import { useState, useCallback } from "react"
3
+
4
+ export function useCopyToClipboard() {
5
+ const [isCopied, setIsCopied] = useState(false)
6
+
7
+ const copyToClipboard = useCallback((text: string) => {
8
+ if (typeof window === "undefined") return
9
+ navigator.clipboard.writeText(text).then(() => {
10
+ setIsCopied(true)
11
+ setTimeout(() => setIsCopied(false), 2000)
12
+ })
13
+ }, [])
14
+
15
+ return { copyToClipboard, isCopied }
16
+ }
@@ -0,0 +1,17 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export function useDebounce<T>(value: T, delay: number): T {
4
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
5
+
6
+ useEffect(() => {
7
+ const timer = setTimeout(() => {
8
+ setDebouncedValue(value);
9
+ }, delay);
10
+
11
+ return () => {
12
+ clearTimeout(timer);
13
+ };
14
+ }, [value, delay]);
15
+
16
+ return debouncedValue;
17
+ }
@@ -0,0 +1,97 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ interface UseImageUploadProps {
4
+ onUpload?: (url: string) => void;
5
+ }
6
+
7
+ export function useImageUpload({ onUpload }: UseImageUploadProps = {}) {
8
+ const previewRef = useRef<string | null>(null);
9
+ const fileInputRef = useRef<HTMLInputElement>(null);
10
+ const [previewUrl, setPreviewUrl] = useState<string | null>(null);
11
+ const [fileName, setFileName] = useState<string | null>(null);
12
+ const [uploading, setUploading] = useState(false);
13
+ const [error, setError] = useState<string | null>(null);
14
+
15
+ // Dummy upload function that simulates a delay and returns the local preview URL
16
+ const dummyUpload = async (file: File, localUrl: string): Promise<string> => {
17
+ try {
18
+ setUploading(true);
19
+ // Simulate network delay
20
+ await new Promise(resolve => setTimeout(resolve, 1500));
21
+
22
+ // Simulate random upload errors (20% chance)
23
+ if (Math.random() < 0.2) {
24
+ throw new Error("Upload failed - This is a demo error");
25
+ }
26
+
27
+ setError(null);
28
+ // In a real implementation, this would be the URL from the server
29
+ return localUrl;
30
+ } catch (err) {
31
+ const errorMessage = err instanceof Error ? err.message : "Upload failed";
32
+ setError(errorMessage);
33
+ throw new Error(errorMessage);
34
+ } finally {
35
+ setUploading(false);
36
+ }
37
+ };
38
+
39
+ const handleThumbnailClick = useCallback(() => {
40
+ fileInputRef.current?.click();
41
+ }, []);
42
+
43
+ const handleFileChange = useCallback(
44
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
45
+ const file = event.target.files?.[0];
46
+ if (file) {
47
+ setFileName(file.name);
48
+ const localUrl = URL.createObjectURL(file);
49
+ setPreviewUrl(localUrl);
50
+ previewRef.current = localUrl;
51
+
52
+ try {
53
+ const uploadedUrl = await dummyUpload(file, localUrl);
54
+ onUpload?.(uploadedUrl);
55
+ } catch (err) {
56
+ URL.revokeObjectURL(localUrl);
57
+ setPreviewUrl(null);
58
+ setFileName(null);
59
+ return console.error(err)
60
+ }
61
+ }
62
+ },
63
+ [onUpload]
64
+ );
65
+
66
+ const handleRemove = useCallback(() => {
67
+ if (previewUrl) {
68
+ URL.revokeObjectURL(previewUrl);
69
+ }
70
+ setPreviewUrl(null);
71
+ setFileName(null);
72
+ previewRef.current = null;
73
+ if (fileInputRef.current) {
74
+ fileInputRef.current.value = "";
75
+ }
76
+ setError(null);
77
+ }, [previewUrl]);
78
+
79
+ useEffect(() => {
80
+ return () => {
81
+ if (previewRef.current) {
82
+ URL.revokeObjectURL(previewRef.current);
83
+ }
84
+ };
85
+ }, []);
86
+
87
+ return {
88
+ previewUrl,
89
+ fileName,
90
+ fileInputRef,
91
+ handleThumbnailClick,
92
+ handleFileChange,
93
+ handleRemove,
94
+ uploading,
95
+ error,
96
+ };
97
+ }
@@ -0,0 +1,18 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ export function useMediaQuery(query: string): boolean {
4
+ const [matches, setMatches] = useState(false)
5
+
6
+ useEffect(() => {
7
+ const media = window.matchMedia(query)
8
+ if (media.matches !== matches) {
9
+ setMatches(media.matches)
10
+ }
11
+ const listener = () => setMatches(media.matches)
12
+ window.addEventListener('resize', listener)
13
+ return () => window.removeEventListener('resize', listener)
14
+ }, [matches, query])
15
+
16
+ return matches
17
+ }
18
+
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
Binary file
package/lib/content.ts ADDED
@@ -0,0 +1,39 @@
1
+ export const content = `
2
+ <h1>Explore the Tiptap rich text editor with Shadcn UI components 📝</h1>
3
+ <p>This is a powerful editor that supports many features:</p>
4
+ <ul class="list-disc">
5
+ <li>
6
+ <p>
7
+ Rich text formatting with <strong>bold</strong>, <em>italic</em>, and
8
+ <u>underline</u>
9
+ </p>
10
+ </li>
11
+ <li>
12
+ <p>Different heading levels</p>
13
+ </li>
14
+ <li>
15
+ <p>Lists (ordered and unordered)</p>
16
+ </li>
17
+ <li>
18
+ <p>Text alignment options</p>
19
+ </li>
20
+ <li>
21
+ <p>Image uploads and management</p>
22
+ </li>
23
+ </ul>
24
+ <h2>Try It Out!</h2>
25
+ <p>
26
+ Type '/' to see available commands or use the toolbar above to format your
27
+ content.
28
+ </p>
29
+ <img src="https://res.cloudinary.com/sham007/image/upload/v1737910103/ProfileImages/724shots_so.jpg" alt="Example image"
30
+ width="100%" align="center" caption="SAAS landing page template" aspectratio="1.640340218712029" />
31
+ <p>
32
+ You can also add links like
33
+ <a target="_blank" rel="noopener noreferrer nofollow" href="https://ehtisham.vercel.app">this one</a>
34
+ and use text alignment options.
35
+ </p>
36
+ <pre><code class="language-javascript">// Example code block
37
+ const greeting = "Hello, World!";
38
+ console.log(greeting);</code></pre>
39
+ `
@@ -0,0 +1,19 @@
1
+ // Client-side cookie utilities.
2
+ // These functions manage cookies in the browser only.
3
+ // Server actions handle cookie updates on the server side.
4
+
5
+ export function setClientCookie(key: string, value: string, days = 7) {
6
+ const expires = new Date(Date.now() + days * 864e5).toUTCString()
7
+ document.cookie = `${key}=${value}; expires=${expires}; path=/`
8
+ }
9
+
10
+ export function getClientCookie(key: string) {
11
+ return document.cookie
12
+ .split('; ')
13
+ .find((row) => row.startsWith(`${key}=`))
14
+ ?.split('=')[1]
15
+ }
16
+
17
+ export function deleteClientCookie(key: string) {
18
+ document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`
19
+ }
@@ -0,0 +1,19 @@
1
+ 'use client'
2
+
3
+ export function setLocalStorageValue(key: string, value: string) {
4
+ try {
5
+ window.localStorage.setItem(key, value)
6
+ } catch (error) {
7
+ if (process.env.NODE_ENV !== 'production') {
8
+ console.error('[localStorage] Failed to write value:', error)
9
+ }
10
+ }
11
+ }
12
+
13
+ export function getLocalStorageValue(key: string): string | null {
14
+ try {
15
+ return window.localStorage.getItem(key)
16
+ } catch {
17
+ return null
18
+ }
19
+ }