Files
PIG-Demo/src/components/site/ContrastToggle.tsx
T
karti-ai a21596b3e4
ci / web (push) Successful in 2m42s
ci / python (push) Successful in 3m8s
Redesign in Prime Intellect's design language, both themes, three viewports
Eighteen agents: three design directions judged on whether a COO on a phone
actually learns what an environment is, on craft, and on landing without a
rewrite; one spec; a foundation of measured tokens; six build lanes; three
browser verifiers; a final gate pass.

The materials are Prime Intellect's, measured from their site: near-black
grounds, one green, sharp radii, mono small-caps labels, Geist and Geist Mono
self-hosted because production CSP is font-src 'self'. Two of their own greys
fail contrast on their own ground (#737373 is 4.02:1, #6E6E6E is 3.73:1 on
#0F0F0F), so --muted is lifted and the CSS comment carries the number — or
someone will 'correct' it back. Every text-on-ground pair in both themes is
tabulated in src/index.css with its measured ratio.

The two rules that resolved every conflict: data is mono, sentences are sans;
the language wins on materials, the lesson wins on legibility. Light mode is a
finished paper theme, not an inversion.

What did not change: the derived-tabs contract, the honesty markers, the
isolation lint, every gate. 419 contract checks, entry chunk at 74% of budget,
zero horizontal overflow on any route at 390/1024/1440 in either theme.

Also flips Alert Triage to status 'live' — the pipeline built it but never
promoted it, so it was badged SPEC on its own playable page and the home page
counted one environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
2026-08-28 21:41:15 -07:00

144 lines
4.4 KiB
TypeScript

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 (
<Button
type="button"
variant="outline"
size="touch"
// 44px below lg, 36px at lg — the same step as the icon form, so the
// Play tab's row lines up with the ghost button beside it.
className={cn(
'justify-start lg:min-h-9',
isHigh && 'border-accent-edge bg-accent-subtle text-accent-fg',
className,
)}
aria-pressed={isHigh}
aria-label={ariaLabel}
onClick={() => setHigh(!isHigh)}
>
<Contrast aria-hidden="true" />
<span className="text-ui">High contrast</span>
{/* The state as a word as well as a fill, on the control that exists
for people who cannot rely on the fill. */}
<span className="font-mono text-label uppercase tracking-label text-muted">
{isHigh ? 'on' : 'off'}
</span>
</Button>
);
}
return (
<Button
type="button"
variant="ghost"
size="icon-touch"
className={cn('lg:size-9', isHigh && 'bg-accent-subtle text-accent-fg', className)}
aria-pressed={isHigh}
aria-label={ariaLabel}
title="High contrast tiles"
onClick={() => setHigh(!isHigh)}
>
<Contrast aria-hidden="true" />
</Button>
);
}
/** The Play tab's form of the toggle. Same store, same state, with its name on it. */
export function LabelledContrastToggle({ className }: { className?: string }) {
return <ContrastToggle labelled className={className} />;
}