diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b69c5e0..8990cde 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,8 @@ "Bash(python -m pytest tests/test_ws_smoke.py -q --tb=short)", "Bash(tasklist //FI \"IMAGENAME eq node.exe\")", "Bash(taskkill //F //PID 21928 //PID 11840)", - "Bash(python -c \"import uvicorn; print\\(uvicorn.__version__, uvicorn.__file__\\)\")" + "Bash(python -c \"import uvicorn; print\\(uvicorn.__version__, uvicorn.__file__\\)\")", + "PowerShell(Start-Process \"C:\\\\Users\\\\anand\\\\Downloads\\\\ORION\\\\design\\\\sample-mission-control.html\")" ] } } diff --git a/.github/workflows/sentinel-gate.yml b/.github/workflows/sentinel-gate.yml new file mode 100644 index 0000000..faf0ff0 --- /dev/null +++ b/.github/workflows/sentinel-gate.yml @@ -0,0 +1,120 @@ +# Sentinel promotion gate — runs on every PR, blocks merge unless Sentinel says "promote". +# +# Drop this file into any repo's .github/workflows/. It builds a DeliveryEvent from the PR +# context and POSTs it to a running Sentinel Gateway's /api/v1/simulate, which clones the repo +# server-side, runs the Neuro-SAN review/test/risk pipeline, and returns a promotion decision. +# The job then polls the run and fails the check unless the decision is `promote`. +# +# Required repo secrets: +# SENTINEL_GATEWAY_URL base URL of a reachable Gateway, e.g. https://sentinel.example.com +# (for local dev, expose scripts/run_gateway.py :8000 via a tunnel: +# cloudflared tunnel --url http://localhost:8000) +# SENTINEL_TOKEN an admin API token from the Gateway's API_TOKENS (simulate is admin-only) +# +# ponytail: same-repo PRs only. The Gateway clones repo.url and needs BOTH base and head SHAs +# reachable there; a fork's head SHA is not in the base repo, so fork PRs are skipped. + +name: Sentinel gate + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + inputs: + base_sha: { description: base commit SHA, required: true } + head_sha: { description: head commit SHA, required: true } + +# One in-flight gate per PR; a new push cancels the previous run. +concurrency: + group: sentinel-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + # Which promotion this gate represents. Override per-repo if merging means a later stage. + FROM_ENV: dev + TO_ENV: test + POLL_TIMEOUT_SECONDS: "1200" + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - name: Skip fork PRs + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork + run: | + echo "::warning::Sentinel gate skips fork PRs (head SHA not reachable from base repo)." + exit 0 + + - name: Submit run and gate on the decision + env: + GW: ${{ secrets.SENTINEL_GATEWAY_URL }} + TOKEN: ${{ secrets.SENTINEL_TOKEN }} + GH_TOKEN: ${{ github.token }} + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.inputs.base_sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }} + PR_TITLE: ${{ github.event.pull_request.title || github.ref_name }} + PR_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + PR_AUTHOR: ${{ github.actor }} + run: | + set -euo pipefail + [ -n "$GW" ] || { echo "::error::SENTINEL_GATEWAY_URL secret is not set"; exit 1; } + [ -n "$TOKEN" ] || { echo "::error::SENTINEL_TOKEN secret is not set"; exit 1; } + + # Authenticated clone URL so the Gateway can reach private repos (token expires with the job). + REPO_URL="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + event=$(jq -nc \ + --arg eid "gha-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ + --arg url "$REPO_URL" \ + --arg name "$GITHUB_REPOSITORY" \ + --arg base "$BASE_SHA" --arg head "$HEAD_SHA" \ + --arg branch "$PR_BRANCH" --arg title "$PR_TITLE" --arg author "$PR_AUTHOR" \ + --arg from "$FROM_ENV" --arg to "$TO_ENV" \ + '{event_id:$eid, source:"github", + repo:{url:$url, name:$name, default_branch:"main"}, + change:{base_sha:$base, head_sha:$head, branch:$branch, title:$title, author:$author}, + target_transition:{from_env:$from, to_env:$to}, + requested_by:$author}') + + resp=$(curl -sf -X POST "$GW/api/v1/simulate" \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d "{\"event\": $event}") + run_id=$(echo "$resp" | jq -r '.run_id') + [ -n "$run_id" ] && [ "$run_id" != "null" ] || { echo "::error::no run_id in: $resp"; exit 1; } + echo "Sentinel run: $GW/runs/$run_id" + + # Poll until the run reaches a terminal state. + deadline=$(( $(date +%s) + POLL_TIMEOUT_SECONDS )) + state="" + while [ "$(date +%s)" -lt "$deadline" ]; do + detail=$(curl -sf "$GW/api/v1/runs/$run_id" -H "Authorization: Bearer $TOKEN") + state=$(echo "$detail" | jq -r '.run.state') + echo " state=$state" + [ "$state" = "done" ] || [ "$state" = "failed" ] && break + sleep 10 + done + + decision=$(echo "$detail" | jq -r '.decision.decision // "unknown"') + risk=$(echo "$detail" | jq -r '.risk_score.score // "n/a"') + crit=$(echo "$detail" | jq -r '.review_report.counts.critical // 0') + + { + echo "## Sentinel gate" + echo "" + echo "| field | value |" + echo "|---|---|" + echo "| decision | \`$decision\` |" + echo "| risk | $risk |" + echo "| critical findings | $crit |" + echo "| run | [$run_id]($GW/runs/$run_id) |" + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$state" != "done" ]; then + echo "::error::Sentinel run did not finish (state=$state). See $GW/runs/$run_id" + exit 1 + fi + if [ "$decision" != "promote" ]; then + echo "::error::Sentinel decision is '$decision' (risk $risk, $crit critical). Review at $GW/runs/$run_id" + exit 1 + fi + echo "Sentinel: promote ✅ (risk $risk)" diff --git a/CLAUDE.md b/CLAUDE.md index e377722..42ea7b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,8 +23,9 @@ Two roadmap documents exist. Know the difference: - **`ORION_SAAS_ROADMAP.md`** (v2.0) — governs **priority ordering**. What to build next. When two tasks compete, this doc wins. Source of truth for Phase 0–5 scope, the known-defect register (D-01…D-13), and honest market positioning. - **`AREP_IMPLEMENTATION_ROADMAP.md`** (v1.1) — governs **technical implementation detail**. How to build it. Specific data structures, acceptance criteria, file names. +- **`ORION_UI_DESIGN.md`** (v1.0) — governs **ALL frontend/UI visual work**. The binding design system ("Mission Control"): tokens, typography, components, theming, page-by-page specs. Any change under `orion-frontend/` must conform. Companion `ORION_UI_REDESIGN_PLAN.md` = the migration sequencing; approved sample = `design/sample-mission-control.html`. See Section 9. -When they conflict on priority: SaaS roadmap wins. When you need implementation depth: read the technical roadmap. +When they conflict on priority: SaaS roadmap wins. When you need implementation depth: read the technical roadmap. For anything visual/frontend: `ORION_UI_DESIGN.md` is authoritative. **Current priority: Phase 0 — Security & Score Integrity** (SaaS roadmap v2.0). No Stripe, no new features until Phase 0 exits. See Section 13. @@ -300,13 +301,32 @@ All API errors return `{"detail": "..."}` — match this shape in new error hand React 18, Vite 5, React Router 6. No TypeScript — plain JSX. +### Design system — `ORION_UI_DESIGN.md` is BINDING (read before ANY frontend work) + +**Every visual change under `orion-frontend/` MUST conform to `ORION_UI_DESIGN.md`** (the "Mission Control" design system, approved 2026-06-25). Read it before writing any page, component, or style. It is the source of truth for colors, typography, spacing, radius, components, theming, and per-page layout — the approved render is `design/sample-mission-control.html`. + +- **Use tokens only.** Never hardcode a hex, font size, radius, or one-off color — use the CSS variables defined in `index.css` per the design doc. Old purple/glass tokens (`#6c63ff`, `--accent-primary` gradient, `.glass-card` glow) are retired. +- **Fonts:** Chakra Petch (display) / Saira (UI) / JetBrains Mono (all numerals, tabular). No Inter/Roboto/system body. +- **Theme:** dark-default, light available; `data-theme` on ``, persisted to `localStorage` key `orion-theme`. This is the **only** sanctioned localStorage UI-pref key — it does NOT relax the D-04 auth-token rule below. +- **No emoji as UI icons** — use the inline-SVG set in `src/components/common/Icon.jsx`. +- **Banned aesthetics (permanent):** glassmorphism-as-primary-surface, purple/violet gradients, glow-pulse/float animations, rounded-2xl, generic AI-startup hero. See design doc §1. +- **Out of scope = don't.** Don't add visual features, routes, libraries, or styles not described in the design doc unless the user explicitly asks. New pattern needed → add it to `ORION_UI_DESIGN.md` in the same change, then implement. +- Migration status + phase order live in `ORION_UI_REDESIGN_PLAN.md`. + +**Implementation status: DONE** (redesign Phases A–I complete). The code now matches the design doc. Key infrastructure: +- Tokens + global utilities (`.panel`/`.panel--live`, `.btn*`, `.field`, `.chip*`, `.data-table`, `.mono-label`, `.num`, `.spec-strip`, grid backdrop, `.skeleton`, `.skip-link`) live in `src/index.css`. Legacy purple/`glass-card` aliases were removed — use canonical tokens only. +- Theme: `src/theme/ThemeContext.jsx` (`useTheme()` → `{theme, toggleTheme, setTheme}`), control = `src/components/common/ThemeToggle.jsx`, no-FOUC inline script in `index.html`, key `orion-theme`. Dark default. +- Icons: `src/components/common/Icon.jsx` (``). No emoji. Add new icons there + list in design doc §7. +- Shell: `src/components/common/HudBar.jsx` (sticky status strip) + real `src/components/common/Sidebar.jsx` (numbered nav). `src/components/common/ErrorBoundary.jsx` wraps the app in `main.jsx`; `src/pages/NotFoundPage.jsx` is the `*` route. +- Recharts theming: recompute palette from CSS vars keyed on `useTheme().theme` (see `useChartPalette` in `DashboardPage.jsx`). Never hardcode chart hex. + ### Rules - All HTTP calls through `src/services/api.js` — never use `fetch()` directly in component. - Auth token lives in `AuthContext` — use `const { user, token, logout } = useAuth()` everywhere. - **Token storage (D-04, being redesigned in Phase 0.4)**: today `AuthContext` persists the JWT in `localStorage` (`orion_token` key) — this is a known security defect, target is httpOnly cookie + `GET /api/auth/me` bootstrap. Don't add NEW `localStorage` token reads/writes outside `AuthContext`; don't build features that depend on reading the raw token in components. - `OrgContext.jsx` exists but `OrgProvider` is NOT mounted anywhere — `useOrg()` throws. Mount it or don't call it (Phase 0.6 resolves). -- Dashboard sections: `overview`, `scenarios`, `runs`, `models`, `settings` — string keys used in `Sidebar`. +- Dashboard sections: `overview`, `scenarios`, `runs`, `models`, `batches`, `compare`, `settings` — string keys used in `Sidebar` (numbered nav). Non-`overview` views render styled "coming soon" placeholder panels until wired. - Charts use Recharts (`LineChart`, `RadarChart`, `BarChart`) — don't add Chart.js or D3. - 3D visualization uses `@react-three/fiber` + `@react-three/drei` — don't use raw Three.js imperative API in React components. - CSS co-located: `Component.jsx` + `Component.css` same folder. No CSS modules, no Tailwind. @@ -320,7 +340,7 @@ Add to `src/services/api.js` following existing pattern, then call `api.myNewMet Backend streams at `WS /ws/simulation/{run_id}?token=`; consumer wired end-to-end: - `src/hooks/useSimulationStream.js` — owns WebSocket. Returns `{ frame, isConnected, status, error, latencyRef }`. Handles exponential-backoff reconnect (max 3 attempts), cleans up on unmount. Don't open sockets from components directly. -- `src/components/simulation/SimulationViewer.jsx` — R3F scene (road, ego, NPCs) + HTML HUD overlay (sim time, speed, g-force, metric bars, verdict badge). Mounted at `/simulation/:runId`. Has `← Dashboard` back button (glass style, centered top). +- `src/components/simulation/SimulationViewer.jsx` — R3F scene (road, ego, NPCs) + HTML HUD overlay (sim time, speed, g-force, metric bars, verdict badge). Mounted at `/simulation/:runId`. Has `← Dashboard` back button (`.btn-ghost`, top). Scene + overlay restyled to Mission Control palette; HUD = glassless instrument panels. - Frame shape frozen in `SimulationEngine.get_tick_frame()`. To extend protocol: add fields there, consume in hook/viewer. - Server closes with `{"event": "stream_end", ...}` — hook handles before deciding reconnect. - Latency measured from `frame.emit_ts_ms` against client `Date.now()`; running average and max exposed via `latencyRef.current` for HUD display. diff --git a/ORION_UI_DESIGN.md b/ORION_UI_DESIGN.md new file mode 100644 index 0000000..2b10df0 --- /dev/null +++ b/ORION_UI_DESIGN.md @@ -0,0 +1,421 @@ +# ORION UI Design System — "Mission Control" + +> **Status:** v1.0 · Approved 2026-06-25 · **Governing document for ALL frontend work.** +> **Reference artifact:** `design/sample-mission-control.html` (the approved visual sample — open in a browser). +> +> **Authority:** Every change to `orion-frontend/` MUST conform to this document. Nothing visual ships outside the rules here unless the user explicitly authorizes a deviation. If a request and this doc conflict, stop and ask. When implementation detail is ambiguous, this doc + the sample file win. +> +> Companion: `ORION_UI_REDESIGN_PLAN.md` (how the migration is sequenced). CLAUDE.md §9 points here. + +--- + +## 0. How to use this document + +- Building a new screen/component → read §1–§10 (the system) then find the closest spec in §11. +- Touching colors, type, spacing, radius → use the tokens in §3–§5 **only**. Never hardcode a hex, px font size, or one-off radius. +- Unsure what something looks like → open `design/sample-mission-control.html`; it is the canonical render of this spec. +- Adding a token / component pattern not covered here → add it to this doc in the same edit (CLAUDE.md §0 standing instruction). + +--- + +## 1. Design philosophy + +ORION is a **deterministic, statistically rigorous test harness** for autonomous-driving policies. The UI must read like a **precision measurement instrument**, not a consumer AI app. + +**The five principles (non-negotiable):** + +1. **Instrument, not decoration.** Every element looks like it reports a measured value. Chrome serves data; it never competes with it. +2. **Structure is visible.** Grid lines, borders, corner crop-marks, section indices. The skeleton is part of the aesthetic — we expose it, we don't hide it behind blur. +3. **Numbers are sacred.** All numerals are monospace + tabular. A value never shifts horizontally as it updates. Data is the hero. +4. **Sharp and flat.** Hard 2–4px corners, 1px borders, no glassmorphism, no drop-shadow soup, no glow-float. Depth comes from borders and surface tone, not blur. +5. **Restraint with one signal.** Hazard amber is the single attention color. If everything glows, nothing reads. Amber marks the live/active/primary thing only. + +**Banned (this is the "generic AI slop" we are escaping):** +`Inter`/`Roboto`/system-font body · purple/violet gradients · `backdrop-filter` glass cards as the primary surface · pulsing glow shadows · floating/levitating cards · rounded-2xl everything · emoji as UI icons · centered hero with two gradient blobs. None of these appear anywhere. + +--- + +## 2. Brand & identity + +- **Wordmark:** `ORION//AREP` (HUD contexts) or `ORION` (compact). Set in Chakra Petch 700, letter-spacing `.16–.18em`. The `//` is amber. +- **Mark:** a 45°-rotated amber square (`◆` rendered as a CSS square `transform: rotate(45deg)`), 7–8px, with a faint amber box-shadow. Used as the bullet before the wordmark and as a favicon basis. +- **Tagline:** "Robustness, measured." Secondary: "A test harness — not a simulator." +- **Voice in UI copy:** terse, technical, telemetry-flavored. Uppercase mono micro-labels (`MIN TTC`, `SYSTEM NOMINAL`, `SEED 42`). Avoid marketing fluff in the product surface; the landing page may be slightly warmer. + +--- + +## 3. Color system + +Tokens are CSS custom properties scoped by `[data-theme="dark"]` / `[data-theme="light"]` on ``. **Dark is default.** Never use a color outside this table. + +### 3.1 Dark theme (default) + +```css +[data-theme="dark"]{ + /* surfaces */ + --bg-base: #0A0C10; /* app background (carbon) */ + --bg-sunken: #07090C; /* recessed: sidebar, page gutters */ + --bg-panel: #101420; /* default panel / card surface */ + --bg-elev: #161C2A; /* raised: popovers, menus, modals */ + --bg-inset: #0C0F16; /* inputs, progress tracks, table hover */ + + /* structural grid + borders */ + --grid-line: rgba(128,150,185,0.045); /* 26px minor grid */ + --grid-major: rgba(128,150,185,0.075); /* 130px major grid */ + --border: rgba(140,160,195,0.14); + --border-strong: rgba(140,160,195,0.26); + + /* text */ + --text: #E7ECF5; /* primary */ + --text-2: #95A1B8; /* secondary / labels */ + --text-mute: #5A6478; /* tertiary / hints / disabled */ + + /* brand — hazard amber (single attention color) */ + --amber: #F5A623; + --amber-hi: #FFC766; /* hover / lighter */ + --amber-dim: rgba(245,166,35,0.16); /* fills, active nav bg, focus ring */ + --amber-line: rgba(245,166,35,0.40); /* dashed accents, badges */ + + /* signal cyan — secondary data line, links, secondary metrics */ + --cyan: #25D3EE; + --cyan-dim: rgba(37,211,238,0.14); + + /* semantics (reserved — never used as brand decoration) */ + --pass: #34D399; /* PASS / safe / positive delta */ + --fail: #FF5C5C; /* FAIL / collision / negative delta */ + --warn: #F5A623; /* caution (== amber by design) */ + --info: #25D3EE; /* == cyan */ + + /* misc */ + --tick: rgba(149,161,184,0.55); /* default corner crop-marks */ + --focus: rgba(245,166,35,0.55); /* keyboard focus ring */ + --shadow: 0 18px 50px -24px rgba(0,0,0,0.85); + --scrim: rgba(7,9,12,0.72); /* sticky bar backdrop */ +} +``` + +### 3.2 Light theme (blueprint paper) + +Light mode is **not** "white SaaS." It is technical blueprint paper: cool off-white, the same grid + corner-ticks, amber/cyan darkened for AA contrast. + +```css +[data-theme="light"]{ + --bg-base: #EEF1F6; + --bg-sunken: #E3E8F0; + --bg-panel: #FFFFFF; + --bg-elev: #FFFFFF; + --bg-inset: #F3F5F9; + + --grid-line: rgba(28,46,82,0.05); + --grid-major: rgba(28,46,82,0.08); + --border: rgba(20,34,64,0.13); + --border-strong: rgba(20,34,64,0.24); + + --text: #0D1322; + --text-2: #46526B; + --text-mute: #8590A4; + + --amber: #C97A06; /* darkened for contrast on light */ + --amber-hi: #E8920E; + --amber-dim: rgba(232,146,14,0.14); + --amber-line: rgba(201,122,6,0.45); + + --cyan: #0C8FA8; + --cyan-dim: rgba(12,143,168,0.12); + + --pass: #0E9F6E; + --fail: #E5484D; + --warn: #C97A06; + --info: #0C8FA8; + + --tick: rgba(70,82,107,0.5); + --focus: rgba(201,122,6,0.5); + --shadow: 0 18px 44px -26px rgba(20,34,64,0.4); + --scrim: rgba(238,241,246,0.78); +} +``` + +### 3.3 Color usage rules + +- **Amber** = the live/active/primary signal only: primary button, active nav item, composite metric, "live" panel corner-ticks, the `//` in the wordmark, focus ring. Do **not** flood UI with amber. +- **Cyan** = secondary data + interaction: links, secondary chart line, secondary metric bars, secondary stats, "REC" indicators. +- **Pass/Fail** = verdict/semantic only (PASS/FAIL chips, collision flag, score deltas). Never used as a brand accent. Because `--warn === --amber`, never place an amber brand button immediately beside an amber caution state — disambiguate with a label. +- **Text hierarchy:** `--text` for values/headings, `--text-2` for labels/body, `--text-mute` for hints/placeholders/disabled. +- **Contrast target:** body text ≥ 4.5:1, large/secondary ≥ 3:1, in **both** themes. Amber on `--bg-base` is for accents/large text, not long body copy. + +--- + +## 4. Typography + +Three families, each with one job. Loaded via Google Fonts (preconnect in `index.html`). + +``` +Display / headings / wordmark : 'Chakra Petch' (400 500 600 700) — squared HUD character +UI / body / labels : 'Saira' (300 400 500 600 700 800) +Numerals / data / code / mono : 'JetBrains Mono' (400 500 700) — tabular, all numbers +``` + +```css +:root{ + --ff-disp:'Chakra Petch', sans-serif; + --ff-ui:'Saira', system-ui, sans-serif; + --ff-mono:'JetBrains Mono', ui-monospace, monospace; +} +``` + +### 4.1 Type roles & scale + +| Role | Family | Size | Weight | Tracking | Notes | +|------|--------|------|--------|----------|-------| +| Hero display | Chakra Petch | `clamp(40px,5.2vw,68px)` | 700 | -0.01em | line-height .98 | +| Page title (h1) | Chakra Petch | 26px | 700 | .02em | dashboard header | +| Panel/section title | Chakra Petch | 20px | 600 | .04em | | +| HUD section label (h2) | Chakra Petch | 15px | 600 | .22em UPPER | `01 / OVERVIEW` style | +| Body | Saira | 15px | 400 | normal | | +| Lead paragraph | Saira | 17px | 300 | normal | hero / aside intros | +| Small / secondary | Saira | 13px | 400 | normal | | +| **Mono micro-label** | JetBrains Mono | 10.5px | 400 | .22em UPPER | the signature label style | +| Form field label | JetBrains Mono | 10px | 400 | .18em UPPER | | +| Big metric numeral | JetBrains Mono | 34px | 700 | -0.01em | dashboard metric panels | +| Gauge numeral | JetBrains Mono | 46px | 700 | -0.01em | hero composite gauge | +| Table data | JetBrains Mono | 12px | 400/600 | normal | values 600, labels 400 | + +### 4.2 Numerals rule (hard) + +**Every number a user reads is JetBrains Mono with `font-feature-settings:'tnum' 1,'zero' 1;`** — scores, counts, seeds, timestamps, credits, latency, money, table cells, axis ticks. Reusable `.num` class. No exceptions. This is the core identity device. + +### 4.3 Mono micro-label + +The recurring label treatment (`.mono-label`): JetBrains Mono, 10.5px, `letter-spacing:.22em`, `text-transform:uppercase`, color `--text-mute` (or `--text-2` when prominent). Used for section labels, panel captions, telemetry keys, status text. + +--- + +## 5. Spacing, grid, radius, borders, motion + +### 5.1 Spacing scale (keep existing token names) + +`--space-1:.25rem` `2:.5` `3:.75` `4:1` `5:1.25` `6:1.5` `8:2` `10:2.5` `12:3` `16:4` `20:5` (rem). Default panel padding `16–22px`. Section vertical rhythm `46px`. Gap between cards `14px`. + +### 5.2 Layout grid & structural backdrop + +- App max content width: **1280px**, centered, `0 24px` gutters. +- **Structural grid backdrop** is a fixed, non-interactive layer on the app background: minor lines every **26px**, major lines every **130px**, drawn with `--grid-line` / `--grid-major` via layered `linear-gradient`s. Plus two restrained corner radial glows (amber top-right, cyan bottom-left) at very low alpha. This is global atmosphere; it must never reduce text contrast. +- Dashboard shell grid: `230px` sidebar + fluid main. + +### 5.3 Radius + +```css +--radius-sm:2px; --radius-md:3px; --radius-lg:4px; /* 4px is the MAX */ +``` +Default everything to `--radius-md` (3px). Pills/circles only for status dots, avatars, the gauge. **No `border-radius` > 4px anywhere** except intrinsic circles. + +### 5.4 Borders & corner crop-marks + +- Standard separators/panel edges: `1px solid var(--border)`. Emphasis edges: `--border-strong`. +- **Corner crop-marks** are the panel signature: 9px L-shaped marks (2px) at the top-left and bottom-right of every `.panel`, color `--tick`. On a "live/active/featured" panel (`.panel--live`) the marks are `--amber`. Implemented via `::before`/`::after`. Use them; they are how a card reads as an instrument frame. + +### 5.5 Elevation + +Depth via surface tone + border, not shadow. Order dark→light: `--bg-sunken < --bg-base < --bg-inset < --bg-panel < --bg-elev`. `--shadow` is reserved for genuinely floating layers only (dropdowns, modals, toasts). + +### 5.6 Motion + +```css +--t-fast:150ms; --t-base:200ms; --t-slow:350ms; +--ease:cubic-bezier(.2,.7,.2,1); +``` +- Theme switch: `background`/`color`/`border-color` transition `--t-slow`. +- Hover/focus: `--t-fast`–`--t-base`. +- Allowed motion: staggered hero reveal on load (opacity+translateY, `animation-delay` steps of 80–100ms), gauge stroke-fill animation, number count-up on first paint, sparkline draw, nav/hover transitions, the HUD live-dot blink (2s), and the **ambient corner-glow breathe** — the two background radial-glow layers (amber TR on `body::after`, cyan BL on `#root::before`) drift opacity + a few-% scale/translate on long, out-of-phase loops (~13s / ~17s, `ease-in-out`). Background-only, very low alpha, must never pull focus from data. +- **Banned motion:** float/levitate loops, glow-pulse on cards/buttons/interactive elements (the ambient *background* glow above is the only sanctioned glow animation), slow-spin orbs, parallax star fields as primary decor (the landing 3D element is redesigned per §11.1, not removed wholesale, but stays subtle and instrument-like). +- **`@media (prefers-reduced-motion: reduce)`**: disable count-up, gauge animation, sparkline redraw loops, blink; render final state instantly. Mandatory. + +--- + +## 6. Signature visual devices (the "Mission Control kit") + +These recurring elements make the language identifiable. Reuse them; do not invent parallel patterns. + +1. **HUD status bar** — sticky top strip, 38px, `--scrim` + blur, mono 11px. Contents: wordmark · live status (`● SYSTEM NOMINAL`) · spacer · `SEED 42` · `BUILD ` · UTC clock · `⌘K` hint · theme toggle. Present on app (authed) surfaces. +2. **Structural grid backdrop** — §5.2. +3. **Panel + corner crop-marks** — §5.4. The base container for everything. +4. **Section indices** — headings prefixed `01 /`, `02 /` in amber mono; nav items numbered `01…07`. +5. **Mono micro-labels** — §4.3. +6. **Reticle gauge** — circular SVG progress ring with 4 cardinal tick marks + crosshair, big mono numeral centered, mono label beneath. Used for composite/headline scores. +7. **Sparkline / line chart on grid** — thin lines (amber primary, cyan secondary), square 4px data markers, horizontal grid rules, optional 8%-alpha area fill under the primary line. +8. **Status chips** — rectangular (radius 3px), 1px colored border, uppercase mono 9.5px: `PASS` (pass), `FAIL` (fail), plus `QUEUED`/`RUNNING`/`DONE` variants (§9.9). +9. **Live dot** — small circle with matching box-shadow + 2s blink for "live/online/recording." +10. **Spec strip** — bordered row of `value + mono-label` cells separated by 1px dividers (hero stats, KPI rows). + +--- + +## 7. Iconography + +- **No emoji anywhere in the product UI.** Replace all current emoji (📊🗺️🏁🤖⚙️🛡️⚡💥✅ etc.). +- Single custom **inline-SVG icon set** in `src/components/common/Icon.jsx`: stroke-based, `stroke-width:1.5`, `currentColor`, 18–20px, square/sharp joints (no rounded line-caps where avoidable), 24×24 viewBox. +- Required icons (min set): `overview` (grid/gauge), `scenarios` (route/map-pin), `runs` (flag), `models` (cube), `batches` (layers/stack), `compare` (columns), `settings` (sliders), `billing` (card), `search`, `key`, `upload`, `download`, `play` (launch), `refresh`, `power` (logout), `close`, `check`, `warning` (triangle), `chevron-down/right`, `external`, `copy`. Add to the set (and list it here) when a new one is needed. +- Decorative HUD glyphs (`◆`, `//`, `●`, `▲`/`▼` deltas) are CSS/text, not the icon set. +- No third-party icon library dependency (keeps bundle lean + style exact). + +--- + +## 8. Theme system + +- Source of truth: `data-theme` attribute on `` ∈ {`dark`,`light`}. **Default `dark`.** +- Persisted to `localStorage` key **`orion-theme`**. (This is a UI preference, explicitly exempt from the JWT-in-localStorage rule of CLAUDE.md §9 — never store auth/PII here, theme only.) +- **No-FOUC:** a tiny blocking inline script in `index.html` sets `data-theme` from `localStorage` (fallback `dark`) **before** first paint. +- React access via `ThemeProvider` (`src/theme/ThemeContext.jsx`) exposing `{ theme, toggleTheme, setTheme }`; `useTheme()` hook. The HUD/nav `ThemeToggle` is the only control. +- New-visitor default is dark; we do **not** auto-follow `prefers-color-scheme` (dark is the brand default), but we **do** honor `prefers-reduced-motion`. +- **Charts must re-read CSS variables on theme change** (Recharts colors are passed from JS — recompute from `getComputedStyle` on theme toggle; see §12). + +--- + +## 9. Component library + +All components live in `orion-frontend/src/components/...`, CSS co-located (`Component.jsx` + `Component.css`), no CSS modules, no Tailwind (CLAUDE.md §9). Shared primitives may use global utility classes defined in `index.css`. + +### 9.1 Button (`.btn`) +Chakra Petch 600, 12.5px, `.14em` uppercase, radius 3px, padding `12px 22px`. +- `.btn-primary`: bg `--amber`, text `#0A0C10`; hover `--amber-hi` + amber glow + `translateY(-1px)`. +- `.btn-ghost`: transparent, `--border-strong`; hover border/text `--cyan`. +- `.btn-danger`: transparent, border/text `--fail`; hover fills `--fail`. +- `.btn-sm`: 11px / `8px 14px`. Disabled: `opacity:.45; cursor:not-allowed`. Trailing arrow `→` in mono. + +### 9.2 Input / select / textarea (`.field`) +Label = form-field mono label (§4.1). Control: bg `--bg-inset`, 1px `--border`, radius 3px, JetBrains Mono 14px, padding `12px 14px`. Focus: border `--amber` + `0 0 0 3px var(--amber-dim)`. Placeholder `--text-mute`. Error state: border `--fail` + helper text `--fail`. Selects use a custom chevron icon. + +### 9.3 Panel (`.panel`) +The base surface. bg `--bg-panel`, 1px `--border`, radius 3px, corner crop-marks (§5.4). `.panel--live` = amber crop-marks for the active/featured panel. Hover for interactive panels: border → `--border-strong` (no lift, no glow). + +### 9.4 Metric panel +Panel containing: top row (`.mono-label` + delta `▲/▼ n` colored pass/fail) · big mono numeral (composite = amber, others = `--text`) · thin progress bar (track `--bg-inset`, fill amber for composite else cyan). Composite metric uses `.panel--live`. + +### 9.5 Reticle gauge +SVG per §6.6. Ring track `--bg-inset`, progress stroke `--amber` (`stroke-linecap:butt`), 4 cardinal ticks `--tick`. Center: mono numeral + mono label. Animated fill on mount (skip under reduced-motion). + +### 9.6 Sidebar nav +`230px`, bg `--bg-sunken`, right border. Brand row (mark + `ORION`). Items: numbered (`01…`), icon + uppercase mono 12px label, `2px` transparent left-border. Hover: `--bg-inset` + `--text`. Active: `--amber` text + amber left-border + `--amber-dim` bg. Footer: credits gauge (mono value cyan + mini bar) + user chip (amber square avatar w/ initial + name/email mono). + +### 9.7 Table (`.data-table`) +Full-width, collapsed borders, JetBrains Mono 12px. `th`: mono 9.5px `.14em` uppercase `--text-mute`, bottom 1px border. `td`: 11px padding, 1px row border, `--text-2`; ID cells `--cyan`, model/name cells `--text`, score cells `--text` 600. Row hover `--bg-inset`. Empty state row spans all columns, centered `--text-mute`. + +### 9.8 Charts +Recharts only (CLAUDE.md §9). Theme per §12: grid `--border`, axes `--text-mute` mono 11px, primary series amber, secondary cyan, tooltip bg `--bg-elev` + 1px `--border` + mono. Line `strokeWidth:2`, square dots or none, optional faint area fill. Radar: grid `--border`, single amber series at low fill. Bars: amber/cyan, `radius:[2,2,0,0]`. + +### 9.9 Status chip (`.chip`) +Radius 3px, 1px border, mono 9.5px `.12em` uppercase, transparent bg, colored border+text. Variants: `pass`(--pass) `fail`(--fail) `queued`(--text-mute) `running`(--amber) `done`(--cyan) `error`(--fail). + +### 9.10 Modal / dialog +Overlay `--scrim`. Panel `--bg-elev` + `--shadow` + crop-marks. Mono title-bar caption + close icon. Sharp corners. Focus-trapped, `Esc` closes, returns focus to trigger. + +### 9.11 Toast / inline alert (`AlertBanner`) +Left 2px accent border colored by severity (info=cyan, warn/caution=amber, error=fail, success=pass), bg `--bg-panel`, mono label + body. Toasts top-right, stack, auto-dismiss (pausable on hover); inline alerts inline. + +### 9.12 Empty / loading / error states (every data view MUST define all three) +- **Empty:** bordered panel, centered mono-label + one-line `--text-mute` hint + optional primary action. +- **Loading:** skeleton blocks (bg `--bg-inset`, subtle shimmer respecting reduced-motion) matching final layout — not a spinner-only screen. A thin amber top progress bar is allowed for route/data loads. +- **Error:** panel with `--fail` 2px left border, mono `ERROR` label, message, retry button. + +### 9.13 Tabs / segmented control +Mono uppercase labels, 2px bottom-border indicator amber on active, `--text-2`→`--text` on hover. + +### 9.14 Badge / KPI cell +Spec-strip cell (§6.10): mono value (cyan small unit) + mono micro-label beneath, 1px dividers between cells. + +--- + +## 10. Accessibility & responsive + +- **Contrast** per §3.3 in both themes; verify amber/cyan text sizes meet ratio. +- **Focus:** visible ring `0 0 0 3px var(--focus)` on all interactive elements; never remove outlines without replacement. Logical tab order. Skip-to-content link on app shell. +- **Keyboard:** modals trap + `Esc`; menus arrow-navigable; all actions reachable without a mouse. +- **ARIA/semantics:** real ` +
+ {/* ── Left aside · access-control telemetry ── */} +
- - +
+
SESSIONJWT · httpOnly
+
SCOPEorg_id + role
+
RATE LIMIT5 / min / IP
+
STATUSAWAITING AUTH
+
+ -

