Skip to content

Commit 7cdae09

Browse files
callestenfeltclaude
andcommitted
Add mobile-responsive filter overlay (Patagonia-style)
- Full-screen slide-in filter panel on mobile (≤768px) - "Filter" button in topbar opens overlay - "Show X Results" sticky button closes overlay - Sort options inside mobile filter panel - Chevron arrows on collapsible sections - Responsive grid: 2 cols tablet, 1 col mobile - Count format updated to (n) style Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 50064f1 commit 7cdae09

File tree

3 files changed

+275
-15
lines changed

3 files changed

+275
-15
lines changed

app.js

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
const CSV_PATH = "./pages.csv";
22

3-
// Element
3+
// Elements
44
const gridEl = document.getElementById("grid");
55
const groupsEl = document.getElementById("filterGroups");
66
const countEl = document.getElementById("resultsCount");
77
const sortEl = document.getElementById("sortSelect");
88
const clearBtn = document.getElementById("clearBtn");
99
const activeFiltersEl = document.getElementById("activeFilters");
1010

11+
// Mobile filter elements
12+
const filtersEl = document.querySelector(".filters");
13+
const filterToggleBtn = document.getElementById("filterToggle");
14+
const filterCloseBtn = document.getElementById("filterClose");
15+
const filterOverlay = document.getElementById("filterOverlay");
16+
const showResultsBtn = document.getElementById("showResultsBtn");
17+
const sortMobileRadios = document.querySelectorAll('input[name="sortMobile"]');
18+
1119
// State
1220
let rows = [];
1321
const state = {
@@ -226,7 +234,7 @@ function renderActiveFilters(){
226234
const k = span.dataset.countKey;
227235
const v = span.dataset.countValue;
228236
const n = counts[k]?.get(v) ?? 0;
229-
span.textContent = `${n}`;
237+
span.textContent = `(${n})`;
230238
});
231239
}
232240

@@ -330,7 +338,8 @@ function render(){
330338

331339
renderActiveFilters();
332340
computeCounts();
333-
341+
updateShowResultsBtn();
342+
334343
gridEl.innerHTML = "";
335344
sorted.forEach(r=> gridEl.appendChild(renderCard(r)));
336345
}
@@ -345,13 +354,54 @@ function clearAll(){
345354
render();
346355
}
347356

357+
// Mobile filter overlay functions
358+
function openFilters(){
359+
filtersEl.classList.add("is-open");
360+
filterOverlay.classList.add("is-active");
361+
document.body.style.overflow = "hidden";
362+
}
363+
364+
function closeFilters(){
365+
filtersEl.classList.remove("is-open");
366+
filterOverlay.classList.remove("is-active");
367+
document.body.style.overflow = "";
368+
}
369+
370+
function updateShowResultsBtn(){
371+
const count = rows.filter(matchesFilters).length;
372+
showResultsBtn.textContent = `Show ${count} Results`;
373+
}
374+
375+
function syncSortState(value){
376+
state.sort = value;
377+
// Sync desktop select
378+
sortEl.value = value;
379+
// Sync mobile radios
380+
sortMobileRadios.forEach(radio => {
381+
radio.checked = radio.value === value;
382+
});
383+
render();
384+
}
385+
348386
// Events
349387
sortEl.addEventListener("change", (e)=>{
350-
state.sort = e.target.value;
351-
render();
388+
syncSortState(e.target.value);
352389
});
353390
clearBtn.addEventListener("click", clearAll);
354391

392+
// Mobile filter events
393+
filterToggleBtn.addEventListener("click", openFilters);
394+
filterCloseBtn.addEventListener("click", closeFilters);
395+
filterOverlay.addEventListener("click", closeFilters);
396+
showResultsBtn.addEventListener("click", closeFilters);
397+
398+
// Mobile sort radio events
399+
sortMobileRadios.forEach(radio => {
400+
radio.addEventListener("change", (e) => {
401+
syncSortState(e.target.value);
402+
});
403+
});
404+
355405
if (!window.Papa) {
356406
countEl.textContent = "CSV parser not loaded";
357407
}

