fix: repair devops shell interactions
This commit is contained in:
parent
315e9317cc
commit
94d55a3d4a
@ -6,3 +6,9 @@ html,
|
|||||||
body {
|
body {
|
||||||
font-family: var(--ml-font-body), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: var(--ml-font-body), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
main {
|
||||||
|
padding-top: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export default function RootLayout({
|
|||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
"(function(){try{var t=localStorage.getItem('bytelyst.theme.v1');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t);}}catch(e){}})();",
|
"(function(){try{var t=localStorage.getItem('bytelyst.theme.v1')||localStorage.getItem('theme');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t);document.documentElement.classList.toggle('dark',t==='dark');}}catch(e){}})();",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import Link from 'next/link';
|
|||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Activity,
|
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Cpu,
|
Cpu,
|
||||||
Key,
|
Key,
|
||||||
@ -22,6 +21,10 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '@/lib/auth';
|
import { useAuth } from '@/lib/auth';
|
||||||
|
|
||||||
|
const THEME_STORAGE_KEY = 'bytelyst.theme.v1';
|
||||||
|
const SIDEBAR_STORAGE_KEY = 'bytelyst.devops.sidebar.collapsed.v1';
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ href: '/hermes', label: 'Hermes', icon: Sparkles },
|
{ href: '/hermes', label: 'Hermes', icon: Sparkles },
|
||||||
@ -39,15 +42,56 @@ export function SidebarNav() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [theme, setTheme] = useState<Theme>('dark');
|
||||||
|
|
||||||
// Sync theme from localStorage on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = (localStorage.getItem('theme') as 'light' | 'dark') || 'light';
|
try {
|
||||||
setTheme(saved);
|
const savedTheme = window.localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
|
||||||
document.documentElement.classList.toggle('dark', saved === 'dark');
|
const legacyTheme = window.localStorage.getItem('theme') as Theme | null;
|
||||||
|
const nextTheme = savedTheme === 'light' || savedTheme === 'dark'
|
||||||
|
? savedTheme
|
||||||
|
: legacyTheme === 'light' || legacyTheme === 'dark'
|
||||||
|
? legacyTheme
|
||||||
|
: 'dark';
|
||||||
|
setTheme(nextTheme);
|
||||||
|
applyTheme(nextTheme);
|
||||||
|
|
||||||
|
setCollapsed(window.localStorage.getItem(SIDEBAR_STORAGE_KEY) === 'true');
|
||||||
|
} catch {
|
||||||
|
applyTheme('dark');
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function applyTheme(next: Theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
document.documentElement.classList.toggle('dark', next === 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const next = theme === 'dark' ? 'light' : 'dark';
|
||||||
|
setTheme(next);
|
||||||
|
applyTheme(next);
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(THEME_STORAGE_KEY, next);
|
||||||
|
window.localStorage.setItem('theme', next);
|
||||||
|
} catch {
|
||||||
|
// localStorage can be unavailable in private browsing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
setCollapsed((current) => {
|
||||||
|
const next = !current;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(next));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
@ -57,23 +101,34 @@ export function SidebarNav() {
|
|||||||
? user.email.slice(0, 2).toUpperCase()
|
? user.email.slice(0, 2).toUpperCase()
|
||||||
: '??';
|
: '??';
|
||||||
|
|
||||||
const sidebarContent = (
|
const sidebarContent = (isCollapsed = false, showCollapseControl = false) => (
|
||||||
<>
|
<>
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex h-16 items-center justify-between border-b px-6">
|
<div className="flex h-16 items-center justify-between border-b border-[var(--bl-border)] px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--bl-accent)]">
|
||||||
<LayoutDashboard className="h-4 w-4 text-white" />
|
<LayoutDashboard className="h-4 w-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className={isCollapsed ? 'sr-only' : 'min-w-0'}>
|
||||||
<h1 className="text-sm font-bold">DevOps</h1>
|
<h1 className="truncate text-sm font-bold text-[var(--bl-text-primary)]">DevOps</h1>
|
||||||
<p className="text-[10px] text-gray-500">Dashboard</p>
|
<p className="truncate text-[10px] text-[var(--bl-text-secondary)]">Dashboard</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showCollapseControl && (
|
||||||
|
<button
|
||||||
|
className="hidden rounded-md p-1.5 text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)] md:inline-flex"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
|
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
|
>
|
||||||
|
<Menu className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Mobile close button */}
|
{/* Mobile close button */}
|
||||||
<button
|
<button
|
||||||
className="md:hidden text-gray-500 hover:text-gray-700"
|
className="rounded-md p-1.5 text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)] md:hidden"
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
|
aria-label="Close navigation"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
@ -89,46 +144,44 @@ export function SidebarNav() {
|
|||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
|
title={isCollapsed ? item.label : undefined}
|
||||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-[var(--bl-accent)] text-[var(--bl-accent-foreground)]'
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
: 'text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="h-4 w-4" />
|
<item.icon className="h-4 w-4 shrink-0" />
|
||||||
<span>{item.label}</span>
|
<span className={isCollapsed ? 'sr-only' : 'truncate'}>{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Footer — theme toggle + user info + logout */}
|
{/* Footer — theme toggle + user info + logout */}
|
||||||
<div className="border-t p-4 space-y-3">
|
<div className="space-y-3 border-t border-[var(--bl-border)] p-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={toggleTheme}
|
||||||
const next = theme === 'dark' ? 'light' : 'dark';
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-[var(--bl-text-secondary)] transition-colors hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)]"
|
||||||
setTheme(next);
|
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
||||||
localStorage.setItem('theme', next);
|
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
|
||||||
document.documentElement.classList.toggle('dark', next === 'dark');
|
|
||||||
}}
|
|
||||||
className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors"
|
|
||||||
>
|
>
|
||||||
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{theme === 'dark' ? <Sun className="h-4 w-4 shrink-0" /> : <Moon className="h-4 w-4 shrink-0" />}
|
||||||
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
|
<span className={isCollapsed ? 'sr-only' : 'truncate'}>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-xs font-bold text-gray-700">
|
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--bl-surface-muted)] text-xs font-bold text-[var(--bl-text-secondary)]">
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className={isCollapsed ? 'sr-only' : 'min-w-0 flex-1'}>
|
||||||
<p className="text-sm font-medium truncate text-gray-900">{user?.email ?? 'Admin'}</p>
|
<p className="truncate text-sm font-medium text-[var(--bl-text-primary)]">{user?.email ?? 'Admin'}</p>
|
||||||
<p className="text-xs text-gray-500 truncate">{user?.role ?? 'admin'}</p>
|
<p className="truncate text-xs text-[var(--bl-text-secondary)]">{user?.role ?? 'admin'}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
title="Sign out"
|
title="Sign out"
|
||||||
aria-label="Sign out"
|
aria-label="Sign out"
|
||||||
className="text-gray-500 hover:text-gray-700"
|
className="rounded-md p-1.5 text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)]"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -140,14 +193,12 @@ export function SidebarNav() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile hamburger */}
|
{/* Mobile hamburger */}
|
||||||
<div className="fixed top-0 left-0 right-0 z-30 flex h-14 items-center border-b bg-white px-4 md:hidden">
|
<div className="fixed left-0 right-0 top-0 z-30 flex h-14 items-center border-b border-[var(--bl-border)] bg-[var(--bl-surface-card)] px-4 text-[var(--bl-text-primary)] md:hidden">
|
||||||
<button onClick={() => setMobileOpen(true)}>
|
<button onClick={() => setMobileOpen(true)} aria-label="Open navigation">
|
||||||
<Menu className="h-6 w-6" />
|
<Menu className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
<span className="ml-3 text-sm font-bold">DevOps Dashboard</span>
|
<span className="ml-3 text-sm font-bold">DevOps Dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Spacer for mobile top bar */}
|
|
||||||
<div className="h-14 md:hidden" />
|
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
@ -157,11 +208,16 @@ export function SidebarNav() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Sidebar — static on desktop, fixed on mobile */}
|
{/* Sidebar — fixed on mobile, in-flow and collapsible on desktop */}
|
||||||
<aside
|
<aside
|
||||||
className={`hidden md:flex md:w-64 md:shrink-0 md:flex-col md:border-r md:bg-white fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r bg-white transition-transform duration-200 translate-x-[-100%] md:translate-x-0 ${mobileOpen ? 'translate-x-0' : ''}`}
|
className={`fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-[var(--bl-border)] bg-[var(--bl-surface-card)] text-[var(--bl-text-primary)] shadow-[var(--bl-shadow-md)] transition-transform duration-200 md:hidden ${mobileOpen ? 'translate-x-0' : '-translate-x-full'}`}
|
||||||
>
|
>
|
||||||
{sidebarContent}
|
{sidebarContent(false, false)}
|
||||||
|
</aside>
|
||||||
|
<aside
|
||||||
|
className={`hidden border-r border-[var(--bl-border)] bg-[var(--bl-surface-card)] text-[var(--bl-text-primary)] md:sticky md:top-0 md:flex md:h-screen md:shrink-0 md:flex-col ${collapsed ? 'md:w-20' : 'md:w-64'}`}
|
||||||
|
>
|
||||||
|
{sidebarContent(collapsed, true)}
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user