import * as React from 'react'; import { Contrast } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; const STORAGE_KEY = 'pig-demo:contrast'; /** * `localStorage` is not always readable. In a cross-origin iframe with third- * party storage blocked, and in Safari's private mode, the accessor itself * THROWS rather than returning null — so every access has to be wrapped, not * just null-checked. Unreadable storage means "off", never a crash. */ function readStored(): boolean { try { return window.localStorage.getItem(STORAGE_KEY) === 'high'; } catch { return false; } } function writeStored(high: boolean): void { try { window.localStorage.setItem(STORAGE_KEY, high ? 'high' : 'normal'); } catch { /* Preference is session-only in this context. The toggle still works. */ } } /** * One value shared by every mounted toggle, not one `useState` each. * * This control renders in three places — the desktop bar, the mobile sheet and * the Play tab — and at most two are ever visible at once. With local state the * hidden one keeps a stale `aria-pressed`, so a visitor who toggles on the * board and then opens the menu is told the setting is off while the page is * plainly showing it on. */ let high = false; let initialised = false; const listeners = new Set<() => void>(); function applyToRoot(next: boolean): void { const root = document.documentElement; // Removed rather than set to "normal": the CSS keys off // `:root[data-contrast='high']`, and a leftover attribute makes the DOM lie // about which palette is actually applied. if (next) root.setAttribute('data-contrast', 'high'); else root.removeAttribute('data-contrast'); } function setHigh(next: boolean): void { high = next; applyToRoot(next); writeStored(next); for (const listener of listeners) listener(); } function subscribe(listener: () => void): () => void { if (!initialised) { initialised = true; high = readStored(); applyToRoot(high); } listeners.add(listener); return () => { listeners.delete(listener); }; } function useContrast(): boolean { // The server snapshot is `false` so a prerendered page never claims a // preference it cannot know; the real value lands on the first subscribe. return React.useSyncExternalStore( subscribe, () => high, () => false, ); } export interface ContrastToggleProps { className?: string; /** * Print "High contrast" beside the icon. This is the form the Play tab and * the mobile sheet use: a colour-blind visitor looking at the board finds the * mode by its name, not by guessing what a half-filled circle means. */ labelled?: boolean; } export function ContrastToggle({ className, labelled = false }: ContrastToggleProps) { const isHigh = useContrast(); const ariaLabel = isHigh ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'; if (labelled) { return ( ); } return ( ); } /** The Play tab's form of the toggle. Same store, same state, with its name on it. */ export function LabelledContrastToggle({ className }: { className?: string }) { return ; }