feat(t5): ein Tabs-Baustein für Listen, Einstellungen und Statistik (T5a A8)
- 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:
1 parent
759d85dc67
commit
fce9d10192
26 files changed
+1137
-606
No files matched your search
@@ -1,53 +1,69 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
export type EntityLifecycleTab = "active" | "history";
|
||||
|
||||
interface ActiveHistoryTabsProps {
|
||||
export interface ActiveHistoryTabsProps {
|
||||
value: EntityLifecycleTab;
|
||||
onChange: (value: EntityLifecycleTab) => void;
|
||||
onValueChange: (value: EntityLifecycleTab) => void;
|
||||
activeCount?: number;
|
||||
historyCount?: number;
|
||||
/** A section heading that opens the row; the tabs then sit on its right. */
|
||||
heading?: ReactNode;
|
||||
/** Controls next to the tabs (a filter, a create button), outside the panels. */
|
||||
toolbar?: ReactNode;
|
||||
/** `end` pushes the toolbar to the far right of the row (a create button). */
|
||||
toolbarAlign?: "start" | "end";
|
||||
/** The list of each panel; only the selected one is mounted. */
|
||||
children: { active: ReactNode; history: ReactNode };
|
||||
}
|
||||
|
||||
const withCount = (label: string, count: number | undefined) =>
|
||||
count === undefined ? label : `${label} (${count})`;
|
||||
|
||||
const isLifecycleTab = (value: unknown): value is EntityLifecycleTab =>
|
||||
value === "active" || value === "history";
|
||||
|
||||
/**
|
||||
* Template on Tabs (spec 10.4): „Aktiv“ and „Historie“, each panel holds its list. Both panels
|
||||
* filter data the page has already loaded, so the arrow keys switch at once (activateOnFocus).
|
||||
*/
|
||||
export function ActiveHistoryTabs({
|
||||
value,
|
||||
onChange,
|
||||
onValueChange,
|
||||
activeCount,
|
||||
historyCount,
|
||||
heading,
|
||||
toolbar,
|
||||
toolbarAlign = "start",
|
||||
children,
|
||||
}: ActiveHistoryTabsProps) {
|
||||
return (
|
||||
<div
|
||||
role="tablist"
|
||||
className="inline-flex w-fit rounded-xl border border-border/60 bg-muted/40 p-1"
|
||||
<Tabs
|
||||
value={value}
|
||||
onValueChange={(next) => {
|
||||
if (isLifecycleTab(next)) onValueChange(next);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={value === "active"}
|
||||
onClick={() => onChange("active")}
|
||||
className={cn(
|
||||
"rounded-lg px-4 py-1.5 text-sm font-medium transition-colors",
|
||||
value === "active"
|
||||
? "bg-card text-foreground shadow-[var(--shadow-soft)]"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{heading}
|
||||
<TabsList
|
||||
aria-label="Aktiv oder Historie"
|
||||
activateOnFocus
|
||||
className={cn(heading !== undefined && "ml-auto")}
|
||||
>
|
||||
<TabsTrigger value="active">{withCount("Aktiv", activeCount)}</TabsTrigger>
|
||||
<TabsTrigger value="history">{withCount("Historie", historyCount)}</TabsTrigger>
|
||||
</TabsList>
|
||||
{toolbarAlign === "end" && toolbar !== undefined ? (
|
||||
<div className="ml-auto">{toolbar}</div>
|
||||
) : (
|
||||
toolbar
|
||||
)}
|
||||
>
|
||||
Aktiv{activeCount !== undefined ? ` (${activeCount})` : ""}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={value === "history"}
|
||||
onClick={() => onChange("history")}
|
||||
className={cn(
|
||||
"rounded-lg px-4 py-1.5 text-sm font-medium transition-colors",
|
||||
value === "history"
|
||||
? "bg-card text-foreground shadow-[var(--shadow-soft)]"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
Historie{historyCount !== undefined ? ` (${historyCount})` : ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="active">{children.active}</TabsContent>
|
||||
<TabsContent value="history">{children.history}</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user