@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,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
+ }
@@ -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
+ }
@@ -0,0 +1,144 @@
1
+ import { promises as fs, readdirSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import postcss from 'postcss'
5
+ import postcssNested from 'postcss-nested'
6
+ import type { RegistryItem } from 'shadcn/schema'
7
+
8
+ type PackageJson = {
9
+ name: string
10
+ version: string
11
+ description: string
12
+ dependencies?: Record<string, string>
13
+ devDependencies?: Record<string, string>
14
+ }
15
+
16
+ export const getPackage = async (packageName: string) => {
17
+ const packageDir = join(process.cwd())
18
+ const packagePath = join(packageDir, 'package.json')
19
+ const packageJson = JSON.parse(await readFile(packagePath, 'utf-8')) as PackageJson
20
+
21
+ const kiboDependencies = Object.keys(packageJson.dependencies || {}).filter(
22
+ (dep) => dep.startsWith('@repo') && dep !== '@repo/shadcn-ui',
23
+ )
24
+
25
+ const dependencies = Object.keys(packageJson.dependencies || {}).filter(
26
+ (dep) => !['react', 'react-dom', '@repo/shadcn-ui', ...kiboDependencies].includes(dep),
27
+ )
28
+
29
+ const devDependencies = Object.keys(packageJson.devDependencies || {}).filter(
30
+ (dep) => !['@repo/typescript-config', '@types/react', '@types/react-dom', 'typescript'].includes(dep),
31
+ )
32
+
33
+ const packageFiles = readdirSync(packageDir, { withFileTypes: true })
34
+ const tsxFiles = packageFiles.filter((file) => file.isFile() && file.name.endsWith('.tsx'))
35
+
36
+ const cssFiles = packageFiles.filter((file) => file.isFile() && file.name.endsWith('.css'))
37
+
38
+ const files: RegistryItem['files'] = []
39
+
40
+ for (const file of tsxFiles) {
41
+ const filePath = join(packageDir, file.name)
42
+ const content = await fs.readFile(filePath, 'utf-8')
43
+
44
+ files.push({
45
+ type: 'registry:ui',
46
+ path: file.name,
47
+ content,
48
+ target: `components/${packageName}/${file.name}`,
49
+ })
50
+ }
51
+
52
+ const registryDependencies =
53
+ files
54
+ .map((f) => f.content)
55
+ .join('\n')
56
+ .match(/@\/components\/ui\/([a-z-]+)/g)
57
+ ?.map((path) => path.split('/').pop())
58
+ .filter((name): name is string => !!name) || []
59
+
60
+ const css: RegistryItem['css'] = {}
61
+
62
+ for (const file of cssFiles) {
63
+ const contents = await fs.readFile(join(packageDir, file.name), 'utf-8')
64
+
65
+ // Process CSS with PostCSS to handle nested selectors
66
+ const processed = await postcss([postcssNested]).process(contents, {
67
+ from: undefined,
68
+ })
69
+
70
+ // Parse the processed CSS and convert to JSON structure
71
+ const ast = postcss.parse(processed.css)
72
+
73
+ ast.walkAtRules('layer', (atRule) => {
74
+ const layerName = `@layer ${atRule.params}`
75
+ css[layerName] = {}
76
+
77
+ // First pass: process non-media rules
78
+ atRule.walkRules((rule) => {
79
+ // Skip rules that are inside media queries
80
+ if (rule.parent && rule.parent.type === 'atrule' && (rule.parent as any).name === 'media') {
81
+ return
82
+ }
83
+
84
+ const selector = rule.selector
85
+ const ruleObj: Record<string, string> = {}
86
+
87
+ // Process all declarations
88
+ rule.walkDecls((decl) => {
89
+ ruleObj[decl.prop] = decl.value
90
+ })
91
+
92
+ if (Object.keys(ruleObj).length > 0) {
93
+ css[layerName][selector] = ruleObj
94
+ }
95
+ })
96
+
97
+ // Second pass: process media query rules as top-level entries
98
+ atRule.walkAtRules('media', (mediaRule) => {
99
+ const mediaQuery = `@media ${mediaRule.params}`
100
+
101
+ // Create a top-level media query entry if it doesn't exist
102
+ if (!css[layerName][mediaQuery]) {
103
+ css[layerName][mediaQuery] = {}
104
+ }
105
+
106
+ mediaRule.walkRules((rule) => {
107
+ const selector = rule.selector
108
+ const mediaObj: Record<string, string> = {}
109
+
110
+ rule.walkDecls((decl) => {
111
+ mediaObj[decl.prop] = decl.value
112
+ })
113
+
114
+ if (Object.keys(mediaObj).length > 0) {
115
+ // Store the selector inside the media query
116
+ css[layerName][mediaQuery][selector] = mediaObj
117
+ }
118
+ })
119
+ })
120
+ })
121
+ }
122
+
123
+ let type: RegistryItem['type'] = 'registry:ui'
124
+
125
+ if (!Object.keys(files).length && Object.keys(css).length) {
126
+ type = 'registry:style'
127
+ }
128
+
129
+ const response: RegistryItem = {
130
+ $schema: 'https://ui.shadcn.com/schema/registry-item.json',
131
+ name: packageName,
132
+ type,
133
+ title: packageName,
134
+ description: packageJson.description,
135
+ author: 'Sebastian Döll (@katallaxie)',
136
+ dependencies,
137
+ devDependencies,
138
+ registryDependencies,
139
+ files,
140
+ css,
141
+ }
142
+
143
+ return response
144
+ }