- -

+ {/* ── Right card · sign-in form ── */} +
+
ORION//AREP · SECURE
+

Sign in

- {showForgot && ( -
-

Reset your password

- {forgotSuccess ? ( -

- If that email is registered, a reset link has been sent. -

- ) : ( -
- {forgotError &&
{forgotError}
} -
- + {error && ( +
+ + {error} +
+ )} + + +
+ + setIdentifier(e.target.value)} + required + id="login-identifier" + /> +
+
+ +
setForgotEmail(e.target.value)} + type={showPassword ? 'text' : 'password'} + className="field-input" + placeholder="••••••••••••" + value={password} + onChange={(e) => setPassword(e.target.value)} required - autoFocus + maxLength={72} + id="login-password" /> +
-
+ +
+ + - - )} -

-

+ + -

-
- )} + + + {showForgot && ( +
+
RESET · PASSWORD
+ {forgotSuccess ? ( +
+ + If that email is registered, a reset link has been sent. +
+ ) : ( +
+ {forgotError && ( +
+ + {forgotError} +
+ )} +
+ + setForgotEmail(e.target.value)} + required + autoFocus + /> +
+ +
+ )} +

+ +

+
+ )} -

- Don't have an account? Create one -

+ {!showForgot && ( +
+ No account? Request access → +
+ )} +
+
); diff --git a/orion-frontend/src/components/auth/SignupForm.jsx b/orion-frontend/src/components/auth/SignupForm.jsx index e5c2b12..194e813 100644 --- a/orion-frontend/src/components/auth/SignupForm.jsx +++ b/orion-frontend/src/components/auth/SignupForm.jsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import Icon from '../common/Icon'; import './AuthForms.css'; export default function SignupForm() { @@ -30,116 +31,125 @@ export default function SignupForm() { }; return ( -
-
-
- -

Create Account

-

Join ORION to start evaluating models

+
+
+
+ 02 / +

Authentication · Request Access

+ login · signup · reset share this frame
- {error &&
{error}
} +
+ {/* ── Left aside · access-control telemetry ── */} + -
-
- - setFullName(e.target.value)} - id="signup-fullname" - /> -
-
- - setUsername(e.target.value)} - required - id="signup-username" - /> -
-
- - setEmail(e.target.value)} - required - id="signup-email" - /> -
-
- -
- setPassword(e.target.value)} - required - minLength={6} - maxLength={72} - id="signup-password" - style={{ paddingRight: '40px' }} - /> - +
+
+

