diff --git a/bun.lock b/bun.lock
index 25a61e7..808c4af 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,9 +6,11 @@
"dependencies": {
"@fontsource/fira-code": "^5.1.1",
"@monaco-editor/react": "^4.7.0",
+ "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
+ "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
@@ -19,7 +21,7 @@
"devicons-react": "^1.4.0",
"dockerode": "^4.0.4",
"github-markdown-css": "^5.8.1",
- "lucide-react": "^0.475.0",
+ "lucide-react": "^0.477.0",
"monaco-editor": "<=0.36.1",
"monaco-languageclient": "<=5.0.1",
"next": "15.1.7",
@@ -230,6 +232,8 @@
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q=="],
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.6", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw=="],
+
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.0", "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="],
@@ -258,6 +262,8 @@
"@radix-ui/react-select": ["@radix-ui/react-select@2.1.6", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.1.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg=="],
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ=="],
+
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng=="],
@@ -872,7 +878,7 @@
"lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
- "lucide-react": ["lucide-react@0.475.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.475.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg=="],
+ "lucide-react": ["lucide-react@0.477.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.477.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yCf7aYxerFZAbd8jHJxjwe1j7jEMPptjnaOqdYeirFnEy85cNR3/L+o0I875CYFYya+eEVzZSbNuRk8BZPDpVw=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
diff --git a/package.json b/package.json
index 7d6da4b..79bd8f8 100644
--- a/package.json
+++ b/package.json
@@ -11,9 +11,11 @@
"dependencies": {
"@fontsource/fira-code": "^5.1.1",
"@monaco-editor/react": "^4.7.0",
+ "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
+ "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
@@ -24,7 +26,7 @@
"devicons-react": "^1.4.0",
"dockerode": "^4.0.4",
"github-markdown-css": "^5.8.1",
- "lucide-react": "^0.475.0",
+ "lucide-react": "^0.477.0",
"monaco-editor": "<=0.36.1",
"monaco-languageclient": "<=5.0.1",
"next": "15.1.7",
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..6cfb49d
--- /dev/null
+++ b/src/app/dashboard/page.tsx
@@ -0,0 +1,9 @@
+import { SettingsDialog } from "@/components/settings-dialog"
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 293901f..dccb447 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -32,6 +32,14 @@
--chart-4: 213 3% 78%;
--chart-5: 213 16% 16%;
--radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 213 13% 6%;
+ --sidebar-primary: 213 13% 16%;
+ --sidebar-primary-foreground: 213 13% 76%;
+ --sidebar-accent: 0 0% 85%;
+ --sidebar-accent-foreground: 0 0% 25%;
+ --sidebar-border: 0 0% 95%;
+ --sidebar-ring: 213 13% 16%;
}
.dark {
@@ -59,6 +67,14 @@
--chart-3: 216 28% 22%;
--chart-4: 210 7% 28%;
--chart-5: 210 20% 82%;
+ --sidebar-background: 216 28% 7%;
+ --sidebar-foreground: 210 17% 92%;
+ --sidebar-primary: 210 17% 82%;
+ --sidebar-primary-foreground: 210 17% 22%;
+ --sidebar-accent: 216 28% 22%;
+ --sidebar-accent-foreground: 216 28% 82%;
+ --sidebar-border: 216 18% 12%;
+ --sidebar-ring: 210 17% 82%;
}
}
diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx
new file mode 100644
index 0000000..4d55169
--- /dev/null
+++ b/src/components/settings-dialog.tsx
@@ -0,0 +1,129 @@
+"use client"
+
+import * as React from "react"
+import {
+ Bell,
+ Check,
+ Globe,
+ Home,
+ Keyboard,
+ Link,
+ Lock,
+ Menu,
+ MessageCircle,
+ Paintbrush,
+ Settings,
+ Video,
+} from "lucide-react"
+
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+} from "@/components/ui/breadcrumb"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarProvider,
+} from "@/components/ui/sidebar"
+
+const data = {
+ nav: [
+ { name: "Notifications", icon: Bell },
+ { name: "Navigation", icon: Menu },
+ { name: "Home", icon: Home },
+ { name: "Appearance", icon: Paintbrush },
+ { name: "Messages & media", icon: MessageCircle },
+ { name: "Language & region", icon: Globe },
+ { name: "Accessibility", icon: Keyboard },
+ { name: "Mark as read", icon: Check },
+ { name: "Audio & video", icon: Video },
+ { name: "Connected accounts", icon: Link },
+ { name: "Privacy & visibility", icon: Lock },
+ { name: "Advanced", icon: Settings },
+ ],
+}
+
+export function SettingsDialog() {
+ const [open, setOpen] = React.useState(true)
+
+ return (
+
+ )
+}
diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..1647513
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..69b64fb
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..12d81c4
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..272cb72
--- /dev/null
+++ b/src/components/ui/sheet.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..af58fa0
--- /dev/null
+++ b/src/components/ui/sidebar.tsx
@@ -0,0 +1,773 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { VariantProps, cva } from "class-variance-authority"
+import { PanelLeft } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContext = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile
+ ? setOpenMobile((open) => !open)
+ : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 after:md:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/src/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 266c06d..6113ca2 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -49,6 +49,16 @@ export default {
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
+ },
+ sidebar: {
+ DEFAULT: 'hsl(var(--sidebar-background))',
+ foreground: 'hsl(var(--sidebar-foreground))',
+ primary: 'hsl(var(--sidebar-primary))',
+ 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
+ accent: 'hsl(var(--sidebar-accent))',
+ 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
+ border: 'hsl(var(--sidebar-border))',
+ ring: 'hsl(var(--sidebar-ring))'
}
},
borderRadius: {