);
}
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 (
+
+
+
+ ORION//AREP
+
+
+
+ {status}
+
+
+ {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 (
-