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:
Gjermund Høsøien Wiggen
2026-06-09 23:01:42 +02:00
parent dfcdbc623a
commit 1c780be710

View File

@@ -4,9 +4,8 @@ import { useState, useEffect, Suspense, createContext, useContext } from "react"
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname, useSearchParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { import {
ChevronRightIcon, CircleIcon,
LayoutGridIcon, LayoutGridIcon,
PlusIcon,
UserIcon, UserIcon,
UsersIcon, UsersIcon,
InboxIcon, InboxIcon,
@@ -55,22 +54,19 @@ function SidebarNavItem({
href={href} href={href}
title={collapsed ? label : undefined} title={collapsed ? label : undefined}
className={cn( 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", collapsed ? "justify-center w-full" : "justify-between",
active 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)]" ? "bg-sidebar-accent text-sidebar-foreground font-medium"
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent font-normal" : "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")}> <span className={cn("flex items-center min-w-0", collapsed ? "" : "gap-2.5")}>
<Icon className="w-4 h-4 flex-shrink-0" /> <Icon className={cn("w-4 h-4 flex-shrink-0", active ? "opacity-90" : "opacity-50 group-hover:opacity-70")} />
{!collapsed && label} {!collapsed && <span className="truncate">{label}</span>}
</span> </span>
{!collapsed && count !== undefined && count > 0 && ( {!collapsed && count !== undefined && count > 0 && (
<span className={cn( <span className="min-w-5 rounded px-1 text-right text-[11px] tabular-nums text-sidebar-foreground/35">
"min-w-5 rounded px-1 text-right text-[11px] tabular-nums",
active ? "text-sidebar-primary-foreground/80" : "text-sidebar-foreground/45"
)}>
{count} {count}
</span> </span>
)} )}
@@ -93,11 +89,6 @@ function SidebarNav() {
const [dashboards, setDashboards] = useState<Dashboard[]>([]); const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [currentUserId, setCurrentUserId] = useState<string | null>(null); const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [myTeamId, setMyTeamId] = 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 [newDashboardName, setNewDashboardName] = useState("");
const [addingDashboard, setAddingDashboard] = useState(false); const [addingDashboard, setAddingDashboard] = useState(false);
@@ -220,21 +211,14 @@ function SidebarNav() {
</div> </div>
{dashboards.length > 0 && ( {dashboards.length > 0 && (
<div className="mt-4"> <div className="mt-5">
{!collapsed && ( {!collapsed && (
<button <div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
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")}
/>
Dashboards Dashboards
</button> </div>
)} )}
{expanded.dashboards && dashboards.length > 0 && ( {dashboards.length > 0 && (
<div className="px-2"> <div className="px-2.5">
<select <select
value={dashboards.find((d) => pathname.startsWith("/dashboards/") && pathname.endsWith(d.id))?.id ?? ""} value={dashboards.find((d) => pathname.startsWith("/dashboards/") && pathname.endsWith(d.id))?.id ?? ""}
onChange={(e) => { onChange={(e) => {
@@ -290,20 +274,13 @@ function SidebarNav() {
)} )}
{savedViews.length > 0 && ( {savedViews.length > 0 && (
<div className="mt-4"> <div className="mt-5">
{!collapsed && ( {!collapsed && (
<button <div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
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")}
/>
Saved views Saved views
</button> </div>
)} )}
{expanded.views && savedViews.map((view) => { {savedViews.map((view) => {
const active = const active =
pathname === "/" && searchParams.get("view_id") === view.id; pathname === "/" && searchParams.get("view_id") === view.id;
return ( return (
@@ -320,30 +297,20 @@ function SidebarNav() {
)} )}
{queues.length > 0 && ( {queues.length > 0 && (
<div className="mt-4"> <div className="mt-5">
{!collapsed && ( {!collapsed && (
<button <div className="px-2.5 mb-1 text-[10px] font-medium text-sidebar-foreground/30 uppercase tracking-wider">
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")}
/>
Queues Queues
</button> </div>
)} )}
{expanded.queues && queues.map((queue) => { {queues.map((queue) => {
const active = const active =
pathname === "/" && searchParams.get("queue") === queue.id; 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 ( return (
<SidebarNavItem <SidebarNavItem
key={queue.id} key={queue.id}
href={`/?queue=${queue.id}`} href={`/?queue=${queue.id}`}
icon={QueueIcon} icon={CircleIcon}
label={queue.name} label={queue.name}
count={queue.count} count={queue.count}
active={active} active={active}
@@ -361,32 +328,14 @@ function SidebarBottom() {
const collapsed = useSidebarCollapsed(); const collapsed = useSidebarCollapsed();
return ( return (
<div className="border-t border-sidebar-border p-2"> <div className="border-t border-sidebar-border/50 p-2">
<SidebarNavItem <SidebarNavItem
href="/admin" href="/admin"
icon={SettingsIcon} icon={SettingsIcon}
label="Admin" label="Admin"
active={pathname === "/admin"} active={pathname === "/admin"}
/> />
<div <div className={cn("flex", collapsed ? "justify-center mt-2" : "mt-2 px-1")}>
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")}>
<ThemeToggle /> <ThemeToggle />
</div> </div>
</div> </div>
@@ -421,43 +370,33 @@ export function AppShell({ children }: { children: React.ReactNode }) {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={cn( 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)]", "flex-shrink-0 flex flex-col bg-sidebar border-r border-sidebar-border/50 transition-all duration-200",
sidebarCollapsed ? "w-[60px]" : "w-60" sidebarCollapsed ? "w-[56px]" : "w-[232px]"
)} )}
> >
{/* Brand */} {/* Brand */}
<div className="h-14 flex items-center justify-between gap-2 px-3 border-b border-sidebar-border"> <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"> <Link href="/" className="flex items-center gap-2.5">
<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%)]"> <div className="w-6 h-6 rounded-md bg-sidebar-primary flex items-center justify-center">
<span className="text-sidebar-primary-foreground text-[12px] font-bold"> <span className="text-sidebar-primary-foreground text-[11px] font-bold">T</span>
T
</span>
</div> </div>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<span className="leading-tight"> <span className="text-sm font-semibold text-sidebar-foreground tracking-tight">Tessera</span>
<span className="block font-semibold text-sidebar-foreground text-sm">
Tessera
</span>
<span className="block text-[10px] text-sidebar-foreground/45">
ScripFoundry
</span>
</span>
)} )}
</Link> </Link>
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<button <button
onClick={() => setCommandOpen(true)} 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" aria-label="Open command palette"
> >
<CommandIcon className="h-3.5 w-3.5" /> <CommandIcon className="h-3 w-3" />K
K
</button> </button>
)} )}
</div> </div>
{/* Nav */} {/* 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 <Suspense
fallback={ fallback={
<div className="space-y-1.5 px-2"> <div className="space-y-1.5 px-2">