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:
parent
08e8377309
commit
1e4fcb62f5
1 changed files with 113 additions and 6 deletions
|
|
@ -93,10 +93,11 @@
|
|||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
gap: var(--sp-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: var(--text-xs);
|
||||
|
|
@ -105,7 +106,7 @@
|
|||
}
|
||||
|
||||
.search-input {
|
||||
width: 220px;
|
||||
width: 200px;
|
||||
height: 28px;
|
||||
padding: 0 var(--sp-3);
|
||||
background: var(--bg-surface);
|
||||
|
|
@ -119,6 +120,41 @@
|
|||
.search-input:focus { border-color: var(--accent-border); }
|
||||
.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 {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
@ -422,6 +458,21 @@
|
|||
<span class="asset-count" id="assetCount">0 assets</span>
|
||||
</div>
|
||||
<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 A–Z</option>
|
||||
<option value="name-desc">Name Z–A</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">
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -515,6 +566,8 @@
|
|||
assets: [],
|
||||
thumbCache: {},
|
||||
searchTerm: '',
|
||||
statusFilter: '',
|
||||
sortBy: 'newest',
|
||||
};
|
||||
|
||||
const thumbObserver = new IntersectionObserver((entries) => {
|
||||
|
|
@ -565,6 +618,7 @@
|
|||
await loadProjects();
|
||||
setupDrag();
|
||||
setupSearch();
|
||||
setupFilters();
|
||||
|
||||
// Multi-select bulk actions
|
||||
if (window.SelectionManager) {
|
||||
|
|
@ -619,6 +673,10 @@
|
|||
function handleProjectChange() {
|
||||
state.currentProjectId = document.getElementById('projectSelect').value || 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();
|
||||
}
|
||||
|
||||
|
|
@ -635,7 +693,6 @@
|
|||
|
||||
function renderBins(bins) {
|
||||
const tree = document.getElementById('binTree');
|
||||
const allItem = document.getElementById('allAssetsItem');
|
||||
tree.querySelectorAll('.bin-item:not(#allAssetsItem)').forEach(n => n.remove());
|
||||
bins.forEach(bin => {
|
||||
const el = document.createElement('a');
|
||||
|
|
@ -655,6 +712,10 @@
|
|||
|
||||
function selectBin(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();
|
||||
loadAssets();
|
||||
}
|
||||
|
|
@ -683,12 +744,40 @@
|
|||
|
||||
function renderAssets(assets) {
|
||||
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');
|
||||
grid.innerHTML = '';
|
||||
|
||||
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';
|
||||
|
||||
filtered.forEach(asset => {
|
||||
|
|
@ -729,7 +818,7 @@
|
|||
else img.style.display = 'none';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
@ -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 ───────────────────────────
|
||||
async function saveProject() {
|
||||
const name = document.getElementById('newProjectName').value.trim();
|
||||
|
|
|
|||
Loading…
Reference in a new issue