From b6e829f51f5d65c67d954230d5a007990118d5ec Mon Sep 17 00:00:00 2001 From: Nick Bianchi Date: Tue, 17 Mar 2026 10:22:46 +0000 Subject: [PATCH] feat: redesign login as command center terminal + add skeleton loaders Login page reimagined as a command center authentication terminal: - Animated grid background with radial glow and subtle scanlines - Terminal-style labels (OPERATOR ID, PASSPHRASE) in JetBrains Mono - Shield icon with pulsing glow ring - Typewriter effect on subtitle text - Shake animation on auth errors - Success state with green glow transition - Live UTC clock status readout bar - Full mobile responsive Dashboard loading state upgraded from plain text to shimmer skeleton that mirrors the actual widget layout. Co-Authored-By: Claude Opus 4.6 --- ui/src/components/ui/Skeleton.tsx | 62 ++++ ui/src/index.css | 453 ++++++++++++++++++++++++++++++ ui/src/pages/Dashboard.tsx | 3 +- ui/src/pages/Login.tsx | 189 +++++++++++-- 4 files changed, 681 insertions(+), 26 deletions(-) create mode 100644 ui/src/components/ui/Skeleton.tsx 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 ( +