diff --git a/.gitignore b/.gitignore index e69de29..f650315 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..dc2aea1 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,125 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --font-sans: 'Geist', 'Geist Fallback'; + --font-mono: 'Geist Mono', 'Geist Mono Fallback'; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..53f1953 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const _geist = Geist({ subsets: ["latin"] }); +const _geistMono = Geist_Mono({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: 'v0 App', + description: 'Created with v0', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + {process.env.NODE_ENV === 'production' && } + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..57cbeed --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,528 @@ +"use client" + +import { useState } from "react" +import { + PanelLeft, + PanelRight, + PanelBottom, + File, + Folder, + Settings, + Save, + FolderOpen, + ImageIcon, + Music, + Users, +} from "lucide-react" +import { type Node, NodeType } from "@/types/node" +import type { MenuItem } from "@/types/menu" +import { TreeNode } from "@/components/tree-node" +import { ResizablePanel } from "@/components/resizable-panel" +import { HamburgerMenu } from "@/components/hamburger-menu" +import { useCanvasAnimation } from "@/hooks/use-canvas-animation" +import { Button } from "@/components/ui/button" +import { Separator } from "@/components/ui/separator" + +// Animation duration constant +const PANEL_TRANSITION_DURATION = "300ms" + +// Mock menu data +const mockMenuItems: MenuItem[] = [ + { + id: "file", + label: "File", + icon: , + isExpanded: false, + children: [ + { + id: "new", + label: "New Project", + icon: , + onClick: () => console.log("New Project clicked"), + }, + { + id: "open", + label: "Open Project", + icon: , + onClick: () => console.log("Open Project clicked"), + }, + { + id: "save", + label: "Save Project", + icon: , + onClick: () => console.log("Save Project clicked"), + isChecked: true, + }, + { + id: "recent", + label: "Recent Files", + icon: , + isExpanded: false, + children: [ + { + id: "recent1", + label: "Project Alpha.vnp", + onClick: () => console.log("Recent 1 clicked"), + }, + { + id: "recent2", + label: "Story Beta.vnp", + onClick: () => console.log("Recent 2 clicked"), + }, + { + id: "recent3", + label: "Demo Game.vnp", + onClick: () => console.log("Recent 3 clicked"), + disabled: true, + }, + ], + }, + ], + }, + { + id: "assets", + label: "Assets", + icon: , + isExpanded: true, + children: [ + { + id: "images", + label: "Images", + icon: , + onClick: () => console.log("Images clicked"), + }, + { + id: "audio", + label: "Audio", + icon: , + isExpanded: false, + children: [ + { + id: "music", + label: "Background Music", + onClick: () => console.log("Music clicked"), + }, + { + id: "sfx", + label: "Sound Effects", + onClick: () => console.log("SFX clicked"), + }, + ], + }, + { + id: "characters", + label: "Characters", + icon: , + onClick: () => console.log("Characters clicked"), + }, + ], + }, + { + id: "tools", + label: "Tools", + icon: , + children: [ + { + id: "preferences", + label: "Preferences", + onClick: () => console.log("Preferences clicked"), + }, + { + id: "export", + label: "Export Game", + onClick: () => console.log("Export clicked"), + }, + ], + }, + { + id: "hidden-item", + label: "This should be hidden", + isVisible: false, + onClick: () => console.log("Hidden clicked"), + }, +] + +// Mock data (same as before) +const mockTree: Node = { + id: "root", + type: NodeType.Episode, + actions: [ + { + name: "Play", + imageUrl: "/placeholder.svg?height=24&width=24", + description: "Play this episode", + onClick: () => console.log("Play clicked"), + }, + { + name: "Edit", + imageUrl: "/placeholder.svg?height=24&width=24", + description: "Edit this episode", + onClick: () => console.log("Edit clicked"), + }, + ], + properties: { + title: { + id: "title", + label: "Title", + type: "string", + value: "Episode 1", + }, + duration: { + id: "duration", + label: "Duration", + type: "number", + value: 120, + readOnly: true, + }, + }, + children: [ + { + id: "scene1", + type: NodeType.Scene, + actions: [ + { + name: "Preview", + imageUrl: "/placeholder.svg?height=24&width=24", + description: "Preview this scene", + onClick: () => console.log("Preview clicked"), + }, + ], + properties: { + name: { + id: "name", + label: "Scene Name", + type: "string", + value: "Opening Scene", + }, + }, + children: [ + { + id: "dialogue1", + type: NodeType.Dialogue, + actions: [], + properties: { + text: { + id: "text", + label: "Dialogue Text", + type: "string", + value: "Hello, welcome to our story!", + }, + character: { + id: "character", + label: "Character", + type: "string", + value: "Narrator", + }, + }, + }, + { + id: "image1", + type: NodeType.Image, + actions: [ + { + name: "Delete", + imageUrl: "/placeholder.svg?height=24&width=24", + description: "Delete this image", + onClick: () => console.log("Delete clicked"), + }, + ], + properties: { + src: { + id: "src", + label: "Image Source", + type: "string", + value: "/images/background.jpg", + }, + }, + }, + ], + }, + ], +} + +export default function VisualNovelEditor() { + const [leftPanelVisible, setLeftPanelVisible] = useState(true) + const [rightPanelVisible, setRightPanelVisible] = useState(true) + const [bottomPanelVisible, setBottomPanelVisible] = useState(true) + const [leftPanelWidth, setLeftPanelWidth] = useState(300) + const [rightPanelWidth, setRightPanelWidth] = useState(300) + const [bottomPanelHeight, setBottomPanelHeight] = useState(200) + + // Track when panels are transitioning to enable/disable transitions + const [leftPanelTransitioning, setLeftPanelTransitioning] = useState(false) + const [rightPanelTransitioning, setRightPanelTransitioning] = useState(false) + const [bottomPanelTransitioning, setBottomPanelTransitioning] = useState(false) + + const [selectedNode, setSelectedNode] = useState(null) + const [selectedPath, setSelectedPath] = useState([]) + const [expandedNodes, setExpandedNodes] = useState>(new Set(["root", "scene1"])) + + // Menu state + const [menuItems, setMenuItems] = useState(mockMenuItems) + + const canvasRef = useCanvasAnimation() + + // Handle panel visibility changes with transition states + const toggleLeftPanel = () => { + setLeftPanelTransitioning(true) + setLeftPanelVisible(!leftPanelVisible) + setTimeout(() => setLeftPanelTransitioning(false), 300) + } + + const toggleRightPanel = () => { + setRightPanelTransitioning(true) + setRightPanelVisible(!rightPanelVisible) + setTimeout(() => setRightPanelTransitioning(false), 300) + } + + const toggleBottomPanel = () => { + setBottomPanelTransitioning(true) + setBottomPanelVisible(!bottomPanelVisible) + setTimeout(() => setBottomPanelTransitioning(false), 300) + } + + const handleNodeSelect = (node: Node, path: string[]) => { + setSelectedNode(node) + setSelectedPath(path) + } + + const toggleExpanded = (nodeId: string) => { + const newExpanded = new Set(expandedNodes) + if (newExpanded.has(nodeId)) { + newExpanded.delete(nodeId) + } else { + newExpanded.add(nodeId) + } + setExpandedNodes(newExpanded) + } + + const handleMenuItemClick = (item: MenuItem) => { + console.log("Menu item clicked:", item.label) + } + + const handleMenuItemToggle = (itemId: string) => { + const updateMenuItems = (items: MenuItem[]): MenuItem[] => { + return items.map((item) => { + if (item.id === itemId) { + return { ...item, isExpanded: !item.isExpanded } + } + if (item.children) { + return { ...item, children: updateMenuItems(item.children) } + } + return item + }) + } + setMenuItems(updateMenuItems(menuItems)) + } + + const pathString = selectedPath.join(" > ") + + return ( +
+ {/* Main Menu */} +
+ +

Visual Novel Editor

+
+ + + +
+
+ + {/* Main Content Area */} +
+ {/* Top Section - Left, Main, Right Panels */} +
+ {/* Left Panel - Node Tree */} +
+
+ +
+

Node Tree

+ +
+
+
+
+ + {/* Main Panel */} +
+
+ {/* Action Buttons */} + {selectedNode && selectedNode.actions.length > 0 && ( +
+
+ {selectedNode.actions.map((action, index) => ( + + ))} +
+
+ )} + + {/* Preview Canvas */} +
+ +
+
+

Preview

+

+ {selectedNode ? `Selected: ${pathString}` : "No node selected"} +

+
+
+
+
+
+ + {/* Right Panel - Info */} +
+
+ +
+

Node Information

+ + {selectedNode ? ( + <> +

+ Node Type: {selectedNode.type} +

+

+ This section contains help information about {selectedNode.type} nodes and how they work within + the visual novel engine. Each node type has specific properties and behaviors that affect how + the story flows. +

+ + + +

+ Current Path: {pathString} +

+ + ) : ( +

Select a node to view information about it.

+ )} +
+
+
+
+
+ + {/* Bottom Panel - Object Properties (Full Width) */} +
+
+ +
+

{selectedNode?.type || "No Selection"}

+

+ {selectedNode ? pathString : "Select a node to view its properties"} +

+ + {selectedNode && Object.keys(selectedNode.properties).length > 0 && ( + <> + +
+ {Object.entries(selectedNode.properties).map(([key, property]) => ( +
+ {property.label}: + {String(property.value)} +
+ ))} +
+ + )} +
+
+
+
+
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/hamburger-menu.tsx b/components/hamburger-menu.tsx new file mode 100644 index 0000000..2177405 --- /dev/null +++ b/components/hamburger-menu.tsx @@ -0,0 +1,168 @@ +"use client" + +import { useState } from "react" +import { Menu, X, ChevronDown, ChevronRight, Check } from "lucide-react" +import { Button } from "@/components/ui/button" +import type { MenuItem } from "@/types/menu" + +interface HamburgerMenuProps { + menuItems: MenuItem[] + onMenuItemClick?: (item: MenuItem) => void + onMenuItemToggle?: (itemId: string) => void + transitionDuration?: string +} + +interface MenuItemComponentProps { + item: MenuItem + onItemClick?: (item: MenuItem) => void + onItemToggle?: (itemId: string) => void + transitionDuration: string + depth?: number +} + +function MenuItemComponent({ item, onItemClick, onItemToggle, transitionDuration, depth = 0 }: MenuItemComponentProps) { + if (item.isVisible === false) return null + + const hasChildren = item.children && item.children.length > 0 + const isExpanded = item.isExpanded || false + const paddingLeft = depth * 16 + 16 // 16px base + 16px per depth level + + const handleClick = () => { + if (item.disabled) return + + if (hasChildren) { + onItemToggle?.(item.id) + } else if (item.onClick) { + item.onClick() + onItemClick?.(item) + } + } + + return ( +
+ + + {hasChildren && ( +
+
+ {item.children?.map((child) => ( + + ))} +
+
+ )} +
+ ) +} + +export function HamburgerMenu({ + menuItems, + onMenuItemClick, + onMenuItemToggle, + transitionDuration = "300ms", +}: HamburgerMenuProps) { + const [isOpen, setIsOpen] = useState(false) + + const toggleMenu = () => { + setIsOpen(!isOpen) + } + + const handleMenuItemClick = (item: MenuItem) => { + onMenuItemClick?.(item) + // Close menu after clicking a leaf item (no children) + if (!item.children || item.children.length === 0) { + setIsOpen(false) + } + } + + return ( + <> + + + {/* Overlay */} + {isOpen &&
setIsOpen(false)} />} + + {/* Menu Panel */} +
+
+

Menu

+ +
+ +
+ {menuItems.map((item) => ( + + ))} +
+
+ + ) +} diff --git a/components/resizable-panel.tsx b/components/resizable-panel.tsx new file mode 100644 index 0000000..2dc0950 --- /dev/null +++ b/components/resizable-panel.tsx @@ -0,0 +1,112 @@ +"use client" + +import type React from "react" + +import { type ReactNode, useRef, useEffect, useState } from "react" + +interface ResizablePanelProps { + children: ReactNode + direction: "horizontal" | "vertical" + initialSize: number + minSize: number + maxSize: number + onResize?: (size: number) => void + className?: string + handlePosition?: "start" | "end" +} + +export function ResizablePanel({ + children, + direction, + initialSize, + minSize, + maxSize, + onResize, + className = "", + handlePosition = "end", +}: ResizablePanelProps) { + const [size, setSize] = useState(initialSize) + const [isResizing, setIsResizing] = useState(false) + const panelRef = useRef(null) + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing || !panelRef.current) return + + e.preventDefault() + + const rect = panelRef.current.getBoundingClientRect() + let newSize: number + + if (direction === "horizontal") { + if (handlePosition === "start") { + newSize = rect.right - e.clientX + } else { + newSize = e.clientX - rect.left + } + } else { + if (handlePosition === "start") { + newSize = rect.bottom - e.clientY + } else { + newSize = rect.bottom - e.clientY + } + } + + newSize = Math.max(minSize, Math.min(maxSize, newSize)) + setSize(newSize) + onResize?.(newSize) + } + + const handleMouseUp = () => { + setIsResizing(false) + document.body.style.userSelect = "" + document.body.style.cursor = "" + } + + if (isResizing) { + document.body.style.userSelect = "none" + document.body.style.cursor = direction === "horizontal" ? "col-resize" : "row-resize" + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + return () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + }, [isResizing, direction, minSize, maxSize, onResize, handlePosition]) + + // Update size when initialSize changes (for panel visibility changes) + useEffect(() => { + setSize(initialSize) + }, [initialSize]) + + const style = direction === "horizontal" ? { width: `${size}px` } : { height: `${size}px` } + + const getHandleClasses = () => { + const baseClasses = "absolute bg-border hover:bg-primary/50 transition-colors z-10" + const cursorClass = direction === "horizontal" ? "cursor-col-resize" : "cursor-row-resize" + + if (direction === "horizontal") { + return handlePosition === "start" + ? `${baseClasses} ${cursorClass} top-0 left-0 w-1 h-full` + : `${baseClasses} ${cursorClass} top-0 right-0 w-1 h-full` + } else { + return handlePosition === "start" + ? `${baseClasses} ${cursorClass} top-0 left-0 w-full h-1` + : `${baseClasses} ${cursorClass} bottom-0 left-0 w-full h-1` + } + } + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + setIsResizing(true) + } + + return ( +
+ {children} +
+
+ ) +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/tree-node.tsx b/components/tree-node.tsx new file mode 100644 index 0000000..6d6ef69 --- /dev/null +++ b/components/tree-node.tsx @@ -0,0 +1,64 @@ +"use client" + +import { ChevronDown, ChevronRight } from "lucide-react" +import type { Node } from "@/types/node" +import { Button } from "@/components/ui/button" + +interface TreeNodeProps { + node: Node + selectedNodeId: string | null + onNodeSelect: (node: Node, path: string[]) => void + path: string[] + expandedNodes: Set + onToggleExpanded: (nodeId: string) => void +} + +export function TreeNode({ node, selectedNodeId, onNodeSelect, path, expandedNodes, onToggleExpanded }: TreeNodeProps) { + const hasChildren = node.children && node.children.length > 0 + const isExpanded = expandedNodes.has(node.id) + const isSelected = selectedNodeId === node.id + const currentPath = [...path, node.type] + + return ( +
+
onNodeSelect(node, currentPath)} + > + {hasChildren ? ( + + ) : ( +
+ )} + {node.type} +
+ {hasChildren && isExpanded && ( +
+ {node.children!.map((child) => ( + + ))} +
+ )} +
+ ) +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..e538a33 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..9704452 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..e6751ab --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..40bb120 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..aa98465 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..fc4126b --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1750ff2 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return