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