MUST BE 6–72 CHARACTERS.

+ + +
+ +
+ Already have an account? Sign in →
-

- Must be 6–72 characters. -

- - - -

- Already have an account? Sign in -

+
); diff --git a/orion-frontend/src/components/billing/PlanCard.css b/orion-frontend/src/components/billing/PlanCard.css index efbe892..cca94a3 100644 --- a/orion-frontend/src/components/billing/PlanCard.css +++ b/orion-frontend/src/components/billing/PlanCard.css @@ -1,4 +1,43 @@ -/* ORION — PlanCard styles [Phase stub] */ -.PlanCard { - /* TODO: implement styles */ +/* ══ PlanCard — plan panel (§11.4) ═════════════════════════════════════ */ +.plan-card { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-6); } +.plan-card.is-featured { border-color: var(--border-strong); } + +.plan-head { display: flex; align-items: center; justify-content: space-between; } + +.plan-name { + font-family: var(--ff-disp); + font-weight: 600; + font-size: 20px; + letter-spacing: 0.04em; + color: var(--text); +} + +.plan-price { display: flex; align-items: baseline; gap: 6px; margin-bottom: var(--space-2); } +.plan-amount { font-size: 34px; font-weight: 700; line-height: 1; color: var(--amber); } +.plan-period { font-size: 12px; color: var(--text-mute); } + +.plan-features { + list-style: none; + display: flex; + flex-direction: column; + gap: 9px; + flex: 1; + margin: var(--space-2) 0 var(--space-4); +} +.plan-features li { + display: flex; + align-items: flex-start; + gap: 9px; + font-family: var(--ff-ui); + font-size: 13px; + color: var(--text-2); + line-height: 1.45; +} +.plan-check { color: var(--cyan); flex-shrink: 0; margin-top: 2px; } + +.plan-cta { width: 100%; } diff --git a/orion-frontend/src/components/billing/PlanCard.jsx b/orion-frontend/src/components/billing/PlanCard.jsx index 2f49ace..0d0df2b 100644 --- a/orion-frontend/src/components/billing/PlanCard.jsx +++ b/orion-frontend/src/components/billing/PlanCard.jsx @@ -1,19 +1,68 @@ -// ORION — PlanCard [P1] -// Plan name, price, feature list, upgrade CTA button -// TODO [P1]: Implement this component. +// ORION — PlanCard +// Plan panel (ORION_UI_DESIGN.md §11.4): mono .num price, feature list, CTA .btn. +// .panel--live marks the current/featured plan. Presentational only. -import React from 'react'; +import Icon from '../common/Icon'; import './PlanCard.css'; /** * PlanCard - * Plan name, price, feature list, upgrade CTA button + * + * Props: + * name string — plan name (Chakra Petch title) + * price number|string — monthly price (mono .num); pass string for 'Custom' + * period string — e.g. '/mo' (default) + * features string[] — feature bullet list + * current bool — this is the org's active plan (renders .panel--live + badge) + * featured bool — visually highlight (instrument-frame emphasis) + * ctaLabel string — button text (default derived from current) + * onSelect fn — CTA click handler + * disabled bool — disable CTA */ -export default function PlanCard(/* props */) { +export default function PlanCard({ + name, + price, + period = '/mo', + features = [], + current = false, + featured = false, + ctaLabel, + onSelect, + disabled = false, +}) { + const showPrice = typeof price === 'number' ? `$${price.toLocaleString()}` : price; + const label = ctaLabel ?? (current ? 'Current Plan' : 'Select Plan'); + return ( -
- {/* TODO [P1]: Implement PlanCard */} -

