← back to Whatsmystyle
yolo tick 29: brands page sort selector (Most pieces / A→Z / Sustainability ↓) + density slider (160-320px, --card-min CSS var, localStorage persist, hydrated before first render) + × close button on bulk-upload progress strip (clears poll timer + fade-out) + aria-live=polite on closet-upload-status (screen-reader announces upload progress)
3c7e580f86c5bd716f29647185d024db26c9e1b0 · 2026-05-12 16:14:58 -0700 · SteveStudio2
Files touched
M data/waitlist.csvM data/whatsmystyle.db-shmM data/whatsmystyle.db-walM public/brands.htmlM public/css/app.cssM public/index.htmlM public/js/app.js
Diff
commit 3c7e580f86c5bd716f29647185d024db26c9e1b0
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 16:14:58 2026 -0700
yolo tick 29: brands page sort selector (Most pieces / A→Z / Sustainability ↓) + density slider (160-320px, --card-min CSS var, localStorage persist, hydrated before first render) + × close button on bulk-upload progress strip (clears poll timer + fade-out) + aria-live=polite on closet-upload-status (screen-reader announces upload progress)
---
data/waitlist.csv | 2 ++
data/whatsmystyle.db-shm | Bin 32768 -> 32768 bytes
data/whatsmystyle.db-wal | Bin 4610312 -> 4610312 bytes
public/brands.html | 79 ++++++++++++++++++++++++++++++++++++++++-------
public/css/app.css | 7 +++++
public/index.html | 2 +-
public/js/app.js | 9 +++++-
7 files changed, 86 insertions(+), 13 deletions(-)
diff --git a/data/waitlist.csv b/data/waitlist.csv
index 94b35a3..149d0be 100644
--- a/data/waitlist.csv
+++ b/data/waitlist.csv
@@ -63,3 +63,5 @@ e2e-1778622743627@example.com,e2e,2026-05-12T21:52:23.636Z
e2e-1778623219818@example.com,e2e,2026-05-12T22:00:19.824Z
e2e-1778625279476@example.com,e2e,2026-05-12T22:34:39.477Z
e2e-1778625542846@example.com,e2e,2026-05-12T22:39:02.846Z
+e2e-1778627519615@example.com,e2e,2026-05-12T23:11:59.620Z
+e2e-1778627684022@example.com,e2e,2026-05-12T23:14:44.022Z
diff --git a/data/whatsmystyle.db-shm b/data/whatsmystyle.db-shm
index d52509b..b8ac341 100644
Binary files a/data/whatsmystyle.db-shm and b/data/whatsmystyle.db-shm differ
diff --git a/data/whatsmystyle.db-wal b/data/whatsmystyle.db-wal
index 33e7980..3a67447 100644
Binary files a/data/whatsmystyle.db-wal and b/data/whatsmystyle.db-wal differ
diff --git a/public/brands.html b/public/brands.html
index d1dd355..58fbb9d 100644
--- a/public/brands.html
+++ b/public/brands.html
@@ -12,6 +12,19 @@
.nav { margin-bottom: 18px; }
.nav a { color: #707070; text-decoration: none; font-size: 14px; }
.nav a:hover { color: #1d1d1f; }
+
+ .controls {
+ display: flex; flex-wrap: wrap; align-items: center; gap: 14px;
+ margin-bottom: 14px; padding: 12px 14px;
+ background: #fff; border: 1px solid #e6e1d8; border-radius: 12px;
+ }
+ .controls label { font-size: 12px; color: #707070; display: inline-flex; align-items: center; gap: 8px; }
+ .controls select {
+ font-family: inherit; font-size: 13px; padding: 6px 10px;
+ border: 1px solid #c8c1b3; border-radius: 8px; background: #faf7f2; color: #1d1d1f;
+ }
+ .controls input[type=range] { width: 140px; }
+
.filter-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px; }
.filter-pill {
background: #fff; border: 1px solid #e6e1d8; border-radius: 999px;
@@ -22,7 +35,11 @@
.filter-pill.is-active { background: #1d1d1f; color: #fff; border-color: #1d1d1f; }
.filter-pill .pill-count { color: #999; font-variant-numeric: tabular-nums; margin-left: 4px; font-size: 11px; }
.filter-pill.is-active .pill-count { color: #aaa; }
- .brand-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
+
+ .brand-grid {
+ display: grid; gap: 12px;
+ grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 220px), 1fr));
+ }
.brand-card {
background: #fff; border: 1px solid #e6e1d8; border-radius: 16px;
padding: 18px 22px; text-decoration: none; color: inherit;
@@ -34,7 +51,7 @@
.brand-card .count { font-size: 12px; color: #707070; font-variant-numeric: tabular-nums; }
.brand-card .tier {
font-size: 11px; padding: 2px 8px; border-radius: 999px;
- background: #f3eee2; color: #6e4f0d; margin-top: 4px;
+ background: #f3eee2; color: #6e4f0d; margin-top: 4px; display: inline-block;
}
.brand-card .tier-5 { background: #d4f4d4; color: #1d5e1d; }
.brand-card .tier-1 { background: #fbe4e4; color: #7a1717; }
@@ -44,7 +61,22 @@
<body>
<div class="nav"><a href="/">← back to app</a></div>
<h1>Brands</h1>
- <p class="sub" id="sub">Every brand in our catalog, sorted by piece count.</p>
+ <p class="sub" id="sub">Every brand in our catalog.</p>
+
+ <div class="controls" id="controls" hidden>
+ <label>Sort
+ <select id="sort">
+ <option value="pieces">Most pieces</option>
+ <option value="az">A → Z</option>
+ <option value="sustain">Sustainability ↓</option>
+ </select>
+ </label>
+ <label>Density
+ <input id="density" type="range" min="160" max="320" step="20" value="220" aria-label="Card density" />
+ <span id="density-val" style="font-variant-numeric: tabular-nums; color: #999;">220px</span>
+ </label>
+ </div>
+
<div id="filters" class="filter-row" hidden></div>
<div id="grid" class="brand-grid"></div>
@@ -53,15 +85,21 @@
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
const FILTER_KEY = 'wms.brands.sustain_filter';
+ const SORT_KEY = 'wms.brands.sort';
+ const DENSITY_KEY= 'wms.brands.density';
let ALL = [];
- function currentFilter() {
- return localStorage.getItem(FILTER_KEY) || 'all';
- }
- function setFilter(v) {
- localStorage.setItem(FILTER_KEY, v);
- render();
+ function currentFilter() { return localStorage.getItem(FILTER_KEY) || 'all'; }
+ function currentSort() { return localStorage.getItem(SORT_KEY) || 'pieces'; }
+ function currentDensity() { return Number(localStorage.getItem(DENSITY_KEY) || 220); }
+ function setFilter(v) { localStorage.setItem(FILTER_KEY, v); render(); }
+ function setSort(v) { localStorage.setItem(SORT_KEY, v); render(); }
+ function setDensity(n) {
+ localStorage.setItem(DENSITY_KEY, String(n));
+ document.documentElement.style.setProperty('--card-min', `${n}px`);
+ $('#density-val').textContent = `${n}px`;
}
+
function matches(b, f) {
if (f === 'all') return true;
if (f === 'none') return !b.sustain_tier;
@@ -70,9 +108,17 @@
if (f === '3plus') return (b.sustain_tier || 0) >= 3;
return true;
}
+ function sortList(list, mode) {
+ const arr = list.slice();
+ if (mode === 'az') arr.sort((a, b) => a.brand.localeCompare(b.brand));
+ else if (mode === 'sustain') arr.sort((a, b) => (b.sustain_tier || 0) - (a.sustain_tier || 0) || b.item_count - a.item_count);
+ else arr.sort((a, b) => b.item_count - a.item_count); // 'pieces'
+ return arr;
+ }
function render() {
const f = currentFilter();
+ const s = currentSort();
const counts = {
all: ALL.length,
'5': ALL.filter(b => b.sustain_tier === 5).length,
@@ -95,8 +141,10 @@
</button>
`).join('');
- const filtered = ALL.filter(b => matches(b, f));
- $('#sub').textContent = `${filtered.length} of ${ALL.length} brand${ALL.length === 1 ? '' : 's'} · sorted by piece count.`;
+ $('#sort').value = s;
+ const filtered = sortList(ALL.filter(b => matches(b, f)), s);
+ const sortLabel = { pieces: 'most pieces', az: 'A→Z', sustain: 'sustainability' }[s];
+ $('#sub').textContent = `${filtered.length} of ${ALL.length} brand${ALL.length === 1 ? '' : 's'} · sorted by ${sortLabel}.`;
if (!filtered.length) {
$('#grid').innerHTML = '<div class="empty">No brands match this filter.</div>';
@@ -123,6 +171,15 @@
const r = await fetch('/api/brands');
const j = await r.json();
ALL = j.brands || [];
+ $('#controls').hidden = false;
+ // Hydrate sort + density BEFORE first render (standing rule M-6/M-7).
+ const d = currentDensity();
+ document.documentElement.style.setProperty('--card-min', `${d}px`);
+ $('#density').value = d;
+ $('#density-val').textContent = `${d}px`;
+ $('#sort').value = currentSort();
+ $('#sort').addEventListener('change', () => setSort($('#sort').value));
+ $('#density').addEventListener('input', () => setDensity(Number($('#density').value)));
render();
})();
</script>
diff --git a/public/css/app.css b/public/css/app.css
index ddaa571..c6c5730 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -155,6 +155,13 @@ h2 { font-family: var(--font-display); font-weight: 600; font-size: 36px; margin
.bulk-progress .dot-progress-label {
margin-left: auto; color: #707070; font-variant-numeric: tabular-nums;
}
+.bulk-progress .bulk-progress-close {
+ background: transparent; border: 0; color: #999; cursor: pointer;
+ font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 4px;
+ font-family: inherit;
+}
+.bulk-progress .bulk-progress-close:hover { color: #1d1d1f; background: rgba(0,0,0,.05); }
+.bulk-progress.is-dismissed { opacity: 0; transition: opacity .2s ease; pointer-events: none; }
@media (hover: none) and (pointer: coarse) {
.kb-hint { display: none !important; }
}
diff --git a/public/index.html b/public/index.html
index 02d53bc..8a1748b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -113,7 +113,7 @@
<option value="">— untagged —</option>
</select>
<button type="submit" id="closet-upload-btn">Upload</button>
- <span id="closet-upload-status" class="muted"></span>
+ <span id="closet-upload-status" class="muted" role="status" aria-live="polite" aria-atomic="true"></span>
</form>
<div id="closet-grid" class="grid"></div>
</section>
diff --git a/public/js/app.js b/public/js/app.js
index 6db78ab..b3cdf3d 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -893,8 +893,15 @@ function startBulkProgress(ids) {
(status?.parentNode || $('#closet-form')).insertBefore(host, status?.nextSibling || null);
}
host.hidden = false;
+ host.classList.remove('is-dismissed');
host.innerHTML = ids.map(id => `<span class="dot" data-cid="${id}" aria-label="processing ${id}"></span>`).join('') +
- `<span class="dot-progress-label" id="bulk-progress-label">0 / ${ids.length} tagged</span>`;
+ `<span class="dot-progress-label" id="bulk-progress-label">0 / ${ids.length} tagged</span>` +
+ `<button type="button" class="bulk-progress-close" id="bulk-progress-close" aria-label="Dismiss progress strip">×</button>`;
+ $('#bulk-progress-close')?.addEventListener('click', () => {
+ if (bulkProgressTimer) { clearInterval(bulkProgressTimer); bulkProgressTimer = null; }
+ host.classList.add('is-dismissed');
+ setTimeout(() => { host.hidden = true; }, 250);
+ });
const idSet = new Set(ids);
async function tick() {
try {
← 636d4e8 yolo tick 28: brands sustainability filter pills (All/♻5/♻4/
·
back to Whatsmystyle
·
yolo tick 30: recs grid server-side sort (best/sustain/newes 6515824 →