fix: repair devops shell interactions

This commit is contained in:
Hermes VM 2026-05-31 11:52:03 +00:00
parent 315e9317cc
commit 94d55a3d4a
3 changed files with 103 additions and 41 deletions

View File

@ -6,3 +6,9 @@ html,
body {
font-family: var(--ml-font-body), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
@media (max-width: 767px) {
main {
padding-top: 3.5rem;
}
}

View File

@ -39,7 +39,7 @@ export default function RootLayout({
<script
dangerouslySetInnerHTML={{
__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>

View File

@ -5,7 +5,6 @@ import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
LayoutDashboard,
Activity,
BarChart3,
Cpu,
Key,
@ -22,6 +21,10 @@ import {
} from 'lucide-react';
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 = [
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
{ href: '/hermes', label: 'Hermes', icon: Sparkles },
@ -39,15 +42,56 @@ export function SidebarNav() {
const router = useRouter();
const { user, logout } = useAuth();
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(() => {
const saved = (localStorage.getItem('theme') as 'light' | 'dark') || 'light';
setTheme(saved);
document.documentElement.classList.toggle('dark', saved === 'dark');
try {
const savedTheme = window.localStorage.getItem(THEME_STORAGE_KEY) as Theme | null;
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 = () => {
logout();
router.push('/login');
@ -57,23 +101,34 @@ export function SidebarNav() {
? user.email.slice(0, 2).toUpperCase()
: '??';
const sidebarContent = (
const sidebarContent = (isCollapsed = false, showCollapseControl = false) => (
<>
{/* Logo */}
<div className="flex h-16 items-center justify-between border-b px-6">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600">
<div className="flex h-16 items-center justify-between border-b border-[var(--bl-border)] px-4">
<div className="flex min-w-0 items-center gap-2">
<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" />
</div>
<div>
<h1 className="text-sm font-bold">DevOps</h1>
<p className="text-[10px] text-gray-500">Dashboard</p>
<div className={isCollapsed ? 'sr-only' : 'min-w-0'}>
<h1 className="truncate text-sm font-bold text-[var(--bl-text-primary)]">DevOps</h1>
<p className="truncate text-[10px] text-[var(--bl-text-secondary)]">Dashboard</p>
</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 */}
<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)}
aria-label="Close navigation"
>
<X className="h-5 w-5" />
</button>
@ -89,46 +144,44 @@ export function SidebarNav() {
key={item.href}
href={item.href}
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 ${
isActive
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
? 'bg-[var(--bl-accent)] text-[var(--bl-accent-foreground)]'
: 'text-[var(--bl-text-secondary)] hover:bg-[var(--bl-surface-muted)] hover:text-[var(--bl-text-primary)]'
}`}
>
<item.icon className="h-4 w-4" />
<span>{item.label}</span>
<item.icon className="h-4 w-4 shrink-0" />
<span className={isCollapsed ? 'sr-only' : 'truncate'}>{item.label}</span>
</Link>
);
})}
</nav>
{/* 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
onClick={() => {
const next = theme === 'dark' ? 'light' : 'dark';
setTheme(next);
localStorage.setItem('theme', next);
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"
onClick={toggleTheme}
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)]"
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
>
{theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
{theme === 'dark' ? <Sun className="h-4 w-4 shrink-0" /> : <Moon className="h-4 w-4 shrink-0" />}
<span className={isCollapsed ? 'sr-only' : 'truncate'}>{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
</button>
<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}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate text-gray-900">{user?.email ?? 'Admin'}</p>
<p className="text-xs text-gray-500 truncate">{user?.role ?? 'admin'}</p>
<div className={isCollapsed ? 'sr-only' : 'min-w-0 flex-1'}>
<p className="truncate text-sm font-medium text-[var(--bl-text-primary)]">{user?.email ?? 'Admin'}</p>
<p className="truncate text-xs text-[var(--bl-text-secondary)]">{user?.role ?? 'admin'}</p>
</div>
<button
onClick={handleLogout}
title="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" />
</button>
@ -140,14 +193,12 @@ export function SidebarNav() {
return (
<>
{/* 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">
<button onClick={() => setMobileOpen(true)}>
<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)} aria-label="Open navigation">
<Menu className="h-6 w-6" />
</button>
<span className="ml-3 text-sm font-bold">DevOps Dashboard</span>
</div>
{/* Spacer for mobile top bar */}
<div className="h-14 md:hidden" />
{/* Mobile overlay */}
{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
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>
</>
);