PlanCard — not yet implemented [P1]

+
+
+ Plan + {current && Active} +
+

{name}

+
+ {showPrice} + {typeof price === 'number' && {period}} +
+ +
    + {features.map((f, i) => ( +
  • + + {f} +
  • + ))} +
+ +
); } diff --git a/orion-frontend/src/components/billing/UsageBar.css b/orion-frontend/src/components/billing/UsageBar.css new file mode 100644 index 0000000..6cf1fec --- /dev/null +++ b/orion-frontend/src/components/billing/UsageBar.css @@ -0,0 +1,26 @@ +/* ══ UsageBar — credits used vs total (§11.4) ══════════════════════════ */ +.usage-bar { display: flex; flex-direction: column; gap: 8px; } + +.usage-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); } +.usage-readout { font-size: 13px; color: var(--text); font-weight: 600; } +.usage-sep { color: var(--text-mute); } +.usage-unit { color: var(--cyan); font-size: 11px; } + +.usage-track { + position: relative; + height: 8px; + background: var(--bg-inset); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + overflow: hidden; +} +.usage-fill { + position: absolute; + inset: 0 auto 0 0; + height: 100%; + background: var(--cyan); + transition: width var(--t-slow) var(--ease); +} +.usage-fill.is-near { background: var(--amber); } + +.usage-foot { font-size: 10px; letter-spacing: 0.08em; color: var(--text-mute); } diff --git a/orion-frontend/src/components/billing/UsageBar.jsx b/orion-frontend/src/components/billing/UsageBar.jsx index af8ce62..0690e0b 100644 --- a/orion-frontend/src/components/billing/UsageBar.jsx +++ b/orion-frontend/src/components/billing/UsageBar.jsx @@ -1,19 +1,46 @@ -// ORION — UsageBar [P1] -// Credits used / total as a coloured progress bar -// TODO [P1]: Implement this component. +// ORION — UsageBar +// Credits-used-vs-total horizontal bar (ORION_UI_DESIGN.md §11.4): track --bg-inset, +// fill --cyan, mono .num labels. Presentational only — no data wiring. -import React from 'react'; import './UsageBar.css'; +const fmtNum = (n) => (typeof n === 'number' ? n.toLocaleString() : '—'); + /** * UsageBar - * Credits used / total as a coloured progress bar + * + * Props: + * used number — credits consumed + * total number — total credits in the period + * label string — mono micro-label (default 'Run Credits') + * unit string — value unit suffix (optional) */ -export default function UsageBar(/* props */) { +export default function UsageBar({ used = 0, total = 0, label = 'Run Credits', unit }) { + const safeTotal = total > 0 ? total : 0; + const pct = safeTotal > 0 ? Math.max(0, Math.min(100, (used / safeTotal) * 100)) : 0; + // near-limit (>=90%) surfaces the single attention color + const near = pct >= 90; + return ( -
- {/* TODO [P1]: Implement UsageBar */} -

