feat(t5): Wächter vollständig, Hinweis Sonnenmodus, Nacharbeiten (T5a A10)
- 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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user