diff --git a/e2e/documents.spec.ts b/e2e/documents.spec.ts
index 9d9cfb339..e11580fe6 100644
--- a/e2e/documents.spec.ts
+++ b/e2e/documents.spec.ts
@@ -108,7 +108,7 @@ test.describe("document vault", () => {
// ── §1A — the 30-second doctor flow, three independent routes ─────────
- test("doctor flow A: the type chip reaches the MRT report", async ({
+ test("doctor flow A: the type filter reaches the MRT report", async ({
page,
}) => {
await page.goto("/documents");
@@ -116,7 +116,11 @@ test.describe("document vault", () => {
page.getByRole("heading", { name: "Documents" }),
).toBeVisible();
- await page.getByRole("button", { name: "Imaging", exact: true }).click();
+ // Open the type dropdown, tick Imaging (multi-select stays open), then
+ // dismiss the menu.
+ await page.getByRole("button", { name: "All types" }).click();
+ await page.getByRole("menuitemcheckbox", { name: "Imaging" }).click();
+ await page.keyboard.press("Escape");
// Autumn 2025 sits below the newest sections — scroll the window down.
await scrollUntilVisible(page, openButton(page, "MRT Knie"));
await openButton(page, "MRT Knie").click();
@@ -128,13 +132,15 @@ test.describe("document vault", () => {
await expect(sheet.locator("iframe")).toBeVisible({ timeout: 5_000 });
});
- test("doctor flow B: the condition chip reaches the MRT report", async ({
+ test("doctor flow B: the condition filter reaches the MRT report", async ({
page,
}) => {
await page.goto("/documents");
+ // Open the condition dropdown and pick the linked episode (single-select
+ // → the menu closes on choice).
+ await page.locator('[data-slot="document-condition-filter"]').click();
await page
- .locator('[data-slot="document-filter-bar"]')
- .getByRole("button", { name: "Knie", exact: true })
+ .getByRole("menuitemcheckbox", { name: "Knie", exact: true })
.click();
// The Knie filter narrows the corpus to the linked MRT trio.
@@ -162,12 +168,13 @@ test.describe("document vault", () => {
page,
}) => {
await page.goto("/documents?kind=IMAGING&year=2025");
+ // A deep-linked facet surfaces on its dropdown trigger label.
await expect(
- page.getByRole("button", { name: "Imaging", exact: true }),
- ).toHaveAttribute("aria-pressed", "true");
+ page.locator('[data-slot="document-type-filter"]'),
+ ).toContainText("Imaging");
await expect(
- page.getByRole("button", { name: "2025", exact: true }),
- ).toHaveAttribute("aria-pressed", "true");
+ page.locator('[data-slot="document-year-filter"]'),
+ ).toContainText("2025");
await scrollUntilVisible(page, openButton(page, "MRT Knie"));
await page.goto(`/documents?episode=${KNIE_EPISODE_ID}`);
@@ -345,7 +352,7 @@ test.describe("document vault", () => {
await expect(openButton(page, "MRT Knie")).toBeVisible();
// Tab from the search into the grid's single roving slot (bounded walk
- // across the chip rail), then arrows move the active card.
+ // across the filter dropdowns), then arrows move the active card.
let reachedGrid = false;
for (let i = 0; i < 40; i++) {
await page.keyboard.press("Tab");
diff --git a/messages/de.json b/messages/de.json
index 3e310cb6e..0e996f26f 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Dokumente durchsuchen",
"searchPlaceholder": "Titel oder Dateiname suchen…",
- "groupLabel": "Dokumente filtern",
"typeGroup": "Dokumenttyp",
"conditionGroup": "Erkrankung",
"yearGroup": "Jahr",
diff --git a/messages/en.json b/messages/en.json
index 40376a2a6..bcc1379c8 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Search documents",
"searchPlaceholder": "Search title or filename…",
- "groupLabel": "Filter documents",
"typeGroup": "Document type",
"conditionGroup": "Condition",
"yearGroup": "Year",
diff --git a/messages/es.json b/messages/es.json
index 8ff0241c5..8ef149419 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Buscar documentos",
"searchPlaceholder": "Buscar por título o nombre de archivo…",
- "groupLabel": "Filtrar documentos",
"typeGroup": "Tipo de documento",
"conditionGroup": "Afección",
"yearGroup": "Año",
diff --git a/messages/fr.json b/messages/fr.json
index 0fa043dde..5df61b975 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Rechercher des documents",
"searchPlaceholder": "Rechercher un titre ou un nom de fichier…",
- "groupLabel": "Filtrer les documents",
"typeGroup": "Type de document",
"conditionGroup": "Affection",
"yearGroup": "Année",
diff --git a/messages/it.json b/messages/it.json
index 3b5ed6961..306772067 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Cerca documenti",
"searchPlaceholder": "Cerca titolo o nome file…",
- "groupLabel": "Filtra documenti",
"typeGroup": "Tipo di documento",
"conditionGroup": "Patologia",
"yearGroup": "Anno",
diff --git a/messages/pl.json b/messages/pl.json
index 06dba2454..e4add0d95 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -8044,7 +8044,6 @@
"filter": {
"searchLabel": "Szukaj dokumentów",
"searchPlaceholder": "Szukaj tytułu lub nazwy pliku…",
- "groupLabel": "Filtruj dokumenty",
"typeGroup": "Typ dokumentu",
"conditionGroup": "Schorzenie",
"yearGroup": "Rok",
diff --git a/src/components/documents/document-filter-bar.tsx b/src/components/documents/document-filter-bar.tsx
index c3f068873..74312ce90 100644
--- a/src/components/documents/document-filter-bar.tsx
+++ b/src/components/documents/document-filter-bar.tsx
@@ -19,7 +19,15 @@
* sticks below the top edge inside the shell's scroll container (sticky,
* translucent background, border-b — no viewport-height tricks).
*/
-import { ChevronDown, ScanSearch, Search, X } from "lucide-react";
+import {
+ Activity,
+ Calendar,
+ ChevronDown,
+ ListFilter,
+ ScanSearch,
+ Search,
+ X,
+} from "lucide-react";
import type { RefObject } from "react";
import { Button } from "@/components/ui/button";
@@ -42,6 +50,19 @@ 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";
+/**
+ * The facet label is the whole point of the trigger — but three text labels
+ * plus the search will not fit a 360px phone. So the label shows from `sm`
+ * up, and on a phone only when the facet is ACTIVE (truncated), while the
+ * leading icon always carries the facet identity. The trailing chevron is
+ * desktop-only chrome. Net: inactive phone triggers collapse to a single
+ * icon, the row stays one line at every width, and the accessible name is
+ * pinned via `aria-label` so an icon-only trigger still announces its state.
+ */
+function facetLabelClass(active: boolean): string {
+ return cn("max-w-28 truncate", active ? "inline" : "hidden sm:inline");
+}
+
export interface ConditionChip {
episodeId: string;
name: string;
@@ -150,10 +171,17 @@ export function DocumentFilterBar({
variant="outline"
size="sm"
data-slot="document-type-filter"
+ aria-label={typeLabel}
className={cn(TRIGGER_CLASSES, kindCount > 0 && TRIGGER_ACTIVE)}
>
- {typeLabel}
-
+
+ 0)}>
+ {typeLabel}
+
+
- {conditionLabel}
-
+
+
+ {conditionLabel}
+
+
- {yearLabel}
-
+
+
+ {yearLabel}
+
+