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:
co-authored by
Claude Opus 4.6
parent
bd166843d6
commit
9a00d7c5ee
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user