Redesign in Prime Intellect's design language, both themes, three viewports
ci / web (push) Successful in 2m42s
ci / python (push) Successful in 3m8s

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
This commit is contained in:
karti-ai
2026-08-28 21:41:15 -07:00
parent af15ba584e
commit a21596b3e4
85 changed files with 4717 additions and 2230 deletions
+92 -30
View File
@@ -1,4 +1,4 @@
import { memo } from 'react';
import { memo, useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
@@ -7,9 +7,22 @@ import { MAX_GUESSES, WORD_LENGTH, type BoardState, type Pattern } from './engin
/**
* The board. One component for all three jobs — you playing, the replay, and
* the gallery thumbnail — because three near-identical boards is how they drift.
*
* It draws no outer chrome: the shell's card supplies the border, ground and
* padding. Every colour is a `tile-*` token, which is what lets the
* high-contrast palette swap the fills without this file knowing.
*/
const TILE_CLASS: Record<string, string> = {
type TileState = 'G' | 'Y' | 'X';
/**
* Fill, border and glyph ink per state. The fills are the same in both themes
* (the board is the one theme-invariant object on the site) and the `-fg`
* tokens are measured against their own fill: exact 13.08, present 11.16,
* absent 7.75 dark / 5.74 light; in high contrast white on each fill clears
* 4.9 or better. The numbers live beside the tokens in index.css.
*/
const TILE_CLASS: Record<TileState, string> = {
G: 'bg-tile-exact text-tile-exact-fg border-tile-exact',
Y: 'bg-tile-present text-tile-present-fg border-tile-present',
X: 'bg-tile-absent text-tile-absent-fg border-tile-absent',
@@ -18,12 +31,40 @@ const TILE_CLASS: Record<string, string> = {
/**
* A glyph per state, shown only in high-contrast mode.
*
* Green/yellow/grey is a colour-only distinction, which is exactly why the
* Green/amber/grey is a colour-only distinction, which is exactly why the
* original game ships a high-contrast mode. A second channel means the result
* survives deuteranopia, a projector with the colour balance wrong, and a
* screenshot printed in black and white.
* screenshot printed in black and white. Absent has no glyph: a filled grey
* tile with nothing in the corner is the third state.
*/
const TILE_GLYPH: Record<string, string> = { G: '●', Y: '◆', X: '' };
const TILE_GLYPH: Record<TileState, string> = { G: '●', Y: '◆', X: '' };
/** Flip length and per-tile stagger, matching the keyframes in index.css. */
const FLIP_MS = 520;
const STAGGER_MS = 100;
/**
* Whether a revealing tile has reached the half-way point of its flip.
*
* The real flip lands the colour at 90°, when the face is edge-on: before that
* the tile still looks typed, after it the tile is scored. Applying the colour
* on mount and rotating a coloured tile reads as a wobble, not a reveal. Under
* reduced motion the colour lands immediately — there is no flip to hide it
* behind, and the row's announcement carries the result regardless.
*/
function useLanded(revealing: boolean, index: number): boolean {
const [landed, setLanded] = useState(!revealing);
useEffect(() => {
if (!revealing) return;
if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setLanded(true);
return;
}
const id = window.setTimeout(() => setLanded(true), index * STAGGER_MS + FLIP_MS / 2);
return () => window.clearTimeout(id);
}, [revealing, index]);
return landed;
}
function Tile({
letter,
@@ -33,35 +74,45 @@ function Tile({
compact,
}: {
letter: string;
tile: Pattern[number] | null;
tile: TileState | null;
index: number;
revealing: boolean;
compact?: boolean;
}) {
const filled = letter !== '';
const landed = useLanded(revealing && tile !== null, index);
const scored = tile !== null && landed;
return (
<div
className={cn(
'relative grid place-items-center select-none font-semibold uppercase',
'aspect-square rounded-md border-2 transition-colors',
compact ? 'text-[0.55rem] border' : 'text-xl sm:text-2xl',
tile
'relative grid select-none place-items-center font-sans font-semibold uppercase',
'aspect-square rounded-tile transition-colors duration-1',
compact ? 'border text-[0.55rem]' : 'border-2 text-[1.375rem] sm:text-2xl',
scored
? TILE_CLASS[tile]
: filled
? 'border-muted/60 bg-surface text-fg'
: 'border-border bg-surface-2/40 text-fg',
// The flip is a rotation about X with the colour landing at the
// half-way point, staggered along the row. `prefers-reduced-motion`
? // Typed, unscored: a strong edge says "this is yours", the letter is
// plain ink. No fill, so the scored fill is an unmistakable change.
'border-border-strong bg-transparent text-fg'
: 'border-tile-empty bg-transparent text-fg',
// The flip is a rotation about X, staggered along the row; the colour
// lands at the half-way point (see useLanded). `prefers-reduced-motion`
// clamps it to nothing globally, which is why announceRow() exists.
revealing && tile && 'motion-safe:animate-[tile-flip_520ms_ease-enter_both]',
filled && !tile && 'motion-safe:animate-[tile-pop_120ms_ease-enter]',
)}
style={revealing ? { animationDelay: `${index * 100}ms` } : undefined}
style={revealing && tile ? { animationDelay: `${index * STAGGER_MS}ms` } : undefined}
aria-hidden="true"
>
{letter}
{tile ? (
<span className="pointer-events-none absolute bottom-0 right-0.5 text-[0.5em] leading-none opacity-0 [:root[data-contrast='high']_&]:opacity-90">
{scored ? (
<span
className={cn(
'pointer-events-none absolute bottom-0 right-0.5 leading-none opacity-0',
"[:root[data-contrast='high']_&]:opacity-100",
compact ? 'text-[0.6em]' : 'text-[0.5em]',
)}
>
{TILE_GLYPH[tile]}
</span>
) : null}
@@ -83,19 +134,25 @@ function Row({
compact?: boolean;
}) {
const letters = guess.padEnd(WORD_LENGTH, ' ').slice(0, WORD_LENGTH);
// The shake is armed by the prop and disarmed by its own end event, so a
// second refusal shakes the row again instead of leaving the class in place.
const [shaking, setShaking] = useState(false);
useEffect(() => {
if (shake) setShaking(true);
}, [shake]);
return (
<div
className={cn(
'grid gap-1 sm:gap-1.5',
shake && 'motion-safe:animate-[tile-shake_600ms_ease-enter]',
)}
className={cn('grid gap-1 sm:gap-1.5', shaking && 'motion-safe:animate-[tile-shake_400ms_ease-enter]')}
style={{ gridTemplateColumns: `repeat(${WORD_LENGTH}, minmax(0, 1fr))` }}
onAnimationEnd={(event) => {
if (event.animationName === 'tile-shake') setShaking(false);
}}
>
{[...letters].map((letter, i) => (
<Tile
key={i}
letter={letter.trim()}
tile={pattern ? (pattern[i] as Pattern[number]) : null}
tile={pattern ? (pattern[i] as TileState) : null}
index={i}
revealing={revealing}
compact={compact}
@@ -105,6 +162,14 @@ function Row({
);
}
function boardLabel(state: BoardState): string {
const played = state.rows.length;
if (state.status === 'won') return `Solved in ${played} of ${MAX_GUESSES} guesses.`;
if (state.status === 'lost') return `All ${MAX_GUESSES} guesses played, not solved.`;
if (played === 0) return `Empty board, ${MAX_GUESSES} guesses remaining.`;
return `${played} of ${MAX_GUESSES} guesses played.`;
}
export const Board = memo(function Board({
state,
compact,
@@ -119,26 +184,23 @@ export const Board = memo(function Board({
return (
<div
className={cn(
'grid gap-1 sm:gap-1.5',
'grid',
// An explicit width, not `w-full max-w-sm`. An empty board has no
// intrinsic width, so inside a shrink-to-fit parent `w-full` resolves
// to nothing and the board collapses to a stub — which is exactly the
// state the interactive board starts in.
compact ? 'w-[7rem]' : 'w-[min(100%,20rem)] min-w-[15rem]',
compact ? 'w-[7rem] gap-0.5' : 'w-[min(100%,20rem)] min-w-[15rem] gap-1 sm:gap-1.5',
)}
role="img"
aria-label={
rows.length === 0
? 'Empty board, six guesses remaining.'
: `${rows.length} of ${MAX_GUESSES} guesses played.`
}
aria-label={boardLabel(state)}
>
{rows.map((row, i) => (
<Row
key={`${row.guess}-${i}`}
guess={row.guess}
pattern={row.pattern}
revealing={i === rows.length - 1}
// The thumbnail is the shape of the run, not a replay: no flip there.
revealing={!compact && i === rows.length - 1}
compact={compact}
/>
))}