UsageBar — not yet implemented [P1]

+
+
+ {label} + + {fmtNum(used)} / {fmtNum(safeTotal)} + {unit && {unit}} + +
+
+ +
+
{pct.toFixed(0)}% used
); } diff --git a/orion-frontend/src/components/common/AlertBanner.css b/orion-frontend/src/components/common/AlertBanner.css index 219fbde..9fe5e2a 100644 --- a/orion-frontend/src/components/common/AlertBanner.css +++ b/orion-frontend/src/components/common/AlertBanner.css @@ -1,4 +1,46 @@ -/* ORION — AlertBanner styles [Phase stub] */ -.AlertBanner { - /* TODO: implement styles */ +/* ══ AlertBanner — inline alert / toast (§9.11) ════════════════════════ */ +.alert-banner { + display: flex; + align-items: flex-start; + gap: var(--space-3); + padding: 12px 14px; + background: var(--bg-panel); + border: 1px solid var(--border); + border-left: 2px solid var(--text-mute); + border-radius: var(--radius-sm); } + +/* severity left-border + lead-icon color */ +.alert-info { border-left-color: var(--cyan); } +.alert-warn { border-left-color: var(--amber); } +.alert-error { border-left-color: var(--fail); } +.alert-success { border-left-color: var(--pass); } + +.alert-lead { flex-shrink: 0; display: grid; place-items: center; margin-top: 1px; } +.alert-info .alert-lead { color: var(--cyan); } +.alert-warn .alert-lead { color: var(--amber); } +.alert-error .alert-lead { color: var(--fail); } +.alert-success .alert-lead { color: var(--pass); } + +.alert-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } +.alert-title { color: var(--text-2); } +.alert-info .alert-title { color: var(--cyan); } +.alert-warn .alert-title { color: var(--amber); } +.alert-error .alert-title { color: var(--fail); } +.alert-success .alert-title { color: var(--pass); } + +.alert-text { font-family: var(--ff-ui); font-size: 13px; color: var(--text-2); line-height: 1.5; } + +.alert-dismiss { + flex-shrink: 0; + background: transparent; + border: none; + color: var(--text-mute); + cursor: pointer; + padding: 2px; + display: grid; + place-items: center; + border-radius: var(--radius-sm); + transition: color var(--t-fast) ease; +} +.alert-dismiss:hover { color: var(--text); } diff --git a/orion-frontend/src/components/common/AlertBanner.jsx b/orion-frontend/src/components/common/AlertBanner.jsx index 11ad88d..9c55547 100644 --- a/orion-frontend/src/components/common/AlertBanner.jsx +++ b/orion-frontend/src/components/common/AlertBanner.jsx @@ -1,19 +1,59 @@ -// ORION — AlertBanner [P2] -// Dismissible alert banner for regression warnings and notifications -// TODO [P2]: Implement this component. +// ORION — AlertBanner +// Inline alert / toast (ORION_UI_DESIGN.md §9.11): 2px left accent border colored +// by severity, --bg-panel surface, mono micro-label title + Saira body. +// Presentational only — no data wiring. -import React from 'react'; +import Icon from './Icon'; import './AlertBanner.css'; +const SEVERITIES = ['info', 'warn', 'error', 'success']; + /** * AlertBanner - * Dismissible alert banner for regression warnings and notifications + * + * Props: + * severity 'info' | 'warn' | 'error' | 'success' (default 'info') + * title string — mono micro-label (optional) + * children node — body copy (Saira) + * message string — body copy alternative to children + * onDismiss fn — when provided, renders a close button + * icon bool — show leading warning glyph (default true for warn/error) + * className string */ -export default function AlertBanner(/* props */) { +export default function AlertBanner({ + severity = 'info', + title, + children, + message, + onDismiss, + icon, + className = '', +}) { + const sev = SEVERITIES.includes(severity) ? severity : 'info'; + const showIcon = icon ?? (sev === 'warn' || sev === 'error'); + const body = children ?? message; + return ( -
- {/* TODO [P2]: Implement AlertBanner */} -

