From 7643f5b42ba9fa59e54a3aa50d61ca3dfe22f3d1 Mon Sep 17 00:00:00 2001 From: Fazil Raja Date: Sat, 6 Jun 2026 02:37:50 -0500 Subject: [PATCH 01/12] feat(web): celestial theme exploration checkpoint First pass at a dark "Star Chart" theme for the dashboard while exploring a celestial identity for Seer (the seer of AI apps and neural networks). Checkpoint before reworking toward the brass + ink "Star Atlas" (dark) / "Aged Chart" (light) instrument direction. --- web/src/routes/__root.tsx | 25 ++-- web/src/routes/index.tsx | 267 +++++++++++++++++++++++++++++--------- web/src/styles.css | 28 ++++ web/tailwind.config.cjs | 33 +++++ 4 files changed, 282 insertions(+), 71 deletions(-) diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index 2309e6e..c64a213 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -49,17 +49,26 @@ function AppShell() { -
-
+
+
- - Seer + + + Seer @@ -84,15 +93,15 @@ function AppShell() {
-
+
-
+
-

Seer v0.3.0 · Built on Glean

+

Seer v0.3.0 · Built on Glean

diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 9113288..1e21f9e 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -1,6 +1,6 @@ import { createFileRoute, Link } from '@tanstack/react-router' import { formatRunStatus } from '#/lib/run-status' -import { formatOverallScore, scoreTextClass } from '#/lib/score-format' +import { formatOverallScore, getScoreTone } from '#/lib/score-format' import { getEvalSetsWithStats } from '#/server/dashboard' import { NewEvalSetButton } from '@/features/new-eval-set/NewEvalSetButton' @@ -9,87 +9,228 @@ export const Route = createFileRoute('/')({ component: Dashboard, }) +type EvalSetWithStats = Awaited>[number] + +const NIGHT_TONE_FG = { + success: '#7ECF90', + warning: '#EDA063', + fail: '#E86F6F', +} as const + +function nightToneClasses(score: number, mode: string | null | undefined): string { + const tone = getScoreTone(score, mode) + if (tone === 'success') return 'text-score-success-night bg-score-success-night-bg' + if (tone === 'warning') return 'text-score-warning-night bg-score-warning-night-bg' + return 'text-score-fail-night bg-score-fail-night-bg' +} + +/** + * The signature score component: a dark celestial orb. The reading's + * constellation glows inside, a score-toned arc reads the value, and a + * thin gold bezel marks it as the seer's instrument. + */ +function ScoreOrb({ score, mode }: { score: number; mode: string | null | undefined }) { + const isGolden = mode === 'golden' + const fraction = Math.max(0, Math.min(1, isGolden ? score : score / 10)) + const tone = getScoreTone(score, mode) + const toneHex = NIGHT_TONE_FG[tone] + const r = 56 + const circ = 2 * Math.PI * r + const offset = circ * (1 - fraction) + + return ( +
+ +
+
+
+ {formatOverallScore(score, mode)} +
+
{isGolden ? 'match' : '/ 10'}
+
+
+
+ ) +} + +function HeroSet({ set }: { set: EvalSetWithStats }) { + const hasScore = set.lastScore !== null && set.lastScore !== undefined + return ( + +
+ {hasScore ? : } +
+
+

{set.name}

+ {set.agentId.slice(0, 8)}… +
+

{set.description}

+
+ + {set.lastRunDate && } + {set.lastRunStatus && ( + + )} + {set.avgScore !== null && set.avgScore !== undefined && set.runCount > 1 && ( + + )} +
+
+
+ + ) +} + +function ScoreOrbEmpty() { + return ( +
+ +
+ no runs +
+
+ ) +} + +function Stat({ label, value, gold }: { label: string; value: string; gold?: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ) +} + +function SetRow({ set }: { set: EvalSetWithStats }) { + const hasScore = set.lastScore !== null && set.lastScore !== undefined + return ( + +
+
{set.name}
+
+ {set.caseCount} cases + {set.lastRunDate ? ` · ${new Date(set.lastRunDate).toLocaleDateString()}` : ' · never run'} +
+
+ {hasScore ? ( + + + {formatOverallScore(set.lastScore as number, set.mode)} + + ) : set.lastRunStatus === 'running' ? ( + + + running + + ) : ( + + )} + + ) +} + function Dashboard() { const evalSetsData = Route.useLoaderData() + const [hero, ...rest] = evalSetsData return (
-
+
-

Evaluation Sets

-

Manage and run agent evaluations

+

Evaluation sets

+

+ The seer of AI apps and neural networks. Read the chart before you ship. +

- + New eval set
{evalSetsData.length === 0 ? ( -
-

No evaluation sets yet

- +
+
+ +
+

No charts drawn yet

+

+ Create an evaluation set to start judging an agent's behavior. +

+ Create your first eval set →
) : ( -
- {evalSetsData.map((set) => ( - -

{set.name}

-

{set.description}

- -
-
- Agent - {set.agentId.slice(0, 8)}… -
-
- Cases - {set.caseCount} -
- {set.lastRunDate && ( - <> -
- Last Run - {new Date(set.lastRunDate).toLocaleDateString()} -
-
- Status - - {formatRunStatus(set.lastRunStatus, set.lastRunCompletedAt)} - -
- {set.lastScore !== null && set.lastScore !== undefined && ( -
- Score - - {formatOverallScore(set.lastScore, set.mode, { showGuidanceScale: true })} - -
- )} - {set.avgScore !== null && set.runCount > 1 && ( -
- Avg ({set.runCount} runs) - - {formatOverallScore(set.avgScore, set.mode, { showGuidanceScale: true })} - -
- )} - - )} +
+ {hero && } + {rest.length > 0 && ( +
+

Other sets

+
+ {rest.map((set) => ( + + ))}
- - ))} +
+ )}
)}
diff --git a/web/src/styles.css b/web/src/styles.css index 2eb383b..1078850 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -12,6 +12,34 @@ } @layer components { + /* Seer "Star Chart" — deep navy canvas with nebula glow + a faint constellation field */ + .starfield-canvas { + background-color: #060c19; + background-image: + radial-gradient(80% 60% at 82% 6%, rgba(58, 86, 150, 0.32), transparent 60%), + radial-gradient(72% 55% at 6% 102%, rgba(42, 52, 110, 0.3), transparent 62%); + } + .starfield-canvas::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 0; + opacity: 0.7; + background-image: + radial-gradient(1.5px 1.5px at 12% 18%, #bef1ff, transparent 60%), + radial-gradient(1.2px 1.2px at 26% 42%, #bef1ff, transparent 60%), + radial-gradient(1.6px 1.6px at 47% 14%, #bef1ff, transparent 60%), + radial-gradient(1px 1px at 63% 33%, #bef1ff, transparent 60%), + radial-gradient(1.3px 1.3px at 78% 22%, #bef1ff, transparent 60%), + radial-gradient(1px 1px at 90% 48%, #bef1ff, transparent 60%), + radial-gradient(1.2px 1.2px at 38% 60%, #bef1ff, transparent 60%), + radial-gradient(1px 1px at 8% 72%, #bef1ff, transparent 60%), + radial-gradient(1.4px 1.4px at 70% 70%, #bef1ff, transparent 60%), + radial-gradient(1px 1px at 54% 86%, #bef1ff, transparent 60%); + background-repeat: no-repeat; + } + .prose-seer { @apply prose prose-sm max-w-none; --tw-prose-body: #1A1A1A; diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs index be118bb..dbcfb12 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.cjs @@ -42,12 +42,45 @@ module.exports = { 'success-bg': '#DCFCE7', 'warning-bg': '#FEF3C7', 'fail-bg': '#FEE2E2', + // Dark "Starfield" theme score pairs (desaturated to sit on navy) + 'success-night': '#7ECF90', + 'warning-night': '#EDA063', + 'fail-night': '#E86F6F', + 'success-night-bg': '#1E3A26', + 'warning-night-bg': '#4B2D1C', + 'fail-night-bg': '#502826', + }, + // === Seer "Star Chart" celestial theme === + // A neural net is a constellation: nodes are stars, edges the lines between. + night: { + DEFAULT: '#060C19', // canvas — deep navy starfield + deep: '#02050F', // vignette floor + surface: '#101827', // panels, cards + raised: '#192333', // nested / hover + border: '#2A3342', + 'border-subtle': '#1F2733', + ink: '#F1EEE6', // starlight cream (primary text) + muted: '#A8B2BE', // secondary text + faint: '#768190', // tertiary text (AA at 4.5:1 on surface) + star: '#BEF1FF', // constellation star points + }, + constellation: { + DEFAULT: '#4FB9EB', // the single action / insight voice + strong: '#06A0DD', + bg: '#0A3149', + }, + gold: { + DEFAULT: '#DFBE75', // rare ceremonial voice — foresight / calibration only + deep: '#C69E58', + bg: '#3E3116', }, }, boxShadow: { card: '0 1px 3px rgba(26, 26, 26, 0.04), 0 1px 2px rgba(26, 26, 26, 0.06)', 'card-hover': '0 4px 12px rgba(26, 26, 26, 0.08), 0 2px 4px rgba(26, 26, 26, 0.04)', modal: '0 20px 60px rgba(26, 26, 26, 0.15), 0 8px 20px rgba(26, 26, 26, 0.1)', + // Depth on the night canvas (dark shadows read, not gray smudges) + 'night-card': '0 1px 2px rgba(0, 0, 0, 0.35), 0 14px 36px -20px rgba(0, 0, 0, 0.55)', }, }, }, From 86be38b986bcc9b295f0f0d10b87fda6423a1e11 Mon Sep 17 00:00:00 2001 From: Fazil Raja Date: Sat, 6 Jun 2026 02:53:26 -0500 Subject: [PATCH 02/12] docs: add Star Atlas / Aged Chart UI upgrade plan Build spec for Seer's dual-theme celestial instrument identity: brass + ink token system (both themes), no-flash theme switch, star-grid mark, astrolabe score orb, teaching empty state, and a trace surface (flame timeline + tool-call pill). Includes verified WCAG AA contrast tables and phased file-by-file sequencing. --- docs/ui-upgrade-plan.md | 536 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 docs/ui-upgrade-plan.md diff --git a/docs/ui-upgrade-plan.md b/docs/ui-upgrade-plan.md new file mode 100644 index 0000000..297628a --- /dev/null +++ b/docs/ui-upgrade-plan.md @@ -0,0 +1,536 @@ +# Seer UI Upgrade — "Star Atlas" / "Aged Chart" + +**Status:** plan (branch `ui-upgrade`) +**Goal:** Replace Seer's bland warm-neutral skin with a dual-theme celestial *instrument* identity that fits the name (the seer of AI apps and neural networks), and harden the dashboard, trace, and empty-state surfaces. + +This document is the build spec. Code blocks are the proposed implementation, written for Seer's stack (TanStack Start + Tailwind + DM Sans/DM Mono) and identity (brass + ink, Glean lineage). + +--- + +## 1. Identity + +Two themes, one instrument. Both built on the same token contract, switchable at runtime. + +| | Star Atlas (dark, default) | Aged Chart (light) | +|---|---|---| +| Ground | warm near-black | cool aged paper (not cream) | +| Surfaces | low-alpha *light* washes on the ground | low-alpha *dark* washes on the paper | +| One voice | **brass** `#d8b878` | **brass** `#7c4b02` | +| Text | parchment ink ramp (6 steps) | dark ink ramp (6 steps) | +| Signature | astrolabe score orb · pulsing star-grid mark · flame timeline | + +**Core principles (technique over decoration):** + +1. Surfaces are alpha washes + 1px hairline borders, never gradient cards. Depth comes from a solid ground ladder, not shadow. +2. A **6-step foreground ramp** (`fg0`–`fg5`) gives dense screens precise hierarchy. +3. **Color is meaning.** Chrome is colorless; brass is the only accent; score tones and deterministic per-category trace colors are the only other hues. +4. Tiny, named type scale for data density. Mono for everything measured. +5. Motion is state, never decoration. Short, ease-out, always with a reduced-motion fallback. +6. Hairline scrollbars; chrome recedes. +7. Empty states teach the next action. + +**Identity anchors:** brass is the single accent, DM Mono carries everything measured (Glean lineage), and the light theme mirrors the dark alpha technique as dark-on-paper rather than inverting colors. + +--- + +## 2. Token system + +Implemented as CSS variables scoped by `data-theme`, with Tailwind mapping color utilities to those variables. One class set works in both themes. + +### 2.1 `web/src/styles.css` — token definitions + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + /* ---------- Star Atlas (dark) — default ---------- */ + :root, + :root[data-theme="dark"] { + --bg: #0b0b0d; /* warm near-black ground */ + --surface: #131316; /* cards / panels */ + --elevated: #1a1a1e; /* popovers / nested */ + + --border: rgba(245, 225, 180, 0.08); /* brass-tinted hairline */ + --border-strong: rgba(245, 225, 180, 0.16); + --fill: rgba(245, 225, 180, 0.05); /* resting wash */ + --fill-hover: rgba(245, 225, 180, 0.08); + + /* 6-step ink ramp (faint -> brightest) */ + --fg0: #6b6862; /* decorative / disabled only (AA-large) */ + --fg1: #8a867e; /* tertiary, large text */ + --fg2: #a8a39a; /* secondary body */ + --fg3: #c7c2b8; /* strong body */ + --fg4: #e0dccf; /* emphasis */ + --fg5: #f3efe6; /* headings (parchment) */ + + --brass: #d8b878; /* the one voice */ + --brass-strong: #e6c98a; /* hover */ + --brass-bg: rgba(216, 184, 120, 0.12); + + --score-success: #7ecf90; + --score-warning: #e7b375; + --score-fail: #dd766f; + --score-success-bg: rgba(126, 207, 144, 0.14); + --score-warning-bg: rgba(231, 179, 117, 0.14); + --score-fail-bg: rgba(221, 111, 111, 0.14); + + --star: #e8e2d4; /* star-grid dots */ + --focus: var(--brass); + color-scheme: dark; + } + + /* ---------- Aged Chart (light) — mirror the alpha technique ---------- */ + :root[data-theme="light"] { + --bg: #cdd2d7; /* cool aged paper, NOT cream */ + --surface: #e8ebee; + --elevated: #f1f3f5; + + --border: rgba(30, 40, 55, 0.12); + --border-strong: rgba(30, 40, 55, 0.22); + --fill: rgba(30, 40, 55, 0.04); + --fill-hover: rgba(30, 40, 55, 0.07); + + --fg0: #858c94; /* decorative only (fails text contrast on bg) */ + --fg1: #6a717a; /* tertiary, large text */ + --fg2: #525961; /* secondary body (AA on surface) */ + --fg3: #444c56; + --fg4: #2c333c; + --fg5: #1b2027; /* headings (ink) */ + + --brass: #7c4b02; + --brass-strong: #6b3f00; + --brass-bg: rgba(124, 75, 2, 0.12); + + --score-success: #21763c; + --score-warning: #8a5018; + --score-fail: #b33736; + --score-success-bg: rgba(33, 118, 60, 0.12); + --score-warning-bg: rgba(138, 80, 24, 0.12); + --score-fail-bg: rgba(179, 55, 54, 0.12); + + --star: #4a525c; + --focus: var(--brass); + color-scheme: light; + } + + * { border-color: var(--border); } + body { + background: var(--bg); + color: var(--fg2); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + /* Hairline scrollbars — chrome recedes */ + * { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; } + *::-webkit-scrollbar { width: 5px; height: 5px; } + *::-webkit-scrollbar-track { background: transparent; } + *::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } +} +``` + +> **Contrast rule for light theme:** score-tone *text* and `fg1` are AA only on `--surface`, not on the bare `--bg` paper. Score text always lives inside a card and always pairs with a dot + number, so this is satisfied by construction. `fg0` is decorative/non-text in both themes. + +### 2.2 `web/tailwind.config.cjs` — map utilities to the variables + +```js +theme: { + extend: { + colors: { + bg: 'var(--bg)', + surface: 'var(--surface)', + elevated: 'var(--elevated)', + hairline: 'var(--border)', + 'hairline-strong': 'var(--border-strong)', + fill: 'var(--fill)', + 'fill-hover': 'var(--fill-hover)', + fg: { + 0: 'var(--fg0)', 1: 'var(--fg1)', 2: 'var(--fg2)', + 3: 'var(--fg3)', 4: 'var(--fg4)', 5: 'var(--fg5)', + }, + brass: { DEFAULT: 'var(--brass)', strong: 'var(--brass-strong)', bg: 'var(--brass-bg)' }, + score: { + success: 'var(--score-success)', warning: 'var(--score-warning)', fail: 'var(--score-fail)', + 'success-bg': 'var(--score-success-bg)', 'warning-bg': 'var(--score-warning-bg)', 'fail-bg': 'var(--score-fail-bg)', + }, + star: 'var(--star)', + }, + fontFamily: { + sans: ['DM Sans', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'system-ui', 'sans-serif'], + mono: ['DM Mono', 'SF Mono', 'Monaco', 'Cascadia Code', 'monospace'], + }, + fontSize: { + // tight, dense scale: 12px UI text, 11px labels, 21px titles + label: ['0.6875rem', { lineHeight: '1rem', letterSpacing: '0.04em' }], // 11px + sm: ['0.75rem', { lineHeight: '1.1rem' }], // 12px + base: ['0.8125rem', { lineHeight: '1.35rem' }], // 13px + md: ['0.875rem', { lineHeight: '1.5rem' }], // 14px + lg: ['1.0625rem', { lineHeight: '1.4rem' }], // 17px + title: ['1.3125rem', { lineHeight: '1.6rem', letterSpacing: '-0.01em' }], // 21px + }, + }, +} +``` + +> Migration note: the existing `glean-*`, `cement`, `surface-page`, hard-coded `#1A1A1A`, and the interim `night-*`/`constellation`/`gold` tokens are removed. Every surface migrates to `bg`/`surface`/`fg-*`/`brass`. This is the bulk of the diff and is done route-by-route (see §9). + +--- + +## 3. Theme switching (no flash) + +`data-theme` lives on ``. An inline script sets it before first paint from `localStorage` (fallback dark), so there's no flash. A toggle in the nav flips and persists it. + +### 3.1 No-flash script in `web/src/routes/__root.tsx` `RootDocument` + +```tsx +function RootDocument({ children }: { children: ReactNode }) { + return ( + + + + + * Re-scan: window.impeccableScan() + */ +(function () { +if (typeof window === 'undefined') return; +// --- cli/engine/shared/constants.mjs --- +// ─── Section 1: Constants ─────────────────────────────────────────────────── + +const SAFE_TAGS = new Set([ + 'blockquote', 'nav', 'a', 'input', 'textarea', 'select', + 'pre', 'code', 'span', 'th', 'td', 'tr', 'li', 'label', + 'button', 'hr', 'html', 'head', 'body', 'script', 'style', + 'link', 'meta', 'title', 'br', 'img', 'svg', 'path', 'circle', + 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'use', +]); + +// Per-check safe-tags override for the border (side-tab / border-accent) +// rule. We intentionally re-allow