Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions app/(root)/(with-right-sidebar)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const Home = () => (
<h1>Hello Root page with heading H1</h1>

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

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

{/* Footer boundary marker */}
<div className="bg-primary/30 p-4">FOOTER: {"words ".repeat(50)}</div>
<div className="bg-primary/30 p-4">{"words ".repeat(50)}</div>

{/* Question boxes for testing sticky sidebar scroll */}
<div className="mt-8 space-y-6">
{[1, 2, 3, 4].map((num) => (
<article
key={`question-${num}`}
className="rounded-lg border border-border bg-card p-6 shadow-sm"
>
<div className="mb-3 flex items-center gap-2">
<span className="rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary">
Question #{num}
</span>
<span className="text-xs text-muted-foreground">
Asked {num} hours ago
</span>
</div>

<h2 className="mb-2 text-xl font-semibold">
How to implement a sticky sidebar in Next.js with Tailwind CSS?
</h2>

<p className="mb-4 text-muted-foreground">
I&apos;m building a dashboard layout and need the sidebar to remain
visible while scrolling the main content. The sidebar should stick
below the navbar and scroll independently if its content overflows.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
</p>

{/* Tags */}
<div className="mb-4 flex flex-wrap gap-2">
{["next.js", "tailwindcss", "react", "css"].map((tag) => (
<span
key={tag}
className="rounded-md bg-secondary px-2.5 py-1 text-xs font-medium text-secondary-foreground"
>
{tag}
</span>
))}
</div>

{/* Stats */}
<div className="flex items-center gap-6 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 7}</span>{" "}
votes
</span>
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 3}</span>{" "}
answers
</span>
<span className="flex items-center gap-1">
<span className="font-semibold text-foreground">{num * 47}</span>{" "}
views
</span>
</div>
</article>
))}

{/* End marker */}
<div className="rounded-lg bg-muted p-6 text-center text-muted-foreground">
You&apos;ve reached the end — sidebar should still be sticky!
</div>
</div>
</>
);

Expand Down
15 changes: 11 additions & 4 deletions app/(root)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { LeftSidebar } from "@/components/navigation/left-sidebar";
import { Navbar } from "@/components/navigation/navbar";
import { SidebarProvider } from "@/components/sidebar-provider";

const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
</>
<SidebarProvider>
<div className="flex min-h-screen flex-col">
<Navbar />
<div className="flex flex-1">
<LeftSidebar />
<main className="flex-1">{children}</main>
</div>
</div>
</SidebarProvider>
);
};

Expand Down
28 changes: 16 additions & 12 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-mobile-nav: var(--mobile-nav);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
Expand All @@ -51,18 +50,15 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);

/* NOT SHADCN VARIABLES */
--color-mobile-nav: var(--mobile-nav);
--color-overlay: var(--overlay);
}

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

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

/* Centralised logo theming */
/* NOT SHADCN VARIABLES */
--primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */
--gradient-primary: linear-gradient(
93.22deg,
var(--primary) -13.95%,
var(--primary-gradient-to) 99.54%
);
--mobile-nav: oklch(1 0 0); /* #FFFFFF */
--overlay: oklch(0 0 0); /* black, both modes */
--logo-full-themed: url("/images/logo-light.svg");
--auth-bg: url("/images/auth-bg-light.webp");
}
Expand Down Expand Up @@ -135,9 +138,10 @@
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.551 0.027 264.364);
--mobile-nav: oklch(0.1288 0.0406 264.7); /* #07080b */

/* Centralised logo theming (dark) */
/* NOT SHADCN VARIABLES */
--mobile-nav: oklch(0.1288 0.0406 264.7); /* #07080b */
--overlay: oklch(0 0 0); /* black, both modes */
--logo-full-themed: url("/images/logo-dark.svg");
--auth-bg: url("/images/auth-bg-dark.webp");
}
Expand Down
10 changes: 9 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down Expand Up @@ -31,6 +31,14 @@
"enabled": true,
"rules": {
"recommended": true,
"style": {
"useConsistentTypeDefinitions": {
"level": "error",
"options": {
"style": "type"
}
}
},
Comment on lines +34 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

LGTM! Enforcing consistent type definitions.

The addition of the useConsistentTypeDefinitions rule with "style": "type" is appropriate and aligns with the broader PR objective to standardise TypeScript type definitions across the codebase. Setting the level to "error" ensures consistent enforcement.

Note: This rule enforces type aliases over interface declarations. Whilst both are valid TypeScript constructs, type offers more flexibility for unions, intersections, and primitives. This is a sound stylistic choice for consistency.

🤖 Prompt for AI Agents
In biome.json around lines 34 to 41, the new useConsistentTypeDefinitions rule
is already correct but ensure consistency: keep "style": "type" and "level":
"error" as added, remove or update any other Biome/TS lint rules that prefer
interfaces elsewhere (search for conflicting rules like
useConsistentTypeDefinitions or interface-preference in other config files) so
there are no contradictory settings, and run the linter/CI to verify no
violations remain.

"suspicious": {
"noUnknownAtRules": "off"
}
Expand Down
1 change: 1 addition & 0 deletions components/clerk-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function ClerkProvider({
formButtonPrimary:
"bg-[image:var(--gradient-primary)] text-white hover:opacity-90",
footer: "bg-card",
avatarBox: "size-8",
},
...appearance,
}}
Expand Down
4 changes: 2 additions & 2 deletions components/navigation/full-logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { cn } from "@/lib/utils";
const LOGO_WIDTH = 137;
const LOGO_HEIGHT = 23;

interface ThemeLogoProps {
type ThemeLogoProps = {
className?: string;
}
};

/**
* Theme-aware logo that switches between light/dark variants via CSS variable.
Expand Down
59 changes: 59 additions & 0 deletions components/navigation/left-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { SignedIn, UserButton } from "@clerk/nextjs";
import { NavLink } from "@/components/navigation/nav-link";
import { NAV_LINKS } from "@/components/navigation/nav-links.constants";
import { SidebarToggle } from "@/components/navigation/sidebar-toggle";
import { useSidebar } from "@/components/sidebar-provider";
import { cn } from "@/lib/utils";

// Navbar height minus overlap to hide shadow-sm
const SIDEBAR_TOP_OFFSET = 72; // 73px navbar - 1px overlap

export function LeftSidebar() {
const { isCollapsed } = useSidebar();

return (
<aside
style={{
top: SIDEBAR_TOP_OFFSET,
height: `calc(100vh - ${SIDEBAR_TOP_OFFSET}px)`,
}}
className={cn(
"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",
isCollapsed ? "w-16" : "w-56",
)}
>
{/* Top: Navigation Links */}
<nav
className={cn("flex flex-col gap-3", isCollapsed && "items-center")}
aria-label="Main navigation"
>
{NAV_LINKS.map((link) => (
<NavLink
key={link.route}
imgURL={link.imgURL}
route={link.route}
label={link.label}
variant="rail"
/>
))}
</nav>

{/* Bottom: User Avatar + Toggle */}
<div
className={cn(
"flex items-center gap-2",
// Collapsed: vertical stack (UserButton above, toggle below)
// Expanded: horizontal row (UserButton left, toggle right)
isCollapsed ? "flex-col" : "justify-between",
)}
>
<SignedIn>
<UserButton />
</SignedIn>
<SidebarToggle />
</div>
</aside>
);
}
Loading
Loading