From 71698f3248e76b11d32b3eb36768347341bdfceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 7 Jul 2026 23:29:24 +0200 Subject: [PATCH 1/3] feat(documents): replace inline type-chip row with compact dropdown filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vault filter bar rendered the type/condition/year facets as a horizontally-scrolling chip set beside the search field, which wrapped to a second line on phones. Replace it with three compact dropdown facets — type (multi-select), condition and year (single-select) — that share one non-wrapping row with the flexing search and the clear control at every width. The content-search hint keeps its own helper line below. --- messages/de.json | 6 +- messages/en.json | 6 +- messages/es.json | 6 +- messages/fr.json | 6 +- messages/it.json | 6 +- messages/pl.json | 6 +- .../__tests__/documents-view-states.test.tsx | 21 ++ .../documents/document-filter-bar.tsx | 257 +++++++++++------- 8 files changed, 212 insertions(+), 102 deletions(-) diff --git a/messages/de.json b/messages/de.json index 4fa9fe2f1..3e310cb6e 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8048,7 +8048,11 @@ "typeGroup": "Dokumenttyp", "conditionGroup": "Erkrankung", "yearGroup": "Jahr", - "clear": "Filter zurücksetzen" + "clear": "Filter zurücksetzen", + "typeAll": "Alle Typen", + "typeCount": "{count} Typen", + "conditionAll": "Alle Erkrankungen", + "yearAll": "Alle Jahre" }, "card": { "untitled": "Unbenanntes Dokument", diff --git a/messages/en.json b/messages/en.json index 11139b420..40376a2a6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8048,7 +8048,11 @@ "typeGroup": "Document type", "conditionGroup": "Condition", "yearGroup": "Year", - "clear": "Clear filters" + "clear": "Clear filters", + "typeAll": "All types", + "typeCount": "{count} types", + "conditionAll": "All conditions", + "yearAll": "All years" }, "card": { "untitled": "Untitled document", diff --git a/messages/es.json b/messages/es.json index 771e37580..8ff0241c5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8048,7 +8048,11 @@ "typeGroup": "Tipo de documento", "conditionGroup": "Afección", "yearGroup": "Año", - "clear": "Borrar filtros" + "clear": "Borrar filtros", + "typeAll": "Todos los tipos", + "typeCount": "{count} tipos", + "conditionAll": "Todas las afecciones", + "yearAll": "Todos los años" }, "card": { "untitled": "Documento sin título", diff --git a/messages/fr.json b/messages/fr.json index 251d74cb5..0fa043dde 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8048,7 +8048,11 @@ "typeGroup": "Type de document", "conditionGroup": "Affection", "yearGroup": "Année", - "clear": "Effacer les filtres" + "clear": "Effacer les filtres", + "typeAll": "Tous les types", + "typeCount": "{count} types", + "conditionAll": "Toutes les affections", + "yearAll": "Toutes les années" }, "card": { "untitled": "Document sans titre", diff --git a/messages/it.json b/messages/it.json index 3fc8f452a..3b5ed6961 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8048,7 +8048,11 @@ "typeGroup": "Tipo di documento", "conditionGroup": "Patologia", "yearGroup": "Anno", - "clear": "Cancella filtri" + "clear": "Cancella filtri", + "typeAll": "Tutti i tipi", + "typeCount": "{count} tipi", + "conditionAll": "Tutte le patologie", + "yearAll": "Tutti gli anni" }, "card": { "untitled": "Documento senza titolo", diff --git a/messages/pl.json b/messages/pl.json index fd8299a7a..06dba2454 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8048,7 +8048,11 @@ "typeGroup": "Typ dokumentu", "conditionGroup": "Schorzenie", "yearGroup": "Rok", - "clear": "Wyczyść filtry" + "clear": "Wyczyść filtry", + "typeAll": "Wszystkie typy", + "typeCount": "{count} typy", + "conditionAll": "Wszystkie schorzenia", + "yearAll": "Wszystkie lata" }, "card": { "untitled": "Dokument bez tytułu", diff --git a/src/components/documents/__tests__/documents-view-states.test.tsx b/src/components/documents/__tests__/documents-view-states.test.tsx index 0bfbd899d..56c270cf1 100644 --- a/src/components/documents/__tests__/documents-view-states.test.tsx +++ b/src/components/documents/__tests__/documents-view-states.test.tsx @@ -80,6 +80,27 @@ describe(" states", () => { expect(html).not.toContain("Your document vault is empty"); }); + it("filter bar: type facet is a compact dropdown, not an inline chip row", () => { + const html = render(""); + // The type facet renders as a single dropdown trigger labelled by the + // active selection ("All types" while nothing is picked)… + expect(html).toContain('data-slot="document-type-filter"'); + expect(html).toContain("All types"); + // …and the controls share ONE row — the container never wraps. + expect(html).toContain("flex-nowrap"); + // The old horizontal chip scroller is gone. + expect(html).not.toContain("overflow-x-auto"); + }); + + it("filter bar: an active type selection labels the dropdown trigger", () => { + const html = render("kind=IMAGING", { kinds: ["IMAGING"] }); + // Selection flows into the URL (→ the list query) AND surfaces on the + // trigger label rather than a pressed chip. + expect(html).toContain('data-slot="document-type-filter"'); + expect(html).toContain(">Imaging<"); + expect(html).not.toContain("All types"); + }); + it("empty unfiltered corpus: the teaching empty state with an upload CTA", () => { const html = render("", {}); expect(html).toContain('data-slot="empty-state"'); diff --git a/src/components/documents/document-filter-bar.tsx b/src/components/documents/document-filter-bar.tsx index c71ef91df..c3f068873 100644 --- a/src/components/documents/document-filter-bar.tsx +++ b/src/components/documents/document-filter-bar.tsx @@ -1,30 +1,46 @@ "use client"; /** - * The vault's sticky filter rail: debounced title/filename search, the - * nine-kind type-chip row (multi-select, OR inside the facet), condition - * chips (episodes that actually carry links), a year segmenter (years - * present in the corpus), and a one-tap clear with the active-facet count. + * The vault's sticky filter rail: debounced title/filename search plus three + * compact dropdown facets — type (multi-select, OR inside the facet), + * condition (episodes that actually carry links, single-select), and year + * (years present in the corpus, single-select) — and a one-tap clear with + * the active-facet count. + * + * The controls sit on ONE row at every width: the search flexes and can + * shrink to nothing while each dropdown trigger stays a fixed compact + * control, so the bar never wraps to a second line and never grows a + * horizontal chip scroller. The content-search hint (indexed-content match + * + corpus backfill) rides its own helper line below, only when indexing is + * live. * * Purely presentational — the filter state lives in the page URL and is * owned by `documents-view.tsx`; this bar only renders the controls. It * sticks below the top edge inside the shell's scroll container (sticky, * translucent background, border-b — no viewport-height tricks). */ -import { ScanSearch, Search, X } from "lucide-react"; +import { ChevronDown, ScanSearch, Search, X } from "lucide-react"; import type { RefObject } from "react"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; import type { InboundDocumentKindValue } from "@/lib/validations/inbound-documents"; import { DOCUMENT_KIND_ICONS, DOCUMENT_KIND_ORDER } from "./document-kind-meta"; -/** Shared chip chrome — mirrors the app-wide FilterBar pill vocabulary. */ -const CHIP_CLASSES = - "border-border bg-card text-foreground inline-flex min-h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-3 text-sm whitespace-nowrap shadow-xs transition-colors focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:outline-none"; -const CHIP_ACTIVE_CLASSES = "border-primary/40 bg-primary/10"; +/** Active-facet tint on a dropdown trigger — mirrors the app pill vocabulary. */ +const TRIGGER_ACTIVE = "border-primary/40 bg-primary/10 text-foreground"; +/** Shared trigger chrome: compact, fixed, never shrinks below its label. */ +const TRIGGER_CLASSES = "shrink-0 gap-1.5 font-normal"; export interface ConditionChip { episodeId: string; @@ -73,17 +89,36 @@ export function DocumentFilterBar({ }) { const { t } = useTranslations(); + const kindCount = activeKinds.size; + const typeLabel = + kindCount === 0 + ? t("documents.filter.typeAll") + : kindCount === 1 + ? t(`documents.kind.${[...activeKinds][0]}`) + : t("documents.filter.typeCount", { count: kindCount }); + + const activeCondition = conditionChips.find( + (chip) => chip.episodeId === activeEpisodeId, + ); + const conditionLabel = + activeCondition?.name ?? t("documents.filter.conditionAll"); + + const yearLabel = + activeYear !== undefined + ? String(activeYear) + : t("documents.filter.yearAll"); + return (
- {/* Search shares one row with the tag chips on desktop — it wraps to - its own line only on phone widths where a chip scroller plus an - input can't share a row. The chip scroller flexes to fill the - rest; the clear button pins to the trailing edge. */} -
-
+ {/* One row at every width: the search flexes (min-w-0 → it can shrink to + nothing), every dropdown trigger stays a fixed compact control, and + the clear pins to the trailing edge. `flex-nowrap` guarantees the bar + never breaks to a second line. */} +
+
-
- + + + + + + {t("documents.filter.typeGroup")} + + {DOCUMENT_KIND_ORDER.map((kind) => { const Icon = DOCUMENT_KIND_ICONS[kind]; - const active = activeKinds.has(kind); return ( - + ); })} - + + - {conditionChips.length > 0 ? ( - <> - - 0 ? ( + + + - ); - })} - - - ) : null} + {conditionLabel} + + + + + + {t("documents.filter.conditionGroup")} + + + {conditionChips.map((chip) => ( + onToggleEpisode(chip.episodeId)} + > + {chip.name} + + ))} + + + ) : null} - {years.length > 0 ? ( - <> - - 0 ? ( + + + - ); - })} - - - ) : null} -
+ {yearLabel} + + + + + + {t("documents.filter.yearGroup")} + + + {years.map((year) => ( + onToggleYear(year)} + className="tabular-nums" + > + {year} + + ))} + + + ) : null} {activeCount > 0 ? ( - {conditionLabel} - + + + {conditionLabel} + + - {yearLabel} - + + + {yearLabel} + + Date: Wed, 8 Jul 2026 00:23:23 +0200 Subject: [PATCH 3/3] =?UTF-8?q?chore(release):=20v1.27.25=20=E2=80=94=20do?= =?UTF-8?q?cuments=20filter=20dropdowns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef49af76b..017f68dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [1.27.25] — 2026-07-08 — Documents filter dropdowns + +### Changed + +- The Documents page filters (type, condition, year) move from an inline chip row into compact dropdowns beside the search field, so the filter bar stays on a single line at every width — on a phone the dropdowns collapse to icons rather than wrapping or scrolling. Selection behaviour is unchanged (type is multi-select; condition and year pick one). + ## [1.27.24] — 2026-07-07 — Share documents ### Added diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 80f4f580b..be34a2aa1 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.27.24 + version: 1.27.25 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index 6a06103fd..9b8fdf122 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.27.24", + "version": "1.27.25", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 7a85579c5..a75583ec7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.27.24"; + /* @sw-version-fallback */ "v1.27.25"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`