feat(ui): implement v2 three-panel layout

- Collapsible sidebar (68px icons / 230px expanded, toggle via logo)
- LibraryPanel: sessions expand inline to show tracks, search + filter chips
- PlayerPanel: extracted from SongPage, used as embeddable panel
- BandPage: Library + Player side by side; song selection via ?song= URL param
- SongPage: thin wrapper around PlayerPanel (kept for direct deep-links)
- CSS palette updated to v2 violet/cyan/emerald scheme
- Mobile (<900px): BandPage shows library or player, never both

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mistral Vibe
2026-04-09 23:40:37 +02:00
parent 48a73246a1
commit d73377ec2f
6 changed files with 1168 additions and 1712 deletions

View File

@@ -8,60 +8,70 @@ import { getInitials } from "../utils";
import type { MemberRead } from "../api/auth";
import { usePlayerStore } from "../stores/playerStore";
// ── Icons (inline SVG) ──────────────────────────────────────────────────────
function IconWaveform() {
// ── Icons ────────────────────────────────────────────────────────────────────
function IconMenu() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<rect x="1" y="1.5" width="12" height="2" rx="1" fill="white" opacity=".9" />
<rect x="1" y="5.5" width="9" height="2" rx="1" fill="white" opacity=".7" />
<rect x="1" y="9.5" width="11" height="2" rx="1" fill="white" opacity=".8" />
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M3 5h12M3 9h12M3 13h8" stroke="white" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
function IconLibrary() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
<path d="M2 3.5h10v1.5H2zm0 3h10v1.5H2zm0 3h7v1.5H2z" />
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<rect x="2" y="3.5" width="14" height="2" rx="1" fill="currentColor" />
<rect x="2" y="8" width="14" height="2" rx="1" fill="currentColor" />
<rect x="2" y="12.5" width="14" height="2" rx="1" fill="currentColor" />
</svg>
);
}
function IconPlay() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
<path d="M3 2l9 5-9 5V2z" />
</svg>
);
}
function IconSettings() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3">
<circle cx="7" cy="7" r="2" />
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13" />
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
<path d="M5 3l11 6-11 6V3z" />
</svg>
);
}
function IconMembers() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
<circle cx="5" cy="4.5" r="2" />
<path d="M1 12c0-2.2 1.8-3.5 4-3.5s4 1.3 4 3.5H1z" />
<circle cx="10.5" cy="4.5" r="1.5" opacity=".6" />
<path d="M10.5 8.5c1.4 0 2.5 1 2.5 2.5H9.5" opacity=".6" />
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="7" cy="6.5" r="2.5" stroke="currentColor" strokeWidth="1.4" />
<path d="M1.5 14.5c0-2.761 2.462-4.5 5.5-4.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
<circle cx="13" cy="11" r="2" stroke="currentColor" strokeWidth="1.4" />
<path d="M16 16c0-1.657-1.343-2.5-3-2.5S10 14.343 10 16" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
);
}
function IconSettings() {
return (
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="2.5" stroke="currentColor" strokeWidth="1.4" />
<path d="M9 2v1.5M9 14.5V16M2 9h1.5M14.5 9H16M3.7 3.7l1.06 1.06M13.24 13.24l1.06 1.06M14.3 3.7l-1.06 1.06M4.76 13.24l-1.06 1.06" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
);
}
function IconStorage() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
<rect x="1" y="3" width="12" height="3" rx="1.5" />
<rect x="1" y="8" width="12" height="3" rx="1.5" />
<circle cx="11" cy="4.5" r=".75" fill="#0b0b0e" />
<circle cx="11" cy="9.5" r=".75" fill="#0b0b0e" />
<svg width="18" height="18" viewBox="0 0 18 18" fill="currentColor">
<rect x="1" y="4" width="16" height="3.5" rx="1.5" />
<rect x="1" y="10.5" width="16" height="3.5" rx="1.5" />
<circle cx="14" cy="5.75" r="0.9" fill="#0c0e1a" />
<circle cx="14" cy="12.25" r="0.9" fill="#0c0e1a" />
</svg>
);
}
function IconSignOut() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M5.5 2.5H3A1.5 1.5 0 0 0 1.5 4v7A1.5 1.5 0 0 0 3 12.5H5.5" />
<path d="M10 10.5l3-3-3-3M13 7.5H6" />
</svg>
);
}
@@ -74,39 +84,33 @@ function IconChevron() {
);
}
function IconSignOut() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 2H2.5A1.5 1.5 0 0 0 1 3.5v7A1.5 1.5 0 0 0 2.5 12H5" />
<path d="M9 10l3-3-3-3M12 7H5" />
</svg>
);
}
// ── NavItem ──────────────────────────────────────────────────────────────────
// ── NavItem ─────────────────────────────────────────────────────────────────
interface NavItemProps {
icon: React.ReactNode;
label: string;
active: boolean;
onClick: () => void;
disabled?: boolean;
badge?: number;
collapsed: boolean;
}
function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) {
function NavItem({ icon, label, active, onClick, disabled, badge, collapsed }: NavItemProps) {
const [hovered, setHovered] = useState(false);
const color = active
? "#e8a22a"
const fg = active
? "#a78bfa"
: disabled
? "rgba(255,255,255,0.18)"
? "rgba(255,255,255,0.16)"
: hovered
? "rgba(255,255,255,0.7)"
: "rgba(255,255,255,0.35)";
? "rgba(232,233,240,0.7)"
: "rgba(232,233,240,0.35)";
const bg = active
? "rgba(232,162,42,0.12)"
? "rgba(139,92,246,0.12)"
: hovered && !disabled
? "rgba(255,255,255,0.045)"
? "rgba(255,255,255,0.04)"
: "transparent";
return (
@@ -115,34 +119,61 @@ function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) {
disabled={disabled}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
title={collapsed ? label : undefined}
style={{
display: "flex",
alignItems: "center",
gap: 9,
gap: 10,
width: "100%",
padding: "7px 10px",
borderRadius: 7,
padding: "9px 10px",
borderRadius: 8,
border: "none",
cursor: disabled ? "default" : "pointer",
color,
color: fg,
background: bg,
fontSize: 12,
textAlign: "left",
marginBottom: 1,
transition: "background 0.12s, color 0.12s",
transition: "background 0.15s, color 0.15s",
fontFamily: "inherit",
position: "relative",
overflow: "hidden",
flexShrink: 0,
}}
>
{icon}
<span>{label}</span>
{/* Active indicator */}
{active && (
<div style={{
position: "absolute", left: 0, top: "20%", bottom: "20%",
width: 2, borderRadius: "0 2px 2px 0",
background: "linear-gradient(to bottom, #7b5cf6, #22d3ee)",
}} />
)}
<span style={{ width: 20, height: 20, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
{icon}
</span>
{!collapsed && (
<span style={{ fontSize: 13, fontWeight: 600, flex: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{label}
</span>
)}
{!collapsed && badge != null && badge > 0 && (
<span style={{
fontSize: 9, fontWeight: 700, padding: "2px 6px", borderRadius: 20,
background: "linear-gradient(135deg, #7b5cf6, #22d3ee)", color: "white",
flexShrink: 0,
}}>
{badge}
</span>
)}
</button>
);
}
// ── Sidebar ────────────────────────────────────────────────────────────────
// ── Sidebar ───────────────────────────────────────────────────────────────────
export function Sidebar({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const location = useLocation();
const [collapsed, setCollapsed] = useState(true);
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -152,14 +183,12 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
queryFn: () => api.get<MemberRead>("/auth/me"),
});
// Derive active band from the current URL
const bandMatch =
matchPath("/bands/:bandId/*", location.pathname) ??
matchPath("/bands/:bandId", location.pathname);
const activeBandId = bandMatch?.params?.bandId ?? null;
const activeBand = bands?.find((b) => b.id === activeBandId) ?? null;
// Nav active states
const isLibrary = !!(
matchPath({ path: "/bands/:bandId", end: true }, location.pathname) ||
matchPath("/bands/:bandId/sessions/:sessionId", location.pathname) ||
@@ -169,12 +198,10 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
const isSettings = location.pathname.startsWith("/settings");
const isBandSettings = !!matchPath("/bands/:bandId/settings/*", location.pathname);
const bandSettingsPanel = matchPath("/bands/:bandId/settings/:panel", location.pathname)?.params?.panel ?? null;
// Player state
const { currentSongId, currentBandId: playerBandId, isPlaying: isPlayerPlaying } = usePlayerStore();
const hasActiveSong = !!currentSongId && !!playerBandId;
// Close dropdown on outside click
useEffect(() => {
if (!dropdownOpen) return;
function handleClick(e: MouseEvent) {
@@ -186,231 +213,126 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
return () => document.removeEventListener("mousedown", handleClick);
}, [dropdownOpen]);
const sidebarWidth = collapsed ? 68 : 230;
const border = "rgba(255,255,255,0.06)";
return (
<div
style={{
<div style={{ display: "flex", height: "100vh", overflow: "hidden", background: "#0c0e1a", color: "#e8e9f0", fontFamily: "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif", fontSize: 13 }}>
{/* ── Sidebar ── */}
<aside style={{
width: sidebarWidth,
minWidth: sidebarWidth,
background: "#10131f",
borderRight: `1px solid ${border}`,
display: "flex",
height: "100vh",
flexDirection: "column",
overflow: "hidden",
background: "#0f0f12",
color: "#eeeef2",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif",
fontSize: 13,
}}
>
{/* ── Sidebar ──────────────────────────────────────────────────── */}
<aside
style={{
width: 210,
minWidth: 210,
background: "#0b0b0e",
borderRight: `1px solid ${border}`,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
{/* Logo */}
<div
style={{
padding: "17px 14px 14px",
display: "flex",
alignItems: "center",
gap: 10,
borderBottom: `1px solid ${border}`,
flexShrink: 0,
}}
>
<div
transition: "width 0.22s cubic-bezier(0.4,0,0.2,1), min-width 0.22s cubic-bezier(0.4,0,0.2,1)",
flexShrink: 0,
zIndex: 20,
}}>
{/* Logo / toggle */}
<div style={{ padding: "18px 14px 14px", display: "flex", alignItems: "center", gap: 10, borderBottom: `1px solid ${border}`, flexShrink: 0 }}>
<button
onClick={() => setCollapsed((c) => !c)}
title={collapsed ? "Expand menu" : "Collapse menu"}
style={{
width: 28,
height: 28,
background: "#e8a22a",
borderRadius: 7,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
width: 40, height: 40, borderRadius: 12, flexShrink: 0,
background: "linear-gradient(135deg, #7b5cf6, #06b6d4)",
display: "flex", alignItems: "center", justifyContent: "center",
border: "none", cursor: "pointer",
boxShadow: "0 0 20px rgba(139,92,246,0.3)",
transition: "box-shadow 0.2s",
}}
onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "0 0 30px rgba(139,92,246,0.5)")}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "0 0 20px rgba(139,92,246,0.3)")}
>
<IconWaveform />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: "#eeeef2", letterSpacing: -0.2 }}>
RehearsalHub
</div>
{activeBand && (
<div style={{ fontSize: 10, color: "rgba(255,255,255,0.25)", marginTop: 1 }}>
{activeBand.name}
<IconMenu />
</button>
{!collapsed && (
<div style={{ overflow: "hidden" }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#e8e9f0", letterSpacing: -0.3, whiteSpace: "nowrap" }}>
RehearsalHub
</div>
)}
</div>
</div>
)}
</div>
{/* Band switcher */}
<div
ref={dropdownRef}
style={{
padding: "10px 8px",
borderBottom: `1px solid ${border}`,
position: "relative",
flexShrink: 0,
}}
>
<div ref={dropdownRef} style={{ padding: "10px 12px", borderBottom: `1px solid ${border}`, position: "relative", flexShrink: 0 }}>
<button
onClick={() => setDropdownOpen((o) => !o)}
title={collapsed ? (activeBand?.name ?? "Select band") : undefined}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 8,
padding: "7px 9px",
background: "rgba(255,255,255,0.05)",
width: "100%", display: "flex", alignItems: "center", gap: 8,
padding: "7px 8px",
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.07)",
borderRadius: 8,
cursor: "pointer",
color: "#eeeef2",
textAlign: "left",
fontFamily: "inherit",
borderRadius: 8, cursor: "pointer", color: "#e8e9f0",
textAlign: "left", fontFamily: "inherit",
transition: "border-color 0.15s",
overflow: "hidden",
}}
onMouseEnter={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.12)")}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.07)")}
>
<div
style={{
width: 26,
height: 26,
background: "rgba(232,162,42,0.15)",
border: "1px solid rgba(232,162,42,0.3)",
borderRadius: 7,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 10,
fontWeight: 700,
color: "#e8a22a",
flexShrink: 0,
}}
>
<div style={{
width: 28, height: 28, borderRadius: 8, flexShrink: 0,
background: "rgba(139,92,246,0.15)",
border: "1px solid rgba(139,92,246,0.3)",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 10, fontWeight: 800, color: "#a78bfa",
}}>
{activeBand ? getInitials(activeBand.name) : "?"}
</div>
<span
style={{
flex: 1,
fontSize: 12,
fontWeight: 500,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{activeBand?.name ?? "Select a band"}
</span>
<span style={{ opacity: 0.3, flexShrink: 0, display: "flex" }}>
<IconChevron />
</span>
{!collapsed && (
<>
<span style={{ flex: 1, fontSize: 12, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{activeBand?.name ?? "Select a band"}
</span>
<span style={{ opacity: 0.3, flexShrink: 0, display: "flex" }}>
<IconChevron />
</span>
</>
)}
</button>
{dropdownOpen && (
<div
style={{
position: "absolute",
top: "calc(100% - 2px)",
left: 8,
right: 8,
background: "#18181e",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 10,
padding: 6,
zIndex: 100,
boxShadow: "0 8px 24px rgba(0,0,0,0.5)",
}}
>
<div style={{
position: "absolute", top: "calc(100% - 2px)",
left: 12, right: 12,
background: "#1a1e30",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 10, padding: 6, zIndex: 100,
boxShadow: "0 8px 24px rgba(0,0,0,0.5)",
}}>
{bands?.map((band) => (
<button
key={band.id}
onClick={() => {
navigate(`/bands/${band.id}`);
setDropdownOpen(false);
}}
onClick={() => { navigate(`/bands/${band.id}`); setDropdownOpen(false); }}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 8,
padding: "7px 9px",
marginBottom: 1,
background: band.id === activeBandId ? "rgba(232,162,42,0.08)" : "transparent",
border: "none",
borderRadius: 6,
cursor: "pointer",
color: "#eeeef2",
textAlign: "left",
fontFamily: "inherit",
width: "100%", display: "flex", alignItems: "center", gap: 8,
padding: "7px 9px", marginBottom: 1,
background: band.id === activeBandId ? "rgba(139,92,246,0.1)" : "transparent",
border: "none", borderRadius: 6, cursor: "pointer",
color: "#e8e9f0", textAlign: "left", fontFamily: "inherit",
}}
>
<div
style={{
width: 22,
height: 22,
borderRadius: 5,
background: "rgba(232,162,42,0.15)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 9,
fontWeight: 700,
color: "#e8a22a",
flexShrink: 0,
}}
>
<div style={{ width: 22, height: 22, borderRadius: 6, background: "rgba(139,92,246,0.15)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 9, fontWeight: 700, color: "#a78bfa", flexShrink: 0 }}>
{getInitials(band.name)}
</div>
<span
style={{
flex: 1,
fontSize: 12,
color: "rgba(255,255,255,0.62)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
<span style={{ flex: 1, fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "rgba(232,233,240,0.7)" }}>
{band.name}
</span>
{band.id === activeBandId && (
<span style={{ fontSize: 10, color: "#e8a22a", flexShrink: 0 }}></span>
)}
{band.id === activeBandId && <span style={{ fontSize: 10, color: "#a78bfa", flexShrink: 0 }}></span>}
</button>
))}
<div
style={{
borderTop: "1px solid rgba(255,255,255,0.06)",
marginTop: 4,
paddingTop: 4,
}}
>
<div style={{ borderTop: "1px solid rgba(255,255,255,0.06)", marginTop: 4, paddingTop: 4 }}>
<button
onClick={() => {
navigate("/");
setDropdownOpen(false);
}}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: 8,
padding: "7px 9px",
background: "transparent",
border: "none",
borderRadius: 6,
cursor: "pointer",
color: "rgba(255,255,255,0.35)",
fontSize: 12,
textAlign: "left",
fontFamily: "inherit",
}}
onClick={() => { navigate("/"); setDropdownOpen(false); }}
style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, padding: "7px 9px", background: "transparent", border: "none", borderRadius: 6, cursor: "pointer", color: "rgba(232,233,240,0.35)", fontSize: 12, textAlign: "left", fontFamily: "inherit" }}
>
<span style={{ fontSize: 14, opacity: 0.5 }}>+</span>
Create new band
@@ -421,205 +343,75 @@ export function Sidebar({ children }: { children: React.ReactNode }) {
</div>
{/* Navigation */}
<nav style={{ flex: 1, padding: "10px 8px", overflowY: "auto" }}>
<nav style={{ flex: 1, padding: "10px 12px", overflowY: "auto", overflowX: "hidden", display: "flex", flexDirection: "column", gap: 2 }}>
{activeBand && (
<>
<SectionLabel>{activeBand.name}</SectionLabel>
<NavItem
icon={<IconLibrary />}
label="Library"
active={isLibrary}
onClick={() => navigate(`/bands/${activeBand.id}`)}
/>
<NavItem icon={<IconLibrary />} label="Library" active={isLibrary} onClick={() => navigate(`/bands/${activeBand.id}`)} collapsed={collapsed} />
<NavItem
icon={<IconPlay />}
label="Player"
label="Now Playing"
active={hasActiveSong && (isPlayer || isPlayerPlaying)}
onClick={() => {
if (hasActiveSong) {
navigate(`/bands/${playerBandId}/songs/${currentSongId}`);
}
}}
onClick={() => { if (hasActiveSong) navigate(`/bands/${playerBandId}/songs/${currentSongId}`); }}
disabled={!hasActiveSong}
collapsed={collapsed}
/>
</>
)}
{activeBand && (
<>
<SectionLabel style={{ paddingTop: 14 }}>Band Settings</SectionLabel>
<NavItem
icon={<IconMembers />}
label="Members"
active={isBandSettings && bandSettingsPanel === "members"}
onClick={() => navigate(`/bands/${activeBand.id}/settings/members`)}
/>
<NavItem
icon={<IconStorage />}
label="Storage"
active={isBandSettings && bandSettingsPanel === "storage"}
onClick={() => navigate(`/bands/${activeBand.id}/settings/storage`)}
/>
<NavItem
icon={<IconSettings />}
label="Band Settings"
active={isBandSettings && bandSettingsPanel === "band"}
onClick={() => navigate(`/bands/${activeBand.id}/settings/band`)}
/>
<div style={{ height: 1, background: border, margin: "10px 0", flexShrink: 0 }} />
<NavItem icon={<IconMembers />} label="Members" active={isBandSettings && bandSettingsPanel === "members"} onClick={() => navigate(`/bands/${activeBand.id}/settings/members`)} collapsed={collapsed} />
<NavItem icon={<IconStorage />} label="Storage" active={isBandSettings && bandSettingsPanel === "storage"} onClick={() => navigate(`/bands/${activeBand.id}/settings/storage`)} collapsed={collapsed} />
<NavItem icon={<IconSettings />} label="Band Settings" active={isBandSettings && bandSettingsPanel === "band"} onClick={() => navigate(`/bands/${activeBand.id}/settings/band`)} collapsed={collapsed} />
</>
)}
<SectionLabel style={{ paddingTop: 14 }}>Account</SectionLabel>
<NavItem
icon={<IconSettings />}
label="Settings"
active={isSettings}
onClick={() => navigate("/settings")}
/>
<div style={{ height: 1, background: border, margin: "10px 0", flexShrink: 0 }} />
<NavItem icon={<IconSettings />} label="Settings" active={isSettings} onClick={() => navigate("/settings")} collapsed={collapsed} />
</nav>
{/* User row */}
<div
style={{
padding: "10px",
borderTop: `1px solid ${border}`,
flexShrink: 0,
}}
>
<div style={{ padding: "10px 12px", borderTop: `1px solid ${border}`, flexShrink: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<button
onClick={() => navigate("/settings")}
style={{
flex: 1,
display: "flex",
alignItems: "center",
gap: 8,
padding: "6px 8px",
background: "transparent",
border: "none",
borderRadius: 8,
cursor: "pointer",
color: "#eeeef2",
textAlign: "left",
minWidth: 0,
fontFamily: "inherit",
}}
title={collapsed ? (me?.display_name ?? "Account") : undefined}
style={{ flex: 1, display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", background: "transparent", border: "none", borderRadius: 8, cursor: "pointer", color: "#e8e9f0", textAlign: "left", minWidth: 0, fontFamily: "inherit", overflow: "hidden" }}
>
{me?.avatar_url ? (
<img
src={me.avatar_url}
alt=""
style={{
width: 28,
height: 28,
borderRadius: "50%",
objectFit: "cover",
flexShrink: 0,
}}
/>
<img src={me.avatar_url} alt="" style={{ width: 28, height: 28, borderRadius: "50%", objectFit: "cover", flexShrink: 0 }} />
) : (
<div
style={{
width: 28,
height: 28,
borderRadius: "50%",
background: "rgba(232,162,42,0.18)",
border: "1.5px solid rgba(232,162,42,0.35)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 10,
fontWeight: 700,
color: "#e8a22a",
flexShrink: 0,
}}
>
<div style={{ width: 28, height: 28, borderRadius: "50%", background: "rgba(52,211,153,0.15)", border: "1.5px solid rgba(52,211,153,0.3)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "#34d399", flexShrink: 0 }}>
{getInitials(me?.display_name ?? "?")}
</div>
)}
<span
style={{
flex: 1,
fontSize: 12,
color: "rgba(255,255,255,0.55)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{me?.display_name ?? "…"}
</span>
{!collapsed && (
<span style={{ flex: 1, fontSize: 12, color: "rgba(232,233,240,0.55)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{me?.display_name ?? "…"}
</span>
)}
</button>
<button
onClick={() => logout()}
title="Sign out"
style={{
flexShrink: 0,
width: 30,
height: 30,
borderRadius: 7,
background: "transparent",
border: "1px solid transparent",
cursor: "pointer",
color: "rgba(255,255,255,0.2)",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "border-color 0.12s, color 0.12s",
padding: 0,
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)";
e.currentTarget.style.color = "rgba(255,255,255,0.5)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "transparent";
e.currentTarget.style.color = "rgba(255,255,255,0.2)";
}}
>
<IconSignOut />
</button>
{!collapsed && (
<button
onClick={() => logout()}
title="Sign out"
style={{ flexShrink: 0, width: 30, height: 30, borderRadius: 7, background: "transparent", border: "1px solid transparent", cursor: "pointer", color: "rgba(255,255,255,0.2)", display: "flex", alignItems: "center", justifyContent: "center", transition: "border-color 0.12s, color 0.12s", padding: 0 }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "rgba(255,255,255,0.1)"; e.currentTarget.style.color = "rgba(255,255,255,0.5)"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "transparent"; e.currentTarget.style.color = "rgba(255,255,255,0.2)"; }}
>
<IconSignOut />
</button>
)}
</div>
</div>
</aside>
{/* ── Main content ──────────────────────────────────────────────── */}
<main
style={{
flex: 1,
overflow: "auto",
display: "flex",
flexDirection: "column",
background: "#0f0f12",
}}
>
{/* ── Main content ── */}
<main style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column", background: "#0c0e1a", minWidth: 0 }}>
{children}
</main>
</div>
);
}
function SectionLabel({
children,
style,
}: {
children: React.ReactNode;
style?: React.CSSProperties;
}) {
return (
<div
style={{
fontSize: 10,
fontWeight: 500,
color: "rgba(255,255,255,0.2)",
textTransform: "uppercase",
letterSpacing: "0.7px",
padding: "0 6px 5px",
...style,
}}
>
{children}
</div>
);
}