feat: brain colored folders/tags — color + icon fields, mobile pills
Backend: - Added color (hex) and icon (lucide name) columns to folders and tags - Default folders seeded with colors: Home=green, Work=indigo, Travel=blue, etc. - API returns color/icon in sidebar and CRUD responses - Create/update endpoints accept color and icon Frontend: - Mobile: horizontal scrollable pill tabs with colored dots - Desktop sidebar: colored dots next to folder names - Active pill gets tinted border matching folder color Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,8 @@
|
||||
assets: { id: string; asset_type: string; filename: string; content_type: string | null }[];
|
||||
}
|
||||
|
||||
interface SidebarFolder { id: string; name: string; slug: string; is_active: boolean; item_count: number; }
|
||||
interface SidebarTag { id: string; name: string; slug: string; is_active: boolean; item_count: number; }
|
||||
interface SidebarFolder { id: string; name: string; slug: string; color?: string; icon?: string; is_active: boolean; item_count: number; }
|
||||
interface SidebarTag { id: string; name: string; slug: string; color?: string; icon?: string; is_active: boolean; item_count: number; }
|
||||
|
||||
let loading = $state(true);
|
||||
let items = $state<BrainItem[]>([]);
|
||||
@@ -366,6 +366,7 @@
|
||||
<nav class="sidebar-nav">
|
||||
{#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; mobileSidebarOpen = false; loadItems(); }}>
|
||||
{#if folder.color}<span class="nav-dot" style="background: {folder.color}"></span>{/if}
|
||||
<span class="nav-label">{folder.name}</span>
|
||||
{#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>
|
||||
@@ -437,11 +438,20 @@
|
||||
{#if uploading}<div class="upload-status">Uploading...</div>{/if}
|
||||
</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>
|
||||
<!-- Mobile: horizontal pill filters -->
|
||||
<div class="mobile-pills">
|
||||
<button class="pill" class:active={!activeFolder && !activeTag} onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; mobileSidebarOpen = false; loadItems(); }}>
|
||||
All
|
||||
</button>
|
||||
{#each sidebarFolders.filter(f => f.is_active) as folder}
|
||||
<button class="pill" class:active={activeFolder === folder.name} onclick={() => { activeFolder = folder.name; activeFolderId = folder.id; activeTag = null; activeTagId = null; loadItems(); }}
|
||||
style={folder.color ? `--pill-color: ${folder.color}` : ''}>
|
||||
{#if folder.color}<span class="pill-dot" style="background: {folder.color}"></span>{/if}
|
||||
{folder.name}
|
||||
{#if folder.item_count > 0}<span class="pill-count">{folder.item_count}</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Active filter indicator -->
|
||||
{#if activeFolder || activeTag}
|
||||
@@ -867,26 +877,56 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ═══ Mobile filter button ═══ */
|
||||
.mobile-filter-btn {
|
||||
/* ═══ Mobile pills ═══ */
|
||||
.mobile-pills {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.mobile-pills::-webkit-scrollbar { display: none; }
|
||||
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(35,26,17,0.1);
|
||||
background: rgba(255,252,248,0.7);
|
||||
color: #5c5046;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--font);
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: all 160ms;
|
||||
}
|
||||
.mobile-filter-btn:hover { background: rgba(255,248,241,0.9); color: #1e1812; }
|
||||
.pill:hover { background: rgba(255,248,241,0.9); }
|
||||
.pill.active {
|
||||
background: rgba(255,248,241,0.95);
|
||||
color: #1e1812;
|
||||
font-weight: 600;
|
||||
border-color: var(--pill-color, rgba(35,26,17,0.2));
|
||||
box-shadow: inset 0 0 0 1px var(--pill-color, transparent);
|
||||
}
|
||||
.pill-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.pill-count {
|
||||
font-size: 0.65rem; font-family: var(--mono); color: #8c7b69;
|
||||
}
|
||||
|
||||
.nav-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-filter-btn { display: flex; }
|
||||
.mobile-pills { display: flex; }
|
||||
}
|
||||
|
||||
/* ═══ Active filter ═══ */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user