feat(ui): visual refresh — custom theme, refined layout, fixed bugs

- Custom color system (surface-0..4, accent, mint) replacing raw gray-xxx
- Inter + JetBrains Mono fonts via Google Fonts
- Refined sidebar: compact logo, geometric icons, subtle active states
- Fixed bg-gray-850 bug (invalid Tailwind class) in 9 components
- Polished login page with centered card + gradient logo
- Unified card/table/button/input styling across all components
- Subtle grain texture overlay for depth
- Smoother animations (fade-in, slide-up)
This commit is contained in:
小糯 🐱 2026-04-13 17:56:32 +08:00
parent bc12a4bb18
commit f950654827
18 changed files with 385 additions and 263 deletions

File diff suppressed because one or more lines are too long

View File

@ -48,7 +48,6 @@ function App() {
const [tokenInput, setTokenInput] = useState('') const [tokenInput, setTokenInput] = useState('')
const [checking, setChecking] = useState(true) const [checking, setChecking] = useState(true)
// Sync hash → state on popstate (browser back/forward)
useEffect(() => { useEffect(() => {
const onHashChange = () => setPageState(getPageFromHash()) const onHashChange = () => setPageState(getPageFromHash())
window.addEventListener('hashchange', onHashChange) window.addEventListener('hashchange', onHashChange)
@ -59,7 +58,6 @@ function App() {
} }
}, []) }, [])
// Navigate: update hash + state
const setPage = (p: Page) => { const setPage = (p: Page) => {
setHash(p) setHash(p)
setPageState(p) setPageState(p)
@ -83,7 +81,7 @@ function App() {
if (checking) { if (checking) {
return ( return (
<div className="flex items-center justify-center h-screen bg-gray-950"> <div className="flex items-center justify-center h-screen bg-surface-0">
<Spinner /> <Spinner />
</div> </div>
) )
@ -91,37 +89,43 @@ function App() {
if (needsAuth) { if (needsAuth) {
return ( return (
<div className="flex items-center justify-center h-screen bg-gray-950 text-gray-100 px-4"> <div className="flex items-center justify-center h-screen bg-surface-0 text-gray-100 px-4 grain">
<div className="bg-gray-900/80 backdrop-blur border border-gray-800 p-8 rounded-xl shadow-2xl max-w-md w-full animate-fadeIn"> <div className="animate-slide-up">
<div className="text-center mb-6"> <div className="bg-surface-1 border border-white/[0.06] p-8 rounded-2xl shadow-2xl max-w-sm w-full">
<h2 className="text-3xl font-bold mb-2 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> {/* Logo */}
OGraph UI <div className="text-center mb-8">
</h2> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-lg font-bold mx-auto mb-4 shadow-lg shadow-accent/25">
<p className="text-gray-400 text-sm">Enter your API token to continue</p> G
</div> </div>
<h2 className="text-xl font-bold text-white tracking-tight">OGraph</h2>
<p className="text-gray-500 text-sm mt-1">Enter your API token</p>
</div>
<input <input
type="text" type="password"
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" className="w-full px-4 py-2.5 bg-surface-3 border border-white/[0.08] rounded-lg text-sm placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent/40 transition-all"
placeholder="API Token" placeholder="Bearer token"
value={tokenInput} value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)} onChange={(e) => setTokenInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAuth()} onKeyDown={(e) => e.key === 'Enter' && handleAuth()}
autoFocus autoFocus
/> />
<button <button
onClick={handleAuth} onClick={handleAuth}
className="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold shadow-lg shadow-blue-500/30 hover:shadow-blue-500/50 transition-all" disabled={!tokenInput.trim()}
className="w-full mt-3 px-4 py-2.5 bg-accent hover:bg-accent-dim rounded-lg text-sm font-semibold text-white shadow-lg shadow-accent/20 hover:shadow-accent/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
> >
Continue Continue
</button> </button>
</div> </div>
</div> </div>
</div>
) )
} }
return ( return (
<Layout page={page} onPageChange={setPage}> <Layout page={page} onPageChange={setPage}>
<div className="animate-fadeIn">
{page === 'health' && <Health />} {page === 'health' && <Health />}
{page === 'object-defs' && <ObjectDefs />} {page === 'object-defs' && <ObjectDefs />}
{page === 'objects' && <Objects />} {page === 'objects' && <Objects />}
@ -133,7 +137,6 @@ function App() {
{page === 'api-keys' && <ApiKeys />} {page === 'api-keys' && <ApiKeys />}
{page === 'reaction-logs' && <ReactionLogs />} {page === 'reaction-logs' && <ReactionLogs />}
{page === 'request-logs' && <RequestLogs />} {page === 'request-logs' && <RequestLogs />}
</div>
</Layout> </Layout>
) )
} }

View File

@ -81,7 +81,7 @@ export default function ApiKeys() {
return ( return (
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">API Keys</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">API Keys</h2>
{/* Key reveal modal */} {/* Key reveal modal */}
{shownKey && ( {shownKey && (
@ -91,7 +91,7 @@ export default function ApiKeys() {
<p className="text-sm text-gray-400 mb-4"> <p className="text-sm text-gray-400 mb-4">
This key will only be shown once. Copy it now and store it securely. This key will only be shown once. Copy it now and store it securely.
</p> </p>
<div className="bg-gray-800 rounded-lg p-3 font-mono text-sm text-green-400 break-all mb-4">{shownKey}</div> <div className="bg-surface-3 rounded-lg p-3 font-mono text-sm text-green-400 break-all mb-4">{shownKey}</div>
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<button <button
onClick={handleCopy} onClick={handleCopy}
@ -111,7 +111,7 @@ export default function ApiKeys() {
)} )}
{/* Create form */} {/* Create form */}
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6"> <div className="bg-surface-1 rounded-lg border border-white/[0.06] p-4 mb-6">
<div className="flex flex-wrap items-end gap-3"> <div className="flex flex-wrap items-end gap-3">
<div> <div>
<label className="block text-xs text-gray-400 mb-1">Name</label> <label className="block text-xs text-gray-400 mb-1">Name</label>
@ -120,7 +120,7 @@ export default function ApiKeys() {
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="my-service" placeholder="my-service"
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
<div> <div>
@ -130,7 +130,7 @@ export default function ApiKeys() {
value={allowedEvents} value={allowedEvents}
onChange={(e) => setAllowedEvents(e.target.value)} onChange={(e) => setAllowedEvents(e.target.value)}
placeholder="order.created, user.signed_up" placeholder="order.created, user.signed_up"
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-64" className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-64"
/> />
</div> </div>
<div> <div>
@ -140,7 +140,7 @@ export default function ApiKeys() {
value={rateLimit} value={rateLimit}
onChange={(e) => setRateLimit(e.target.value)} onChange={(e) => setRateLimit(e.target.value)}
placeholder="1000" placeholder="1000"
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-24" className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-24"
/> />
</div> </div>
<button <button
@ -153,12 +153,12 @@ export default function ApiKeys() {
</div> </div>
</div> </div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No API keys found" /> <EmptyState message="No API keys found" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -184,8 +184,8 @@ export default function ApiKeys() {
<tr <tr
key={key.id} key={key.id}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{key.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{key.id}</td>
<td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td> <td className="px-4 py-3 text-sm text-gray-200 font-medium">{key.name}</td>

View File

@ -1,24 +1,22 @@
export function Spinner() { export function Spinner() {
return ( return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-16">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> <div className="relative">
<div className="w-8 h-8 rounded-full border-2 border-surface-3" />
<div className="absolute inset-0 w-8 h-8 rounded-full border-2 border-transparent border-t-accent animate-spin" />
</div>
</div> </div>
) )
} }
export function EmptyState({ message = 'No data found' }: { message?: string }) { export function EmptyState({ message = 'No data found' }: { message?: string }) {
return ( return (
<div className="flex items-center justify-center p-12 text-gray-500"> <div className="flex items-center justify-center py-16 text-gray-500">
<div className="text-center"> <div className="text-center">
<svg className="mx-auto h-12 w-12 text-gray-600 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div className="mx-auto w-10 h-10 rounded-xl bg-surface-3 flex items-center justify-center mb-3">
<path <span className="text-lg text-gray-600"></span>
strokeLinecap="round" </div>
strokeLinejoin="round" <p className="text-sm">{message}</p>
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<p>{message}</p>
</div> </div>
</div> </div>
) )
@ -27,10 +25,79 @@ export function EmptyState({ message = 'No data found' }: { message?: string })
export function HashBadge({ hash, short = true }: { hash: string; short?: boolean }) { export function HashBadge({ hash, short = true }: { hash: string; short?: boolean }) {
const display = short && hash.length > 8 ? hash.slice(0, 8) : hash const display = short && hash.length > 8 ? hash.slice(0, 8) : hash
return ( return (
<span className="inline-block px-2 py-1 bg-gray-800/50 rounded font-mono text-xs text-gray-400">{display}</span> <span
className="inline-block px-1.5 py-0.5 bg-surface-3/60 rounded font-mono text-[11px] text-gray-500 cursor-default select-all"
title={hash}
>
{display}
</span>
) )
} }
export function SectionHeader({ title, count, action }: { title: string; count?: number; action?: React.ReactNode }) {
return (
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<h2 className="text-lg font-semibold text-white tracking-tight">{title}</h2>
{count !== undefined && (
<span className="px-2 py-0.5 rounded-full bg-surface-3 text-[11px] font-medium text-gray-400">
{count}
</span>
)}
</div>
{action}
</div>
)
}
export function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
return (
<div className={`bg-surface-1 border border-white/[0.06] rounded-xl overflow-hidden ${className}`}>
{children}
</div>
)
}
export function ErrorBanner({ message }: { message: string }) {
return (
<div className="rounded-lg bg-red-500/[0.08] border border-red-500/20 px-4 py-3 text-sm text-red-400 mb-4">
{message}
</div>
)
}
export function JsonBlock({ data }: { data: unknown }) {
const text = JSON.stringify(data, null, 2)
return (
<pre className="p-3 bg-surface-0 rounded-lg text-xs font-mono overflow-x-auto border border-white/[0.04] text-gray-400 leading-relaxed">
{text.split('\n').map((line, i) => {
const match = line.match(/^(\s*)"([^"]+)":\s*(.*)/)
if (match) {
return (
<div key={i}>
<span>{match[1]}</span>
<span className="text-blue-400">"{match[2]}"</span>
<span className="text-gray-600">: </span>
<span className="text-emerald-400">{match[3]}</span>
</div>
)
}
return <div key={i} className="text-gray-500">{line}</div>
})}
</pre>
)
}
export function StatusDot({ status }: { status: 'ok' | 'error' | 'warning' | 'idle' }) {
const colors = {
ok: 'bg-emerald-500',
error: 'bg-red-500',
warning: 'bg-amber-500',
idle: 'bg-gray-600',
}
return <div className={`w-2 h-2 rounded-full ${colors[status]}`} />
}
export function Pagination({ export function Pagination({
total, total,
limit, limit,
@ -53,44 +120,41 @@ export function Pagination({
const canNext = offset + limit < total const canNext = offset + limit < total
return ( return (
<div className="flex items-center justify-between border-t border-gray-800 bg-gray-900/50 px-4 py-3 mt-4"> <div className="flex items-center justify-between border-t border-white/[0.06] px-4 py-3 mt-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-sm text-gray-400"> <span className="text-xs text-gray-500">
{startItem}-{endItem} of {total} {startItem}{endItem} of {total}
</span> </span>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-500">Per page:</label>
<select <select
value={limit} value={limit}
onChange={(e) => { onChange={(e) => {
onLimitChange(parseInt(e.target.value, 10)) onLimitChange(parseInt(e.target.value, 10))
onPageChange(0) // reset to first page onPageChange(0)
}} }}
className="bg-gray-800 text-gray-300 border border-gray-700 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" className="bg-surface-3 text-gray-300 border border-white/[0.06] rounded-md px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-accent/50"
> >
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
</div> </div>
</div> <div className="flex items-center gap-1.5">
<div className="flex items-center gap-2">
<button <button
onClick={() => onPageChange(offset - limit)} onClick={() => onPageChange(offset - limit)}
disabled={!canPrev} disabled={!canPrev}
className="px-3 py-1 bg-gray-800 text-gray-300 rounded border border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-700 text-sm" className="px-2.5 py-1 bg-surface-3 text-gray-400 rounded-md border border-white/[0.06] disabled:opacity-30 disabled:cursor-not-allowed hover:bg-surface-4 hover:text-gray-200 text-xs font-medium"
> >
Previous Prev
</button> </button>
<span className="text-sm text-gray-400"> <span className="text-xs text-gray-500 px-2">
Page {currentPage} / {totalPages || 1} {currentPage}/{totalPages || 1}
</span> </span>
<button <button
onClick={() => onPageChange(offset + limit)} onClick={() => onPageChange(offset + limit)}
disabled={!canNext} disabled={!canNext}
className="px-3 py-1 bg-gray-800 text-gray-300 rounded border border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-700 text-sm" className="px-2.5 py-1 bg-surface-3 text-gray-400 rounded-md border border-white/[0.06] disabled:opacity-30 disabled:cursor-not-allowed hover:bg-surface-4 hover:text-gray-200 text-xs font-medium"
> >
Next Next
</button> </button>
</div> </div>
</div> </div>

View File

@ -77,7 +77,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
type="checkbox" type="checkbox"
checked={!!values[name]} checked={!!values[name]}
onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.checked }))} onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.checked }))}
className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500 focus:ring-offset-0" className="w-4 h-4 rounded border-gray-600 bg-surface-3 text-blue-500 focus:ring-blue-500 focus:ring-offset-0"
/> />
<span className="text-sm text-gray-200 font-mono">{name}</span> <span className="text-sm text-gray-200 font-mono">{name}</span>
</label> </label>
@ -94,7 +94,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
type={isNumeric ? 'number' : 'text'} type={isNumeric ? 'number' : 'text'}
value={values[name]} value={values[name]}
onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, [name]: e.target.value }))}
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-100 text-sm font-mono placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-gray-100 text-sm font-mono placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={def.type === 'ref' ? 'Enter object ID' : `Enter ${def.type}`} placeholder={def.type === 'ref' ? 'Enter object ID' : `Enter ${def.type}`}
/> />
</div> </div>
@ -108,7 +108,7 @@ export default function EmitEventModal({ eventDef, onClose, onSuccess }: EmitEve
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-800"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.06]">
<h3 className="text-lg font-semibold text-gray-100"> <h3 className="text-lg font-semibold text-gray-100">
Emit <span className="text-blue-400 font-mono">{eventDef.name}</span> Emit <span className="text-blue-400 font-mono">{eventDef.name}</span>
</h3> </h3>

View File

@ -39,13 +39,13 @@ export default function EventDefs() {
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Event Definitions</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Event Definitions</h2>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No event definitions found" /> <EmptyState message="No event definitions found" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Name Name
@ -69,8 +69,8 @@ export default function EventDefs() {
<tr <tr
key={i} key={i}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-gray-100">{def.name}</td> <td className="px-4 py-3 font-mono text-gray-100">{def.name}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@ -87,7 +87,7 @@ export default function EventDefs() {
{expanded.has(def.hash) ? 'Hide' : 'Show'} {expanded.has(def.hash) ? 'Hide' : 'Show'}
</button> </button>
{expanded.has(def.hash) && ( {expanded.has(def.hash) && (
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800"> <pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
{JSON.stringify(def.schema, null, 2) {JSON.stringify(def.schema, null, 2)
.split('\n') .split('\n')
.map((line, i) => { .map((line, i) => {

View File

@ -51,29 +51,29 @@ export default function Events() {
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">Events</h2> <h2 className="text-lg font-semibold text-white tracking-tight">Events</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all" className="px-4 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
placeholder="Filter by ref..." placeholder="Filter by ref..."
value={refFilter} value={refFilter}
onChange={(e) => setRefFilter(e.target.value)} onChange={(e) => setRefFilter(e.target.value)}
/> />
<button <button
onClick={handleSearch} onClick={handleSearch}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold transition-all shadow-lg shadow-blue-500/20 hover:shadow-blue-500/40" className="px-4 py-2 bg-accent hover:bg-accent-dim rounded-lg text-sm font-medium transition-all"
> >
Search Search
</button> </button>
</div> </div>
</div> </div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No events yet" /> <EmptyState message="No events yet" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -92,8 +92,8 @@ export default function Events() {
<tr <tr
key={i} key={i}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{event.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{event.id}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@ -108,7 +108,7 @@ export default function Events() {
{expanded.has(event.id) ? 'Hide' : 'Show'} {expanded.has(event.id) ? 'Hide' : 'Show'}
</button> </button>
{expanded.has(event.id) && ( {expanded.has(event.id) && (
<pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800"> <pre className="mt-2 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
{JSON.stringify(event.payload, null, 2) {JSON.stringify(event.payload, null, 2)
.split('\n') .split('\n')
.map((line, i) => { .map((line, i) => {

View File

@ -19,8 +19,8 @@ export default function Health() {
return ( return (
<div className="max-w-2xl mx-auto"> <div className="max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Health Check</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Health Check</h2>
<div className="bg-gray-900/50 backdrop-blur border border-gray-800 rounded-lg p-8"> <div className="bg-surface-1 border border-white/[0.06] rounded-lg p-8">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="relative"> <div className="relative">
<div className="w-4 h-4 bg-green-500 rounded-full"></div> <div className="w-4 h-4 bg-green-500 rounded-full"></div>
@ -30,7 +30,7 @@ export default function Health() {
</div> </div>
<div className="text-gray-400 mt-4 flex items-baseline gap-2"> <div className="text-gray-400 mt-4 flex items-baseline gap-2">
<span>Version:</span> <span>Version:</span>
<span className="text-white font-mono bg-gray-800/50 px-3 py-1 rounded">{data?.version}</span> <span className="text-white font-mono bg-surface-3/50 px-3 py-1 rounded">{data?.version}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -23,85 +23,98 @@ const navGroups: Array<{ label: string; items: Array<{ id: Page; label: string;
{ {
label: 'Schema', label: 'Schema',
items: [ items: [
{ id: 'object-defs', label: 'Object Defs', icon: '📦' }, { id: 'object-defs', label: 'Object Defs', icon: '' },
{ id: 'event-defs', label: 'Event Defs', icon: '📋' }, { id: 'event-defs', label: 'Event Defs', icon: '' },
{ id: 'projection-defs', label: 'Projection Defs', icon: '📊' }, { id: 'projection-defs', label: 'Projection Defs', icon: '' },
], ],
}, },
{ {
label: 'Data', label: 'Data',
items: [ items: [
{ id: 'objects', label: 'Objects', icon: '🗂️' }, { id: 'objects', label: 'Objects', icon: '' },
{ id: 'events', label: 'Events', icon: '⚡' }, { id: 'events', label: 'Events', icon: '⚡' },
{ id: 'projections', label: 'Projections', icon: '📈' }, { id: 'projections', label: 'Projections', icon: '' },
{ id: 'reactions', label: 'Reactions', icon: '🔔' }, { id: 'reactions', label: 'Reactions', icon: '' },
], ],
}, },
{ {
label: 'Observability', label: 'Logs',
items: [ items: [
{ id: 'reaction-logs', label: 'Reaction Logs', icon: '📜' }, { id: 'reaction-logs', label: 'Reaction Logs', icon: '' },
{ id: 'request-logs', label: 'Request Logs', icon: '📝' }, { id: 'request-logs', label: 'Request Logs', icon: '' },
], ],
}, },
{ {
label: 'Security', label: 'Admin',
items: [{ id: 'api-keys', label: 'API Keys', icon: '🔑' }], items: [
}, { id: 'api-keys', label: 'API Keys', icon: '⚿' },
{ { id: 'health', label: 'System', icon: '●' },
label: 'System', ],
items: [{ id: 'health', label: 'Health', icon: '🏠' }],
}, },
] ]
export default function Layout({ page, onPageChange, children }: Props) { export default function Layout({ page, onPageChange, children }: Props) {
return ( return (
<div className="flex h-screen bg-gray-950 text-gray-100"> <div className="flex h-screen bg-surface-0 text-gray-200 grain">
{/* Sidebar */} {/* Sidebar */}
<aside className="w-64 bg-gray-900/50 backdrop-blur border-r border-gray-800 flex flex-col"> <aside className="w-56 bg-surface-1 border-r border-white/[0.06] flex flex-col shrink-0">
<div className="p-6 border-b border-gray-800"> {/* Logo */}
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent"> <div className="px-5 py-5 border-b border-white/[0.06]">
OGraph UI <div className="flex items-center gap-2.5">
</h1> <div className="w-7 h-7 rounded-lg bg-gradient-to-br from-accent to-blue-400 flex items-center justify-center text-white text-xs font-bold shadow-lg shadow-accent/20">
<p className="text-xs text-gray-500 mt-1">Event Sourcing Dashboard</p> G
</div> </div>
<nav className="flex-1 overflow-y-auto p-3"> <div>
<h1 className="text-sm font-bold text-white tracking-tight">OGraph</h1>
<p className="text-[10px] text-gray-500 font-medium">Event Engine</p>
</div>
</div>
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto py-3 px-2.5">
{navGroups.map((group) => ( {navGroups.map((group) => (
<div key={group.label} className="mb-4"> <div key={group.label} className="mb-3">
<div className="px-4 py-1 text-xs font-semibold text-gray-500 uppercase tracking-wider"> <div className="px-2.5 py-1.5 text-[10px] font-semibold text-gray-500 uppercase tracking-[0.08em]">
{group.label} {group.label}
</div> </div>
{group.items.map((item) => ( {group.items.map((item) => {
const isActive = page === item.id
return (
<button <button
key={item.id} key={item.id}
onClick={() => onPageChange(item.id)} onClick={() => onPageChange(item.id)}
className={` className={`
w-full text-left px-4 py-2.5 rounded-lg mb-1 flex items-center gap-3 font-medium transition-all w-full text-left px-2.5 py-[7px] rounded-md mb-0.5 flex items-center gap-2.5 text-[13px] font-medium transition-all
${ ${isActive
page === item.id ? 'bg-accent/[0.12] text-accent-glow'
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'text-gray-400 hover:bg-white/[0.04] hover:text-gray-200'
: 'text-gray-400 hover:bg-gray-800/60 hover:text-gray-100'
} }
`} `}
> >
<span className="text-lg">{item.icon}</span> <span className={`text-xs w-4 text-center ${isActive ? 'text-accent-glow' : 'text-gray-600'}`}>
<span className="text-sm">{item.label}</span> {item.icon}
</span>
<span>{item.label}</span>
</button> </button>
))} )
})}
</div> </div>
))} ))}
</nav> </nav>
<div className="p-4 border-t border-gray-800 text-xs text-gray-600">
<div className="flex items-center gap-2"> {/* Footer */}
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> <div className="px-4 py-3 border-t border-white/[0.06]">
<div className="flex items-center gap-2 text-[11px] text-gray-500">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full animate-pulse-slow" />
<span>Connected</span> <span>Connected</span>
</div> </div>
</div> </div>
</aside> </aside>
{/* Main content */} {/* Main */}
<main className="flex-1 overflow-y-auto bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950"> <main className="flex-1 overflow-y-auto bg-surface-0">
<div className="p-8 max-w-[1600px] mx-auto">{children}</div> <div className="p-8 max-w-[1440px] mx-auto animate-fade-in">{children}</div>
</main> </main>
</div> </div>
) )

View File

@ -19,13 +19,13 @@ export default function ObjectDefs() {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Object Definitions</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Object Definitions</h2>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No object definitions found" /> <EmptyState message="No object definitions found" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
Name Name
@ -37,8 +37,8 @@ export default function ObjectDefs() {
<tr <tr
key={i} key={i}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-gray-100">{def.name}</td> <td className="px-4 py-3 font-mono text-gray-100">{def.name}</td>
</tr> </tr>

View File

@ -73,7 +73,7 @@ export default function Objects() {
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-2xl font-bold">Objects</h2> <h2 className="text-lg font-semibold text-white tracking-tight">Objects</h2>
{!showCreate ? ( {!showCreate ? (
<button <button
onClick={() => { setShowCreate(true); setCreateType(types[0] || '') }} onClick={() => { setShowCreate(true); setCreateType(types[0] || '') }}
@ -87,7 +87,7 @@ export default function Objects() {
<select <select
value={createType} value={createType}
onChange={(e) => setCreateType(e.target.value)} onChange={(e) => setCreateType(e.target.value)}
className="px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-blue-500 focus:outline-none" className="px-3 py-1.5 bg-surface-3 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-blue-500 focus:outline-none"
> >
{types.map((t) => ( {types.map((t) => (
<option key={t} value={t}>{t}</option> <option key={t} value={t}>{t}</option>
@ -116,7 +116,7 @@ export default function Objects() {
</div> </div>
<Listbox value={filter} onChange={handleFilterChange}> <Listbox value={filter} onChange={handleFilterChange}>
<div className="relative w-64"> <div className="relative w-64">
<Listbox.Button className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all"> <Listbox.Button className="w-full px-4 py-2 bg-surface-3 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
<span className={filter ? 'text-gray-100' : 'text-gray-500'}>{filter || 'All Types'}</span> <span className={filter ? 'text-gray-100' : 'text-gray-500'}>{filter || 'All Types'}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"> <svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
@ -128,7 +128,7 @@ export default function Objects() {
</svg> </svg>
</span> </span>
</Listbox.Button> </Listbox.Button>
<Listbox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
<Listbox.Option <Listbox.Option
value="" value=""
className={({ active }) => className={({ active }) =>
@ -156,12 +156,12 @@ export default function Objects() {
</div> </div>
</Listbox> </Listbox>
</div> </div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No objects found" /> <EmptyState message="No objects found" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -177,8 +177,8 @@ export default function Objects() {
<tr <tr
key={i} key={i}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{obj.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{obj.id}</td>
<td className="px-4 py-3 text-gray-100"> <td className="px-4 py-3 text-gray-100">

View File

@ -32,7 +32,7 @@ export default function ProjectionDefs() {
return ( return (
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Projection Definitions</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Projection Definitions</h2>
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No projection definitions found" /> <EmptyState message="No projection definitions found" />
) : ( ) : (
@ -40,7 +40,7 @@ export default function ProjectionDefs() {
{data.map((def, i) => ( {data.map((def, i) => (
<div <div
key={i} key={i}
className="bg-gray-900/50 backdrop-blur rounded-lg p-5 border border-gray-800 hover:border-gray-700 transition-colors" className="bg-surface-1 rounded-xl p-5 border border-white/[0.06] hover:border-white/[0.1] transition-colors"
> >
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div> <div>
@ -57,7 +57,7 @@ export default function ProjectionDefs() {
</button> </button>
</div> </div>
{expanded.has(def.hash || i) && ( {expanded.has(def.hash || i) && (
<div className="mt-4 space-y-3 text-sm bg-gray-800/30 rounded-lg p-4 border border-gray-700/50"> <div className="mt-4 space-y-3 text-sm bg-surface-3/30 rounded-lg p-4 border border-gray-700/50">
{def.sources && ( {def.sources && (
<div> <div>
<span className="text-gray-400 font-medium">Sources:</span> <span className="text-gray-400 font-medium">Sources:</span>
@ -81,7 +81,7 @@ export default function ProjectionDefs() {
{def.params && ( {def.params && (
<div> <div>
<span className="text-gray-400 font-medium">Params:</span> <span className="text-gray-400 font-medium">Params:</span>
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800"> <pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06]">
{JSON.stringify(def.params, null, 2) {JSON.stringify(def.params, null, 2)
.split('\n') .split('\n')
.map((line, i) => { .map((line, i) => {
@ -106,7 +106,7 @@ export default function ProjectionDefs() {
{def.value_schema && ( {def.value_schema && (
<div> <div>
<span className="text-gray-400 font-medium">Value Schema:</span> <span className="text-gray-400 font-medium">Value Schema:</span>
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800 text-purple-300"> <pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06] text-purple-300">
{JSON.stringify(def.value_schema, null, 2)} {JSON.stringify(def.value_schema, null, 2)}
</pre> </pre>
</div> </div>
@ -114,7 +114,7 @@ export default function ProjectionDefs() {
{def.initial_value !== undefined && ( {def.initial_value !== undefined && (
<div> <div>
<span className="text-gray-400 font-medium">Initial Value:</span> <span className="text-gray-400 font-medium">Initial Value:</span>
<pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-gray-800 text-blue-300"> <pre className="mt-1.5 p-3 bg-gray-950 rounded-lg text-xs overflow-x-auto border border-white/[0.06] text-blue-300">
{JSON.stringify(def.initial_value, null, 2)} {JSON.stringify(def.initial_value, null, 2)}
</pre> </pre>
</div> </div>

View File

@ -81,14 +81,14 @@ export default function Projections() {
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Query Projections</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Query Projections</h2>
<div className="bg-gray-900/50 backdrop-blur rounded-lg p-6 space-y-6 border border-gray-800"> <div className="bg-surface-1 rounded-xl p-6 space-y-6 border border-white/[0.06]">
{/* Projection selector with Listbox */} {/* Projection selector with Listbox */}
<div> <div>
<label className="block text-sm font-medium text-gray-400 mb-2">Projection</label> <label className="block text-sm font-medium text-gray-400 mb-2">Projection</label>
<Listbox value={selectedDef} onChange={handleSelectDef}> <Listbox value={selectedDef} onChange={handleSelectDef}>
<div className="relative"> <div className="relative">
<Listbox.Button className="w-full px-4 py-2.5 bg-gray-800 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all"> <Listbox.Button className="w-full px-4 py-2.5 bg-surface-3 border border-gray-700 rounded-lg text-left focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all">
<span className={selectedDef ? 'text-gray-100' : 'text-gray-500'}> <span className={selectedDef ? 'text-gray-100' : 'text-gray-500'}>
{selectedDef?.name || 'Select a projection...'} {selectedDef?.name || 'Select a projection...'}
</span> </span>
@ -102,7 +102,7 @@ export default function Projections() {
</svg> </svg>
</span> </span>
</Listbox.Button> </Listbox.Button>
<Listbox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
{defs.map((d) => ( {defs.map((d) => (
<Listbox.Option <Listbox.Option
key={d.name} key={d.name}
@ -133,7 +133,7 @@ export default function Projections() {
)} )}
</span> </span>
</div> </div>
<div className="space-y-3 bg-gray-800/30 rounded-lg p-4 border border-gray-700/50"> <div className="space-y-3 bg-surface-3/30 rounded-lg p-4 border border-gray-700/50">
{Object.entries(selectedDef.params).map(([key, schema]) => ( {Object.entries(selectedDef.params).map(([key, schema]) => (
<div key={key}> <div key={key}>
<label className="block text-xs font-medium text-gray-500 mb-1.5"> <label className="block text-xs font-medium text-gray-500 mb-1.5">
@ -155,7 +155,7 @@ export default function Projections() {
) : ( ) : (
<input <input
type={schema.type === 'number' ? 'number' : 'text'} type={schema.type === 'number' ? 'number' : 'text'}
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all" className="w-full px-3 py-2 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
placeholder={`Enter ${schema.type} value...`} placeholder={`Enter ${schema.type} value...`}
value={params[key] || ''} value={params[key] || ''}
onChange={(e) => setParams({ ...params, [key]: e.target.value })} onChange={(e) => setParams({ ...params, [key]: e.target.value })}
@ -169,7 +169,7 @@ export default function Projections() {
{/* Projection info */} {/* Projection info */}
{selectedDef && ( {selectedDef && (
<div className="text-xs space-y-2 bg-gray-800/20 rounded-lg p-4 border border-gray-700/30"> <div className="text-xs space-y-2 bg-surface-3/20 rounded-lg p-4 border border-gray-700/30">
<div> <div>
<span className="text-gray-500 font-medium">sources:</span> <span className="text-gray-500 font-medium">sources:</span>
<div className="mt-1 space-y-2"> <div className="mt-1 space-y-2">
@ -216,7 +216,7 @@ export default function Projections() {
{result !== null && ( {result !== null && (
<div> <div>
<h3 className="text-sm font-medium text-gray-400 mb-2">Result</h3> <h3 className="text-sm font-medium text-gray-400 mb-2">Result</h3>
<pre className="p-4 bg-gray-950 rounded-lg overflow-x-auto text-sm text-green-300 border border-gray-800"> <pre className="p-4 bg-gray-950 rounded-lg overflow-x-auto text-sm text-green-300 border border-white/[0.06]">
{JSON.stringify(result, null, 2)} {JSON.stringify(result, null, 2)}
</pre> </pre>
</div> </div>
@ -280,7 +280,7 @@ function RefCombobox({
<div className="relative"> <div className="relative">
<div className="relative"> <div className="relative">
<Combobox.Input <Combobox.Input
className="w-full px-3 py-2 pr-10 bg-gray-800 border border-gray-700 rounded-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 transition-all" className="w-full px-3 py-2 pr-10 bg-surface-3 border border-white/[0.08] rounded-lg focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent/30 transition-all"
placeholder="Type object ID or select..." placeholder="Type object ID or select..."
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
displayValue={(val: string) => val} displayValue={(val: string) => val}
@ -295,13 +295,13 @@ function RefCombobox({
</svg> </svg>
</Combobox.Button> </Combobox.Button>
</div> </div>
<Combobox.Options className="absolute z-10 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none"> <Combobox.Options className="absolute z-10 mt-1 w-full bg-surface-3 border border-gray-700 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
{Object.entries(relevantByType).map(([type, ids]) => { {Object.entries(relevantByType).map(([type, ids]) => {
const filteredIds = ids.filter((id) => !query || id.toLowerCase().includes(query.toLowerCase())) const filteredIds = ids.filter((id) => !query || id.toLowerCase().includes(query.toLowerCase()))
if (filteredIds.length === 0) return null if (filteredIds.length === 0) return null
return ( return (
<div key={type}> <div key={type}>
<div className="sticky top-0 px-3 py-1.5 text-xs font-medium text-gray-500 bg-gray-850 border-b border-gray-700"> <div className="sticky top-0 px-3 py-1.5 text-xs font-medium text-gray-500 bg-surface-3 border-b border-white/[0.06]">
{type} {type}
</div> </div>
{filteredIds.map((id) => ( {filteredIds.map((id) => (

View File

@ -64,10 +64,10 @@ export default function ReactionLogs() {
return ( return (
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Reaction Logs</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Reaction Logs</h2>
{/* Filter */} {/* Filter */}
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6"> <div className="bg-surface-1 rounded-lg border border-white/[0.06] p-4 mb-6">
<div className="flex items-end gap-3"> <div className="flex items-end gap-3">
<div> <div>
<label className="block text-xs text-gray-400 mb-1">Filter by Reaction ID</label> <label className="block text-xs text-gray-400 mb-1">Filter by Reaction ID</label>
@ -76,7 +76,7 @@ export default function ReactionLogs() {
value={filterReactionId} value={filterReactionId}
onChange={(e) => setFilterReactionId(e.target.value)} onChange={(e) => setFilterReactionId(e.target.value)}
placeholder="Reaction ID" placeholder="Reaction ID"
className="px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-40" className="px-3 py-2 bg-surface-3 border border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 w-40"
/> />
</div> </div>
<button <button
@ -103,13 +103,13 @@ export default function ReactionLogs() {
</div> </div>
</div> </div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No reaction logs found" /> <EmptyState message="No reaction logs found" />
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
ID ID
@ -151,8 +151,8 @@ export default function ReactionLogs() {
<tr <tr
key={log.id} key={log.id}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{log.id}</td>
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.reaction_id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{log.reaction_id}</td>
@ -184,7 +184,7 @@ export default function ReactionLogs() {
{expandedRows.has(log.id) ? 'Hide' : 'Show'} {expandedRows.has(log.id) ? 'Hide' : 'Show'}
</button> </button>
{expandedRows.has(log.id) && ( {expandedRows.has(log.id) && (
<pre className="mt-2 p-2 bg-gray-800 rounded text-xs text-gray-300 max-w-xs overflow-auto whitespace-pre-wrap"> <pre className="mt-2 p-2 bg-surface-3 rounded text-xs text-gray-300 max-w-xs overflow-auto whitespace-pre-wrap">
{typeof log.handler_output === 'string' {typeof log.handler_output === 'string'
? log.handler_output ? log.handler_output
: JSON.stringify(log.handler_output, null, 2)} : JSON.stringify(log.handler_output, null, 2)}

View File

@ -41,13 +41,13 @@ export default function Reactions() {
return ( return (
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Reactions</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Reactions</h2>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No reactions found" /> <EmptyState message="No reactions found" />
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-800/80 border-b border-gray-700"> <thead className="bg-surface-3/80 border-b border-gray-700">
<tr> <tr>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">ID</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider"> <th className="px-4 py-3 text-left text-xs font-semibold text-gray-400 uppercase tracking-wider">
@ -73,8 +73,8 @@ export default function Reactions() {
<tr <tr
key={i} key={i}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{reaction.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{reaction.id}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">

View File

@ -60,10 +60,10 @@ export default function RequestLogs() {
return ( return (
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Request Logs</h2> <h2 className="text-lg font-semibold text-white tracking-tight mb-5">Request Logs</h2>
{/* Filter */} {/* Filter */}
<div className="bg-gray-900/50 backdrop-blur rounded-lg border border-gray-800 p-4 mb-6"> <div className="bg-surface-1 rounded-lg border border-white/[0.06] p-4 mb-6">
<div className="flex items-end gap-3"> <div className="flex items-end gap-3">
<div> <div>
<label className="block text-xs text-gray-400 mb-1">Filter by API Key ID</label> <label className="block text-xs text-gray-400 mb-1">Filter by API Key ID</label>
@ -99,7 +99,7 @@ export default function RequestLogs() {
</div> </div>
</div> </div>
<div className="bg-gray-900/50 backdrop-blur rounded-lg overflow-hidden border border-gray-800"> <div className="bg-surface-1 rounded-xl overflow-hidden border border-white/[0.06]">
{data.length === 0 ? ( {data.length === 0 ? (
<EmptyState message="No request logs found" /> <EmptyState message="No request logs found" />
) : ( ) : (
@ -138,8 +138,8 @@ export default function RequestLogs() {
<tr <tr
key={log.id} key={log.id}
className={`transition-colors ${ className={`transition-colors ${
i % 2 === 0 ? 'bg-gray-900/30' : 'bg-gray-850/30' i % 2 === 0 ? 'bg-gray-900/30' : 'bg-white/[0.02]'
} hover:bg-gray-800/60`} } hover:bg-surface-3/60`}
> >
<td className="px-4 py-3 font-mono text-sm text-gray-300">{log.id}</td> <td className="px-4 py-3 font-mono text-sm text-gray-300">{log.id}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">

View File

@ -1,13 +1,18 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; font-family: 'Inter', system-ui, -apple-system, sans-serif;
background: linear-gradient(to bottom, #050505, #0a0a0f); background: #07080a;
color: #f3f4f6; color: #e5e7eb;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
#root { #root {
@ -15,46 +20,45 @@ body {
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
}
/* Custom scrollbar for dark theme */ /* Scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 6px;
height: 8px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #1a1a1f; background: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #374151; background: #2a2d36;
border-radius: 4px; border-radius: 3px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #4b5563; background: #3a3d46;
} }
/* Smooth transitions for interactive elements */ /* Global transitions */
button, button, a, input, select, textarea {
a, transition: all 0.15s ease;
input,
select {
transition: all 0.2s ease;
} }
/* Custom animations */ /* Subtle noise grain overlay */
@keyframes fadeIn { .grain::after {
from { content: '';
opacity: 0; position: fixed;
transform: translateY(10px); inset: 0;
} background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
to { pointer-events: none;
opacity: 1; z-index: 9999;
transform: translateY(0); opacity: 0.4;
}
} }
.animate-fadeIn { /* Table row animation */
animation: fadeIn 0.3s ease-out; @keyframes rowFadeIn {
from { opacity: 0; transform: translateX(-4px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-row {
animation: rowFadeIn 0.2s ease-out both;
} }

View File

@ -2,7 +2,45 @@
export default { export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: {}, extend: {
colors: {
surface: {
0: '#07080a',
1: '#0d0f12',
2: '#13161b',
3: '#1a1d24',
4: '#22262f',
},
accent: {
DEFAULT: '#3b82f6',
dim: '#2563eb',
glow: '#60a5fa',
},
mint: {
DEFAULT: '#34d399',
dim: '#059669',
},
},
fontFamily: {
sans: ['"Inter"', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['"JetBrains Mono"', '"Fira Code"', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.25s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(8px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
},
}, },
plugins: [], plugins: [],
} }