Collapse status and filter chip bars on mobile

Both chip bars now start collapsed with a summary header showing
the active selection (e.g. "In Progress" / "Rooms view · Mine only").
Tap the header to expand and change filters.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-20 22:35:30 +00:00
parent 06d1d01f91
commit fc993c63bc
2 changed files with 85 additions and 35 deletions

View file

@ -261,7 +261,28 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
.badge-unusable { background: var(--danger); } .badge-unusable { background: var(--danger); }
/* ── Filter chips ──────────────────────────────────────────── */ /* ── Filter chips ──────────────────────────────────────────── */
.chip-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; align-items: center; } .filter-section { margin-bottom: 10px; }
.filter-section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 8px;
background: var(--card-bg);
border: 1px solid var(--card-border);
cursor: pointer;
user-select: none;
font-size: 12px;
color: var(--text-mid);
transition: border-color .12s;
}
.filter-section-header:hover { border-color: var(--gold); }
.filter-section-label { font-weight: 600; color: var(--text-dark); white-space: nowrap; }
.filter-section-summary { flex: 1; color: var(--gold); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filter-section-summary.muted { color: var(--text-mid); font-weight: 400; }
.filter-section-chevron { flex-shrink: 0; transition: transform .18s; }
.filter-section-chevron.open { transform: rotate(180deg); }
.chip-bar { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 4px; align-items: center; }
.chip { .chip {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { Plus, RefreshCw, Camera, BedDouble, Layers } from 'lucide-react' import { Plus, RefreshCw, Camera, BedDouble, Layers, ChevronDown } from 'lucide-react'
import type { Task, Category, Location, AppConfig, TaskStatus } from '../types' import type { Task, Category, Location, AppConfig, TaskStatus } from '../types'
import { STATUS_LABELS, can } from '../types' import { STATUS_LABELS, can } from '../types'
import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api' import { fetchTasks, fetchLocations, fetchConfig, fetchOccupancy } from '../api'
@ -27,6 +27,9 @@ export default function Summary() {
const [roomsView, setRoomsView] = useState(false) const [roomsView, setRoomsView] = useState(false)
const [groupByLocation, setGroupByLocation] = useState(false) const [groupByLocation, setGroupByLocation] = useState(false)
const [statusOpen, setStatusOpen] = useState(false)
const [filtersOpen, setFiltersOpen] = useState(false)
const [showNew, setShowNew] = useState(false) const [showNew, setShowNew] = useState(false)
const [openTask, setOpenTask] = useState<number | null>(null) const [openTask, setOpenTask] = useState<number | null>(null)
@ -118,42 +121,68 @@ export default function Summary() {
{error && <div className="error-banner">{error}</div>} {error && <div className="error-banner">{error}</div>}
<div className="chip-bar"> <div className="filter-section">
<button className={`chip ${statusFilter === null ? 'active' : ''}`} onClick={() => setStatusFilter(null)}> <div className="filter-section-header" onClick={() => setStatusOpen(v => !v)}>
All open <span className="filter-section-label">Status</span>
</button> <span className={`filter-section-summary${statusFilter === null ? ' muted' : ''}`}>
{OPEN_STATUSES.map(s => ( {statusFilter === null ? 'All open' : `${STATUS_LABELS[statusFilter]}${counts[statusFilter] ? ` (${counts[statusFilter]})` : ''}`}
<button key={s} className={`chip ${statusFilter === s ? 'active' : ''}`} onClick={() => setStatusFilter(statusFilter === s ? null : s)}> </span>
{STATUS_LABELS[s]}{counts[s] ? ` (${counts[s]})` : ''} <ChevronDown size={14} strokeWidth={1.75} className={`filter-section-chevron${statusOpen ? ' open' : ''}`} />
</button> </div>
))} {statusOpen && (
<div className="chip-bar">
<button className={`chip ${statusFilter === null ? 'active' : ''}`} onClick={() => setStatusFilter(null)}>
All open
</button>
{OPEN_STATUSES.map(s => (
<button key={s} className={`chip ${statusFilter === s ? 'active' : ''}`} onClick={() => setStatusFilter(statusFilter === s ? null : s)}>
{STATUS_LABELS[s]}{counts[s] ? ` (${counts[s]})` : ''}
</button>
))}
</div>
)}
</div> </div>
<div className="chip-bar"> <div className="filter-section">
{/* Category chips hidden while in rooms-view (rooms filter is the active "category") */} <div className="filter-section-header" onClick={() => setFiltersOpen(v => !v)}>
{!roomsView && categories.map(c => ( <span className="filter-section-label">Filters</span>
<button key={c.id} className={`chip ${categoryFilter === c.id ? 'active' : ''}`} onClick={() => setCategoryFilter(categoryFilter === c.id ? null : c.id)}> <span className={`filter-section-summary${!categoryFilter && !mineOnly && !roomsView && !groupByLocation ? ' muted' : ''}`}>
{c.name}{categoryCounts[c.id] ? ` (${categoryCounts[c.id]})` : ''} {[
</button> !roomsView && categoryFilter ? categories.find(c => c.id === categoryFilter)?.name : null,
))} roomsView ? 'Rooms view' : null,
<button className={`chip ${mineOnly ? 'active' : ''}`} onClick={() => setMineOnly(!mineOnly)}> mineOnly ? 'Mine only' : null,
Mine groupByLocation ? 'Grouped' : null,
</button> ].filter(Boolean).join(' · ') || 'All categories'}
{roomsCategoryExists && ( </span>
<button <ChevronDown size={14} strokeWidth={1.75} className={`filter-section-chevron${filtersOpen ? ' open' : ''}`} />
className={`chip ${roomsView ? 'active' : ''}`} </div>
title="Show all rooms tasks — greyed tasks have an in-house guest so access may be limited" {filtersOpen && (
onClick={toggleRoomsView} <div className="chip-bar">
> {!roomsView && categories.map(c => (
<BedDouble size={13} strokeWidth={1.75} /> <button key={c.id} className={`chip ${categoryFilter === c.id ? 'active' : ''}`} onClick={() => setCategoryFilter(categoryFilter === c.id ? null : c.id)}>
{roomsView && roomsCounts {c.name}{categoryCounts[c.id] ? ` (${categoryCounts[c.id]})` : ''}
? `Rooms (${roomsCounts.free} free${roomsCounts.arriving ? `${roomsCounts.arriving} arriving` : ''}${roomsCounts.occupied ? `${roomsCounts.occupied} occupied` : ''})` </button>
: 'Rooms Accessible View'} ))}
</button> <button className={`chip ${mineOnly ? 'active' : ''}`} onClick={() => setMineOnly(!mineOnly)}>
Mine
</button>
{roomsCategoryExists && (
<button
className={`chip ${roomsView ? 'active' : ''}`}
title="Show all rooms tasks — greyed tasks have an in-house guest so access may be limited"
onClick={toggleRoomsView}
>
<BedDouble size={13} strokeWidth={1.75} />
{roomsView && roomsCounts
? `Rooms (${roomsCounts.free} free${roomsCounts.arriving ? `${roomsCounts.arriving} arriving` : ''}${roomsCounts.occupied ? `${roomsCounts.occupied} occupied` : ''})`
: 'Rooms Accessible View'}
</button>
)}
<button className={`chip ${groupByLocation ? 'active' : ''}`} onClick={() => setGroupByLocation(v => !v)}>
<Layers size={13} strokeWidth={1.75} /> Group by location
</button>
</div>
)} )}
<button className={`chip ${groupByLocation ? 'active' : ''}`} onClick={() => setGroupByLocation(v => !v)}>
<Layers size={13} strokeWidth={1.75} /> Group by location
</button>
</div> </div>
{tasks.length === 0 && !loading && ( {tasks.length === 0 && !loading && (