- 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]>
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
"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>
|
||
);
|
||
}
|