feat: mobile-responsive Web UI — sidebar drawer, hamburger menu, touch targets

On viewports < 768px (md breakpoint):
- Sidebar slides in as a drawer with dark overlay backdrop
- Hamburger button in the header toggles the sidebar
- Nav link clicks auto-close the drawer
- Sidebar items get 44px min-height for touch-friendly tapping
- Search input uses fluid width instead of fixed 320px

Desktop (>= 768px) behavior is unchanged: sidebar always visible,
main content offset by 260px, no hamburger button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Algis Dumbris
2026-03-16 20:28:49 +02:00
co-authored by Claude Opus 4.6
parent bd166843d6
commit 9a00d7c5ee
4 changed files with 45 additions and 8 deletions
+1 -1
View File
@@ -128,7 +128,7 @@
background-color: var(--bg-tertiary);
}
.sidebar-item {
@apply flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors cursor-pointer;
@apply flex items-center gap-2 px-3 py-2 md:py-1.5 min-h-[44px] md:min-h-0 rounded text-sm transition-colors cursor-pointer;
color: var(--text-secondary);
}
.sidebar-item:hover {
+16 -3
View File
@@ -2,6 +2,8 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let { onMenuToggle = () => {} }: { onMenuToggle?: () => void } = $props();
let searchQuery = $state('');
function handleSearch(e: SubmitEvent) {
@@ -27,13 +29,24 @@
});
</script>
<header class="flex items-center gap-4 h-12 px-5 border-b border-border bg-bg-primary flex-shrink-0">
<h2 class="font-display font-bold text-text-primary text-base">{pageTitle()}</h2>
<header class="flex items-center gap-3 md:gap-4 h-12 px-3 md:px-5 border-b border-border bg-bg-primary flex-shrink-0">
<!-- Hamburger menu button (mobile only) -->
<button
class="md:hidden flex items-center justify-center w-8 h-8 rounded text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors flex-shrink-0"
onclick={onMenuToggle}
aria-label="Toggle sidebar"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<h2 class="font-display font-bold text-text-primary text-base truncate">{pageTitle()}</h2>
<div class="flex-1"></div>
<!-- Search -->
<form class="w-80" onsubmit={handleSearch}>
<form class="w-full max-w-xs md:max-w-none md:w-80" onsubmit={handleSearch}>
<div class="relative">
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
+24 -1
View File
@@ -5,6 +5,8 @@
import { notifications } from '$lib/stores/notifications';
import { channels as channelsApi, agents as agentsApi, deadLetters as deadLettersApi } from '$lib/api/client';
let { open = false, onclose = () => {} }: { open?: boolean; onclose?: () => void } = $props();
let channelList = $state<any[]>([]);
let agentList = $state<any[]>([]);
let deadLetterCount = $state(0);
@@ -36,6 +38,10 @@
}
}
function handleNavClick() {
onclose();
}
function isActive(href: string): boolean {
return $page.url.pathname === href || ($page.url.pathname.startsWith(href) && href !== '/');
}
@@ -55,7 +61,17 @@
];
</script>
<aside class="fixed top-0 left-0 z-40 h-screen w-[260px] bg-bg-secondary border-r border-border flex flex-col select-none">
<!-- Mobile overlay backdrop -->
{#if open}
<button
class="fixed inset-0 z-30 bg-black/50 md:hidden"
onclick={onclose}
aria-label="Close sidebar"
tabindex="-1"
></button>
{/if}
<aside class="fixed top-0 left-0 z-40 h-screen w-[260px] bg-bg-secondary border-r border-border flex flex-col select-none transition-transform duration-200 {open ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0">
<!-- Workspace header -->
<div class="flex items-center gap-2.5 h-14 px-4 border-b border-border flex-shrink-0">
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-accent-purple to-[#06b6d4] flex items-center justify-center">
@@ -97,6 +113,7 @@
<a
href="/"
class="sidebar-item mb-1 {isActive('/') && $page.url.pathname === '/' ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
@@ -108,6 +125,7 @@
<a
href="/conversations"
class="sidebar-item mb-3 {isActive('/conversations') ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -148,6 +166,7 @@
<a
href="/channels/{ch.name}"
class="sidebar-item {isActive('/channels/' + ch.name) ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
{#if ch.name.startsWith('my-agents-')}
<svg class="w-4 h-4 flex-shrink-0 text-accent-purple" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
@@ -196,6 +215,7 @@
<a
href="/dm/{agent.name}"
class="sidebar-item {isActive('/dm/' + agent.name) ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
<span class="relative flex-shrink-0">
<span class="w-5 h-5 rounded-full bg-bg-tertiary flex items-center justify-center text-[10px] font-bold text-text-secondary">
@@ -238,6 +258,7 @@
<a
href={link.href}
class="sidebar-item {isActive(link.href) ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
{#if link.label === 'Agents'}
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
@@ -255,6 +276,7 @@
<a
href="/dead-letters"
class="sidebar-item {isActive('/dead-letters') && $page.url.pathname === '/dead-letters' ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 9v.906a2.25 2.25 0 01-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 001.183 1.981l6.478 3.488m8.839 2.51l-4.66-2.51m0 0l-1.023-.55a2.25 2.25 0 00-2.134 0l-1.022.55m0 0l-4.661 2.51m16.5 1.615a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V8.844a2.25 2.25 0 011.183-1.98l7.5-4.04a2.25 2.25 0 012.134 0l7.5 4.04a2.25 2.25 0 011.183 1.98V19.5z" />
@@ -267,6 +289,7 @@
<a
href="/dead-letters/webhooks"
class="sidebar-item {isActive('/dead-letters/webhooks') ? 'sidebar-item-active' : ''}"
onclick={handleNavClick}
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
+4 -3
View File
@@ -13,6 +13,7 @@
let sseClient: SSEClient | null = $state(null);
let sseUnsubscribe: (() => void) | null = $state(null);
let initialized = $state(false);
let sidebarOpen = $state(false);
let isLoginPage = $derived($page.url.pathname === '/login');
@@ -79,9 +80,9 @@
{@render children()}
{:else if $user}
<div class="h-screen flex overflow-hidden bg-bg-primary">
<Sidebar />
<div class="ml-[260px] flex-1 flex flex-col min-w-0">
<Header />
<Sidebar open={sidebarOpen} onclose={() => (sidebarOpen = false)} />
<div class="md:ml-[260px] flex-1 flex flex-col min-w-0">
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} />
<main class="flex-1 overflow-y-auto flex flex-col">
{@render children()}
</main>