feat: add status filter chips and sort controls to library

Adds an "All / Ready / Processing / Error / Live" pill filter row and
a "Newest / Oldest / Name / Duration / Size" sort selector to the asset
toolbar. Both operate client-side on the loaded asset list so there is
no additional API overhead. State resets to "All / Newest" whenever a
different project or bin is selected.
This commit is contained in:
Zac Gaetano 2026-05-19 00:35:23 -04:00
parent 08e8377309
commit 1e4fcb62f5

View file

@ -93,10 +93,11 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
gap: var(--sp-3); gap: var(--sp-3);
flex-wrap: wrap;
} }
.asset-toolbar-left { display: flex; align-items: center; gap: var(--sp-3); } .asset-toolbar-left { display: flex; align-items: center; gap: var(--sp-3); }
.asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); } .asset-toolbar-right { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.asset-count { .asset-count {
font-size: var(--text-xs); font-size: var(--text-xs);
@ -105,7 +106,7 @@
} }
.search-input { .search-input {
width: 220px; width: 200px;
height: 28px; height: 28px;
padding: 0 var(--sp-3); padding: 0 var(--sp-3);
background: var(--bg-surface); background: var(--bg-surface);
@ -119,6 +120,41 @@
.search-input:focus { border-color: var(--accent-border); } .search-input:focus { border-color: var(--accent-border); }
.search-input::placeholder { color: var(--text-tertiary); } .search-input::placeholder { color: var(--text-tertiary); }
/* Filter chips */
.filter-chips { display: flex; gap: 3px; }
.filter-chip {
padding: 2px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border);
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
transition: all var(--t-fast);
line-height: 20px;
}
.filter-chip:hover { border-color: var(--border-strong); color: var(--text-primary); }
.filter-chip.active {
background: var(--accent-subtle);
border-color: var(--accent-border);
color: var(--accent);
}
/* Sort select */
.sort-select {
height: 26px;
font-size: 12px;
padding: 0 20px 0 8px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--r-md);
color: var(--text-secondary);
outline: none;
cursor: pointer;
}
.sort-select:focus { border-color: var(--accent-border); }
.asset-grid-wrap { .asset-grid-wrap {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@ -422,6 +458,21 @@
<span class="asset-count" id="assetCount">0 assets</span> <span class="asset-count" id="assetCount">0 assets</span>
</div> </div>
<div class="asset-toolbar-right"> <div class="asset-toolbar-right">
<div class="filter-chips" id="filterChips" role="group" aria-label="Filter by status">
<button class="filter-chip active" data-status="">All</button>
<button class="filter-chip" data-status="ready">Ready</button>
<button class="filter-chip" data-status="processing">Processing</button>
<button class="filter-chip" data-status="error">Error</button>
<button class="filter-chip" data-status="live">Live</button>
</div>
<select class="sort-select" id="sortSelect" aria-label="Sort order">
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="name">Name AZ</option>
<option value="name-desc">Name ZA</option>
<option value="duration">Longest</option>
<option value="size">Largest</option>
</select>
<input class="search-input" id="searchInput" type="text" placeholder="Search assets…" aria-label="Search assets"> <input class="search-input" id="searchInput" type="text" placeholder="Search assets…" aria-label="Search assets">
</div> </div>
</div> </div>
@ -515,6 +566,8 @@
assets: [], assets: [],
thumbCache: {}, thumbCache: {},
searchTerm: '', searchTerm: '',
statusFilter: '',
sortBy: 'newest',
}; };
const thumbObserver = new IntersectionObserver((entries) => { const thumbObserver = new IntersectionObserver((entries) => {
@ -565,6 +618,7 @@
await loadProjects(); await loadProjects();
setupDrag(); setupDrag();
setupSearch(); setupSearch();
setupFilters();
// Multi-select bulk actions // Multi-select bulk actions
if (window.SelectionManager) { if (window.SelectionManager) {
@ -619,6 +673,10 @@
function handleProjectChange() { function handleProjectChange() {
state.currentProjectId = document.getElementById('projectSelect').value || null; state.currentProjectId = document.getElementById('projectSelect').value || null;
state.currentBinId = null; state.currentBinId = null;
state.statusFilter = '';
state.sortBy = 'newest';
document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === ''));
document.getElementById('sortSelect').value = 'newest';
loadBinsAndAssets(); loadBinsAndAssets();
} }
@ -635,7 +693,6 @@
function renderBins(bins) { function renderBins(bins) {
const tree = document.getElementById('binTree'); const tree = document.getElementById('binTree');
const allItem = document.getElementById('allAssetsItem');
tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove()); tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove());
bins.forEach(bin => { bins.forEach(bin => {
const el = document.createElement('a'); const el = document.createElement('a');
@ -655,6 +712,10 @@
function selectBin(binId) { function selectBin(binId) {
state.currentBinId = binId; state.currentBinId = binId;
state.statusFilter = '';
state.sortBy = 'newest';
document.querySelectorAll('.filter-chip').forEach(c => c.classList.toggle('active', c.dataset.status === ''));
document.getElementById('sortSelect').value = 'newest';
updateBinActive(); updateBinActive();
loadAssets(); loadAssets();
} }
@ -683,12 +744,40 @@
function renderAssets(assets) { function renderAssets(assets) {
const term = state.searchTerm.toLowerCase(); const term = state.searchTerm.toLowerCase();
const filtered = term ? assets.filter(a => a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term)) : assets;
// Apply status filter
let filtered = state.statusFilter
? assets.filter(a => {
if (state.statusFilter === 'processing') return a.status === 'processing' || a.status === 'ingesting';
return a.status === state.statusFilter;
})
: assets;
// Apply text search
if (term) filtered = filtered.filter(a =>
a.filename?.toLowerCase().includes(term) || a.display_name?.toLowerCase().includes(term)
);
// Sort
filtered = [...filtered].sort((a, b) => {
switch (state.sortBy) {
case 'oldest': return new Date(a.created_at) - new Date(b.created_at);
case 'name': return (a.display_name || a.filename || '').localeCompare(b.display_name || b.filename || '');
case 'name-desc': return (b.display_name || b.filename || '').localeCompare(a.display_name || a.filename || '');
case 'duration': return (b.duration_ms || 0) - (a.duration_ms || 0);
case 'size': return (b.file_size || 0) - (a.file_size || 0);
default: return new Date(b.created_at) - new Date(a.created_at);
}
});
const grid = document.getElementById('assetGrid'); const grid = document.getElementById('assetGrid');
grid.innerHTML = ''; grid.innerHTML = '';
const count = filtered.length; const count = filtered.length;
document.getElementById('assetCount').textContent = `${count} asset${count !== 1 ? 's' : ''}`; const totalCount = assets.length;
document.getElementById('assetCount').textContent =
count === totalCount ? `${count} asset${count !== 1 ? 's' : ''}` :
`${count} of ${totalCount} asset${totalCount !== 1 ? 's' : ''}`;
document.getElementById('assetEmpty').style.display = count === 0 ? 'flex' : 'none'; document.getElementById('assetEmpty').style.display = count === 0 ? 'flex' : 'none';
filtered.forEach(asset => { filtered.forEach(asset => {
@ -729,7 +818,7 @@
else img.style.display = 'none'; else img.style.display = 'none';
card.addEventListener('click', (e) => { card.addEventListener('click', (e) => {
if (e.target.closest('.asset-action-btn')) return; // delete button etc. if (e.target.closest('.asset-action-btn')) return;
if (window.openAssetPreview) window.openAssetPreview(asset.id); if (window.openAssetPreview) window.openAssetPreview(asset.id);
}); });
@ -781,6 +870,24 @@
}); });
} }
// ── Filter chips + sort ───────────────────
function setupFilters() {
document.querySelectorAll('.filter-chip').forEach(chip => {
chip.addEventListener('click', () => {
state.statusFilter = chip.dataset.status;
document.querySelectorAll('.filter-chip').forEach(c =>
c.classList.toggle('active', c.dataset.status === state.statusFilter)
);
renderAssets(state.assets);
});
});
document.getElementById('sortSelect').addEventListener('change', e => {
state.sortBy = e.target.value;
renderAssets(state.assets);
});
}
// ── New project ─────────────────────────── // ── New project ───────────────────────────
async function saveProject() { async function saveProject() {
const name = document.getElementById('newProjectName').value.trim(); const name = document.getElementById('newProjectName').value.trim();