feat(t5): Wächter vollständig, Hinweis Sonnenmodus, Nacharbeiten (T5a A10)
Backend Image / build-and-push (push) Successful in 15m27s
Frontend Image / build-and-push (push) Successful in 23m9s

- 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]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-19 13:47:59 +02:00
1 parent 355a353671
commit 427969d8b9
42 files changed
+1053 -400

No files matched your search

@@ -0,0 +1,103 @@
"use client";
import { useEffect, useId, useSyncExternalStore } from "react";
import { Sun } from "lucide-react";
import { toast } from "sonner";
import { useAppearance } from "@/lib/appearance/appearance";
import { Button } from "@/components/ui/button";
/** Remembered per device (spec 15.4): the notice is shown until it is closed or tried once. */
export const SUN_NOTICE_KEY = "apicore.notice.sun";
const SEEN = "seen";
const CHANGE_EVENT = "apicore:notice";
/** Closed in this visit; covers blocked storage (private mode). */
let seenThisVisit = false;
function isSeen(): boolean {
if (seenThisVisit) return true;
try {
return window.localStorage.getItem(SUN_NOTICE_KEY) === SEEN;
} catch {
return false;
}
}
function markSeen(): void {
seenThisVisit = true;
try {
window.localStorage.setItem(SUN_NOTICE_KEY, SEEN);
} catch {
// Not persisted - gone for this visit only.
}
window.dispatchEvent(new Event(CHANGE_EVENT));
}
function subscribe(onChange: () => void): () => void {
window.addEventListener(CHANGE_EVENT, onChange);
window.addEventListener("storage", onChange);
return () => {
window.removeEventListener(CHANGE_EVENT, onChange);
window.removeEventListener("storage", onChange);
};
}
/**
* The notice unmounts on close; focus inside it would drop to <body> (WCAG 2.4.3). It goes to
* the page heading (h1 with tabIndex -1) instead.
*/
function keepFocus(): void {
const heading = document.querySelector<HTMLElement>("main h1, h1");
heading?.focus();
}
const TEXT = "Neu: Sonnenmodus für draußen – im Benutzermenü.";
/** One-time hint on the start page: try the sun mode or close the hint. */
export function AppearanceNotice() {
const textId = useId();
const { sunActive, setContrast } = useAppearance();
// The server never shows it, so a device that has seen it gets no flash after hydration.
const seen = useSyncExternalStore(subscribe, isSeen, () => true);
// Sun switched on elsewhere (user menu, device setting) means the hint has done its job.
useEffect(() => {
if (sunActive && !seen) markSeen();
}, [sunActive, seen]);
if (seen || sunActive) return null;
return (
<aside
aria-labelledby={textId}
className="flex flex-wrap items-center gap-3 rounded-xl border border-border bg-card p-3"
>
<Sun className="size-5 shrink-0 text-primary-ink" aria-hidden />
<p id={textId} className="min-w-0 flex-1 text-base">
{TEXT}
</p>
<div className="flex flex-wrap gap-2">
<Button
size="touch"
variant="outline"
onClick={() => {
keepFocus();
setContrast("sun");
markSeen();
toast.success("Sonnenmodus an");
}}
>
Ausprobieren
</Button>
<Button
size="touch"
variant="ghost"
onClick={() => {
keepFocus();
markSeen();
}}
>
Schließen
</Button>
</div>
</aside>
);
}