index.html

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,53 @@
2020
<div class="brand__title">Mips content explorer</div>
2121
</div>
2222

23-
<div class="sort">
24-
<label class="sort__label" for="sortSelect">Sort</label>
25-
<select id="sortSelect" class="sort__select">
26-
<option value="relevant" selected>Most relevant</option>
27-
<option value="newest">Newest</option>
28-
</select>
23+
<div class="topbar__actions">
24+
<button id="filterToggle" class="btn btn--filter" type="button" aria-label="Open filters">Filter</button>
25+
<div class="sort">
26+
<label class="sort__label" for="sortSelect">Sort</label>
27+
<select id="sortSelect" class="sort__select">
28+
<option value="relevant" selected>Most relevant</option>
29+
<option value="newest">Newest</option>
30+
</select>
31+
</div>
2932
</div>
3033
</div>
3134
</header>
3235

3336
<main class="layout">
37+
<div id="filterOverlay" class="filter-overlay"></div>
3438
<aside class="filters" aria-label="Filters">
3539
<div class="filters__header">
36-
<div class="filters__title">Filter</div>
37-
<button id="clearBtn" class="btn btn--ghost" type="button" aria-label="Clear all filters">Clear</button>
40+
<div class="filters__title">Filter & Sort</div>
41+
<div class="filters__header-actions">
42+
<button id="clearBtn" class="btn btn--ghost" type="button" aria-label="Clear all filters">Clear All</button>
43+
<button id="filterClose" class="btn btn--close" type="button" aria-label="Close filters">&times;</button>
44+
</div>
45+
</div>
46+
47+
<div class="filters__body">
48+
<div id="sortGroup" class="sort-group">
49+
<details>
50+
<summary>Sort By</summary>
51+
<div class="group">
52+
<label class="check">
53+
<input type="radio" name="sortMobile" value="relevant" checked>
54+
<span>Most relevant</span>
55+
</label>
56+
<label class="check">
57+
<input type="radio" name="sortMobile" value="newest">
58+
<span>Newest</span>
59+
</label>
60+
</div>
61+
</details>
62+
</div>
63+
64+
<div id="filterGroups" class="filter-groups"></div>
65+
</div>
66+
67+
<div class="filters__footer">
68+
<button id="showResultsBtn" class="btn btn--primary" type="button">Show Results</button>
3869
</div>
39-
40-
<div id="filterGroups" class="filter-groups"></div>
4170
</aside>
4271

4372
<section class="results" aria-label="Results">

styles.css

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,20 @@ summary{
138138
cursor: pointer;
139139
font-weight: 600;
140140
list-style: none;
141+
display: flex;
142+
justify-content: space-between;
143+
align-items: center;
141144
}
142145
summary::-webkit-details-marker{ display:none; }
146+
summary::after{
147+
content: "▼";
148+
font-size: 10px;
149+
color: var(--muted);
150+
transition: transform 0.2s ease;
151+
}
152+
details[open] summary::after{
153+
transform: rotate(180deg);
154+
}
143155

