feat: brain mobile sidebar — slide-out drawer with overlay

- "Folders & Tags" button shown only on mobile
- Click opens sidebar as slide-out drawer from left
- Dark overlay behind, click to dismiss
- Selecting a folder/tag auto-closes the drawer
- Desktop sidebar unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-01 22:13:48 -05:00
parent a52b13eb28
commit 8f3afd46c3

View File

@@ -40,6 +40,7 @@
let sidebarFolders = $state<SidebarFolder[]>([]); let sidebarFolders = $state<SidebarFolder[]>([]);
let sidebarTags = $state<SidebarTag[]>([]); let sidebarTags = $state<SidebarTag[]>([]);
let sidebarView = $state<'folders' | 'tags'>('folders'); let sidebarView = $state<'folders' | 'tags'>('folders');
let mobileSidebarOpen = $state(false);
let showManage = $state(false); let showManage = $state(false);
let newTaxName = $state(''); let newTaxName = $state('');
@@ -327,7 +328,11 @@
<div class="brain-layout"> <div class="brain-layout">
<!-- Second sidebar (like Reader) --> <!-- Second sidebar (like Reader) -->
<aside class="brain-sidebar"> <!-- svelte-ignore a11y_no_static_element_interactions -->
{#if mobileSidebarOpen}
<div class="mobile-sidebar-overlay" onclick={() => mobileSidebarOpen = false}></div>
{/if}
<aside class="brain-sidebar" class:mobile-open={mobileSidebarOpen}>
<div class="sidebar-header"> <div class="sidebar-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<span class="sidebar-title">Brain</span> <span class="sidebar-title">Brain</span>
@@ -341,7 +346,7 @@
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<button class="nav-item" class:active={!activeFolder && !activeTag} onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; loadItems(); }}> <button class="nav-item" class:active={!activeFolder && !activeTag} onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; mobileSidebarOpen = false; loadItems(); }}>
<span class="nav-label">All items</span> <span class="nav-label">All items</span>
<span class="nav-count">{total}</span> <span class="nav-count">{total}</span>
</button> </button>
@@ -360,7 +365,7 @@
{/if} {/if}
<nav class="sidebar-nav"> <nav class="sidebar-nav">
{#each sidebarFolders.filter(f => f.is_active) as folder} {#each sidebarFolders.filter(f => f.is_active) as folder}
<button class="nav-item" class:active={activeFolder === folder.name} onclick={() => { activeFolder = folder.name; activeFolderId = folder.id; activeTag = null; activeTagId = null; loadItems(); }}> <button class="nav-item" class:active={activeFolder === folder.name} onclick={() => { activeFolder = folder.name; activeFolderId = folder.id; activeTag = null; activeTagId = null; mobileSidebarOpen = false; loadItems(); }}>
<span class="nav-label">{folder.name}</span> <span class="nav-label">{folder.name}</span>
{#if showManage} {#if showManage}
<!-- svelte-ignore a11y_no_static_element_interactions --><span class="nav-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete "${folder.name}"? Items will be moved.`)) deleteTaxonomy(folder.id); }}>×</span> <!-- svelte-ignore a11y_no_static_element_interactions --><span class="nav-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete "${folder.name}"? Items will be moved.`)) deleteTaxonomy(folder.id); }}>×</span>
@@ -383,7 +388,7 @@
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
{#each sidebarTags.filter(t => t.is_active) as tag} {#each sidebarTags.filter(t => t.is_active) as tag}
<button class="nav-item" class:active={activeTag === tag.name} onclick={() => { activeTag = tag.name; activeTagId = tag.id; activeFolder = null; activeFolderId = null; loadItems(); }}> <button class="nav-item" class:active={activeTag === tag.name} onclick={() => { activeTag = tag.name; activeTagId = tag.id; activeFolder = null; activeFolderId = null; mobileSidebarOpen = false; loadItems(); }}>
<span class="nav-label">{tag.name}</span> <span class="nav-label">{tag.name}</span>
<!-- svelte-ignore a11y_no_static_element_interactions --><span class="nav-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete tag "${tag.name}"?`)) deleteTaxonomy(tag.id); }}>×</span> <!-- svelte-ignore a11y_no_static_element_interactions --><span class="nav-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete tag "${tag.name}"?`)) deleteTaxonomy(tag.id); }}>×</span>
</button> </button>
@@ -392,7 +397,7 @@
{:else} {:else}
<nav class="sidebar-nav"> <nav class="sidebar-nav">
{#each sidebarTags.filter(t => t.is_active && t.item_count > 0) as tag} {#each sidebarTags.filter(t => t.is_active && t.item_count > 0) as tag}
<button class="nav-item" class:active={activeTag === tag.name} onclick={() => { activeTag = tag.name; activeTagId = tag.id; activeFolder = null; activeFolderId = null; loadItems(); }}> <button class="nav-item" class:active={activeTag === tag.name} onclick={() => { activeTag = tag.name; activeTagId = tag.id; activeFolder = null; activeFolderId = null; mobileSidebarOpen = false; loadItems(); }}>
<span class="nav-label">{tag.name}</span> <span class="nav-label">{tag.name}</span>
<span class="nav-count">{tag.item_count}</span> <span class="nav-count">{tag.item_count}</span>
</button> </button>
@@ -432,12 +437,18 @@
{#if uploading}<div class="upload-status">Uploading...</div>{/if} {#if uploading}<div class="upload-status">Uploading...</div>{/if}
</section> </section>
<!-- Mobile sidebar toggle -->
<button class="mobile-filter-btn" onclick={() => mobileSidebarOpen = true}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/></svg>
Folders & Tags
</button>
<!-- Active filter indicator --> <!-- Active filter indicator -->
{#if activeFolder || activeTag} {#if activeFolder || activeTag}
<div class="active-filter"> <div class="active-filter">
<span class="filter-label">Filtered by {activeFolder ? 'folder' : 'tag'}:</span> <span class="filter-label">Filtered by {activeFolder ? 'folder' : 'tag'}:</span>
<span class="filter-tag">{activeFolder || activeTag}</span> <span class="filter-tag">{activeFolder || activeTag}</span>
<button class="filter-clear" onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; loadItems(); }}> <button class="filter-clear" onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; mobileSidebarOpen = false; loadItems(); }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
Clear Clear
</button> </button>
@@ -856,6 +867,28 @@
overflow-x: hidden; overflow-x: hidden;
} }
/* ═══ Mobile filter button ═══ */
.mobile-filter-btn {
display: none;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 10px;
border: 1px solid rgba(35,26,17,0.1);
background: rgba(255,252,248,0.7);
color: #5c5046;
font-size: 0.82rem;
font-family: var(--font);
cursor: pointer;
margin-bottom: 12px;
transition: all 160ms;
}
.mobile-filter-btn:hover { background: rgba(255,248,241,0.9); color: #1e1812; }
@media (max-width: 768px) {
.mobile-filter-btn { display: flex; }
}
/* ═══ Active filter ═══ */ /* ═══ Active filter ═══ */
.active-filter { .active-filter {
display: flex; display: flex;
@@ -1288,7 +1321,16 @@
@media (max-width: 768px) { @media (max-width: 768px) {
.brain-command { display: grid; gap: 14px; } .brain-command { display: grid; gap: 14px; }
.command-actions { justify-items: start; } .command-actions { justify-items: start; }
.brain-sidebar { display: none; } .brain-sidebar {
display: none;
position: fixed; left: 0; top: 0; bottom: 0; z-index: 50;
width: 280px; box-shadow: 8px 0 24px rgba(0,0,0,0.1);
}
.brain-sidebar.mobile-open { display: flex; }
.mobile-sidebar-overlay {
position: fixed; inset: 0; z-index: 49;
background: rgba(17,13,10,0.3);
}
.brain-layout { margin: 0; } .brain-layout { margin: 0; }
.masonry { columns: 1; } .masonry { columns: 1; }
.detail-sheet { width: 100%; padding: 20px; } .detail-sheet { width: 100%; padding: 20px; }