Skip to content

Commit ac5442e

Browse files
authored
PR.17 feature/left-sidebar
feat: add responsive left sidebar navigation
2 parents f8fc08a + be08e45 commit ac5442e

27 files changed

Lines changed: 1415 additions & 1030 deletions

app/(root)/(with-right-sidebar)/page.tsx

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const Home = () => (
33
<h1>Hello Root page with heading H1</h1>
44

55
{/* Header boundary marker */}
6-
<div className="mt-8 bg-primary/30 p-4">HEADER: {"words ".repeat(50)}</div>
6+
<div className="mt-8 bg-primary/30 p-4">{"words ".repeat(50)}</div>
77

88
{/* Flexbox demo: grow vs flex-none */}
99
<div className="my-8 flex gap-4 border-2 border-dashed border-primary p-4">
@@ -24,7 +24,71 @@ const Home = () => (
2424
</div>
2525

2626
{/* Footer boundary marker */}
27-
<div className="bg-primary/30 p-4">FOOTER: {"words ".repeat(50)}</div>
27+
<div className="bg-primary/30 p-4">{"words ".repeat(50)}</div>
28+
29+
{/* Question boxes for testing sticky sidebar scroll */}
30+
<div className="mt-8 space-y-6">
31+
{[1, 2, 3, 4].map((num) => (
32+
<article
33+
key={`question-${num}`}
34+
className="rounded-lg border border-border bg-card p-6 shadow-sm"
35+
>
36+
<div className="mb-3 flex items-center gap-2">
37+
<span className="rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
38+
Question #{num}
39+
</span>
40+
<span className="text-xs text-muted-foreground">
41+
Asked {num} hours ago
42+
</span>
43+
</div>
44+
45+
<h2 className="mb-2 text-xl font-semibold">
46+
How to implement a sticky sidebar in Next.js with Tailwind CSS?
47+
</h2>
48+
49+
<p className="mb-4 text-muted-foreground">
50+
I&apos;m building a dashboard layout and need the sidebar to remain
51+
visible while scrolling the main content. The sidebar should stick
52+
below the navbar and scroll independently if its content overflows.
53+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
54+
eiusmod tempor incididunt ut labore et dolore magna aliqua.
55+
</p>
56+
57+
{/* Tags */}
58+
<div className="mb-4 flex flex-wrap gap-2">
59+
{["next.js", "tailwindcss", "react", "css"].map((tag) => (
60+
<span
61+
key={tag}
62+
className="rounded-md bg-secondary px-2.5 py-1 text-xs font-medium text-secondary-foreground"
63+
>
64+
{tag}
65+
</span>
66+
))}
67+
</div>
68+
69+
{/* Stats */}
70+
<div className="flex items-center gap-6 text-sm text-muted-foreground">
71+
<span className="flex items-center gap-1">
72+
<span className="font-semibold text-foreground">{num * 7}</span>{" "}
73+
votes
74+
</span>
75+
<span className="flex items-center gap-1">
76+
<span className="font-semibold text-foreground">{num * 3}</span>{" "}
77+
answers
78+
</span>
79+
<span className="flex items-center gap-1">
80+
<span className="font-semibold text-foreground">{num * 47}</span>{" "}
81+
views
82+
</span>
83+
</div>
84+
</article>
85+
))}
86+
87+
{/* End marker */}
88+
<div className="rounded-lg bg-muted p-6 text-center text-muted-foreground">
89+
You&apos;ve reached the end — sidebar should still be sticky!
90+
</div>
91+
</div>
2892
</>
2993
);
3094

app/(root)/layout.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
import { LeftSidebar } from "@/components/navigation/left-sidebar";
12
import { Navbar } from "@/components/navigation/navbar";
3+
import { SidebarProvider } from "@/components/sidebar-provider";
24

35
const RootLayout = ({ children }: { children: React.ReactNode }) => {
46
return (
5-
<>
6-
<Navbar />
7-
{children}
8-
</>
7+
<SidebarProvider>
8+
<div className="flex min-h-screen flex-col">
9+
<Navbar />
10+
<div className="flex flex-1">
11+
<LeftSidebar />
12+
<main className="flex-1">{children}</main>
13+
</div>
14+
</div>
15+
</SidebarProvider>
916
);
1017
};
1118

app/globals.css

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
--color-sidebar-primary: var(--sidebar-primary);
3030
--color-sidebar-foreground: var(--sidebar-foreground);
3131
--color-sidebar: var(--sidebar);
32-
--color-mobile-nav: var(--mobile-nav);
3332
--color-chart-5: var(--chart-5);
3433
--color-chart-4: var(--chart-4);
3534
--color-chart-3: var(--chart-3);
@@ -51,18 +50,15 @@
5150
--color-popover: var(--popover);
5251
--color-card-foreground: var(--card-foreground);
5352
--color-card: var(--card);
53+
54+
/* NOT SHADCN VARIABLES */
55+
--color-mobile-nav: var(--mobile-nav);
56+
--color-overlay: var(--overlay);
5457
}
5558