144156
.group{
145157
padding: 8px 12px 12px 12px;
@@ -299,4 +311,173 @@ button.chip:hover{
299311
color: var(--muted);
300312
font-size: 14px;
301313
margin-right: 4px;
314+
}
315+
316+
/* Filter toggle button (mobile only) */
317+
.btn--filter{
318+
background: var(--text);
319+
color: #fff;
320+
border: none;
321+
font-weight: 500;
322+
}
323+
.btn--filter:hover{
324+
background: #333;
325+
}
326+
327+
/* Close button */
328+
.btn--close{
329+
display: none;
330+
background: transparent;
331+
border: none;
332+
font-size: 28px;
333+
line-height: 1;
334+
padding: 4px 8px;
335+
color: var(--text);
336+
}
337+
338+
/* Primary button (Show Results) */
339+
.btn--primary{
340+
background: var(--text);
341+
color: #fff;
342+
border: none;
343+
font-weight: 500;
344+
width: 100%;
345+
padding: 16px 24px;
346+
font-size: 16px;
347+
}
348+
.btn--primary:hover{
349+
background: #333;
350+
}
351+
352+
/* Filter overlay backdrop */
353+
.filter-overlay{
354+
display: none;
355+
position: fixed;
356+
inset: 0;
357+
background: rgba(0,0,0,0.5);
358+
z-index: 99;
359+
}
360+
.filter-overlay.is-active{
361+
display: block;
362+
}
363+
364+
/* Sort group inside filters (mobile only) */
365+
.sort-group{
366+
display: none;
367+
}
368+
369+
/* Filters body wrapper */
370+
.filters__body{
371+
flex: 1;
372+
overflow: auto;
373+
}
374+
375+
/* Filters footer with sticky button (mobile only) */
376+
.filters__footer{
377+
display: none;
378+
}
379+
380+
/* Topbar actions wrapper */
381+
.topbar__actions{
382+
display: flex;
383+
align-items: center;
384+
gap: 16px;
385+
}
386+
387+
/* Hide filter toggle on desktop */
388+
#filterToggle{
389+
display: none;
390+
}
391+
392+
/* Desktop: hide header actions that are mobile-only */
393+
.filters__header-actions{
394+
display: flex;
395+
align-items: center;
396+
gap: 8px;
397+
}
398+
399+
/* ========================
400+
MOBILE STYLES (768px)
401+
======================== */
402+
@media (max-width: 768px){
403+
/* Show filter toggle button */
404+
#filterToggle{
405+
display: inline-flex;
406+
}
407+
408+
/* Hide desktop sort in topbar */
409+
.topbar__actions .sort{
410+
display: none;
411+
}
412+
413+
/* Layout: single column, no sidebar */
414+
.layout{
415+
grid-template-columns: 1fr;
416+
padding: 12px;
417+
}
418+
419+
/* Grid: 2 columns on tablet, 1 on small mobile */
420+
.grid{
421+
grid-template-columns: repeat(2, 1fr);
422+
gap: 12px;
423+
}
424+
425+
/* Filters: full-screen overlay */
426+
.filters{
427+
position: fixed;
428+
inset: 0;
429+
z-index: 100;
430+
border-radius: 0;
431+
border: none;
432+
height: 100%;
433+
display: flex;
434+
flex-direction: column;
435+
transform: translateX(-100%);
436+
transition: transform 0.3s ease;
437+
}
438+
.filters.is-open{
439+
transform: translateX(0);
440+
}
441+
442+
/* Show close button */
443+
.btn--close{
444+
display: block;
445+
}
446+
447+
/* Show sort group in mobile overlay */
448+
.sort-group{
449+
display: block;
450+
margin-bottom: 10px;
451+
}
452+
453+
/* Show sticky footer */
454+
.filters__footer{
455+
display: block;
456+
padding: 12px 14px;
457+
border-top: 1px solid var(--border);
458+
background: var(--surface);
459+
}
460+
461+
/* Update header for mobile */
462+
.filters__header{
463+
padding-bottom: 14px;
464+
border-bottom: 1px solid var(--border);
465+
}
466+
.filters__title{
467+
font-size: 18px;
468+
}
469+
470+
/* Results meta: stack on mobile */
471+
.results__meta{
472+
flex-direction: column;
473+
align-items: flex-start;
474+
gap: 8px;
475+
}
476+
}
477+
478+
/* Small mobile: single column grid */
479+
@media (max-width: 480px){
480+
.grid{
481+
grid-template-columns: 1fr;
482+
}
302483
}

0 commit comments

Comments
 (0)