refactor: brain sidebar lives in AppShell nav, not in-page
- Removed duplicate in-page sidebar from AtelierBrainPage - AppShell sub-items now full-sized, matching main nav style - Folders/Tags tabs as pill toggle in sidebar - All folders shown (not just ones with items) - All tags shown (not limited to 12) - Brain page is now just capture + search + masonry grid - Sidebar links use /brain?folder=X and /brain?tag=X Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,14 +126,14 @@
|
||||
<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}
|
||||
{#each brainFolders 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}
|
||||
{#each brainTags 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>
|
||||
@@ -340,46 +340,49 @@
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Brain sub-sidebar */
|
||||
/* Brain sub-nav */
|
||||
.rail-sub {
|
||||
padding: 4px 0 4px 18px;
|
||||
padding: 2px 0 6px 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
}
|
||||
.rail-sub-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 6px;
|
||||
margin: 0 8px 6px;
|
||||
padding: 3px;
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.rail-sub-tab {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--shell-muted);
|
||||
font-family: inherit;
|
||||
transition: all 140ms;
|
||||
}
|
||||
.rail-sub-tab.active {
|
||||
background: rgba(255,255,255,0.5);
|
||||
background: rgba(255,255,255,0.7);
|
||||
color: var(--shell-ink);
|
||||
}
|
||||
.rail-sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
gap: 12px;
|
||||
padding: 9px 14px 9px 28px;
|
||||
border-radius: 999px;
|
||||
color: var(--shell-muted);
|
||||
font-size: 0.78rem;
|
||||
transition: all 140ms;
|
||||
font-size: 0.82rem;
|
||||
transition: background 160ms ease, color 160ms ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.rail-sub-item:hover {
|
||||
@@ -387,15 +390,20 @@
|
||||
color: var(--shell-ink);
|
||||
}
|
||||
.rail-sub-item.active {
|
||||
background: rgba(255,255,255,0.6);
|
||||
background: rgba(255,255,255,0.62);
|
||||
color: var(--shell-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.rail-sub-item span:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
.rail-sub-count {
|
||||
font-size: 0.65rem;
|
||||
font-size: 0.68rem;
|
||||
font-family: var(--mono, monospace);
|
||||
color: var(--shell-muted);
|
||||
opacity: 0.7;
|
||||
opacity: 0.6;
|
||||
min-width: 18px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rail-bottom {
|
||||
|
||||
@@ -367,86 +367,31 @@
|
||||
</section>
|
||||
|
||||
<!-- ═══ Sidebar + Main layout ═══ -->
|
||||
<div class="brain-layout">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="brain-sidebar">
|
||||
<div class="sidebar-tabs">
|
||||
<button class="sidebar-tab" class:active={sidebarView === 'folders'} onclick={() => sidebarView = 'folders'}>Folders</button>
|
||||
<button class="sidebar-tab" class:active={sidebarView === 'tags'} onclick={() => sidebarView = 'tags'}>Tags</button>
|
||||
</div>
|
||||
|
||||
<!-- All items -->
|
||||
<button class="sidebar-item" class:active={!activeFolder && !activeTag} onclick={() => { selectFolder(null); selectTag(null); }}>
|
||||
<span class="sidebar-item-name">All items</span>
|
||||
<span class="sidebar-item-count">{total}</span>
|
||||
</button>
|
||||
|
||||
{#if sidebarView === 'folders'}
|
||||
{#each sidebarFolders.filter(f => f.is_active) as folder}
|
||||
<button class="sidebar-item" class:active={activeFolderId === folder.id} onclick={() => selectFolder(folder)}>
|
||||
<span class="sidebar-item-name">{folder.name}</span>
|
||||
<span class="sidebar-item-count">{folder.item_count}</span>
|
||||
{#if showManage}
|
||||
<button class="sidebar-item-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete folder "${folder.name}"? Items will be moved.`)) deleteTaxonomy(folder.id); }}>
|
||||
<svg width="12" height="12" 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>
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each sidebarTags.filter(t => t.is_active) as tag}
|
||||
<button class="sidebar-item" class:active={activeTagId === tag.id} onclick={() => selectTag(tag)}>
|
||||
<span class="sidebar-item-name">{tag.name}</span>
|
||||
<span class="sidebar-item-count">{tag.item_count}</span>
|
||||
{#if showManage}
|
||||
<button class="sidebar-item-delete" onclick={(e) => { e.stopPropagation(); if (confirm(`Delete tag "${tag.name}"?`)) deleteTaxonomy(tag.id); }}>
|
||||
<svg width="12" height="12" 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>
|
||||
</button>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Add / Manage -->
|
||||
<div class="sidebar-actions">
|
||||
{#if showManage}
|
||||
<div class="sidebar-add">
|
||||
<input class="sidebar-add-input" placeholder="New {sidebarView === 'folders' ? 'folder' : 'tag'}..." bind:value={newTaxName} onkeydown={(e) => { if (e.key === 'Enter') addTaxonomy(); }} />
|
||||
<button class="sidebar-add-btn" onclick={addTaxonomy}>Add</button>
|
||||
</div>
|
||||
{/if}
|
||||
<button class="sidebar-manage-toggle" onclick={() => showManage = !showManage}>
|
||||
{showManage ? 'Done' : 'Manage'}
|
||||
<!-- Main content -->
|
||||
<div>
|
||||
<!-- Active filter indicator -->
|
||||
{#if activeFolder || activeTag}
|
||||
<div class="active-filter">
|
||||
<span class="filter-label">Filtered by {activeFolder ? 'folder' : 'tag'}:</span>
|
||||
<span class="filter-tag">{activeFolder || activeTag}</span>
|
||||
<button class="filter-clear" onclick={() => { activeFolder = null; activeTag = null; activeFolderId = null; activeTagId = null; 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>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="brain-main">
|
||||
<!-- Active filter indicator -->
|
||||
{#if activeFolder || activeTag}
|
||||
<div class="active-filter">
|
||||
<span class="filter-label">Filtered by {activeFolder ? 'folder' : 'tag'}:</span>
|
||||
<span class="filter-tag">{activeFolder || activeTag}</span>
|
||||
<button class="filter-clear" onclick={() => { selectFolder(null); selectTag(null); }}>
|
||||
<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
|
||||
<!-- Search -->
|
||||
<div class="search-section">
|
||||
<div class="search-wrap">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input type="text" class="search-input" placeholder="Search your brain..." bind:value={searchQuery} oninput={handleSearchInput} />
|
||||
{#if searchQuery}
|
||||
<button class="search-clear" onclick={() => { searchQuery = ''; loadItems(); }}>
|
||||
<svg 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>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Search -->
|
||||
<div class="search-section">
|
||||
<div class="search-wrap">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input type="text" class="search-input" placeholder="Search your brain..." bind:value={searchQuery} oninput={handleSearchInput} />
|
||||
{#if searchQuery}
|
||||
<button class="search-clear" onclick={() => { searchQuery = ''; loadItems(); }}>
|
||||
<svg 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>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Masonry card grid -->
|
||||
@@ -534,8 +479,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div><!-- .brain-main -->
|
||||
</div><!-- .brain-layout -->
|
||||
</div><!-- main content -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -377,14 +377,6 @@
|
||||
</section>
|
||||
|
||||
<section class="stats-strip reveal" aria-label="Budget summary">
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">Spent</div>
|
||||
<div class="stat-value">{spendMagnitude}</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">Income</div>
|
||||
<div class="stat-value">{incomeMagnitude}</div>
|
||||
</div>
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">To sort</div>
|
||||
<div class="stat-value">{uncatCount}</div>
|
||||
@@ -393,81 +385,81 @@
|
||||
<div class="stat-label">On budget cash</div>
|
||||
<div class="stat-value">{formatBalance(onBudgetTotal)}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="account-ribbon reveal" class:open={accountsOpen}>
|
||||
<div class="section-head">
|
||||
<h2>Accounts</h2>
|
||||
<span>{accounts.length + offBudgetAccounts.length + 1} visible</span>
|
||||
</div>
|
||||
<div class="account-list items-card">
|
||||
<button class="account-row" class:active={!activeAccountId} onclick={() => { selectAccount(null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">All accounts</div>
|
||||
<div class="account-meta">On-budget overview</div>
|
||||
</div>
|
||||
<div class="account-balance">{formatBalance(onBudgetTotal)}</div>
|
||||
</button>
|
||||
{#each accounts as acct}
|
||||
<button class="account-row" class:active={activeAccountId === acct.id} onclick={() => { selectAccount(acct.id || null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">{acct.name}</div>
|
||||
<div class="account-meta">On budget</div>
|
||||
</div>
|
||||
<div class="account-balance" class:negative={!acct.positive}>{formatBalance(acct.balance)}</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if offBudgetAccounts.length > 0}
|
||||
{#each offBudgetAccounts as acct}
|
||||
<button class="account-row quiet" class:active={activeAccountId === acct.id} onclick={() => { selectAccount(acct.id || null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent quiet"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">{acct.name}</div>
|
||||
<div class="account-meta">Off budget</div>
|
||||
</div>
|
||||
<div class="account-balance">{formatBalance(acct.balance)}</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="stat-block">
|
||||
<div class="stat-label">Spent</div>
|
||||
<div class="stat-value">{spendMagnitude}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if activeView === 'transactions'}
|
||||
{#if suggestedTransfers.length > 0}
|
||||
<section class="transfer-strip reveal">
|
||||
<div class="section-head">
|
||||
<h2>Suggested transfers</h2>
|
||||
<span>{suggestedTransfers.length} open</span>
|
||||
</div>
|
||||
<div class="transfer-list items-card">
|
||||
{#each suggestedTransfers as s}
|
||||
<div class="transfer-row">
|
||||
<div class="row-accent transfer-accent"></div>
|
||||
<div class="transfer-copy">
|
||||
<div class="transfer-route">{s.from.account} to {s.to.account}</div>
|
||||
<div class="transfer-note">{s.from.payee} matched with {s.to.payee}</div>
|
||||
</div>
|
||||
<div class="transfer-meta">
|
||||
<strong>${s.amount.toLocaleString('en-US', { minimumFractionDigits: 2 })}</strong>
|
||||
<div class="transfer-actions">
|
||||
<button class="mini-btn primary" onclick={() => linkSuggestedTransfer(s)}>Link</button>
|
||||
<button class="mini-btn" onclick={() => dismissSuggestion(s.id)}>Skip</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section class="ledger-surface reveal">
|
||||
<section class="ledger-head">
|
||||
<div class="section-head ledger-title-row">
|
||||
<h2>Ledger</h2>
|
||||
<span>{filteredTransactions.length} visible</span>
|
||||
</div>
|
||||
{#if accountsOpen}
|
||||
<div class="account-ribbon open">
|
||||
<div class="account-list items-card">
|
||||
<button class="account-row" class:active={!activeAccountId} onclick={() => { selectAccount(null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">All accounts</div>
|
||||
<div class="account-meta">On-budget overview</div>
|
||||
</div>
|
||||
<div class="account-balance">{formatBalance(onBudgetTotal)}</div>
|
||||
</button>
|
||||
{#each accounts as acct}
|
||||
<button class="account-row" class:active={activeAccountId === acct.id} onclick={() => { selectAccount(acct.id || null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">{acct.name}</div>
|
||||
<div class="account-meta">On budget</div>
|
||||
</div>
|
||||
<div class="account-balance" class:negative={!acct.positive}>{formatBalance(acct.balance)}</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if offBudgetAccounts.length > 0}
|
||||
{#each offBudgetAccounts as acct}
|
||||
<button class="account-row quiet" class:active={activeAccountId === acct.id} onclick={() => { selectAccount(acct.id || null); accountsOpen = false; }}>
|
||||
<div class="row-accent account-accent quiet"></div>
|
||||
<div class="account-main">
|
||||
<div class="account-name">{acct.name}</div>
|
||||
<div class="account-meta">Off budget</div>
|
||||
</div>
|
||||
<div class="account-balance">{formatBalance(acct.balance)}</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if suggestedTransfers.length > 0}
|
||||
<div class="inline-transfers">
|
||||
<div class="section-head compact-head">
|
||||
<h2>Suggested transfers</h2>
|
||||
<span>{suggestedTransfers.length} open</span>
|
||||
</div>
|
||||
<div class="transfer-list items-card">
|
||||
{#each suggestedTransfers as s}
|
||||
<div class="transfer-row">
|
||||
<div class="row-accent transfer-accent"></div>
|
||||
<div class="transfer-copy">
|
||||
<div class="transfer-route">{s.from.account} to {s.to.account}</div>
|
||||
<div class="transfer-note">{s.from.payee} matched with {s.to.payee}</div>
|
||||
</div>
|
||||
<div class="transfer-meta">
|
||||
<strong>${s.amount.toLocaleString('en-US', { minimumFractionDigits: 2 })}</strong>
|
||||
<div class="transfer-actions">
|
||||
<button class="mini-btn primary" onclick={() => linkSuggestedTransfer(s)}>Link</button>
|
||||
<button class="mini-btn" onclick={() => dismissSuggestion(s.id)}>Skip</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="filter-strip">
|
||||
<button class:active={activeTab === 'all'} onclick={() => { activeTab = 'all'; loadTransactions(); }}>All</button>
|
||||
<button class:active={activeTab === 'uncategorized'} onclick={() => { activeTab = 'uncategorized'; loadTransactions(); }}>
|
||||
@@ -718,7 +710,7 @@
|
||||
|
||||
.stats-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -741,6 +733,10 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.account-ribbon.open {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.account-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
@@ -816,7 +812,6 @@
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.transfer-strip,
|
||||
.ledger-head,
|
||||
.ledger,
|
||||
.budget-board {
|
||||
@@ -842,6 +837,10 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.compact-head {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ledger-title-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -940,6 +939,11 @@
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.inline-transfers {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1274,8 +1278,7 @@
|
||||
}
|
||||
|
||||
.account-ribbon.open {
|
||||
display: grid;
|
||||
margin-top: -4px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.ledger-surface,
|
||||
@@ -1286,8 +1289,19 @@
|
||||
}
|
||||
|
||||
.stats-strip {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 14px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.stats-strip::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
min-width: 112px;
|
||||
}
|
||||
|
||||
.transfer-row,
|
||||
|
||||
Reference in New Issue
Block a user