├── .eslintrc.json
├── src
├── app
│ ├── icon.ico
│ ├── twitter-image.png
│ ├── opengraph-image.png
│ ├── (demo)
│ │ ├── layout.tsx
│ │ ├── tags
│ │ │ └── page.tsx
│ │ ├── users
│ │ │ └── page.tsx
│ │ ├── account
│ │ │ └── page.tsx
│ │ ├── posts
│ │ │ ├── page.tsx
│ │ │ └── new
│ │ │ │ └── page.tsx
│ │ ├── categories
│ │ │ └── page.tsx
│ │ └── dashboard
│ │ │ └── page.tsx
│ ├── layout.tsx
│ ├── globals.css
│ └── page.tsx
├── lib
│ ├── utils.ts
│ └── menu-list.ts
├── components
│ ├── providers
│ │ └── theme-provider.tsx
│ ├── ui
│ │ ├── collapsible.tsx
│ │ ├── label.tsx
│ │ ├── tooltip.tsx
│ │ ├── switch.tsx
│ │ ├── avatar.tsx
│ │ ├── scroll-area.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── sheet.tsx
│ │ └── dropdown-menu.tsx
│ ├── admin-panel
│ │ ├── content-layout.tsx
│ │ ├── sidebar-toggle.tsx
│ │ ├── navbar.tsx
│ │ ├── footer.tsx
│ │ ├── admin-panel-layout.tsx
│ │ ├── sheet-menu.tsx
│ │ ├── sidebar.tsx
│ │ ├── user-nav.tsx
│ │ ├── menu.tsx
│ │ └── collapse-menu-button.tsx
│ ├── demo
│ │ └── placeholder-content.tsx
│ └── mode-toggle.tsx
└── hooks
│ ├── use-store.ts
│ └── use-sidebar.ts
├── public
├── placeholder.png
├── demo-dark-min.png
├── demo-light-min.png
├── demo-mobile-dark-min.png
├── demo-mobile-light-min.png
├── vercel.svg
├── next.svg
└── registry
│ └── shadcn-sidebar.json
├── .vscode
└── settings.json
├── next.config.mjs
├── screenshots
├── screenshot-1.png
├── screenshot-2.png
├── screenshot-3.png
└── screenshot-4.png
├── registry
├── index.ts
├── registry-components.ts
└── schema.ts
├── postcss.config.mjs
├── components.json
├── .gitignore
├── tsconfig.json
├── LICENSE
├── package.json
├── README.md
├── scripts
└── build-registry.ts
└── tailwind.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/src/app/icon.ico
--------------------------------------------------------------------------------
/public/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/public/placeholder.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.singleQuote": false,
3 | "prettier.trailingComma": "none"
4 | }
5 |
--------------------------------------------------------------------------------
/public/demo-dark-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/public/demo-dark-min.png
--------------------------------------------------------------------------------
/public/demo-light-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/public/demo-light-min.png
--------------------------------------------------------------------------------
/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/src/app/twitter-image.png
--------------------------------------------------------------------------------
/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/screenshots/screenshot-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/screenshots/screenshot-2.png
--------------------------------------------------------------------------------
/screenshots/screenshot-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/screenshots/screenshot-3.png
--------------------------------------------------------------------------------
/screenshots/screenshot-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/screenshots/screenshot-4.png
--------------------------------------------------------------------------------
/public/demo-mobile-dark-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/public/demo-mobile-dark-min.png
--------------------------------------------------------------------------------
/public/demo-mobile-light-min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/HEAD/public/demo-mobile-light-min.png
--------------------------------------------------------------------------------
/registry/index.ts:
--------------------------------------------------------------------------------
1 | import { ui } from "./registry-components"
2 |
3 | export const registryComponents = [
4 | ...ui
5 | ]
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/(demo)/layout.tsx:
--------------------------------------------------------------------------------
1 | import AdminPanelLayout from "@/components/admin-panel/admin-panel-layout";
2 |
3 | export default function DemoLayout({
4 | children
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | ThemeProvider as NextThemesProvider,
6 | type ThemeProviderProps
7 | } from "next-themes";
8 |
9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
10 | return {children} ;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/src/components/admin-panel/content-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Navbar } from "@/components/admin-panel/navbar";
2 |
3 | interface ContentLayoutProps {
4 | title: string;
5 | children: React.ReactNode;
6 | }
7 |
8 | export function ContentLayout({ title, children }: ContentLayoutProps) {
9 | return (
10 |
11 |
12 |
{children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/hooks/use-store.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | /**
3 | * This hook fix hydration when use persist to save hook data to localStorage
4 | */
5 | export const useStore = (
6 | store: (callback: (state: T) => unknown) => unknown,
7 | callback: (state: T) => F
8 | ) => {
9 | const result = store(callback) as F;
10 | const [data, setData] = useState();
11 |
12 | useEffect(() => {
13 | setData(result);
14 | }, [result]);
15 |
16 | return data;
17 | };
18 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sidebar-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { ChevronLeft } from "lucide-react";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Button } from "@/components/ui/button";
5 |
6 | interface SidebarToggleProps {
7 | isOpen: boolean | undefined;
8 | setIsOpen?: () => void;
9 | }
10 |
11 | export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
12 | return (
13 |
14 | setIsOpen?.()}
16 | className="rounded-md w-8 h-8"
17 | variant="outline"
18 | size="icon"
19 | >
20 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/admin-panel/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { ModeToggle } from "@/components/mode-toggle";
2 | import { UserNav } from "@/components/admin-panel/user-nav";
3 | import { SheetMenu } from "@/components/admin-panel/sheet-menu";
4 |
5 | interface NavbarProps {
6 | title: string;
7 | }
8 |
9 | export function Navbar({ title }: NavbarProps) {
10 | return (
11 |
12 |
13 |
14 |
15 |
{title}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/admin-panel/footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export function Footer() {
4 | return (
5 |
6 |
7 |
8 | Built on top of{" "}
9 |
15 | shadcn/ui
16 |
17 | . The source code is available on{" "}
18 |
24 | GitHub
25 |
26 | .
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Salimi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/demo/placeholder-content.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | import { Card, CardContent } from "@/components/ui/card";
5 |
6 | export default function PlaceholderContent() {
7 | return (
8 |
9 |
10 |
11 |
12 |
19 |
20 |
26 | Designed by Freepik
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/admin-panel/admin-panel-layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Footer } from "@/components/admin-panel/footer";
4 | import { Sidebar } from "@/components/admin-panel/sidebar";
5 | import { useSidebar } from "@/hooks/use-sidebar";
6 | import { useStore } from "@/hooks/use-store";
7 | import { cn } from "@/lib/utils";
8 |
9 | export default function AdminPanelLayout({
10 | children
11 | }: {
12 | children: React.ReactNode;
13 | }) {
14 | const sidebar = useStore(useSidebar, (x) => x);
15 | if (!sidebar) return null;
16 | const { getOpenState, settings } = sidebar;
17 | return (
18 | <>
19 |
20 |
26 | {children}
27 |
28 |
36 | >
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/(demo)/tags/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function TagsPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Tags
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/(demo)/users/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function UsersPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Users
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/(demo)/account/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function AccountPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Account
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/(demo)/posts/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function PostsPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Posts
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/(demo)/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function CategoriesPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 | Categories
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
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,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sheet-menu.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { MenuIcon, PanelsTopLeft } from "lucide-react";
3 |
4 | import { Button } from "@/components/ui/button";
5 | import { Menu } from "@/components/admin-panel/menu";
6 | import {
7 | Sheet,
8 | SheetHeader,
9 | SheetContent,
10 | SheetTrigger,
11 | SheetTitle
12 | } from "@/components/ui/sheet";
13 |
14 | export function SheetMenu() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 | Brand
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useTheme } from "next-themes";
5 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipTrigger,
12 | TooltipProvider
13 | } from "@/components/ui/tooltip";
14 |
15 | export function ModeToggle() {
16 | const { setTheme, theme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
22 | setTheme(theme === "dark" ? "light" : "dark")}
27 | >
28 |
29 |
30 | Switch Theme
31 |
32 |
33 | Switch Theme
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/(demo)/posts/new/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import PlaceholderContent from "@/components/demo/placeholder-content";
4 | import { ContentLayout } from "@/components/admin-panel/content-layout";
5 | import {
6 | Breadcrumb,
7 | BreadcrumbItem,
8 | BreadcrumbLink,
9 | BreadcrumbList,
10 | BreadcrumbPage,
11 | BreadcrumbSeparator
12 | } from "@/components/ui/breadcrumb";
13 |
14 | export default function NewPostPage() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Home
22 |
23 |
24 |
25 |
26 |
27 | Dashboard
28 |
29 |
30 |
31 |
32 |
33 | Posts
34 |
35 |
36 |
37 |
38 | New
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadcn-sidebar",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "registry:build": "tsx ./scripts/build-registry.ts"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-avatar": "^1.0.4",
14 | "@radix-ui/react-collapsible": "^1.0.3",
15 | "@radix-ui/react-dialog": "^1.0.5",
16 | "@radix-ui/react-dropdown-menu": "^2.0.6",
17 | "@radix-ui/react-icons": "^1.3.0",
18 | "@radix-ui/react-label": "^2.1.0",
19 | "@radix-ui/react-scroll-area": "^1.0.5",
20 | "@radix-ui/react-slot": "^1.0.2",
21 | "@radix-ui/react-switch": "^1.1.1",
22 | "@radix-ui/react-tooltip": "^1.1.3",
23 | "class-variance-authority": "^0.7.0",
24 | "clsx": "^2.1.1",
25 | "geist": "^1.3.0",
26 | "immer": "^10.1.1",
27 | "lucide-react": "^0.377.0",
28 | "next": "14.2.3",
29 | "next-themes": "^0.4.3",
30 | "react": "^18",
31 | "react-dom": "^18",
32 | "tailwind-merge": "^2.3.0",
33 | "tailwindcss-animate": "^1.0.7",
34 | "zod": "^3.23.8",
35 | "zustand": "^4.5.2"
36 | },
37 | "devDependencies": {
38 | "@types/node": "^20",
39 | "@types/react": "^18",
40 | "@types/react-dom": "^18",
41 | "eslint": "^8",
42 | "eslint-config-next": "14.2.3",
43 | "postcss": "^8",
44 | "tailwindcss": "^3.4.1",
45 | "tsx": "^4.19.1",
46 | "typescript": "^5"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/hooks/use-sidebar.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist, createJSONStorage } from "zustand/middleware";
3 | import { produce } from "immer";
4 |
5 | type SidebarSettings = { disabled: boolean; isHoverOpen: boolean };
6 | type SidebarStore = {
7 | isOpen: boolean;
8 | isHover: boolean;
9 | settings: SidebarSettings;
10 | toggleOpen: () => void;
11 | setIsOpen: (isOpen: boolean) => void;
12 | setIsHover: (isHover: boolean) => void;
13 | getOpenState: () => boolean;
14 | setSettings: (settings: Partial) => void;
15 | };
16 |
17 | export const useSidebar = create(
18 | persist(
19 | (set, get) => ({
20 | isOpen: true,
21 | isHover: false,
22 | settings: { disabled: false, isHoverOpen: false },
23 | toggleOpen: () => {
24 | set({ isOpen: !get().isOpen });
25 | },
26 | setIsOpen: (isOpen: boolean) => {
27 | set({ isOpen });
28 | },
29 | setIsHover: (isHover: boolean) => {
30 | set({ isHover });
31 | },
32 | getOpenState: () => {
33 | const state = get();
34 | return state.isOpen || (state.settings.isHoverOpen && state.isHover);
35 | },
36 | setSettings: (settings: Partial) => {
37 | set(
38 | produce((state: SidebarStore) => {
39 | state.settings = { ...state.settings, ...settings };
40 | })
41 | );
42 | }
43 | }),
44 | {
45 | name: "sidebar",
46 | storage: createJSONStorage(() => localStorage)
47 | }
48 | )
49 | );
50 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { GeistSans } from "geist/font/sans";
3 |
4 | import "./globals.css";
5 |
6 | import { ThemeProvider } from "@/components/providers/theme-provider";
7 |
8 | export const metadata: Metadata = {
9 | metadataBase: new URL(
10 | process.env.APP_URL
11 | ? `${process.env.APP_URL}`
12 | : process.env.VERCEL_URL
13 | ? `https://${process.env.VERCEL_URL}`
14 | : `http://localhost:${process.env.PORT || 3000}`
15 | ),
16 | title: "shadcn/ui sidebar",
17 | description:
18 | "A stunning and functional retractable sidebar for Next.js built on top of shadcn/ui complete with desktop and mobile responsiveness.",
19 | alternates: {
20 | canonical: "/"
21 | },
22 | openGraph: {
23 | url: "/",
24 | title: "shadcn/ui sidebar",
25 | description:
26 | "A stunning and functional retractable sidebar for Next.js built on top of shadcn/ui complete with desktop and mobile responsiveness.",
27 | type: "website"
28 | },
29 | twitter: {
30 | card: "summary_large_image",
31 | title: "shadcn/ui sidebar",
32 | description:
33 | "A stunning and functional retractable sidebar for Next.js built on top of shadcn/ui complete with desktop and mobile responsiveness."
34 | }
35 | };
36 |
37 | export default function RootLayout({
38 | children
39 | }: Readonly<{
40 | children: React.ReactNode;
41 | }>) {
42 | return (
43 |
44 |
45 |
46 | {children}
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/src/lib/menu-list.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tag,
3 | Users,
4 | Settings,
5 | Bookmark,
6 | SquarePen,
7 | LayoutGrid,
8 | LucideIcon
9 | } from "lucide-react";
10 |
11 | type Submenu = {
12 | href: string;
13 | label: string;
14 | active?: boolean;
15 | };
16 |
17 | type Menu = {
18 | href: string;
19 | label: string;
20 | active?: boolean;
21 | icon: LucideIcon;
22 | submenus?: Submenu[];
23 | };
24 |
25 | type Group = {
26 | groupLabel: string;
27 | menus: Menu[];
28 | };
29 |
30 | export function getMenuList(pathname: string): Group[] {
31 | return [
32 | {
33 | groupLabel: "",
34 | menus: [
35 | {
36 | href: "/dashboard",
37 | label: "Dashboard",
38 | icon: LayoutGrid,
39 | submenus: []
40 | }
41 | ]
42 | },
43 | {
44 | groupLabel: "Contents",
45 | menus: [
46 | {
47 | href: "",
48 | label: "Posts",
49 | icon: SquarePen,
50 | submenus: [
51 | {
52 | href: "/posts",
53 | label: "All Posts"
54 | },
55 | {
56 | href: "/posts/new",
57 | label: "New Post"
58 | }
59 | ]
60 | },
61 | {
62 | href: "/categories",
63 | label: "Categories",
64 | icon: Bookmark
65 | },
66 | {
67 | href: "/tags",
68 | label: "Tags",
69 | icon: Tag
70 | }
71 | ]
72 | },
73 | {
74 | groupLabel: "Settings",
75 | menus: [
76 | {
77 | href: "/users",
78 | label: "Users",
79 | icon: Users
80 | },
81 | {
82 | href: "/account",
83 | label: "Account",
84 | icon: Settings
85 | }
86 | ]
87 | }
88 | ];
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/src/components/admin-panel/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Menu } from "@/components/admin-panel/menu";
3 | import { SidebarToggle } from "@/components/admin-panel/sidebar-toggle";
4 | import { Button } from "@/components/ui/button";
5 | import { useSidebar } from "@/hooks/use-sidebar";
6 | import { useStore } from "@/hooks/use-store";
7 | import { cn } from "@/lib/utils";
8 | import { PanelsTopLeft } from "lucide-react";
9 | import Link from "next/link";
10 |
11 | export function Sidebar() {
12 | const sidebar = useStore(useSidebar, (x) => x);
13 | if (!sidebar) return null;
14 | const { isOpen, toggleOpen, getOpenState, setIsHover, settings } = sidebar;
15 | return (
16 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/registry/registry-components.ts:
--------------------------------------------------------------------------------
1 | import { Registry } from "./schema";
2 | import * as fs from "fs";
3 | import * as path from "path";
4 |
5 | function getAllFiles(dirPath: string, arrayOfFiles: string[] = []): string[] {
6 | const files = fs.readdirSync(dirPath);
7 |
8 | files.forEach((file) => {
9 | const filePath = path.join(dirPath, file).replaceAll("\\", "/");
10 | if (fs.statSync(filePath).isDirectory()) {
11 | arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
12 | } else {
13 | arrayOfFiles.push(filePath);
14 | }
15 | });
16 |
17 | return arrayOfFiles;
18 | }
19 |
20 | const ignoreFolders = ["src/components/ui", "src/components/demo"];
21 |
22 | const hooks = getAllFiles("./src/hooks").map((x) => {
23 | return x.split("src/")[1];
24 | });
25 |
26 | const components = getAllFiles("./src/components")
27 | .filter((x) => !ignoreFolders.some((igf) => x.startsWith(igf)))
28 | .map((x) => x.split("src/")[1]);
29 |
30 | export const ui: Registry = [
31 | {
32 | name: "shadcn-sidebar",
33 | type: "registry:block",
34 | registryDependencies: [
35 | "avatar",
36 | "button",
37 | "card",
38 | "collapsible",
39 | "dropdown-menu",
40 | "scroll-area",
41 | "sheet",
42 | "tooltip"
43 | ],
44 | dependencies: ["immer", "zustand", "next-themes"],
45 | tailwind: {
46 | config: {
47 | theme: {
48 | extend: {
49 | keyframes: {
50 | "accordion-down": {
51 | from: { height: "0" },
52 | to: { height: "var(--radix-accordion-content-height)" }
53 | },
54 | "accordion-up": {
55 | from: { height: "var(--radix-accordion-content-height)" },
56 | to: { height: "0" }
57 | },
58 | "collapsible-down": {
59 | from: { height: "0" },
60 | to: { height: "var(--radix-collapsible-content-height)" }
61 | },
62 | "collapsible-up": {
63 | from: { height: "var(--radix-collapsible-content-height)" },
64 | to: { height: "0" }
65 | }
66 | },
67 | animation: {
68 | "accordion-down": "accordion-down 0.2s ease-out",
69 | "accordion-up": "accordion-up 0.2s ease-out",
70 | "collapsible-down": "collapsible-down 0.2s ease-out",
71 | "collapsible-up": "collapsible-up 0.2s ease-out"
72 | }
73 | }
74 | }
75 | }
76 | },
77 | files: [...hooks, ...components, "lib/menu-list.ts"]
78 | }
79 | ];
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [shadcn/ui sidebar](https://shadcn-ui-sidebar.salimi.my) · [](https://www.salimi.my)
2 |
3 | A stunning and functional retractable sidebar for Next.js built on top of [shadcn/ui](https://ui.shadcn.com) complete with desktop and mobile responsiveness.
4 |
5 | ## Features
6 |
7 | - Retractable mini and wide sidebar
8 | - Scrollable sidebar menu
9 | - Sheet menu for mobile
10 | - Grouped menu with labels
11 | - Collapsible submenu
12 | - Extracted menu items list
13 |
14 | ## Tech/framework used
15 |
16 | - Next.js 14
17 | - Shadcn/ui
18 | - Tailwind CSS
19 | - TypeScript
20 | - Zustand
21 |
22 | ## Installation
23 |
24 | ### Custom registry
25 |
26 | If you are using @shadcn/ui 2.0.0 or later, you can install the component directly from the registry.
27 |
28 | ```bash
29 | npx shadcn@latest add https://raw.githubusercontent.com/salimi-my/shadcn-ui-sidebar/refs/heads/master/public/registry/shadcn-sidebar.json
30 |
31 | or
32 |
33 | npx shadcn@latest add https://shadcn-ui-sidebar.salimi.my/registry/shadcn-sidebar.json
34 | ```
35 |
36 | ### Usage example for Nextjs
37 | ```tsx
38 | //layout.tsx
39 | import AdminPanelLayout from "@/components/admin-panel/admin-panel-layout";
40 |
41 | export default async function Layout({
42 | children,
43 | }: Readonly<{
44 | children: React.ReactNode;
45 | }>) {
46 | return {children} ;
47 | }
48 |
49 | //page.tsx
50 | import { ContentLayout } from "@/components/admin-panel/content-layout";
51 |
52 | export default function Page() {
53 | return (
54 |
55 | Test
56 |
57 | );
58 | }
59 | ```
60 |
61 | ## Starting the project locally
62 |
63 | 1. Clone the repository
64 |
65 | ```bash
66 | git clone https://github.com/salimi-my/shadcn-ui-sidebar
67 | ```
68 |
69 | 2. Install dependencies
70 |
71 | ```bash
72 | cd shadcn-ui-sidebar
73 | npm install
74 | ```
75 |
76 | 3. Run the development server
77 |
78 | ```bash
79 | npm run dev
80 | ```
81 |
82 | ## Demo
83 |
84 | The app is hosted on Vercel. [Click here](https://shadcn-ui-sidebar.salimi.my) to visit.
85 |
86 | Direct demo link: `https://shadcn-ui-sidebar.salimi.my`
87 |
88 | ## Screenshots
89 |
90 | #### Light mode
91 |
92 | 
93 |
94 | #### Dark mode
95 |
96 | 
97 |
98 | #### Mini sidebar
99 |
100 | 
101 |
102 | #### Sheet menu
103 |
104 |
105 |
--------------------------------------------------------------------------------
/scripts/build-registry.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from "fs";
2 | import path from "path";
3 | import { z } from "zod";
4 | import { registryComponents } from "../registry";
5 | import { registryItemFileSchema } from "../registry/schema";
6 |
7 | const REGISTRY_BASE_PATH = "src";
8 | const PUBLIC_FOLDER_BASE_PATH = "public/registry";
9 | const COMPONENT_FOLDER_PATH = "components";
10 |
11 | type File = z.infer;
12 | const FolderToComponentTypeMap = {
13 | block: "registry:block",
14 | components: "registry:component",
15 | hooks: "registry:hook",
16 | ui: "registry:ui",
17 | lib: "registry:lib"
18 | };
19 |
20 | async function writeFileRecursive(filePath: string, data: string) {
21 | const dir = path.dirname(filePath); // Extract the directory path
22 |
23 | try {
24 | // Ensure the directory exists, recursively creating directories as needed
25 | await fs.mkdir(dir, { recursive: true });
26 |
27 | // Write the file
28 | await fs.writeFile(filePath, data, "utf-8");
29 | console.log(`File written to ${filePath}`);
30 | } catch (error) {
31 | console.error(`Error writing file`);
32 | console.error(error);
33 | }
34 | }
35 |
36 | const getComponentFiles = async (files: File[]) => {
37 | const filesArrayPromises = (files ?? []).map(async (file) => {
38 | if (typeof file === "string") {
39 | const filePath = `${REGISTRY_BASE_PATH}/${file}`;
40 | const fileContent = await fs.readFile(filePath, "utf-8");
41 | console.log("Build Registry:", file);
42 | return {
43 | type: FolderToComponentTypeMap[
44 | file.split("/")[0] as keyof typeof FolderToComponentTypeMap
45 | ],
46 | content: fileContent,
47 | path: file,
48 | target: file
49 | };
50 | }
51 | });
52 | const filesArray = await Promise.all(filesArrayPromises);
53 |
54 | return filesArray;
55 | };
56 |
57 | const main = async () => {
58 | // make a json file and put it in public folder
59 | for (let i = 0; i < registryComponents.length; i++) {
60 | const component = registryComponents[i];
61 | const files = component.files;
62 | if (!files) throw new Error("No files found for component");
63 |
64 | const filesArray = await getComponentFiles(files);
65 |
66 | const json = JSON.stringify(
67 | {
68 | ...component,
69 | files: filesArray
70 | },
71 | null,
72 | 2
73 | );
74 | const jsonPath = `${PUBLIC_FOLDER_BASE_PATH}/${component.name}.json`;
75 | await writeFileRecursive(jsonPath, json);
76 | // console.log(json);
77 | }
78 | };
79 |
80 | main()
81 | .then(() => {
82 | console.log("done");
83 | })
84 | .catch((err) => {
85 | console.error(err);
86 | });
87 |
--------------------------------------------------------------------------------
/registry/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const blockChunkSchema = z.object({
4 | name: z.string(),
5 | description: z.string(),
6 | component: z.any(),
7 | file: z.string(),
8 | code: z.string().optional(),
9 | container: z
10 | .object({
11 | className: z.string().nullish()
12 | })
13 | .optional()
14 | });
15 |
16 | export const registryItemTypeSchema = z.enum([
17 | "registry:style",
18 | "registry:lib",
19 | "registry:example",
20 | "registry:block",
21 | "registry:component",
22 | "registry:ui",
23 | "registry:hook",
24 | "registry:theme",
25 | "registry:page"
26 | ]);
27 |
28 | export const registryItemFileSchema = z.union([
29 | z.string(),
30 | z.object({
31 | path: z.string(),
32 | content: z.string().optional(),
33 | type: registryItemTypeSchema,
34 | target: z.string().optional()
35 | })
36 | ]);
37 |
38 | export const registryItemTailwindSchema = z.object({
39 | config: z.object({
40 | content: z.array(z.string()).optional(),
41 | theme: z.record(z.string(), z.any()).optional(),
42 | plugins: z.array(z.string()).optional()
43 | })
44 | });
45 |
46 | export const registryItemCssVarsSchema = z.object({
47 | light: z.record(z.string(), z.string()).optional(),
48 | dark: z.record(z.string(), z.string()).optional()
49 | });
50 |
51 | export const registryEntrySchema = z.object({
52 | name: z.string(),
53 | type: registryItemTypeSchema,
54 | description: z.string().optional(),
55 | dependencies: z.array(z.string()).optional(),
56 | devDependencies: z.array(z.string()).optional(),
57 | registryDependencies: z.array(z.string()).optional(),
58 | files: z.array(registryItemFileSchema).optional(),
59 | tailwind: registryItemTailwindSchema.optional(),
60 | cssVars: registryItemCssVarsSchema.optional(),
61 | source: z.string().optional(),
62 | category: z.string().optional(),
63 | subcategory: z.string().optional(),
64 | chunks: z.array(blockChunkSchema).optional(),
65 | docs: z.string().optional()
66 | });
67 |
68 | export const registrySchema = z.array(registryEntrySchema);
69 |
70 | export type RegistryEntry = z.infer;
71 |
72 | export type Registry = z.infer;
73 |
74 | export const blockSchema = registryEntrySchema.extend({
75 | type: z.literal("registry:block"),
76 | style: z.enum(["default", "new-york"]),
77 | component: z.any(),
78 | container: z
79 | .object({
80 | height: z.string().nullish(),
81 | className: z.string().nullish()
82 | })
83 | .optional(),
84 | code: z.string(),
85 | highlightedCode: z.string()
86 | });
87 |
88 | export type Block = z.infer;
89 |
90 | export type BlockChunk = z.infer;
91 |
--------------------------------------------------------------------------------
/src/app/(demo)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import { ContentLayout } from "@/components/admin-panel/content-layout";
4 | import {
5 | Breadcrumb,
6 | BreadcrumbItem,
7 | BreadcrumbLink,
8 | BreadcrumbList,
9 | BreadcrumbPage,
10 | BreadcrumbSeparator
11 | } from "@/components/ui/breadcrumb";
12 | import { Label } from "@/components/ui/label";
13 | import { Switch } from "@/components/ui/switch";
14 | import {
15 | Tooltip,
16 | TooltipContent,
17 | TooltipProvider,
18 | TooltipTrigger
19 | } from "@/components/ui/tooltip";
20 | import { useSidebar } from "@/hooks/use-sidebar";
21 | import { useStore } from "@/hooks/use-store";
22 |
23 | export default function DashboardPage() {
24 | const sidebar = useStore(useSidebar, (x) => x);
25 | if (!sidebar) return null;
26 | const { settings, setSettings } = sidebar;
27 | return (
28 |
29 |
30 |
31 |
32 |
33 | Home
34 |
35 |
36 |
37 |
38 | Dashboard
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | setSettings({ isHoverOpen: x })}
50 | checked={settings.isHoverOpen}
51 | />
52 | Hover Open
53 |
54 |
55 |
56 | When hovering on the sidebar in mini state, it will open
57 |
58 |
59 |
60 |
61 |
62 |
69 |
70 |
71 | Hide sidebar
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./app/**/*.{ts,tsx}",
9 | "./src/**/*.{ts,tsx}"
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px"
18 | }
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))"
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))"
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))"
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))"
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))"
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))"
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))"
54 | }
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)"
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" }
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" }
69 | },
70 | "collapsible-down": {
71 | from: { height: "0" },
72 | to: { height: "var(--radix-collapsible-content-height)" }
73 | },
74 | "collapsible-up": {
75 | from: { height: "var(--radix-collapsible-content-height)" },
76 | to: { height: "0" }
77 | }
78 | },
79 | animation: {
80 | "accordion-down": "accordion-down 0.2s ease-out",
81 | "accordion-up": "accordion-up 0.2s ease-out",
82 | "collapsible-down": "collapsible-down 0.2s ease-out",
83 | "collapsible-up": "collapsible-up 0.2s ease-out"
84 | }
85 | }
86 | },
87 | plugins: [require("tailwindcss-animate")]
88 | } satisfies Config;
89 |
90 | export default config;
91 |
--------------------------------------------------------------------------------
/src/components/admin-panel/user-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { LayoutGrid, LogOut, User } from "lucide-react";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
8 | import {
9 | Tooltip,
10 | TooltipContent,
11 | TooltipTrigger,
12 | TooltipProvider
13 | } from "@/components/ui/tooltip";
14 | import {
15 | DropdownMenu,
16 | DropdownMenuContent,
17 | DropdownMenuGroup,
18 | DropdownMenuItem,
19 | DropdownMenuLabel,
20 | DropdownMenuSeparator,
21 | DropdownMenuTrigger
22 | } from "@/components/ui/dropdown-menu";
23 |
24 | export function UserNav() {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
37 | JD
38 |
39 |
40 |
41 |
42 | Profile
43 |
44 |
45 |
46 |
47 |
48 |
49 |
John Doe
50 |
51 | johndoe@example.com
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Dashboard
61 |
62 |
63 |
64 |
65 |
66 | Account
67 |
68 |
69 |
70 |
71 | {}}>
72 |
73 | Sign out
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
3 | import { Slot } from "@radix-ui/react-slot"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { Cross2Icon } from "@radix-ui/react-icons"
6 | import { cva, type VariantProps } from "class-variance-authority"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | 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",
42 | right:
43 | "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",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import { PanelsTopLeft } from "lucide-react";
4 | import { ArrowRightIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import { ModeToggle } from "@/components/mode-toggle";
8 |
9 | export default function HomePage() {
10 | return (
11 |
12 |
13 |
14 |
18 |
19 |
shadcn/ui sidebar
20 |
shadcn/ui sidebar
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Sidebar example built on top of shadcn/ui
42 |
43 |
44 | A stunning and functional retractable sidebar for Next.js using
45 | shadcn/ui complete with desktop and mobile responsiveness.
46 |
47 |
48 |
49 |
50 | Demo
51 |
52 |
53 |
54 |
55 |
60 | Learn shadcn/ui
61 |
62 |
63 |
64 |
65 |
66 |
74 |
82 |
89 |
96 |
97 |
98 |
99 |
124 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/admin-panel/menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { Ellipsis, LogOut } from "lucide-react";
5 | import { usePathname } from "next/navigation";
6 |
7 | import { cn } from "@/lib/utils";
8 | import { getMenuList } from "@/lib/menu-list";
9 | import { Button } from "@/components/ui/button";
10 | import { ScrollArea } from "@/components/ui/scroll-area";
11 | import { CollapseMenuButton } from "@/components/admin-panel/collapse-menu-button";
12 | import {
13 | Tooltip,
14 | TooltipTrigger,
15 | TooltipContent,
16 | TooltipProvider
17 | } from "@/components/ui/tooltip";
18 |
19 | interface MenuProps {
20 | isOpen: boolean | undefined;
21 | }
22 |
23 | export function Menu({ isOpen }: MenuProps) {
24 | const pathname = usePathname();
25 | const menuList = getMenuList(pathname);
26 |
27 | return (
28 |
29 |
30 |
31 | {menuList.map(({ groupLabel, menus }, index) => (
32 |
33 | {(isOpen && groupLabel) || isOpen === undefined ? (
34 |
35 | {groupLabel}
36 |
37 | ) : !isOpen && isOpen !== undefined && groupLabel ? (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {groupLabel}
47 |
48 |
49 |
50 | ) : (
51 |
52 | )}
53 | {menus.map(
54 | ({ href, label, icon: Icon, active, submenus }, index) =>
55 | !submenus || submenus.length === 0 ? (
56 |
57 |
58 |
59 |
60 |
71 |
72 |
75 |
76 |
77 |
85 | {label}
86 |
87 |
88 |
89 |
90 | {isOpen === false && (
91 |
92 | {label}
93 |
94 | )}
95 |
96 |
97 |
98 | ) : (
99 |
100 |
111 |
112 | )
113 | )}
114 |
115 | ))}
116 |
117 |
118 |
119 |
120 | {}}
122 | variant="outline"
123 | className="w-full justify-center h-10 mt-5"
124 | >
125 |
126 |
127 |
128 |
134 | Sign out
135 |
136 |
137 |
138 | {isOpen === false && (
139 | Sign out
140 | )}
141 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | }
149 |
--------------------------------------------------------------------------------
/src/components/admin-panel/collapse-menu-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { useState } from "react";
5 | import { ChevronDown, Dot, LucideIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 | import { Button } from "@/components/ui/button";
9 | import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu";
10 | import {
11 | Collapsible,
12 | CollapsibleContent,
13 | CollapsibleTrigger
14 | } from "@/components/ui/collapsible";
15 | import {
16 | Tooltip,
17 | TooltipTrigger,
18 | TooltipContent,
19 | TooltipProvider
20 | } from "@/components/ui/tooltip";
21 | import {
22 | DropdownMenu,
23 | DropdownMenuItem,
24 | DropdownMenuLabel,
25 | DropdownMenuTrigger,
26 | DropdownMenuContent,
27 | DropdownMenuSeparator
28 | } from "@/components/ui/dropdown-menu";
29 | import { usePathname } from "next/navigation";
30 |
31 | type Submenu = {
32 | href: string;
33 | label: string;
34 | active?: boolean;
35 | };
36 |
37 | interface CollapseMenuButtonProps {
38 | icon: LucideIcon;
39 | label: string;
40 | active: boolean;
41 | submenus: Submenu[];
42 | isOpen: boolean | undefined;
43 | }
44 |
45 | export function CollapseMenuButton({
46 | icon: Icon,
47 | label,
48 | active,
49 | submenus,
50 | isOpen
51 | }: CollapseMenuButtonProps) {
52 | const pathname = usePathname();
53 | const isSubmenuActive = submenus.some((submenu) =>
54 | submenu.active === undefined ? submenu.href === pathname : submenu.active
55 | );
56 | const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive);
57 |
58 | return isOpen ? (
59 |
64 |
68 |
72 |
73 |
74 |
75 |
76 |
77 |
85 | {label}
86 |
87 |
88 |
96 |
100 |
101 |
102 |
103 |
104 |
105 | {submenus.map(({ href, label, active }, index) => (
106 |
116 |
117 |
118 |
119 |
120 |
128 | {label}
129 |
130 |
131 |
132 | ))}
133 |
134 |
135 | ) : (
136 |
137 |
138 |
139 |
140 |
141 |
145 |
146 |
147 |
148 |
149 |
150 |
156 | {label}
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | {label}
165 |
166 |
167 |
168 |
169 |
170 | {label}
171 |
172 |
173 | {submenus.map(({ href, label, active }, index) => (
174 |
175 |
182 | {label}
183 |
184 |
185 | ))}
186 |
187 |
188 |
189 | );
190 | }
191 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import {
6 | CheckIcon,
7 | ChevronRightIcon,
8 | DotFilledIcon,
9 | } from "@radix-ui/react-icons"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | const DropdownMenu = DropdownMenuPrimitive.Root
14 |
15 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
16 |
17 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
18 |
19 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
20 |
21 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
22 |
23 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
24 |
25 | const DropdownMenuSubTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef & {
28 | inset?: boolean
29 | }
30 | >(({ className, inset, children, ...props }, ref) => (
31 |
40 | {children}
41 |
42 |
43 | ))
44 | DropdownMenuSubTrigger.displayName =
45 | DropdownMenuPrimitive.SubTrigger.displayName
46 |
47 | const DropdownMenuSubContent = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, ...props }, ref) => (
51 |
59 | ))
60 | DropdownMenuSubContent.displayName =
61 | DropdownMenuPrimitive.SubContent.displayName
62 |
63 | const DropdownMenuContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, sideOffset = 4, ...props }, ref) => (
67 |
68 |
78 |
79 | ))
80 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
81 |
82 | const DropdownMenuItem = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef & {
85 | inset?: boolean
86 | }
87 | >(({ className, inset, ...props }, ref) => (
88 |
97 | ))
98 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
99 |
100 | const DropdownMenuCheckboxItem = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, children, checked, ...props }, ref) => (
104 |
113 |
114 |
115 |
116 |
117 |
118 | {children}
119 |
120 | ))
121 | DropdownMenuCheckboxItem.displayName =
122 | DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
161 | ))
162 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
163 |
164 | const DropdownMenuSeparator = React.forwardRef<
165 | React.ElementRef,
166 | React.ComponentPropsWithoutRef
167 | >(({ className, ...props }, ref) => (
168 |
173 | ))
174 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
175 |
176 | const DropdownMenuShortcut = ({
177 | className,
178 | ...props
179 | }: React.HTMLAttributes) => {
180 | return (
181 |
185 | )
186 | }
187 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
188 |
189 | export {
190 | DropdownMenu,
191 | DropdownMenuTrigger,
192 | DropdownMenuContent,
193 | DropdownMenuItem,
194 | DropdownMenuCheckboxItem,
195 | DropdownMenuRadioItem,
196 | DropdownMenuLabel,
197 | DropdownMenuSeparator,
198 | DropdownMenuShortcut,
199 | DropdownMenuGroup,
200 | DropdownMenuPortal,
201 | DropdownMenuSub,
202 | DropdownMenuSubContent,
203 | DropdownMenuSubTrigger,
204 | DropdownMenuRadioGroup,
205 | }
206 |
--------------------------------------------------------------------------------
/public/registry/shadcn-sidebar.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadcn-sidebar",
3 | "type": "registry:block",
4 | "registryDependencies": [
5 | "avatar",
6 | "button",
7 | "card",
8 | "collapsible",
9 | "dropdown-menu",
10 | "scroll-area",
11 | "sheet",
12 | "tooltip"
13 | ],
14 | "dependencies": [
15 | "immer",
16 | "zustand",
17 | "next-themes"
18 | ],
19 | "tailwind": {
20 | "config": {
21 | "theme": {
22 | "extend": {
23 | "keyframes": {
24 | "accordion-down": {
25 | "from": {
26 | "height": "0"
27 | },
28 | "to": {
29 | "height": "var(--radix-accordion-content-height)"
30 | }
31 | },
32 | "accordion-up": {
33 | "from": {
34 | "height": "var(--radix-accordion-content-height)"
35 | },
36 | "to": {
37 | "height": "0"
38 | }
39 | },
40 | "collapsible-down": {
41 | "from": {
42 | "height": "0"
43 | },
44 | "to": {
45 | "height": "var(--radix-collapsible-content-height)"
46 | }
47 | },
48 | "collapsible-up": {
49 | "from": {
50 | "height": "var(--radix-collapsible-content-height)"
51 | },
52 | "to": {
53 | "height": "0"
54 | }
55 | }
56 | },
57 | "animation": {
58 | "accordion-down": "accordion-down 0.2s ease-out",
59 | "accordion-up": "accordion-up 0.2s ease-out",
60 | "collapsible-down": "collapsible-down 0.2s ease-out",
61 | "collapsible-up": "collapsible-up 0.2s ease-out"
62 | }
63 | }
64 | }
65 | }
66 | },
67 | "files": [
68 | {
69 | "type": "registry:hook",
70 | "content": "import { create } from \"zustand\";\nimport { persist, createJSONStorage } from \"zustand/middleware\";\nimport { produce } from \"immer\";\n\ntype SidebarSettings = { disabled: boolean; isHoverOpen: boolean };\ntype SidebarStore = {\n isOpen: boolean;\n isHover: boolean;\n settings: SidebarSettings;\n toggleOpen: () => void;\n setIsOpen: (isOpen: boolean) => void;\n setIsHover: (isHover: boolean) => void;\n getOpenState: () => boolean;\n setSettings: (settings: Partial) => void;\n};\n\nexport const useSidebar = create(\n persist(\n (set, get) => ({\n isOpen: true,\n isHover: false,\n settings: { disabled: false, isHoverOpen: false },\n toggleOpen: () => {\n set({ isOpen: !get().isOpen });\n },\n setIsOpen: (isOpen: boolean) => {\n set({ isOpen });\n },\n setIsHover: (isHover: boolean) => {\n set({ isHover });\n },\n getOpenState: () => {\n const state = get();\n return state.isOpen || (state.settings.isHoverOpen && state.isHover);\n },\n setSettings: (settings: Partial) => {\n set(\n produce((state: SidebarStore) => {\n state.settings = { ...state.settings, ...settings };\n })\n );\n }\n }),\n {\n name: \"sidebar\",\n storage: createJSONStorage(() => localStorage)\n }\n )\n);\n",
71 | "path": "hooks/use-sidebar.ts",
72 | "target": "hooks/use-sidebar.ts"
73 | },
74 | {
75 | "type": "registry:hook",
76 | "content": "import { useState, useEffect } from \"react\";\n/**\n * This hook fix hydration when use persist to save hook data to localStorage\n */\nexport const useStore = (\n store: (callback: (state: T) => unknown) => unknown,\n callback: (state: T) => F\n) => {\n const result = store(callback) as F;\n const [data, setData] = useState();\n\n useEffect(() => {\n setData(result);\n }, [result]);\n\n return data;\n};\n",
77 | "path": "hooks/use-store.ts",
78 | "target": "hooks/use-store.ts"
79 | },
80 | {
81 | "type": "registry:component",
82 | "content": "\"use client\";\n\nimport { Footer } from \"@/components/admin-panel/footer\";\nimport { Sidebar } from \"@/components/admin-panel/sidebar\";\nimport { useSidebar } from \"@/hooks/use-sidebar\";\nimport { useStore } from \"@/hooks/use-store\";\nimport { cn } from \"@/lib/utils\";\n\nexport default function AdminPanelLayout({\n children\n}: {\n children: React.ReactNode;\n}) {\n const sidebar = useStore(useSidebar, (x) => x);\n if (!sidebar) return null;\n const { getOpenState, settings } = sidebar;\n return (\n <>\n \n \n {children}\n \n \n >\n );\n}\n",
83 | "path": "components/admin-panel/admin-panel-layout.tsx",
84 | "target": "components/admin-panel/admin-panel-layout.tsx"
85 | },
86 | {
87 | "type": "registry:component",
88 | "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { useState } from \"react\";\nimport { ChevronDown, Dot, LucideIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { DropdownMenuArrow } from \"@radix-ui/react-dropdown-menu\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger\n} from \"@/components/ui/collapsible\";\nimport {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n TooltipProvider\n} from \"@/components/ui/tooltip\";\nimport {\n DropdownMenu,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuSeparator\n} from \"@/components/ui/dropdown-menu\";\nimport { usePathname } from \"next/navigation\";\n\ntype Submenu = {\n href: string;\n label: string;\n active?: boolean;\n};\n\ninterface CollapseMenuButtonProps {\n icon: LucideIcon;\n label: string;\n active: boolean;\n submenus: Submenu[];\n isOpen: boolean | undefined;\n}\n\nexport function CollapseMenuButton({\n icon: Icon,\n label,\n active,\n submenus,\n isOpen\n}: CollapseMenuButtonProps) {\n const pathname = usePathname();\n const isSubmenuActive = submenus.some((submenu) =>\n submenu.active === undefined ? submenu.href === pathname : submenu.active\n );\n const [isCollapsed, setIsCollapsed] = useState(isSubmenuActive);\n\n return isOpen ? (\n \n div>div>svg]:rotate-180 mb-1\"\n asChild\n >\n \n \n
\n
\n \n \n
\n {label}\n
\n
\n
\n \n
\n
\n \n \n \n {submenus.map(({ href, label, active }, index) => (\n \n \n \n \n \n \n {label}\n
\n \n \n ))}\n \n \n ) : (\n \n \n \n \n \n \n \n
\n
\n \n \n
\n {label}\n
\n
\n
\n \n \n \n \n {label}\n \n \n \n \n \n {label}\n \n \n {submenus.map(({ href, label, active }, index) => (\n \n \n {label}
\n \n \n ))}\n \n \n \n );\n}\n",
89 | "path": "components/admin-panel/collapse-menu-button.tsx",
90 | "target": "components/admin-panel/collapse-menu-button.tsx"
91 | },
92 | {
93 | "type": "registry:component",
94 | "content": "import { Navbar } from \"@/components/admin-panel/navbar\";\n\ninterface ContentLayoutProps {\n title: string;\n children: React.ReactNode;\n}\n\nexport function ContentLayout({ title, children }: ContentLayoutProps) {\n return (\n \n );\n}\n",
95 | "path": "components/admin-panel/content-layout.tsx",
96 | "target": "components/admin-panel/content-layout.tsx"
97 | },
98 | {
99 | "type": "registry:component",
100 | "content": "import Link from \"next/link\";\n\nexport function Footer() {\n return (\n \n
\n
\n Built on top of{\" \"}\n \n shadcn/ui\n \n . The source code is available on{\" \"}\n \n GitHub\n \n .\n
\n
\n
\n );\n}\n",
101 | "path": "components/admin-panel/footer.tsx",
102 | "target": "components/admin-panel/footer.tsx"
103 | },
104 | {
105 | "type": "registry:component",
106 | "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Ellipsis, LogOut } from \"lucide-react\";\nimport { usePathname } from \"next/navigation\";\n\nimport { cn } from \"@/lib/utils\";\nimport { getMenuList } from \"@/lib/menu-list\";\nimport { Button } from \"@/components/ui/button\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { CollapseMenuButton } from \"@/components/admin-panel/collapse-menu-button\";\nimport {\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n TooltipProvider\n} from \"@/components/ui/tooltip\";\n\ninterface MenuProps {\n isOpen: boolean | undefined;\n}\n\nexport function Menu({ isOpen }: MenuProps) {\n const pathname = usePathname();\n const menuList = getMenuList(pathname);\n\n return (\n div>div[style]]:!block\">\n \n \n {menuList.map(({ groupLabel, menus }, index) => (\n \n {(isOpen && groupLabel) || isOpen === undefined ? (\n \n {groupLabel}\n
\n ) : !isOpen && isOpen !== undefined && groupLabel ? (\n \n \n \n \n \n
\n \n \n {groupLabel}
\n \n \n \n ) : (\n
\n )}\n {menus.map(\n ({ href, label, icon: Icon, active, submenus }, index) =>\n !submenus || submenus.length === 0 ? (\n \n
\n \n \n \n \n \n \n \n \n {label}\n
\n \n \n \n {isOpen === false && (\n \n {label}\n \n )}\n \n \n
\n ) : (\n \n \n
\n )\n )}\n \n ))}\n \n \n \n \n {}}\n variant=\"outline\"\n className=\"w-full justify-center h-10 mt-5\"\n >\n \n \n \n \n Sign out\n
\n \n \n {isOpen === false && (\n Sign out \n )}\n \n \n \n \n \n \n );\n}\n",
107 | "path": "components/admin-panel/menu.tsx",
108 | "target": "components/admin-panel/menu.tsx"
109 | },
110 | {
111 | "type": "registry:component",
112 | "content": "import { ModeToggle } from \"@/components/mode-toggle\";\nimport { UserNav } from \"@/components/admin-panel/user-nav\";\nimport { SheetMenu } from \"@/components/admin-panel/sheet-menu\";\n\ninterface NavbarProps {\n title: string;\n}\n\nexport function Navbar({ title }: NavbarProps) {\n return (\n \n \n
\n \n
{title} \n \n
\n \n \n
\n
\n \n );\n}\n",
113 | "path": "components/admin-panel/navbar.tsx",
114 | "target": "components/admin-panel/navbar.tsx"
115 | },
116 | {
117 | "type": "registry:component",
118 | "content": "import Link from \"next/link\";\nimport { MenuIcon, PanelsTopLeft } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Menu } from \"@/components/admin-panel/menu\";\nimport {\n Sheet,\n SheetHeader,\n SheetContent,\n SheetTrigger,\n SheetTitle\n} from \"@/components/ui/sheet\";\n\nexport function SheetMenu() {\n return (\n \n \n \n \n \n \n \n \n \n \n \n Brand \n \n \n \n \n \n \n );\n}\n",
119 | "path": "components/admin-panel/sheet-menu.tsx",
120 | "target": "components/admin-panel/sheet-menu.tsx"
121 | },
122 | {
123 | "type": "registry:component",
124 | "content": "import { ChevronLeft } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface SidebarToggleProps {\n isOpen: boolean | undefined;\n setIsOpen?: () => void;\n}\n\nexport function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {\n return (\n \n setIsOpen?.()}\n className=\"rounded-md w-8 h-8\"\n variant=\"outline\"\n size=\"icon\"\n >\n \n \n
\n );\n}\n",
125 | "path": "components/admin-panel/sidebar-toggle.tsx",
126 | "target": "components/admin-panel/sidebar-toggle.tsx"
127 | },
128 | {
129 | "type": "registry:component",
130 | "content": "\"use client\";\nimport { Menu } from \"@/components/admin-panel/menu\";\nimport { SidebarToggle } from \"@/components/admin-panel/sidebar-toggle\";\nimport { Button } from \"@/components/ui/button\";\nimport { useSidebar } from \"@/hooks/use-sidebar\";\nimport { useStore } from \"@/hooks/use-store\";\nimport { cn } from \"@/lib/utils\";\nimport { PanelsTopLeft } from \"lucide-react\";\nimport Link from \"next/link\";\n\nexport function Sidebar() {\n const sidebar = useStore(useSidebar, (x) => x);\n if (!sidebar) return null;\n const { isOpen, toggleOpen, getOpenState, setIsHover, settings } = sidebar;\n return (\n \n );\n}\n",
131 | "path": "components/admin-panel/sidebar.tsx",
132 | "target": "components/admin-panel/sidebar.tsx"
133 | },
134 | {
135 | "type": "registry:component",
136 | "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { LayoutGrid, LogOut, User } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n TooltipProvider\n} from \"@/components/ui/tooltip\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger\n} from \"@/components/ui/dropdown-menu\";\n\nexport function UserNav() {\n return (\n \n \n \n \n \n \n \n \n JD \n \n \n \n \n Profile \n \n \n\n \n \n \n
John Doe
\n
\n johndoe@example.com\n
\n
\n \n \n \n \n \n \n Dashboard\n \n \n \n \n \n Account\n \n \n \n \n {}}>\n \n Sign out\n \n \n \n );\n}\n",
137 | "path": "components/admin-panel/user-nav.tsx",
138 | "target": "components/admin-panel/user-nav.tsx"
139 | },
140 | {
141 | "type": "registry:component",
142 | "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { MoonIcon, SunIcon } from \"@radix-ui/react-icons\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n TooltipProvider\n} from \"@/components/ui/tooltip\";\n\nexport function ModeToggle() {\n const { setTheme, theme } = useTheme();\n\n return (\n \n \n \n setTheme(theme === \"dark\" ? \"light\" : \"dark\")}\n >\n \n \n Switch Theme \n \n \n Switch Theme \n \n \n );\n}\n",
143 | "path": "components/mode-toggle.tsx",
144 | "target": "components/mode-toggle.tsx"
145 | },
146 | {
147 | "type": "registry:component",
148 | "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes/dist/types\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n return {children} ;\n}\n",
149 | "path": "components/providers/theme-provider.tsx",
150 | "target": "components/providers/theme-provider.tsx"
151 | },
152 | {
153 | "type": "registry:lib",
154 | "content": "import {\n Tag,\n Users,\n Settings,\n Bookmark,\n SquarePen,\n LayoutGrid,\n LucideIcon\n} from \"lucide-react\";\n\ntype Submenu = {\n href: string;\n label: string;\n active?: boolean;\n};\n\ntype Menu = {\n href: string;\n label: string;\n active?: boolean;\n icon: LucideIcon;\n submenus?: Submenu[];\n};\n\ntype Group = {\n groupLabel: string;\n menus: Menu[];\n};\n\nexport function getMenuList(pathname: string): Group[] {\n return [\n {\n groupLabel: \"\",\n menus: [\n {\n href: \"/dashboard\",\n label: \"Dashboard\",\n icon: LayoutGrid,\n submenus: []\n }\n ]\n },\n {\n groupLabel: \"Contents\",\n menus: [\n {\n href: \"\",\n label: \"Posts\",\n icon: SquarePen,\n submenus: [\n {\n href: \"/posts\",\n label: \"All Posts\"\n },\n {\n href: \"/posts/new\",\n label: \"New Post\"\n }\n ]\n },\n {\n href: \"/categories\",\n label: \"Categories\",\n icon: Bookmark\n },\n {\n href: \"/tags\",\n label: \"Tags\",\n icon: Tag\n }\n ]\n },\n {\n groupLabel: \"Settings\",\n menus: [\n {\n href: \"/users\",\n label: \"Users\",\n icon: Users\n },\n {\n href: \"/account\",\n label: \"Account\",\n icon: Settings\n }\n ]\n }\n ];\n}\n",
155 | "path": "lib/menu-list.ts",
156 | "target": "lib/menu-list.ts"
157 | }
158 | ]
159 | }
--------------------------------------------------------------------------------