redesign: sidebar nav — clean 2026 style
- Softer sidebar border (50% opacity) - Cleaner nav items: subtle accent bg on active, no inset shadow - Icons dimmed when inactive, brighten on hover - Section headers: simple text labels, no chevron toggles - Removed collapsible sections (all always visible) - Cleaner brand: single-line name, shorter height - Bottom: removed hardcoded User placeholder, just Admin + Theme - Softer border dividers throughout Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,9 +4,8 @@ import { useState, useEffect, Suspense, createContext, useContext } from "react"
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CircleIcon,
|
||||
LayoutGridIcon,
|
||||
PlusIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
InboxIcon,
|
||||
@@ -55,22 +54,19 @@ function SidebarNavItem({
|
||||
href={href}
|
||||
title={collapsed ? label : undefined}
|
||||
className={cn(
|
||||
"group flex items-center px-2 py-1.5 rounded-md text-[13px] transition-all duration-150 mb-0.5",
|
||||
"group flex items-center px-2.5 py-1.5 rounded-md text-[13px] transition-colors",
|
||||
collapsed ? "justify-center w-full" : "justify-between",
|
||||
active
|
||||
? "bg-sidebar-primary text-sidebar-primary-foreground font-semibold shadow-[inset_3px_0_0_color-mix(in_oklch,var(--sidebar-primary-foreground)_55%,transparent)]"
|
||||
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent font-normal"
|
||||
? "bg-sidebar-accent text-sidebar-foreground font-medium"
|
||||
: "text-sidebar-foreground/55 hover:text-sidebar-foreground hover:bg-sidebar-accent/50 font-normal"
|
||||
)}
|
||||
>
|
||||
<span className={cn("flex items-center", collapsed ? "" : "gap-2.5")}>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
{!collapsed && label}
|
||||
<span className={cn("flex items-center min-w-0", collapsed ? "" : "gap-2.5")}>
|
||||
<Icon className={cn("w-4 h-4 flex-shrink-0", active ? "opacity-90" : "opacity-50 group-hover:opacity-70")} />
|
||||
{!collapsed && <span className="truncate">{label}</span>}
|
||||
</span>
|
||||
{!collapsed && count !== undefined && count > 0 && (
|
||||
<span className={cn(
|
||||
"min-w-5 rounded px-1 text-right text-[11px] tabular-nums",
|
||||
active ? "text-sidebar-primary-foreground/80" : "text-sidebar-foreground/45"
|
||||
)}>
|
||||
<span className="min-w-5 rounded px-1 text-right text-[11px] tabular-nums text-sidebar-foreground/35">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
@@ -93,11 +89,6 @@ function SidebarNav() {
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
|
||||
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
|
||||
const [myTeamId, setMyTeamId] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({
|
||||
dashboards: true,
|
||||
queues: true,
|
||||
views: true,
|
||||
});
|
||||
const [newDashboardName, setNewDashboardName] = useState("");
|
||||
const [addingDashboard, setAddingDashboard] = useState(false);
|
||||
|
||||
@@ -220,21 +211,14 @@ function SidebarNav() {
|
||||
</div>
|
||||
|
||||
{dashboards.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-5">
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((e) => ({ ...e, dashboards: !e.dashboards }))}
|
||||
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={cn("h-3 w-3 transition-transform", expanded.dashboards && "rotate-90")}
|
||||
/>
|
||||
<div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
|
||||
Dashboards
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{expanded.dashboards && dashboards.length > 0 && (
|
||||
<div className="px-2">
|
||||
{dashboards.length > 0 && (
|
||||
<div className="px-2.5">
|
||||
<select
|
||||
value={dashboards.find((d) => pathname.startsWith("/dashboards/") && pathname.endsWith(d.id))?.id ?? ""}
|
||||
onChange={(e) => {
|
||||
@@ -290,20 +274,13 @@ function SidebarNav() {
|
||||
)}
|
||||
|
||||
{savedViews.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-5">
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((e) => ({ ...e, views: !e.views }))}
|
||||
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={cn("h-3 w-3 transition-transform", expanded.views && "rotate-90")}
|
||||
/>
|
||||
<div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
|
||||
Saved views
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{expanded.views && savedViews.map((view) => {
|
||||
{savedViews.map((view) => {
|
||||
const active =
|
||||
pathname === "/" && searchParams.get("view_id") === view.id;
|
||||
return (
|
||||
@@ -320,30 +297,20 @@ function SidebarNav() {
|
||||
)}
|
||||
|
||||
{queues.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-5">
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((e) => ({ ...e, queues: !e.queues }))}
|
||||
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={cn("h-3 w-3 transition-transform", expanded.queues && "rotate-90")}
|
||||
/>
|
||||
<div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
|
||||
Queues
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{expanded.queues && queues.map((queue) => {
|
||||
{queues.map((queue) => {
|
||||
const active =
|
||||
pathname === "/" && searchParams.get("queue") === queue.id;
|
||||
const QueueIcon = () => (
|
||||
<span className="w-2 h-2 rounded-full bg-sidebar-primary flex-shrink-0 shadow-[0_0_0_3px_color-mix(in_oklch,var(--sidebar-primary)_18%,transparent)]" />
|
||||
);
|
||||
return (
|
||||
<SidebarNavItem
|
||||
key={queue.id}
|
||||
href={`/?queue=${queue.id}`}
|
||||
icon={QueueIcon}
|
||||
icon={CircleIcon}
|
||||
label={queue.name}
|
||||
count={queue.count}
|
||||
active={active}
|
||||
@@ -361,32 +328,14 @@ function SidebarBottom() {
|
||||
const collapsed = useSidebarCollapsed();
|
||||
|
||||
return (
|
||||
<div className="border-t border-sidebar-border p-2">
|
||||
<div className="border-t border-sidebar-border/50 p-2">
|
||||
<SidebarNavItem
|
||||
href="/admin"
|
||||
icon={SettingsIcon}
|
||||
label="Admin"
|
||||
active={pathname === "/admin"}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center mt-0.5 px-2 py-1.5",
|
||||
collapsed ? "justify-center" : "gap-2"
|
||||
)}
|
||||
title={collapsed ? "User" : undefined}
|
||||
>
|
||||
<div className="w-5 h-5 rounded-md bg-sidebar-primary flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-sidebar-primary-foreground text-[10px] font-semibold">
|
||||
U
|
||||
</span>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="text-[13px] text-sidebar-foreground/65 truncate">
|
||||
User
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("flex", collapsed ? "justify-center mt-1" : "mt-1")}>
|
||||
<div className={cn("flex", collapsed ? "justify-center mt-2" : "mt-2 px-1")}>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
@@ -421,43 +370,33 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={cn(
|
||||
"flex-shrink-0 flex flex-col bg-sidebar border-r border-sidebar-border transition-all duration-150 shadow-[16px_0_42px_color-mix(in_oklch,var(--sidebar)_18%,transparent)]",
|
||||
sidebarCollapsed ? "w-[60px]" : "w-60"
|
||||
"flex-shrink-0 flex flex-col bg-sidebar border-r border-sidebar-border/50 transition-all duration-200",
|
||||
sidebarCollapsed ? "w-[56px]" : "w-[232px]"
|
||||
)}
|
||||
>
|
||||
{/* Brand */}
|
||||
<div className="h-14 flex items-center justify-between gap-2 px-3 border-b border-sidebar-border">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-md bg-sidebar-primary flex items-center justify-center shadow-[0_0_0_1px_color-mix(in_oklch,var(--sidebar-primary)_55%,white_20%)]">
|
||||
<span className="text-sidebar-primary-foreground text-[12px] font-bold">
|
||||
T
|
||||
</span>
|
||||
<div className="h-12 flex items-center justify-between gap-2 px-3 border-b border-sidebar-border/50">
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<div className="w-6 h-6 rounded-md bg-sidebar-primary flex items-center justify-center">
|
||||
<span className="text-sidebar-primary-foreground text-[11px] font-bold">T</span>
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="leading-tight">
|
||||
<span className="block font-semibold text-sidebar-foreground text-sm">
|
||||
Tessera
|
||||
</span>
|
||||
<span className="block text-[10px] text-sidebar-foreground/45">
|
||||
ScripFoundry
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-sidebar-foreground tracking-tight">Tessera</span>
|
||||
)}
|
||||
</Link>
|
||||
{!sidebarCollapsed && (
|
||||
<button
|
||||
onClick={() => setCommandOpen(true)}
|
||||
className="flex h-7 items-center gap-1 rounded-md border border-sidebar-border px-2 text-[11px] text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
className="flex h-6 items-center gap-1 rounded border border-sidebar-border/50 px-1.5 text-[10px] text-sidebar-foreground/40 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground/60"
|
||||
aria-label="Open command palette"
|
||||
>
|
||||
<CommandIcon className="h-3.5 w-3.5" />
|
||||
K
|
||||
<CommandIcon className="h-3 w-3" />K
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 overflow-y-auto py-3 px-2">
|
||||
<nav className="flex-1 overflow-y-auto py-2.5 px-2">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="space-y-1.5 px-2">
|
||||
|
||||
Reference in New Issue
Block a user