- Baukasten-Wächter: jede Regel prüft eigene Beispiele (muss fangen, muss ignorieren), veraltete Ausnahmen und Ordner-Ausschlüsse schlagen fehl; neue Regeln für text-primary ohne -ink, dark:, Schrift unter 13 px (Tailwind, SVG, CSS, style, *_SIZE-Konstanten) und Verlaufs-Hintergründe. Aufgeteilt in baukasten-guard, touch-target-guard und einen Scan-Helfer. - Hinweis „Neu: Sonnenmodus für draußen“ auf dem Dashboard, einmal je Gerät, „Ausprobieren“ schaltet live um; Fokus geht danach zur Überschrift. - Varroa-Diagramm: Beschriftungen als HTML mit fester Größe statt SVG-Text. - Medikamente zeigen die Tabs auch leer; Zahlen in Tabs nicht beim Laden oder Fehler; inaktive Tabs mit Hover für die Maus; TabPicker (Auswahl unter 640 px) für Automatisierung und Statistik; Rollen-Dialoge breit genug für die Rechte-Matrix. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
export type EntityLifecycleTab = "active" | "history";
|
|
|
|
export interface ActiveHistoryTabsProps {
|
|
value: EntityLifecycleTab;
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* The count for a tab: unknown (no number) until the list has loaded, so a failed or pending
|
|
* first load never reads „Aktiv (0)“. Cached data still counts while a refetch fails.
|
|
*/
|
|
export function countOf(data: readonly unknown[] | undefined, items: readonly unknown[]): number | undefined {
|
|
return data === undefined ? undefined : items.length;
|
|
}
|
|
|
|
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,
|
|
onValueChange,
|
|
activeCount,
|
|
historyCount,
|
|
heading,
|
|
toolbar,
|
|
toolbarAlign = "start",
|
|
children,
|
|
}: ActiveHistoryTabsProps) {
|
|
return (
|
|
<Tabs
|
|
value={value}
|
|
onValueChange={(next) => {
|
|
if (isLifecycleTab(next)) onValueChange(next);
|
|
}}
|
|
>
|
|
<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
|
|
)}
|
|
</div>
|
|
<TabsContent value="active">{children.active}</TabsContent>
|
|
<TabsContent value="history">{children.history}</TabsContent>
|
|
</Tabs>
|
|
);
|
|
}
|