feat: brain sub-sidebar in AppShell — folders/tags under Brain nav item
- Brain nav item expands when on /brain page - Shows Folders/Tags toggle tabs - Folder links: /brain?folder=Work etc - Tag links: /brain?tag=dev etc - Counts shown next to each - Brain page reads filter from URL params - Only shows folders with items + key defaults (Home, Work, Knowledge) - Tags limited to 12 most-used Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
BookOpen,
|
||||
Brain,
|
||||
@@ -7,6 +8,7 @@
|
||||
CircleDot,
|
||||
Compass,
|
||||
Dumbbell,
|
||||
FolderOpen,
|
||||
Landmark,
|
||||
LibraryBig,
|
||||
Menu,
|
||||
@@ -14,6 +16,7 @@
|
||||
Search,
|
||||
Settings2,
|
||||
SquareCheckBig,
|
||||
Tag,
|
||||
X
|
||||
} from '@lucide/svelte';
|
||||
|
||||
@@ -50,6 +53,33 @@
|
||||
|
||||
let mobileNavOpen = $state(false);
|
||||
|
||||
// Brain sidebar sub-items
|
||||
interface BrainFolder { id: string; name: string; item_count: number; }
|
||||
interface BrainTag { id: string; name: string; item_count: number; }
|
||||
let brainFolders = $state<BrainFolder[]>([]);
|
||||
let brainTags = $state<BrainTag[]>([]);
|
||||
let brainSubView = $state<'folders' | 'tags'>('folders');
|
||||
let brainExpanded = $derived(page.url.pathname.startsWith('/brain'));
|
||||
|
||||
async function loadBrainSidebar() {
|
||||
try {
|
||||
const res = await fetch('/api/brain/taxonomy/sidebar', { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
brainFolders = data.folders || [];
|
||||
brainTags = data.tags || [];
|
||||
}
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (brainExpanded) loadBrainSidebar();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (brainExpanded) loadBrainSidebar();
|
||||
});
|
||||
|
||||
function isActive(href: string): boolean {
|
||||
if (href === '/') return page.url.pathname === '/';
|
||||
return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
|
||||
@@ -82,6 +112,36 @@
|
||||
<item.icon size={16} strokeWidth={1.8} />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
{#if item.id === 'brain' && brainExpanded}
|
||||
<div class="rail-sub">
|
||||
<div class="rail-sub-tabs">
|
||||
<button class="rail-sub-tab" class:active={brainSubView === 'folders'} onclick={() => brainSubView = 'folders'}>
|
||||
<FolderOpen size={11} strokeWidth={2} /> Folders
|
||||
</button>
|
||||
<button class="rail-sub-tab" class:active={brainSubView === 'tags'} onclick={() => brainSubView = 'tags'}>
|
||||
<Tag size={11} strokeWidth={2} /> Tags
|
||||
</button>
|
||||
</div>
|
||||
{#if brainSubView === 'folders'}
|
||||
<a href="/brain" class="rail-sub-item" class:active={page.url.pathname === '/brain' && !page.url.searchParams.get('folder')}>
|
||||
<span>All</span>
|
||||
</a>
|
||||
{#each brainFolders.filter(f => f.item_count > 0 || f.name === 'Home' || f.name === 'Work' || f.name === 'Knowledge') as folder}
|
||||
<a href="/brain?folder={folder.name}" class="rail-sub-item" class:active={page.url.searchParams.get('folder') === folder.name}>
|
||||
<span>{folder.name}</span>
|
||||
<span class="rail-sub-count">{folder.item_count}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each brainTags.filter(t => t.item_count > 0).slice(0, 12) as tag}
|
||||
<a href="/brain?tag={tag.name}" class="rail-sub-item" class:active={page.url.searchParams.get('tag') === tag.name}>
|
||||
<span>{tag.name}</span>
|
||||
<span class="rail-sub-count">{tag.item_count}</span>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
@@ -280,6 +340,64 @@
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Brain sub-sidebar */
|
||||
.rail-sub {
|
||||
padding: 4px 0 4px 18px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
.rail-sub-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
.rail-sub-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--shell-muted);
|
||||
font-family: inherit;
|
||||
transition: all 140ms;
|
||||
}
|
||||
.rail-sub-tab.active {
|
||||
background: rgba(255,255,255,0.5);
|
||||
color: var(--shell-ink);
|
||||
}
|
||||
.rail-sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
color: var(--shell-muted);
|
||||
font-size: 0.78rem;
|
||||
transition: all 140ms;
|
||||
text-decoration: none;
|
||||
}
|
||||
.rail-sub-item:hover {
|
||||
background: rgba(255,255,255,0.4);
|
||||
color: var(--shell-ink);
|
||||
}
|
||||
.rail-sub-item.active {
|
||||
background: rgba(255,255,255,0.6);
|
||||
color: var(--shell-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rail-sub-count {
|
||||
font-size: 0.65rem;
|
||||
font-family: var(--mono, monospace);
|
||||
color: var(--shell-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.rail-bottom {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -307,6 +307,16 @@
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
// Read filters from URL params (set by AppShell sidebar links)
|
||||
const urlFolder = new URL(window.location.href).searchParams.get('folder');
|
||||
const urlTag = new URL(window.location.href).searchParams.get('tag');
|
||||
if (urlFolder) {
|
||||
activeFolder = urlFolder;
|
||||
}
|
||||
if (urlTag) {
|
||||
activeTag = urlTag;
|
||||
}
|
||||
|
||||
await loadSidebar();
|
||||
await loadItems();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user