@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.
- package/.devcontainer/Dockerfile +13 -0
- package/.devcontainer/devcontainer.json +52 -0
- package/.github/dependabot.yml +10 -0
- package/.github/workflows/pages.yml +42 -0
- package/.github/workflows/publish.yml +41 -0
- package/.vscode/settings.json +7 -0
- package/LICENSE +9 -0
- package/README.md +9 -0
- package/app/_component/editor.tsx +383 -0
- package/app/layout.tsx +46 -0
- package/app/page.tsx +11 -0
- package/app/r/registry.json/route.ts +22 -0
- package/components/editorx/editor.tsx +1794 -0
- package/components/editorx/extensions/floating-menu.tsx +376 -0
- package/components/editorx/extensions/floating-toolbar.tsx +97 -0
- package/components/editorx/extensions/image-placeholder.tsx +316 -0
- package/components/editorx/extensions/image.tsx +462 -0
- package/components/editorx/extensions/search-and-replace.tsx +438 -0
- package/components/editorx/rich-text-editor.tsx +383 -0
- package/components/editorx/tiptap.css +421 -0
- package/components/editorx/toolbars/alignment.tsx +126 -0
- package/components/editorx/toolbars/blockquote.tsx +47 -0
- package/components/editorx/toolbars/bold.tsx +48 -0
- package/components/editorx/toolbars/bullet-list.tsx +48 -0
- package/components/editorx/toolbars/code-block.tsx +47 -0
- package/components/editorx/toolbars/code.tsx +43 -0
- package/components/editorx/toolbars/color-and-highlight.tsx +215 -0
- package/components/editorx/toolbars/editor-toolbar.tsx +77 -0
- package/components/editorx/toolbars/hard-break.tsx +46 -0
- package/components/editorx/toolbars/headings.tsx +97 -0
- package/components/editorx/toolbars/horizontal-rule.tsx +42 -0
- package/components/editorx/toolbars/image-placeholder-toolbar.tsx +47 -0
- package/components/editorx/toolbars/italic.tsx +48 -0
- package/components/editorx/toolbars/link.tsx +130 -0
- package/components/editorx/toolbars/mobile-toolbar-group.tsx +76 -0
- package/components/editorx/toolbars/ordered-list.tsx +47 -0
- package/components/editorx/toolbars/redo.tsx +44 -0
- package/components/editorx/toolbars/strikethrough.tsx +48 -0
- package/components/editorx/toolbars/toolbar-provider.tsx +29 -0
- package/components/editorx/toolbars/underline.tsx +48 -0
- package/components/editorx/toolbars/undo.tsx +43 -0
- package/components/layout/theme-switcher.tsx +26 -0
- package/components/main-nav.tsx +24 -0
- package/components/mobile-nav.tsx +46 -0
- package/components/open-in-v0-button.tsx +38 -0
- package/components/page-header.tsx +30 -0
- package/components/site-footer.tsx +41 -0
- package/components/site-header.tsx +32 -0
- package/components/theme-provider.tsx +8 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/checkbox.tsx +30 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +148 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/drawer.tsx +118 -0
- package/components/ui/dropdown-menu.tsx +201 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +33 -0
- package/components/ui/resizable.tsx +40 -0
- package/components/ui/scroll-area.tsx +42 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/sheet.tsx +140 -0
- package/components/ui/sidebar.tsx +763 -0
- package/components/ui/skeleton.tsx +15 -0
- package/components/ui/spinner.tsx +29 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/toggle-group.tsx +61 -0
- package/components/ui/toggle.tsx +45 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components.json +21 -0
- package/config/site.ts +15 -0
- package/eslint.config.mjs +20 -0
- package/hooks/use-character-limit.ts +28 -0
- package/hooks/use-copy-to-clipboard.ts +16 -0
- package/hooks/use-debounce.ts +17 -0
- package/hooks/use-image-upload.ts +97 -0
- package/hooks/use-media-querry.ts +18 -0
- package/hooks/use-mobile.tsx +19 -0
- package/images/editor.png +0 -0
- package/lib/content.ts +39 -0
- package/lib/cookie-client.ts +19 -0
- package/lib/localstorage-client.ts +19 -0
- package/lib/package.ts +144 -0
- package/lib/preferences-config.ts +72 -0
- package/lib/preferences-storage.ts +20 -0
- package/lib/theme-utils.ts +12 -0
- package/lib/theme.ts +50 -0
- package/lib/tiptap-utils.ts +45 -0
- package/lib/utils.ts +11 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +11 -0
- package/package.json +92 -0
- package/postcss.config.mjs +8 -0
- package/prettier.config.mjs +15 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/og.webp +0 -0
- package/public/r/editor-x.json +85 -0
- package/public/r/registry.json +93 -0
- package/public/site.webmanifest +19 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/registry/editor/components/editor.tsx +1794 -0
- package/registry/editor/components/extensions/floating-menu.tsx +376 -0
- package/registry/editor/components/extensions/floating-toolbar.tsx +97 -0
- package/registry/editor/components/extensions/image-placeholder.tsx +316 -0
- package/registry/editor/components/extensions/image.tsx +462 -0
- package/registry/editor/components/extensions/search-and-replace.tsx +438 -0
- package/registry/editor/components/rich-text-editor.tsx +383 -0
- package/registry/editor/components/tiptap.css +421 -0
- package/registry/editor/components/toolbars/alignment.tsx +126 -0
- package/registry/editor/components/toolbars/blockquote.tsx +47 -0
- package/registry/editor/components/toolbars/bold.tsx +48 -0
- package/registry/editor/components/toolbars/bullet-list.tsx +48 -0
- package/registry/editor/components/toolbars/code-block.tsx +47 -0
- package/registry/editor/components/toolbars/code.tsx +43 -0
- package/registry/editor/components/toolbars/color-and-highlight.tsx +215 -0
- package/registry/editor/components/toolbars/editor-toolbar.tsx +77 -0
- package/registry/editor/components/toolbars/hard-break.tsx +46 -0
- package/registry/editor/components/toolbars/headings.tsx +97 -0
- package/registry/editor/components/toolbars/horizontal-rule.tsx +42 -0
- package/registry/editor/components/toolbars/image-placeholder-toolbar.tsx +47 -0
- package/registry/editor/components/toolbars/italic.tsx +48 -0
- package/registry/editor/components/toolbars/link.tsx +130 -0
- package/registry/editor/components/toolbars/mobile-toolbar-group.tsx +76 -0
- package/registry/editor/components/toolbars/ordered-list.tsx +47 -0
- package/registry/editor/components/toolbars/redo.tsx +44 -0
- package/registry/editor/components/toolbars/strikethrough.tsx +48 -0
- package/registry/editor/components/toolbars/toolbar-provider.tsx +29 -0
- package/registry/editor/components/toolbars/underline.tsx +48 -0
- package/registry/editor/components/toolbars/undo.tsx +43 -0
- package/registry/editor/components/ui/button.tsx +57 -0
- package/registry/editor/components/ui/checkbox.tsx +30 -0
- package/registry/editor/components/ui/collapsible.tsx +11 -0
- package/registry/editor/components/ui/command.tsx +148 -0
- package/registry/editor/components/ui/dialog.tsx +122 -0
- package/registry/editor/components/ui/drawer.tsx +118 -0
- package/registry/editor/components/ui/dropdown-menu.tsx +201 -0
- package/registry/editor/components/ui/input.tsx +22 -0
- package/registry/editor/components/ui/label.tsx +26 -0
- package/registry/editor/components/ui/popover.tsx +33 -0
- package/registry/editor/components/ui/resizable.tsx +40 -0
- package/registry/editor/components/ui/scroll-area.tsx +42 -0
- package/registry/editor/components/ui/separator.tsx +31 -0
- package/registry/editor/components/ui/sheet.tsx +140 -0
- package/registry/editor/components/ui/sidebar.tsx +763 -0
- package/registry/editor/components/ui/skeleton.tsx +15 -0
- package/registry/editor/components/ui/spinner.tsx +29 -0
- package/registry/editor/components/ui/tabs.tsx +55 -0
- package/registry/editor/components/ui/toggle-group.tsx +61 -0
- package/registry/editor/components/ui/toggle.tsx +45 -0
- package/registry/editor/components/ui/tooltip.tsx +32 -0
- package/registry/editor/hooks/use-character-limit.ts +28 -0
- package/registry/editor/hooks/use-copy-to-clipboard.ts +16 -0
- package/registry/editor/hooks/use-debounce.ts +17 -0
- package/registry/editor/hooks/use-image-upload.ts +97 -0
- package/registry/editor/hooks/use-media-querry.ts +18 -0
- package/registry/editor/hooks/use-mobile.tsx +19 -0
- package/registry/editor/lib/content.ts +39 -0
- package/registry/editor/lib/cookie-client.ts +19 -0
- package/registry/editor/lib/localstorage-client.ts +19 -0
- package/registry/editor/lib/package.ts +144 -0
- package/registry/editor/lib/preferences-config.ts +72 -0
- package/registry/editor/lib/preferences-storage.ts +20 -0
- package/registry/editor/lib/theme-utils.ts +12 -0
- package/registry/editor/lib/theme.ts +50 -0
- package/registry/editor/lib/tiptap-utils.ts +45 -0
- package/registry/editor/lib/utils.ts +11 -0
- package/registry/editor/page.tsx +9 -0
- package/registry.json +93 -0
- package/reset.d.ts +1 -0
- package/scripts/generate-theme-presets.ts +128 -0
- package/scripts/postCreateCommand.sh +0 -0
- package/scripts/theme-boot.tsx +105 -0
- package/server/server-actions.ts +27 -0
- package/stores/preferences/preferences-provider.tsx +55 -0
- package/stores/preferences/preferences-store.ts +23 -0
- package/styles/globals.css +288 -0
- package/styles/presets/brutalist.css +89 -0
- package/styles/presets/soft-pop.css +89 -0
- package/styles/presets/tangerine.css +89 -0
- 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
|
+
}
|