AlertBanner — not yet implemented [P2]

+
+ {showIcon && ( + + )} +
+ {title &&
{title}
} + {body &&
{body}
} +
+ {onDismiss && ( + + )}
); } diff --git a/orion-frontend/src/components/common/ErrorBoundary.css b/orion-frontend/src/components/common/ErrorBoundary.css new file mode 100644 index 0000000..a64285a --- /dev/null +++ b/orion-frontend/src/components/common/ErrorBoundary.css @@ -0,0 +1,45 @@ +.errboundary { + min-height: 100vh; + display: grid; + place-items: center; + padding: 48px 24px; +} +.errboundary-card { + max-width: 520px; + padding: var(--space-10); + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + border-left: 2px solid var(--fail); +} +.errboundary-eyebrow { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--fail); +} +.errboundary-code { + font-family: var(--ff-mono); + font-weight: 700; + font-size: 64px; + line-height: 1; + color: var(--fail); +} +.errboundary-card p { color: var(--text-mute); } +.errboundary-detail { + width: 100%; + background: var(--bg-inset); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; + font-size: 11px; + color: var(--text-2); + text-align: left; + white-space: pre-wrap; + word-break: break-word; + max-height: 140px; + overflow: auto; +} +.errboundary-actions { display: flex; gap: 10px; margin-top: 4px; } diff --git a/orion-frontend/src/components/common/ErrorBoundary.jsx b/orion-frontend/src/components/common/ErrorBoundary.jsx new file mode 100644 index 0000000..0d0eb57 --- /dev/null +++ b/orion-frontend/src/components/common/ErrorBoundary.jsx @@ -0,0 +1,50 @@ +// ORION — ErrorBoundary +// App-level guard: render faults show a themed instrument panel, never a white screen. +// See ORION_UI_DESIGN.md §9.12 / §11.6. + +import { Component } from 'react'; +import Icon from './Icon'; +import './ErrorBoundary.css'; + +export default class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { error: null }; + } + + static getDerivedStateFromError(error) { + return { error }; + } + + componentDidCatch(error, info) { + // Surface in console for diagnosis; no external logging wired here. + // eslint-disable-next-line no-console + console.error('ORION ErrorBoundary caught:', error, info); + } + + render() { + if (this.state.error) { + return ( +
+
+ + System Fault + +
500
+