5659
:root {
5760
/* Creates CSS variables available to all elements (no utility generation) */
58-
/* Only defined in root, not .dark */
5961
--radius: 0.625rem;
60-
--primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */
61-
--gradient-primary: linear-gradient(
62-
93.22deg,
63-
var(--primary) -13.95%,
64-
var(--primary-gradient-to) 99.54%
65-
);
6662

6763
--background: oklch(0.994 0 0); /* #FDFDFD */
6864
--foreground: oklch(0.129 0.042 264.695);
@@ -95,9 +91,16 @@
9591
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
9692
--sidebar-border: oklch(0.929 0.013 255.508);
9793
--sidebar-ring: oklch(0.704 0.04 256.788);
98-
--mobile-nav: oklch(1 0 0); /* #FFFFFF */
9994

100-
/* Centralised logo theming */
95+
/* NOT SHADCN VARIABLES */
96+
--primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */
97+
--gradient-primary: linear-gradient(
98+
93.22deg,
99+
var(--primary) -13.95%,
100+
var(--primary-gradient-to) 99.54%
101+
);
102+
--mobile-nav: oklch(1 0 0); /* #FFFFFF */
103+
--overlay: oklch(0 0 0); /* black, both modes */
101104
--logo-full-themed: url("/images/logo-light.svg");
102105
--auth-bg: url("/images/auth-bg-light.webp");
103106
}
@@ -135,9 +138,10 @@
135138
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
136139
--sidebar-border: oklch(1 0 0 / 10%);
137140
--sidebar-ring: oklch(0.551 0.027 264.364);
138-
--mobile-nav: oklch(0.1288 0.0406 264.7); /* #07080b */
139141

140-
/* Centralised logo theming (dark) */
142+
/* NOT SHADCN VARIABLES */
143+
--mobile-nav: oklch(0.1288 0.0406 264.7); /* #07080b */
144+
--overlay: oklch(0 0 0); /* black, both modes */
141145
--logo-full-themed: url("/images/logo-dark.svg");
142146
--auth-bg: url("/images/auth-bg-dark.webp");
143147
}

biome.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",
@@ -31,6 +31,14 @@
3131
"enabled": true,
3232
"rules": {
3333
"recommended": true,
34+
"style": {
35+
"useConsistentTypeDefinitions": {
36+
"level": "error",
37+
"options": {
38+
"style": "type"
39+
}
40+
}
41+
},
3442
"suspicious": {
3543
"noUnknownAtRules": "off"
3644
}

components/clerk-provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function ClerkProvider({
2121
formButtonPrimary:
2222
"bg-[image:var(--gradient-primary)] text-white hover:opacity-90",
2323
footer: "bg-card",
24+
avatarBox: "size-8",
2425
},
2526
...appearance,
2627
}}

components/navigation/full-logo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { cn } from "@/lib/utils";
44
const LOGO_WIDTH = 137;
55
const LOGO_HEIGHT = 23;
66

7-
interface ThemeLogoProps {
7+
type ThemeLogoProps = {
88
className?: string;
9-
}
9+
};
1010

1111
/**
1212
* Theme-aware logo that switches between light/dark variants via CSS variable.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import { SignedIn, UserButton } from "@clerk/nextjs";
4+
import { NavLink } from "@/components/navigation/nav-link";
5+
import { NAV_LINKS } from "@/components/navigation/nav-links.constants";
6+
import { SidebarToggle } from "@/components/navigation/sidebar-toggle";
7+
import { useSidebar } from "@/components/sidebar-provider";
8+
import { cn } from "@/lib/utils";
9+
10+
// Navbar height minus overlap to hide shadow-sm
11+
const SIDEBAR_TOP_OFFSET = 72; // 73px navbar - 1px overlap
12+
13+
export function LeftSidebar() {
14+
const { isCollapsed } = useSidebar();
15+
16+
return (
17+
<aside
18+
style={{
19+
top: SIDEBAR_TOP_OFFSET,
20+
height: `calc(100vh - ${SIDEBAR_TOP_OFFSET}px)`,
21+
}}
22+
className={cn(
23+
"sticky z-50 hidden flex-col justify-between overflow-y-auto border-r border-sidebar-border bg-sidebar p-4 transition-[width] duration-500 ease-in-out sm:flex",
24+
isCollapsed ? "w-16" : "w-56",
25+
)}
26+
>
27+
{/* Top: Navigation Links */}
28+
<nav
29+
className={cn("flex flex-col gap-3", isCollapsed && "items-center")}
30+
aria-label="Main navigation"
31+
>
32+
{NAV_LINKS.map((link) => (
33+
<NavLink
34+
key={link.route}
35+
imgURL={link.imgURL}
36+
route={link.route}
37+
label={link.label}
38+
variant="rail"
39+
/>
40+
))}
41+
</nav>
42+
43+
{/* Bottom: User Avatar + Toggle */}
44+
<div
45+
className={cn(
46+
"flex items-center gap-2",
47+
// Collapsed: vertical stack (UserButton above, toggle below)
48+
// Expanded: horizontal row (UserButton left, toggle right)
49+
isCollapsed ? "flex-col" : "justify-between",
50+
)}
51+
>
52+
<SignedIn>
53+
<UserButton />
54+
</SignedIn>
55+
<SidebarToggle />
56+
</div>
57+
</aside>
58+
);
59+
}

0 commit comments

Comments
 (0)