UX-MOBILE-1: FilterPanel — einklappbare Filter auf Mobil (390px)

Neues FilterPanel-Muster (src/components/FilterPanel.tsx + filterPanel.css):
- Mobil (<768px): Suchfeld immer sichtbar; alle weiteren Filter hinter einem
  'Filter (N)'-Button eingeklappt (Badge = Anzahl aktiver Nicht-Default-Filter).
  Tippen öffnet/schließt den Drawer. 'Filter zurücksetzen' im Drawer.
- Desktop (>=768px): Toggle versteckt, Drawer als display:contents → alle Controls
  inline in der bestehenden .filters-Flexzeile, wie bisher.
Auf GerbilsPage, WuerfeListPage und AnfragenPage ausgerollt.
BeckenPage/KontaktePage nur ein Suchfeld → kein Panel nötig.

e2e: filter-panel.spec.ts (8 Tests, phone+desktop); bestand/tiere/anfragen-Specs
erhalten openFilterPanel-Aufruf vor Drawer-Controls; fixtures.ts +openFilterPanel().
Vitest 78 / e2e 102 grün.

Mobil-Pass (390px, Durchsicht): Hauptproblem behoben. Beobachtete Restpunkte
für god (nicht selbst gefixed):
- .genotype-table in Genetik/Probeverpaarung: Genotyp-Strings können eng werden
- Stammbaum (react-d3-tree SVG): kein explizites mobile Viewport-Handling
- BreedingResultView Genotyp-Detail-Tabelle: ggf. horizontal scrollbar-los

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:28:09 +02:00
parent 0a1e63bec6
commit 6eb82da90c
11 changed files with 254 additions and 21 deletions

View File

@@ -0,0 +1,52 @@
import { useState, type ReactNode } from 'react'
import { de } from '../strings/de'
import './filterPanel.css'
interface FilterPanelProps {
/** Always visible on mobile (typically the search text input). Optional. */
searchField?: ReactNode
/** Collapsible filters (hidden behind toggle on mobile; inline on desktop). */
children: ReactNode
/** Number of currently active (non-default) filter values. Shown as badge. */
activeCount: number
/** Called when the reset button is clicked. */
onReset: () => void
}
/**
* UX-MOBILE-1: wraps a set of filter controls so they collapse on mobile.
* Render inside the existing `.filters` div (or replace it entirely).
*
* Desktop (>=768px): renders all children inline, identical to today.
* Mobile (<768px): shows searchField + a "Filter (N)" toggle; tapping reveals
* the rest of the controls in a column drawer + a reset button.
*/
export function FilterPanel({ searchField, children, activeCount, onReset }: FilterPanelProps) {
const [open, setOpen] = useState(false)
const t = de.filterPanel
const label = activeCount > 0 ? `${t.toggleButton} (${activeCount})` : t.toggleButton
return (
<div className={`filters filter-panel${open ? ' filter-panel--open' : ''}`}>
{searchField}
<button
type="button"
className="btn btn--ghost filter-panel__toggle"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-label={label}
>
{label}
</button>
<div className="filter-panel__drawer">
{children}
{activeCount > 0 && (
<button type="button" className="btn btn--ghost filter-panel__reset-btn" onClick={onReset}>
{t.resetButton}
</button>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,53 @@
/*
* UX-MOBILE-1: FilterPanel — collapsible filter drawer on mobile.
*
* Desktop (>=768px): toggle hidden, drawer shows as display:contents so its
* children participate directly in the parent .filters flex row.
* Mobile (<768px): searchField inline, then toggle button. Tap opens a full-
* width drawer (flex column) with the remaining filters + reset button.
*/
/* ── Mobile default ── */
.filter-panel__toggle {
display: inline-flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}
.filter-panel__drawer {
display: none;
flex-direction: column;
gap: 0.75rem;
width: 100%;
padding-top: 0.25rem;
}
.filter-panel--open .filter-panel__drawer {
display: flex;
}
/* ── Desktop ── */
@media (min-width: 768px) {
.filter-panel__toggle {
display: none;
}
.filter-panel__drawer {
/* Let children participate directly in the parent flex row. */
display: contents;
}
/* Reset button sits in the flex row on desktop when active filters exist. */
.filter-panel__reset-btn {
align-self: flex-end;
}
}
/* ── Mobile reset button ── */
@media (max-width: 767px) {
.filter-panel__reset-btn {
align-self: flex-start;
margin-top: 0.25rem;
}
}