An unexpected error interrupted the interface.

+
+              {String(this.state.error?.message || this.state.error)}
+            
+
+ + Home +
+
+
+ ); + } + return this.props.children; + } +} diff --git a/orion-frontend/src/components/common/HudBar.css b/orion-frontend/src/components/common/HudBar.css new file mode 100644 index 0000000..84f1ab9 --- /dev/null +++ b/orion-frontend/src/components/common/HudBar.css @@ -0,0 +1,50 @@ +.hud { + position: sticky; + top: 0; + z-index: 50; + height: var(--hud-h); + display: flex; + align-items: center; + gap: 18px; + padding: 0 16px; + background: var(--scrim); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); + font-family: var(--ff-mono); + font-size: 11px; + letter-spacing: 0.12em; +} +.hud-brand { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--ff-disp); + font-weight: 700; + letter-spacing: 0.18em; + font-size: 13px; + color: var(--text); +} +.hud-brand b { color: var(--amber); } +.hud-dot { + width: 7px; + height: 7px; + background: var(--amber); + box-shadow: 0 0 10px var(--amber); + transform: rotate(45deg); +} +.hud-sep { flex: 1; } +.hud-stat { color: var(--text-2); display: flex; align-items: center; gap: 6px; } +.hud-cyan { color: var(--cyan); } +.hud-mute { color: var(--text-2); } +.hud-kbd { + border: 1px solid var(--border-strong); + padding: 1px 7px; + color: var(--text-2); + border-radius: var(--radius-sm); +} + +@media (max-width: 720px) { + .hud-hide-sm { display: none; } + .hud { gap: 12px; } +} diff --git a/orion-frontend/src/components/common/HudBar.jsx b/orion-frontend/src/components/common/HudBar.jsx new file mode 100644 index 0000000..98d95b2 --- /dev/null +++ b/orion-frontend/src/components/common/HudBar.jsx @@ -0,0 +1,46 @@ +// ORION — HudBar +// Sticky top status strip for authed surfaces. See ORION_UI_DESIGN.md §6.1. + +import { useState, useEffect } from 'react'; +import { BUILD_HASH } from '../../utils/constants'; +import ThemeToggle from './ThemeToggle'; +import './HudBar.css'; + +function utcClock() { + // HH:MM:SS UTC — sim code uses world.sim_time; this is wall-clock chrome only. + return new Date().toUTCString().slice(17, 25); +} + +export default function HudBar({ status = 'SYSTEM NOMINAL', seed = null }) { + const [clock, setClock] = useState(utcClock); + + useEffect(() => { + const id = setInterval(() => setClock(utcClock()), 1000); + return () => clearInterval(id); + }, []); + + return ( +
+ + + + + + {seed != null && ( + + SEED {seed} + + )} + + BUILD {BUILD_HASH} + + {clock} UTC + ⌘K + +
+ ); +} diff --git a/orion-frontend/src/components/common/Icon.jsx b/orion-frontend/src/components/common/Icon.jsx new file mode 100644 index 0000000..0210440 --- /dev/null +++ b/orion-frontend/src/components/common/Icon.jsx @@ -0,0 +1,149 @@ +// ORION — Icon set +// Single inline-SVG icon source. Stroke-based, 1.5px, currentColor, sharp joints. +// No emoji as UI icons (ORION_UI_DESIGN.md §7). Add new icons here + list in the doc. + +const ICONS = { + overview: ( + <> + + + + + + ), + scenarios: ( + <> + + + + ), + runs: ( + <> + + + + ), + models: ( + <> + + + + ), + batches: ( + <> + + + + ), + compare: ( + <> + + + + ), + settings: ( + <> + + + + + + ), + billing: ( + <> + + + + ), + search: ( + <> + + + + ), + key: ( + <> + + + + ), + upload: ( + <> + + + + ), + download: ( + <> + + + + ), + play: , + refresh: ( + <> + + + + ), + power: ( + <> + + + + ), + close: ( + <> + + + + ), + check: , + warning: ( + <> + + + + + ), + 'chevron-down': , + 'chevron-right': , + external: ( + <> + + + + + ), + copy: ( + <> + + + + ), +}; + +export default function Icon({ name, size = 18, className = '', strokeWidth = 1.5, ...rest }) { + const content = ICONS[name]; + if (!content) return null; + return ( + + ); +} + +export const ICON_NAMES = Object.keys(ICONS); diff --git a/orion-frontend/src/components/common/Navbar.css b/orion-frontend/src/components/common/Navbar.css index 2d8bd71..9620e9f 100644 --- a/orion-frontend/src/components/common/Navbar.css +++ b/orion-frontend/src/components/common/Navbar.css @@ -1,17 +1,22 @@ +/* ORION — Navbar (landing). Transparent over hero → scrim+blur on scroll. + ORION_UI_DESIGN.md §11.1. Tokens only. */ + .navbar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; - background: rgba(10, 10, 15, 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border-bottom: 1px solid var(--border-subtle); - height: var(--navbar-height); + height: 60px; + background: var(--scrim); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); + transition: background var(--t-base) var(--ease), border-color var(--t-base) var(--ease); } -.navbar-transparent { +/* over hero: fully transparent, no blur, no border */ +.navbar-transparent:not(.navbar-solid) { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; @@ -22,50 +27,65 @@ max-width: 1280px; margin: 0 auto; height: 100%; - padding: 0 var(--space-8); + padding: 0 var(--space-6); display: flex; align-items: center; justify-content: space-between; } +/* ── Wordmark ───────────────────────────────────────────────────────────── */ .navbar-brand { display: flex; align-items: center; - gap: var(--space-2); - font-size: var(--font-xl); - font-weight: 800; - color: var(--text-primary); + gap: 10px; text-decoration: none; - letter-spacing: 1px; } +.navbar-brand:hover { color: inherit; } -.brand-icon { - color: var(--accent-primary); - font-size: var(--font-2xl); - filter: drop-shadow(0 0 8px var(--accent-glow)); +.brand-mark { + width: 8px; + height: 8px; + background: var(--amber); + box-shadow: 0 0 10px var(--amber-dim); + transform: rotate(45deg); +} +.brand-text { + font-family: var(--ff-disp); + font-weight: 700; + font-size: 16px; + letter-spacing: 0.17em; + color: var(--text); } +.brand-text b { color: var(--amber); font-weight: 700; } +/* ── Links + actions ────────────────────────────────────────────────────── */ .navbar-links { display: flex; align-items: center; - gap: var(--space-4); + gap: 18px; } .nav-link { - color: var(--text-secondary); - font-size: var(--font-sm); - font-weight: 500; - transition: color var(--transition-fast); -} -.nav-link:hover { - color: var(--text-primary); + font-family: var(--ff-mono); + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--text-2); + transition: color var(--t-fast) ease; } +.nav-link:hover { color: var(--amber); } .nav-user { - color: var(--accent-secondary); - font-size: var(--font-sm); - font-weight: 600; - padding: var(--space-1) var(--space-3); - background: rgba(108, 99, 255, 0.1); - border-radius: var(--radius-full); + font-size: 11px; + letter-spacing: 0.06em; + color: var(--cyan); + border: 1px solid var(--border-strong); + padding: 4px 10px; + border-radius: var(--radius-sm); +} + +@media (max-width: 640px) { + .navbar-links { gap: 10px; } + .nav-link-hide-sm { display: none; } + .navbar-inner { padding: 0 var(--space-4); } } diff --git a/orion-frontend/src/components/common/Navbar.jsx b/orion-frontend/src/components/common/Navbar.jsx index 844d407..90b1adf 100644 --- a/orion-frontend/src/components/common/Navbar.jsx +++ b/orion-frontend/src/components/common/Navbar.jsx @@ -1,39 +1,59 @@ +import { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import ThemeToggle from './ThemeToggle'; import './Navbar.css'; export default function Navbar({ transparent = false }) { const { user, logout } = useAuth(); const navigate = useNavigate(); + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 8); + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); const handleLogout = () => { logout(); navigate('/'); }; + const solid = !transparent || scrolled; + return ( -