a21596b3e4
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
144 lines
4.4 KiB
TypeScript
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} />;
|
|
}
|