feat(t5): ein Tabs-Baustein für Listen, Einstellungen und Statistik (T5a A8)
Backend E2E / e2e (push) Failing after 13m2s
Backend Image / build-and-push (push) Successful in 7m55s
Frontend Image / build-and-push (push) Successful in 27m46s

- ui/tabs.tsx nur noch Variante segment: Leiste auf --muted, aktiver Tab
  mit Kartenfläche, fetter Schrift und 3-px-Balken in --primary-ink;
  44 px bei Touch, kompakt mit Maus; inaktive Schrift ≥ 6,27:1.
- ActiveHistoryTabs als Vorlage auf Tabs für 10 Listen (Pfeiltasten
  schalten direkt, Daten sind geladen), mit toolbar und heading.
- Einstellungen (Automatisierung, Benutzer & Rollen) und Statistik mit
  manueller Aktivierung: Pfeil bewegt den Fokus, Enter/Leertaste wählt;
  ?tab= über router.replace. TabSwitcher entfernt.
- Menüeinträge (Dropdown, Select) mit innerem Fokusring ≥ 3:1.
- Aufgaben: der Prioritätsfilter lädt die Liste jetzt neu (queryKey).
- Wächter: keine eigenen tablist/tab/tabpanel außerhalb ui/tabs.tsx.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 20:08:36 +02:00
1 parent 759d85dc67
commit fce9d10192
26 files changed
+1137 -606

No files matched your search

@@ -0,0 +1,115 @@
import { useState } from "react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ActiveHistoryTabs, type EntityLifecycleTab } from "./active-history-tabs";
function Harness({ onValueChange }: { onValueChange?: (value: EntityLifecycleTab) => void }) {
const [value, setValue] = useState<EntityLifecycleTab>("active");
return (
<ActiveHistoryTabs
value={value}
onValueChange={(next) => {
setValue(next);
onValueChange?.(next);
}}
activeCount={1}
historyCount={0}
toolbar={<button type="button">Filter</button>}
>
{{ active: <p>Liste aktiv</p>, history: <p>Liste Historie</p> }}
</ActiveHistoryTabs>
);
}
describe("ActiveHistoryTabs (spec 10.4)", () => {
it("ActiveHistoryTabs shows the counts and renders the matching panel", async () => {
const user = userEvent.setup();
const onValueChange = vi.fn();
render(<Harness onValueChange={onValueChange} />);
const active = screen.getByRole("tab", { name: "Aktiv (1)" });
const history = screen.getByRole("tab", { name: "Historie (0)" });
expect(active).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Aktiv (1)" })).toHaveTextContent("Liste aktiv");
expect(screen.queryByText("Liste Historie")).not.toBeInTheDocument();
await user.click(history);
expect(onValueChange).toHaveBeenLastCalledWith("history");
expect(history).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Historie (0)" })).toHaveTextContent("Liste Historie");
expect(screen.queryByText("Liste aktiv")).not.toBeInTheDocument();
await user.keyboard("{ArrowLeft}");
expect(onValueChange).toHaveBeenLastCalledWith("active");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste aktiv");
});
it("leaves the counts out while they are unknown", () => {
render(
<ActiveHistoryTabs value="active" onValueChange={() => {}}>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
expect(screen.getByRole("tab", { name: "Aktiv" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Historie" })).toBeInTheDocument();
});
it("puts the toolbar next to the tabs, outside the panels", () => {
render(<Harness />);
const filter = screen.getByRole("button", { name: "Filter" });
expect(within(screen.getByRole("tabpanel")).queryByRole("button", { name: "Filter" })).toBeNull();
expect(screen.getByRole("tablist").parentElement).toContainElement(filter);
});
it("arrow keys switch at once here, because both panels filter data that is already loaded", async () => {
const user = userEvent.setup();
render(<Harness />);
await user.click(screen.getByRole("tab", { name: "Aktiv (1)" }));
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { name: "Historie (0)" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel")).toHaveTextContent("Liste Historie");
});
it("toolbarAlign end pushes the toolbar to the far right of the row", () => {
render(
<ActiveHistoryTabs
value="active"
onValueChange={() => {}}
toolbar={<button type="button">Neue Rolle</button>}
toolbarAlign="end"
>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
const wrapper = screen.getByRole("button", { name: "Neue Rolle" }).parentElement as HTMLElement;
expect(wrapper.className.split(/\s+/)).toContain("ml-auto");
expect(screen.getByRole("tablist").parentElement).toContainElement(wrapper);
});
it("a heading opens the row before the tabs, the tabs sit on the right", () => {
render(
<ActiveHistoryTabs
value="active"
onValueChange={() => {}}
heading={<h2>Lagerbehälter</h2>}
>
{{ active: null, history: null }}
</ActiveHistoryTabs>,
);
const heading = screen.getByRole("heading", { name: "Lagerbehälter" });
const list = screen.getByRole("tablist");
expect(heading.compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(list.parentElement).toContainElement(heading);
expect(list.className.split(/\s+/)).toContain("ml-auto");
});
it("the chips are 44 px high on touch and compact only with a fine pointer", () => {
render(<Harness />);
for (const tab of screen.getAllByRole("tab")) {
const classes = tab.className.split(/\s+/);
expect(classes).toContain("h-11");
expect(classes).toContain("pointer-fine:h-9");
}
});
});