Files
ApiCore/ApiCore-Frontend/src/components/appearance-notice.tsx
T
Nick.RosenloecherandClaude Opus 5.5 427969d8b9
Backend Image / build-and-push (push) Successful in 15m27s
Frontend Image / build-and-push (push) Successful in 23m9s
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]>
2026-10-19 13:47:59 +02:00

104 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}