feat: NASA mission-control theme for all OMNL dashboards
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m41s
CI/CD Pipeline / Security Scanning (push) Successful in 2m32s
CI/CD Pipeline / Lint and Format (push) Failing after 51s
CI/CD Pipeline / Terraform Validation (push) Failing after 35s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 30s
Validation / validate-genesis (push) Successful in 34s
Validation / validate-terraform (push) Failing after 36s
Validation / validate-kubernetes (push) Failing after 11s
Validation / validate-smart-contracts (push) Failing after 11s
Validation / validate-security (push) Failing after 1m23s
Validation / validate-documentation (push) Failing after 21s

Replace Binance-style dashboard chrome with a shared telemetry UI, SVG icons, and mission headers across Central Bank, Office 24, trade, swap, and admin screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 17:10:24 -07:00
parent 4e047b9429
commit cbd669c762
18 changed files with 1592 additions and 997 deletions

View File

@@ -10,6 +10,9 @@ import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { MAINNET_TETHER_ABI } from '../../abis/MainnetTether'
import { subscribeToContractEvents } from '../../utils/contractEvents'
import toast from 'react-hot-toast'
import DashboardStatCard from '../../features/omnl-dashboard/DashboardStatCard'
import DashboardSection from '../../features/omnl-dashboard/DashboardSection'
import NasaIcon, { StatusIcon } from '../icons/NasaIcon'
export default function AdminDashboard() {
const { adminActions, auditLogs } = useAdmin()
@@ -68,7 +71,7 @@ export default function AdminDashboard() {
'Paused',
(_event) => {
setMainnetTetherPaused(true)
toast.success('MainnetTether paused event detected', { icon: '🔔' })
toast.success('MainnetTether paused event detected')
}
).then((unsub) => {
unsubscribePaused = unsub
@@ -81,7 +84,7 @@ export default function AdminDashboard() {
'Unpaused',
(_event) => {
setMainnetTetherPaused(false)
toast.success('MainnetTether unpaused event detected', { icon: '🔔' })
toast.success('MainnetTether unpaused event detected')
}
).then((unsub) => {
unsubscribeUnpaused = unsub
@@ -96,124 +99,77 @@ export default function AdminDashboard() {
}, [publicClient, address])
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="space-y-6 nasa-mission-root">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Total Actions</div>
<div className="text-2xl font-bold text-white">{stats.totalActions}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Pending</div>
<div className="text-2xl font-bold text-yellow-400">{stats.pending}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Executed</div>
<div className="text-2xl font-bold text-green-400">{stats.executed}</div>
</div>
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="text-white/70 text-sm mb-1">Success Rate</div>
<div className="text-2xl font-bold text-blue-400">{stats.successRate}%</div>
</div>
<DashboardStatCard label="Total actions" value={String(stats.totalActions)} accent="blue" icon="admin" />
<DashboardStatCard label="Pending" value={String(stats.pending)} accent="gold" icon="alert" />
<DashboardStatCard label="Executed" value={String(stats.executed)} accent="green" icon="check" />
<DashboardStatCard label="Success rate" value={`${stats.successRate}%`} accent="blue" icon="chart" />
</div>
{/* Contract Status */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-black/20 rounded-xl p-4 border border-white/10">
<div className="dash-card">
<div className="flex items-center justify-between mb-2">
<div className="text-white/70 text-sm">MainnetTether</div>
<span
className={`px-3 py-1 rounded text-xs font-semibold ${
mainnetTetherPaused
? 'bg-red-500/20 text-red-300'
: 'bg-green-500/20 text-green-300'
}`}
>
<div className="dash-stat-label flex items-center gap-1">
<NasaIcon name="bridge" size={14} className="nasa-icon--telemetry" />
MainnetTether
</div>
<span className={`nasa-badge ${mainnetTetherPaused ? 'nasa-badge--caution' : 'nasa-badge--nominal'}`}>
{mainnetTetherPaused ? 'Paused' : 'Active'}
</span>
</div>
<p className="text-white/60 text-xs font-mono">
{CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}
</p>
<p className="text-[#7a9bb8] text-xs font-mono">{CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Recent Actions */}
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Recent Actions</h3>
<DashboardSection title="Recent actions" icon="rocket">
{recentActions.length === 0 ? (
<p className="text-white/60 text-sm">No actions yet</p>
<p className="text-[#7a9bb8] text-sm">No actions yet</p>
) : (
<div className="space-y-3">
{recentActions.map((action) => (
<div
key={action.id}
className="bg-white/5 rounded-lg p-3 border border-white/10"
>
<div key={action.id} className="nasa-inset-panel">
<div className="flex justify-between items-start">
<div>
<p className="text-white font-semibold text-sm">{action.type}</p>
<p className="text-white/60 text-xs font-mono">
{action.contractAddress.slice(0, 20)}...
</p>
<p className="text-[#e8f4ff] font-semibold text-sm font-mono">{action.type}</p>
<p className="text-[#7a9bb8] text-xs font-mono">{action.contractAddress.slice(0, 20)}...</p>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
action.status === TransactionRequestStatus.SUCCESS
? 'bg-green-500/20 text-green-300'
: action.status === TransactionRequestStatus.FAILED
? 'bg-red-500/20 text-red-300'
: 'bg-yellow-500/20 text-yellow-300'
}`}
>
<span className="flex items-center gap-1 text-xs font-mono">
<StatusIcon ok={action.status === TransactionRequestStatus.SUCCESS} />
{action.status}
</span>
</div>
<p className="text-white/50 text-xs mt-2">
{new Date(action.createdAt).toLocaleString()}
</p>
<p className="text-[#7a9bb8] text-xs mt-2">{new Date(action.createdAt).toLocaleString()}</p>
</div>
))}
</div>
)}
</div>
</DashboardSection>
{/* Recent Audit Logs */}
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Recent Audit Logs</h3>
<DashboardSection title="Recent audit logs" icon="ledger">
{recentLogs.length === 0 ? (
<p className="text-white/60 text-sm">No audit logs yet</p>
<p className="text-[#7a9bb8] text-sm">No audit logs yet</p>
) : (
<div className="space-y-3">
{recentLogs.map((log) => (
<div
key={log.id}
className="bg-white/5 rounded-lg p-3 border border-white/10"
>
<div key={log.id} className="nasa-inset-panel">
<div className="flex justify-between items-start">
<div>
<p className="text-white font-semibold text-sm">{log.action}</p>
<p className="text-white/60 text-xs font-mono">{log.user.slice(0, 10)}...</p>
<p className="text-[#e8f4ff] font-semibold text-sm font-mono">{log.action}</p>
<p className="text-[#7a9bb8] text-xs font-mono">{log.user.slice(0, 10)}...</p>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
log.status === 'success'
? 'bg-green-500/20 text-green-300'
: 'bg-red-500/20 text-red-300'
}`}
>
<span className="flex items-center gap-1 text-xs font-mono">
<StatusIcon ok={log.status === 'success'} />
{log.status}
</span>
</div>
<p className="text-white/50 text-xs mt-2">
{new Date(log.timestamp).toLocaleString()}
</p>
<p className="text-[#7a9bb8] text-xs mt-2">{new Date(log.timestamp).toLocaleString()}</p>
</div>
))}
</div>
)}
</div>
</DashboardSection>
</div>
</div>
)

View File

@@ -4,8 +4,11 @@
*/
import { useState, useEffect } from 'react';
import { useAccount } from 'wagmi';
import toast from 'react-hot-toast';
import { useAccount } from 'wagmi';
import DashboardSection from '../../features/omnl-dashboard/DashboardSection';
import NasaIcon, { StatusIcon } from '../icons/NasaIcon';
import NasaPageHeader from '../../features/omnl-dashboard/NasaPageHeader';
interface ChainMetadata {
chainId: number;
@@ -37,13 +40,6 @@ export default function ChainManagementDashboard() {
const loadChains = async () => {
try {
setLoading(true);
// TODO: Connect to ChainRegistry contract
// const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL);
// const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
// const [evmChainIds, evmChains] = await registry.getAllEVMChains();
// const [nonEvmIds, nonEvmChains] = await registry.getAllNonEVMChains();
// Mock data for now
setChains([
{
chainId: 138,
@@ -75,7 +71,6 @@ export default function ChainManagementDashboard() {
const toggleChain = async (currentStatus: boolean) => {
try {
// TODO: Call ChainRegistry.setChainActive()
toast.success(`Chain ${currentStatus ? 'disabled' : 'enabled'}`);
loadChains();
} catch (error: unknown) {
@@ -85,52 +80,56 @@ export default function ChainManagementDashboard() {
if (!isConnected) {
return (
<div className="p-6 bg-white/10 rounded-xl">
<p className="text-white/70">Please connect your wallet to manage chains.</p>
<div className="dash-card nasa-mission-root">
<p className="text-[#7a9bb8] flex items-center gap-2">
<NasaIcon name="wallet" size={16} className="nasa-icon--telemetry" />
Please connect your wallet to manage chains.
</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-bold text-white mb-4">Chain Management</h2>
<div className="space-y-6 nasa-mission-root">
<NasaPageHeader
icon="network"
missionId="NET-ADM"
title="Chain Management"
subtitle="Supported chains, adapters, and deployment registry"
/>
<DashboardSection title="Registered chains" icon="globe">
{loading ? (
<div className="text-white/70">Loading chains...</div>
<p className="text-[#7a9bb8] flex items-center gap-2">
<NasaIcon name="loading" size={14} className="nasa-icon--spin nasa-icon--telemetry" />
Loading chains
</p>
) : (
<div className="space-y-4">
{chains.map((chain) => (
<div
key={chain.chainId || chain.chainIdentifier}
className="bg-white/5 rounded-lg p-4 flex items-center justify-between"
>
<div key={chain.chainId || chain.chainIdentifier} className="nasa-inset-panel flex items-center justify-between gap-4 flex-wrap">
<div>
<h3 className="text-white font-semibold">
<h3 className="text-[#e8f4ff] font-semibold font-mono flex items-center gap-2">
<NasaIcon name="network" size={14} className="nasa-icon--telemetry" />
{chain.chainIdentifier} ({chain.chainType})
</h3>
<p className="text-white/70 text-sm">
Adapter: {chain.adapter.slice(0, 10)}...
</p>
<p className="text-white/70 text-sm">
Explorer: <a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">{chain.explorerUrl}</a>
<p className="text-[#7a9bb8] text-sm font-mono">Adapter: {chain.adapter.slice(0, 10)}</p>
<p className="text-[#7a9bb8] text-sm">
Explorer:{' '}
<a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-[#00d4ff] hover:underline inline-flex items-center gap-1">
<NasaIcon name="link" size={12} />
{chain.explorerUrl}
</a>
</p>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-sm ${
chain.isActive
? 'bg-green-500/20 text-green-200'
: 'bg-red-500/20 text-red-200'
}`}>
<span className={`nasa-badge flex items-center gap-1 ${chain.isActive ? 'nasa-badge--nominal' : 'nasa-badge--caution'}`}>
<StatusIcon ok={chain.isActive} />
{chain.isActive ? 'Active' : 'Inactive'}
</span>
<button
onClick={() => toggleChain(chain.isActive)}
className={`px-4 py-2 rounded-lg font-semibold transition-colors ${
chain.isActive
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
className={`nasa-btn ${chain.isActive ? '' : 'nasa-btn--primary'}`}
>
{chain.isActive ? 'Disable' : 'Enable'}
</button>
@@ -139,17 +138,16 @@ export default function ChainManagementDashboard() {
))}
</div>
)}
</div>
</DashboardSection>
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Add New Chain</h3>
<DashboardSection title="Add new chain" icon="rocket">
<div className="space-y-4">
<select
value={selectedChain}
onChange={(e) => setSelectedChain(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
className="nasa-select"
>
<option value="">Select chain to add...</option>
<option value="">Select chain to add</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
<option value="optimism">Optimism</option>
@@ -159,13 +157,14 @@ export default function ChainManagementDashboard() {
<option value="ethereum">Ethereum Mainnet</option>
</select>
<button
onClick={() => toast('Deployment feature coming soon', { icon: '' })}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold"
onClick={() => toast('Deployment feature coming soon')}
className="nasa-btn nasa-btn--primary w-full"
>
Deploy Chain Adapter
<NasaIcon name="rocket" size={14} />
Deploy chain adapter
</button>
</div>
</div>
</DashboardSection>
</div>
);
}

View File

@@ -0,0 +1,204 @@
import type { ReactNode, SVGProps } from 'react';
export type NasaIconName =
| 'central-bank'
| 'office'
| 'trade'
| 'swap'
| 'bridge'
| 'refresh'
| 'check'
| 'x'
| 'alert'
| 'link'
| 'arrow-right'
| 'network'
| 'satellite'
| 'ledger'
| 'rocket'
| 'search'
| 'loading'
| 'chart'
| 'wallet'
| 'globe'
| 'money'
| 'rail'
| 'admin';
type Props = SVGProps<SVGSVGElement> & {
name: NasaIconName;
size?: number;
};
const paths: Record<NasaIconName, ReactNode> = {
'central-bank': (
<>
<path d="M3 21h18" />
<path d="M5 21V7l7-4 7 4v14" />
<path d="M9 21v-6h6v6" />
<path d="M9 9h.01M15 9h.01M9 13h.01M15 13h.01" />
</>
),
office: (
<>
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18" />
<path d="M6 12h12M10 6h.01M14 6h.01M10 10h.01M14 10h.01M10 14h.01M14 14h.01M10 18h.01M14 18h.01" />
</>
),
trade: (
<>
<path d="M3 3v18h18" />
<path d="M7 14l4-4 3 3 5-6" />
</>
),
swap: (
<>
<path d="M7 16V4M7 4 3 8M7 4l4 4" />
<path d="M17 8v12M17 20l4-4M17 20l-4-4" />
</>
),
bridge: (
<>
<path d="M4 16h16" />
<path d="M6 16V8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8" />
<path d="M8 12h8" />
</>
),
refresh: (
<>
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
<path d="M21 3v6h-6" />
</>
),
check: <path d="M20 6 9 17l-5-5" />,
x: (
<>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</>
),
alert: (
<>
<path d="M12 9v4" />
<path d="M12 17h.01" />
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</>
),
link: (
<>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</>
),
'arrow-right': (
<>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</>
),
network: (
<>
<circle cx="12" cy="12" r="2" />
<path d="M12 2v4M12 18v4M2 12h4M18 12h4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</>
),
satellite: (
<>
<path d="M13 7 9 3 5 7l4 4" />
<path d="m17 11 4 4-4 4-4-4" />
<path d="m8 12 4 4 6-6-4-4Z" />
<path d="m16 8 3-3M8 16l-3 3" />
</>
),
ledger: (
<>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
<path d="M8 7h8M8 11h8M8 15h5" />
</>
),
rocket: (
<>
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
<path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</>
),
search: (
<>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</>
),
loading: <path d="M21 12a9 9 0 1 1-6.219-8.56" />,
chart: (
<>
<path d="M3 3v18h18" />
<path d="M18 17V9M13 17V5M8 17v-3" />
</>
),
wallet: (
<>
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" />
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" />
</>
),
globe: (
<>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</>
),
money: (
<>
<circle cx="12" cy="12" r="10" />
<path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8" />
<path d="M12 6v2M12 16v2" />
</>
),
rail: (
<>
<path d="M2 17h20" />
<path d="M6 17V5h12v12" />
<path d="M6 9h12M10 13h4" />
</>
),
admin: (
<>
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
</>
),
};
export default function NasaIcon({ name, size = 18, className = '', ...props }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`nasa-icon ${className}`.trim()}
aria-hidden={props['aria-label'] ? undefined : true}
{...props}
>
{paths[name]}
</svg>
);
}
export function StatusIcon({ ok, className = '' }: { ok: boolean; className?: string }) {
return (
<NasaIcon
name={ok ? 'check' : 'x'}
size={14}
className={ok ? `nasa-icon--nominal ${className}` : `nasa-icon--critical ${className}`}
/>
);
}

View File

@@ -1,61 +1,60 @@
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
import WalletConnect from '../wallet/WalletConnect';
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
const PRODUCTS = [
{ path: '/central-bank', label: 'Central Bank', short: 'CB' },
{ path: '/office-24', label: 'Office 24', short: 'O24' },
{ path: '/trade', label: 'DBIS Trade', short: 'Trade' },
{ path: '/swap', label: 'Swap', short: 'Swap' },
] as const;
export default function OmnlProductLayout() {
const location = useLocation();
const userInitiatedDisconnectRef = useRef(false);
const isActive = (path: string) => location.pathname.startsWith(path);
return (
<div className="omnl-app min-h-screen flex flex-col bg-[#0b0e11]">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header sticky top-0 z-50 border-b border-[#2b3139] bg-[#181a20]">
<div className="max-w-[1600px] mx-auto px-4 h-14 flex items-center gap-4">
<Link to="/central-bank" className="flex items-center gap-2 shrink-0">
<span className="w-8 h-8 rounded bg-[#f0b90b] text-[#181a20] font-bold flex items-center justify-center text-sm">
O
</span>
<span className="font-semibold text-[#eaecef] hidden sm:inline">OMNL</span>
</Link>
<nav className="flex items-center gap-1 flex-1" aria-label="OMNL products">
{PRODUCTS.map(({ path, label }) => (
<Link
key={path}
to={path}
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
isActive(path)
? 'bg-[#2b3139] text-[#f0b90b]'
: 'text-[#848e9c] hover:text-[#eaecef] hover:bg-[#1e2329]'
}`}
>
{label}
</Link>
))}
</nav>
<Link
to="/"
className="text-xs text-[#848e9c] hover:text-[#f0b90b] hidden md:inline shrink-0"
>
Bridge
</Link>
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true; }} />
</div>
</header>
<main className="flex-1">
<Outlet />
</main>
</div>
);
}
import { Link, Outlet, useLocation } from 'react-router-dom';
import { useRef } from 'react';
import WalletConnect from '../wallet/WalletConnect';
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice';
import NasaIcon, { type NasaIconName } from '../icons/NasaIcon';
const PRODUCTS: { path: string; label: string; icon: NasaIconName }[] = [
{ path: '/central-bank', label: 'Central Bank', icon: 'central-bank' },
{ path: '/office-24', label: 'Office 24', icon: 'office' },
{ path: '/trade', label: 'DBIS Trade', icon: 'trade' },
{ path: '/swap', label: 'Digital Swap', icon: 'swap' },
];
export default function OmnlProductLayout() {
const location = useLocation();
const userInitiatedDisconnectRef = useRef(false);
const isActive = (path: string) => location.pathname.startsWith(path);
return (
<div className="omnl-app min-h-screen flex flex-col">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<header className="omnl-app-header sticky top-0 z-50 border-b">
<div className="max-w-[1600px] mx-auto px-4 h-14 flex items-center gap-4">
<Link to="/central-bank" className="flex items-center gap-2 shrink-0">
<span className="w-8 h-8 border border-[#1e4a8a] bg-[#0a1224] text-[#00d4ff] font-bold flex items-center justify-center text-sm">
<NasaIcon name="satellite" size={16} className="nasa-icon--telemetry" />
</span>
<span className="nasa-nav-brand font-semibold hidden sm:inline text-sm">OMNL Mission Control</span>
</Link>