diff --git a/ui/src/components/ui/Skeleton.tsx b/ui/src/components/ui/Skeleton.tsx
new file mode 100644
index 0000000..68ddb3c
--- /dev/null
+++ b/ui/src/components/ui/Skeleton.tsx
@@ -0,0 +1,62 @@
+import { cn } from '../../lib/utils';
+
+interface SkeletonProps {
+ className?: string;
+}
+
+export function Skeleton({ className }: SkeletonProps) {
+ return (
+
+ );
+}
+
+export function DashboardSkeleton() {
+ return (
+
+ {/* Metric cards skeleton */}
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+
+ ))}
+
+
+ {/* Widget skeletons */}
+
+ {[1, 2].map((i) => (
+
+
+ {[1, 2, 3].map((j) => (
+
+ ))}
+
+ ))}
+
+
+
+ {[1, 2].map((i) => (
+
+
+ {[1, 2].map((j) => (
+
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/ui/src/index.css b/ui/src/index.css
index 672af98..4ee8b40 100644
--- a/ui/src/index.css
+++ b/ui/src/index.css
@@ -242,6 +242,18 @@ h1, h2, .font-display {
box-shadow: inset 0 -2px 0 var(--chitty-500);
}
+/* ── Skeleton shimmer ─────────────────────────── */
+.skeleton-shimmer {
+ background: linear-gradient(
+ 90deg,
+ rgba(200, 205, 225, 0.08) 0%,
+ rgba(200, 205, 225, 0.15) 40%,
+ rgba(200, 205, 225, 0.08) 80%
+ );
+ background-size: 300% 100%;
+ animation: shimmer 1.8s ease-in-out infinite;
+}
+
/* ── Form inputs ───────────────────────────────── */
.input-field {
width: 100%;
@@ -262,3 +274,444 @@ h1, h2, .font-display {
color: var(--card-text-muted);
opacity: 0.6;
}
+
+/* ══════════════════════════════════════════════════
+ LOGIN — Command Center Authentication Terminal
+ ══════════════════════════════════════════════════ */
+
+.login-root {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #060610;
+ position: relative;
+ overflow: hidden;
+ padding: 1rem;
+}
+
+/* ── Animated grid background ─────────────────── */
+.login-grid-bg {
+ position: absolute;
+ inset: 0;
+ overflow: hidden;
+}
+
+.login-grid-lines {
+ position: absolute;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(99, 102, 241, 0.04) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(99, 102, 241, 0.04) 1px, transparent 1px);
+ background-size: 48px 48px;
+ mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, black 30%, transparent 70%);
+ -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 50%, black 30%, transparent 70%);
+ animation: login-grid-drift 20s linear infinite;
+}
+
+@keyframes login-grid-drift {
+ 0% { transform: translate(0, 0); }
+ 100% { transform: translate(48px, 48px); }
+}
+
+.login-radial-glow {
+ position: absolute;
+ top: 30%;
+ left: 50%;
+ width: 800px;
+ height: 600px;
+ transform: translate(-50%, -50%);
+ background: radial-gradient(ellipse, rgba(99, 102, 241, 0.06) 0%, rgba(67, 56, 202, 0.02) 40%, transparent 70%);
+ pointer-events: none;
+}
+
+.login-scanline {
+ position: absolute;
+ inset: 0;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 2px,
+ rgba(99, 102, 241, 0.008) 2px,
+ rgba(99, 102, 241, 0.008) 4px
+ );
+ pointer-events: none;
+ animation: login-scanline-scroll 8s linear infinite;
+}
+
+@keyframes login-scanline-scroll {
+ 0% { transform: translateY(0); }
+ 100% { transform: translateY(4px); }
+}
+
+.login-vignette {
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(ellipse 80% 80% at 50% 50%, transparent 40%, rgba(6, 6, 16, 0.8) 100%);
+ pointer-events: none;
+}
+
+/* ── Container + entrance animation ───────────── */
+.login-container {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ max-width: 400px;
+ opacity: 0;
+ transform: translateY(16px) scale(0.98);
+ transition: opacity 0.6s ease-out, transform 0.6s ease-out;
+}
+
+.login-container--visible {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+}
+
+/* ── Brand header ─────────────────────────────── */
+.login-brand {
+ text-align: center;
+ margin-bottom: 2rem;
+}
+
+.login-logo-ring {
+ width: 56px;
+ height: 56px;
+ margin: 0 auto 1rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--chitty-400);
+ border: 1.5px solid rgba(99, 102, 241, 0.25);
+ background: rgba(99, 102, 241, 0.06);
+ box-shadow: 0 0 40px rgba(99, 102, 241, 0.1), inset 0 0 20px rgba(99, 102, 241, 0.05);
+ animation: login-ring-pulse 4s ease-in-out infinite;
+}
+
+@keyframes login-ring-pulse {
+ 0%, 100% {
+ box-shadow: 0 0 40px rgba(99, 102, 241, 0.1), inset 0 0 20px rgba(99, 102, 241, 0.05);
+ border-color: rgba(99, 102, 241, 0.25);
+ }
+ 50% {
+ box-shadow: 0 0 60px rgba(99, 102, 241, 0.18), inset 0 0 30px rgba(99, 102, 241, 0.08);
+ border-color: rgba(99, 102, 241, 0.4);
+ }
+}
+
+.login-title {
+ font-family: 'Syne', sans-serif;
+ font-size: 1.75rem;
+ font-weight: 800;
+ letter-spacing: 0.12em;
+ line-height: 1;
+ margin: 0;
+}
+
+.login-title-chitty {
+ color: #e4e4f0;
+}
+
+.login-title-command {
+ color: var(--chitty-400);
+ margin-left: 0.15em;
+}
+
+.login-subtitle-wrap {
+ height: 1.5rem;
+ margin-top: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.login-subtitle {
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.65rem;
+ color: var(--chrome-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.2em;
+ margin: 0;
+}
+
+.login-cursor {
+ color: var(--chitty-400);
+ animation: login-blink 0.6s step-end infinite;
+ margin-left: 1px;
+}
+
+@keyframes login-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+/* ── Card ─────────────────────────────────────── */
+.login-card {
+ background: rgba(22, 22, 37, 0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(99, 102, 241, 0.12);
+ border-radius: 16px;
+ padding: 1.5rem;
+ box-shadow:
+ 0 0 0 1px rgba(99, 102, 241, 0.04),
+ 0 4px 24px rgba(0, 0, 0, 0.4),
+ 0 1px 2px rgba(0, 0, 0, 0.2);
+ transition: border-color 0.4s, box-shadow 0.4s;
+}
+
+.login-card:focus-within {
+ border-color: rgba(99, 102, 241, 0.25);
+ box-shadow:
+ 0 0 0 1px rgba(99, 102, 241, 0.08),
+ 0 0 40px rgba(99, 102, 241, 0.06),
+ 0 4px 24px rgba(0, 0, 0, 0.4);
+}
+
+.login-card--success {
+ border-color: rgba(16, 185, 129, 0.3) !important;
+ box-shadow:
+ 0 0 0 1px rgba(16, 185, 129, 0.08),
+ 0 0 60px rgba(16, 185, 129, 0.08),
+ 0 4px 24px rgba(0, 0, 0, 0.4) !important;
+}
+
+.login-card-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.6rem;
+ color: var(--chrome-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ padding-bottom: 1rem;
+ margin-bottom: 1rem;
+ border-bottom: 1px solid rgba(99, 102, 241, 0.08);
+}
+
+/* ── Error ─────────────────────────────────────── */
+.login-error {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.625rem 0.75rem;
+ margin-bottom: 1rem;
+ border-radius: 8px;
+ font-size: 0.8rem;
+ color: var(--urgency-red);
+ background: rgba(244, 63, 94, 0.08);
+ border: 1px solid rgba(244, 63, 94, 0.15);
+ animation: login-shake 0.4s ease-out;
+}
+
+@keyframes login-shake {
+ 0%, 100% { transform: translateX(0); }
+ 20% { transform: translateX(-6px); }
+ 40% { transform: translateX(5px); }
+ 60% { transform: translateX(-3px); }
+ 80% { transform: translateX(2px); }
+}
+
+/* ── Form ─────────────────────────────────────── */
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.login-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+.login-label {
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.6rem;
+ color: var(--chrome-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ font-weight: 500;
+}
+
+.login-input {
+ width: 100%;
+ padding: 0.65rem 0.875rem;
+ font-family: 'Figtree', sans-serif;
+ font-size: 0.875rem;
+ color: #e4e4f0;
+ background: rgba(15, 15, 26, 0.6);
+ border: 1px solid rgba(99, 102, 241, 0.1);
+ border-radius: 10px;
+ outline: none;
+ transition: all 0.25s;
+ box-sizing: border-box;
+}
+
+.login-input::placeholder {
+ color: rgba(136, 136, 168, 0.4);
+}
+
+.login-input:focus {
+ border-color: rgba(99, 102, 241, 0.35);
+ background: rgba(15, 15, 26, 0.8);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.08), inset 0 0 12px rgba(99, 102, 241, 0.03);
+}
+
+.login-input:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* ── Submit button ────────────────────────────── */
+.login-submit {
+ width: 100%;
+ margin-top: 0.5rem;
+ padding: 0.75rem;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: white;
+ background: linear-gradient(135deg, var(--chitty-600) 0%, var(--chitty-700) 100%);
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: all 0.25s;
+ position: relative;
+ overflow: hidden;
+}
+
+.login-submit::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.06) 50%, transparent 100%);
+ transform: translateX(-100%);
+ transition: transform 0.5s;
+}
+
+.login-submit:hover:not(:disabled)::before {
+ transform: translateX(100%);
+}
+
+.login-submit:hover:not(:disabled) {
+ box-shadow: 0 0 24px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3);
+ border-color: rgba(99, 102, 241, 0.5);
+ transform: translateY(-1px);
+}
+
+.login-submit:active:not(:disabled) {
+ transform: translateY(0);
+}
+
+.login-submit:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.login-submit-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+}
+
+.login-submit-check {
+ color: var(--urgency-green);
+ font-size: 1rem;
+}
+
+.login-spinner {
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: login-spin 0.6s linear infinite;
+}
+
+@keyframes login-spin {
+ to { transform: rotate(360deg); }
+}
+
+/* ── Footer ───────────────────────────────────── */
+.login-footer {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-top: 1rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(99, 102, 241, 0.06);
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.55rem;
+ color: rgba(136, 136, 168, 0.5);
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+
+.login-footer-sep {
+ font-size: 0.4rem;
+ opacity: 0.5;
+}
+
+/* ── Status readout bar ───────────────────────── */
+.login-status-readout {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 1.5rem;
+ margin-top: 1.5rem;
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.55rem;
+ color: rgba(136, 136, 168, 0.4);
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+}
+
+.login-status-item {
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+}
+
+.login-status-item--mono {
+ font-variant-numeric: tabular-nums;
+ color: rgba(136, 136, 168, 0.3);
+ min-width: 11em;
+ text-align: center;
+}
+
+.login-status-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+}
+
+.login-status-dot--ok {
+ background: var(--urgency-green);
+ box-shadow: 0 0 6px rgba(16, 185, 129, 0.4);
+ animation: login-dot-pulse 3s ease-in-out infinite;
+}
+
+@keyframes login-dot-pulse {
+ 0%, 100% { opacity: 0.7; }
+ 50% { opacity: 1; }
+}
+
+/* ── Mobile tweaks ────────────────────────────── */
+@media (max-width: 480px) {
+ .login-title {
+ font-size: 1.4rem;
+ }
+ .login-status-readout {
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ }
+ .login-card {
+ padding: 1.25rem;
+ }
+}
diff --git a/ui/src/pages/Dashboard.tsx b/ui/src/pages/Dashboard.tsx
index dc34f9a..1581ca5 100644
--- a/ui/src/pages/Dashboard.tsx
+++ b/ui/src/pages/Dashboard.tsx
@@ -4,6 +4,7 @@ import { useFocusMode } from '../lib/focus-mode';
import { FocusView } from '../components/dashboard/FocusView';
import { FullView } from '../components/dashboard/FullView';
import { ConfirmDialog } from '../components/ui/ConfirmDialog';
+import { DashboardSkeleton } from '../components/ui/Skeleton';
import { useToast } from '../lib/toast';
import { formatCurrency } from '../lib/utils';
@@ -71,7 +72,7 @@ export function Dashboard() {
}
if (!data) {
- return Loading...
;
+ return ;
}
const viewProps = { data, onPayNow: requestPayNow, onExecute: handleExecute, payingId, executingId };
diff --git a/ui/src/pages/Login.tsx b/ui/src/pages/Login.tsx
index 3299f88..314a2e9 100644
--- a/ui/src/pages/Login.tsx
+++ b/ui/src/pages/Login.tsx
@@ -1,79 +1,218 @@
-import { useState } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { authApi } from '../lib/api';
import { setToken, setUser } from '../lib/auth';
+import { Lock, Shield, ChevronRight, AlertCircle } from 'lucide-react';
+
+function useTypingEffect(text: string, speed = 40, startDelay = 600) {
+ const [displayed, setDisplayed] = useState('');
+ const [done, setDone] = useState(false);
+
+ useEffect(() => {
+ setDisplayed('');
+ setDone(false);
+ const timeout = setTimeout(() => {
+ let i = 0;
+ const interval = setInterval(() => {
+ setDisplayed(text.slice(0, i + 1));
+ i++;
+ if (i >= text.length) {
+ clearInterval(interval);
+ setDone(true);
+ }
+ }, speed);
+ return () => clearInterval(interval);
+ }, startDelay);
+ return () => clearTimeout(timeout);
+ }, [text, speed, startDelay]);
+
+ return { displayed, done };
+}
+
+function GridBackground() {
+ return (
+
+ );
+}
+
+function StatusReadout() {
+ const [time, setTime] = useState(new Date());
+
+ useEffect(() => {
+ const interval = setInterval(() => setTime(new Date()), 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const utc = time.toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
+
+ return (
+
+
+
+ SYS ONLINE
+
+
+ {utc}
+
+
+
+ ENCRYPTED
+
+
+ );
+}
export function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+ const [mounted, setMounted] = useState(false);
+ const emailRef = useRef(null);
const navigate = useNavigate();
+ const { displayed: subtitle, done: subtitleDone } = useTypingEffect(
+ 'Unified Command & Control Interface',
+ 35,
+ 800,
+ );
+
+ useEffect(() => {
+ requestAnimationFrame(() => setMounted(true));
+ }, []);
+
+ useEffect(() => {
+ if (subtitleDone && emailRef.current) {
+ emailRef.current.focus();
+ }
+ }, [subtitleDone]);
+
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
- if (!email || !password) return;
+ if (!email || !password || loading) return;
setLoading(true);
setError(null);
try {
const data = await authApi.login(email, password);
setToken(data.token);
setUser({ user_id: data.user_id, scopes: data.scopes });
- navigate('/');
+ setSuccess(true);
+ setTimeout(() => navigate('/'), 600);
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Login failed');
+ setError(err instanceof Error ? err.message : 'Authentication failed');
} finally {
setLoading(false);
}
};
return (
-
-
-
-
ChittyCommand
-
Sign in to your command center
+
+
+
+
+ {/* Brand */}
+
+
+
+
+
+ CHITTY
+ COMMAND
+
+
+
+ {subtitle}
+ {!subtitleDone && |}
+
+
+
+
+ {/* Card */}
+
+
+
+ SECURE AUTHENTICATION
+
{error && (
-
);