chore(frontend): add dashboard styles and ecosystem hub modules for production build
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Has been cancelled
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 25s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-03 02:15:55 -07:00
parent 32a33c1282
commit af7c4e9476
14 changed files with 1516 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import { Link, useLocation } from 'react-router-dom';
import NasaIcon from '../icons/NasaIcon';
type Props = {
onOpenMenu: () => void;
};
const TABS = [
{ to: '/hub', label: 'Home', icon: 'dashboard' as const },
{ to: '/bridge', label: 'Bridge', icon: 'bridge' as const },
{ to: '/trade', label: 'Trade', icon: 'trade' as const },
{ to: '/central-bank', label: 'Z Bank', icon: 'central-bank' as const },
];
export default function MobileBottomNav({ onOpenMenu }: Props) {
const location = useLocation();
const isActive = (path: string) => {
if (path === '/hub') return location.pathname === '/hub' || location.pathname === '/';
if (path === '/bridge') return location.pathname === '/bridge' || location.pathname.startsWith('/bridge');
if (path === '/') return location.pathname === '/';
return location.pathname.startsWith(path);
};
return (
<nav className="dbis-bottom-nav md:hidden" aria-label="Primary navigation">
{TABS.map(({ to, label, icon }) => (
<Link
key={to}
to={to}
className={`dbis-bottom-nav__item${isActive(to) ? ' dbis-bottom-nav__item--active' : ''}`}
>
<NasaIcon name={icon} size={20} />
<span>{label}</span>
</Link>
))}
<button type="button" className="dbis-bottom-nav__item" onClick={onOpenMenu} aria-label="Open menu">
<span className="text-lg leading-none"></span>
<span>More</span>
</button>
</nav>
);
}

View File

@@ -0,0 +1,80 @@
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
export type MobileNavItem = {
to?: string;
href?: string;
label: string;
active?: boolean;
onClick?: () => void;
};
type Props = {
open: boolean;
onClose: () => void;
title: string;
items: MobileNavItem[];
};
export default function MobileNavDrawer({ open, onClose, title, items }: Props) {
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prev;
};
}, [open]);
if (!open) return null;
return (
<div className="fixed inset-0 z-[100] lg:hidden" role="dialog" aria-modal="true" aria-label={title}>
<button
type="button"
className="absolute inset-0 bg-black/75 backdrop-blur-sm"
aria-label="Close menu"
onClick={onClose}
/>
<nav className="absolute top-0 left-0 bottom-0 w-[min(100vw,20rem)] bg-[#141a16] border-r border-green-500/20 flex flex-col shadow-2xl safe-area-pad">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 min-h-[56px]">
<span className="text-white font-semibold text-sm">{title}</span>
<button
type="button"
onClick={onClose}
className="text-white/80 hover:text-white p-2 min-h-[44px] min-w-[44px] flex items-center justify-center rounded-lg"
aria-label="Close"
>
</button>
</div>
<ul className="flex-1 overflow-y-auto py-2">
{items.map((item) => {
const className = `block w-full text-left px-4 py-3 text-sm font-medium min-h-[44px] ${
item.active ? 'bg-green-600/20 text-green-300 border-l-2 border-green-400' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`;
if (item.href) {
return (
<li key={item.href}>
<a href={item.href} target="_blank" rel="noopener noreferrer" className={className} onClick={onClose}>
{item.label}
</a>
</li>
);
}
if (item.to) {
return (
<li key={item.to}>
<Link to={item.to} className={className} onClick={() => { item.onClick?.(); onClose(); }}>
{item.label}
</Link>
</li>
);
}
return null;
})}
</ul>
</nav>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import type { EcosystemLink } from './ecosystemLinks';
/** Paths served outside the React SPA (static HTML or proxied APIs). */
export function isStaticPortalPath(href: string): boolean {
return (
href.startsWith('/omnl') ||
href.startsWith('/api') ||
href.startsWith('/settlement') ||
href.startsWith('/exchange') ||
href.startsWith('/reserve/institutional')
);
}
export function ecosystemLinkUsesAnchor(link: EcosystemLink): boolean {
return Boolean(link.external) || link.href.startsWith('http') || isStaticPortalPath(link.href);
}

View File

@@ -0,0 +1,71 @@
import type { NasaIconName } from '../components/icons/NasaIcon';
import { defaultFrontendExplorerUrl } from './networks';
export type EcosystemLinkAccent = 'green' | 'teal' | 'blue' | 'gold' | 'purple' | 'slate';
export type EcosystemLink = {
id: string;
label: string;
description: string;
icon: NasaIconName;
href: string;
external?: boolean;
accent?: EcosystemLinkAccent;
};
/** In-app React routes (same origin) */
export const IN_APP_LINKS: EcosystemLink[] = [
{ id: 'bridge', label: 'Bridge', description: 'Cross-chain bridge, CCIP, XRPL, trustless', icon: 'bridge', href: '/bridge', accent: 'teal' },
{ id: 'swap', label: 'Digital Swap', description: 'M2 load, token swap, settlement rails', icon: 'swap', href: '/swap', accent: 'blue' },
{ id: 'trade', label: 'DBIS Trade', description: 'Markets, order book, spot trading', icon: 'trade', href: '/trade', accent: 'gold' },
{ id: 'central-bank', label: 'Z Online Bank', description: 'Zardasht banking · balances & payments', icon: 'central-bank', href: '/central-bank', accent: 'green' },
{ id: 'z-full-settlement', label: 'Z M0-M4 settlement', description: 'Full money-supply settlement panel', icon: 'money', href: '/central-bank#z-full-settlement', accent: 'green' },
{ id: 'wire-protocols', label: 'Wire protocols', description: 'MT102, MT103 LC, SBLC, BG settlement', icon: 'terminal', href: '/central-bank#z-settlement-protocols', accent: 'gold' },
{ id: 'office-24', label: 'Office 24 Central Bank', description: '128-bank corridors · IPSAS settlement', icon: 'office', href: '/office-24', accent: 'purple' },
{ id: 'reserve', label: 'Reserve', description: 'Coverage, attestation, peg status', icon: 'money', href: '/reserve', accent: 'green' },
{ id: 'history', label: 'History', description: 'Bridge transfer lookup & tracking', icon: 'ledger', href: '/history', accent: 'slate' },
{ id: 'wallets', label: 'Wallets', description: 'OneX Wallet · email, social, passkey on Shiva', icon: 'wallet', href: '/wallets', accent: 'blue' },
{ id: 'admin', label: 'Admin', description: 'Multisig, channels, bridge operators', icon: 'admin', href: '/admin', accent: 'slate' },
{ id: 'docs', label: 'Developers', description: 'API reference & integration', icon: 'code', href: '/docs', accent: 'teal' },
{ id: 'hub', label: 'DBIS Dashboard', description: 'All apps, consoles & API links', icon: 'dashboard', href: '/hub', accent: 'green' },
];
/** Primary apps shown in dashboard quick-access strip */
export const FEATURED_APP_LINKS: EcosystemLink[] = [
IN_APP_LINKS.find((l) => l.id === 'central-bank')!,
IN_APP_LINKS.find((l) => l.id === 'bridge')!,
IN_APP_LINKS.find((l) => l.id === 'trade')!,
IN_APP_LINKS.find((l) => l.id === 'swap')!,
IN_APP_LINKS.find((l) => l.id === 'office-24')!,
];
/** Static operator consoles — served from /omnl/* (portal-serve or token-aggregation :3000) */
export const OPERATOR_CONSOLE_LINKS: EcosystemLink[] = [
{ id: 'compliance', label: 'OMNL Compliance Console', description: 'Posture, pending actions, Safe tx', icon: 'shield', href: '/omnl/compliance', accent: 'purple' },
{ id: 'omnl-dash', label: 'OMNL API Snapshot', description: 'Registry and Fineract compare JSON', icon: 'chart', href: '/omnl/dashboard', accent: 'blue' },
{ id: 'reserve-dash', label: 'Reserve Dashboard', description: 'Proof rows, coverage, institutional view', icon: 'money', href: '/reserve', accent: 'green' },
{ id: 'settlement-term', label: 'Settlement Terminal', description: 'Operator settlement terminal UI', icon: 'terminal', href: '/omnl/terminal', accent: 'gold' },
{ id: 'token-admin', label: 'Token Aggregation API', description: 'API catalog JSON', icon: 'ledger', href: '/api/v1/omnl/catalog', accent: 'teal' },
];
/** Public production portals */
export const PUBLIC_PORTAL_LINKS: EcosystemLink[] = [
{ id: 'pub-bank', label: 'Z Online Bank', description: 'online.omdnl.org', icon: 'globe', href: 'https://online.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-cb', label: 'Central Bank', description: 'secure.omdnl.org', icon: 'central-bank', href: 'https://secure.omdnl.org/central-bank', external: true, accent: 'green' },
{ id: 'pub-o24', label: 'Office 24 Central Bank', description: 'office24.omdnl.org', icon: 'office', href: 'https://office24.omdnl.org/office-24', external: true, accent: 'purple' },
{ id: 'pub-trade', label: 'Exchange', description: 'exchange.omdnl.org', icon: 'trade', href: 'https://exchange.omdnl.org/trade', external: true, accent: 'gold' },
{ id: 'pub-forex', label: 'Forex', description: 'forex.omdnl.org', icon: 'trade', href: 'https://forex.omdnl.org/trade', external: true, accent: 'gold' },
{ id: 'pub-swap', label: 'Digital Swap', description: 'digital.omdnl.org', icon: 'swap', href: 'https://digital.omdnl.org/swap', external: true, accent: 'teal' },
{ id: 'explorer', label: 'Chain 138 Explorer', description: 'Blocks, contracts, transactions', icon: 'globe', href: defaultFrontendExplorerUrl, external: true, accent: 'slate' },
];
/** API & docs endpoints — same-origin via portal-serve proxy on tunnel/local */
export const API_LINKS: EcosystemLink[] = [
{ id: 'openapi', label: 'OpenAPI JSON', description: 'Machine-readable OMNL API spec', icon: 'code', href: '/api/v1/omnl/openapi.json', accent: 'teal' },
{ id: 'catalog', label: 'API Catalog', description: 'All OMNL routes and auth notes', icon: 'ledger', href: '/api/v1/omnl/catalog', accent: 'blue' },
{ id: 'integration', label: 'Integration Status', description: 'Configured env groups (no secrets)', icon: 'check', href: '/api/v1/omnl/integration-status', accent: 'green' },
{ id: 'registry', label: 'IPSAS Registry', description: 'GL registry JSON', icon: 'money', href: '/api/v1/omnl/ipsas/registry', accent: 'gold' },
{ id: 'settlement-health', label: 'Settlement health', description: '128-chain settlement middleware', icon: 'check', href: '/settlement/health', accent: 'green' },
{ id: 'exchange-health', label: 'Exchange health', description: 'DBIS exchange service', icon: 'check', href: '/exchange/health', accent: 'green' },
{ id: 'fineract-swagger', label: 'Fineract Swagger', description: 'omnl.hybxfinance.io', icon: 'chart', href: 'https://omnl.hybxfinance.io/fineract-provider/swagger-ui/index.html', external: true, accent: 'purple' },
];

View File

@@ -0,0 +1,24 @@
import onexShivaConfig from '../../../config/onex-shiva-integration.v1.json';
export type OnexShivaConfig = typeof onexShivaConfig;
export const onexShiva = onexShivaConfig;
export const SHIVA_ONEX_CHAIN_ID = Number(
import.meta.env.VITE_SHIVA_ONEX_CHAIN_ID || onexShiva.chain.chainId,
);
export const shivaOnexRpcUrl =
import.meta.env.VITE_RPC_URL_900001 ||
import.meta.env.VITE_SHIVA_ONEX_RPC_URL ||
onexShiva.chain.rpcDefault;
export const shivaOnexExplorerUrl =
import.meta.env.VITE_SHIVA_ONEX_EXPLORER_URL || onexShiva.chain.explorerUrl;
export const onexWalletEnabled =
import.meta.env.VITE_ENABLE_ONEX_SHIVA_WALLET !== 'false' &&
import.meta.env.VITE_ENABLE_ONEX_SHIVA_WALLET !== '0';
export const ONEX_BANK_OFFICE_ID = onexShiva.fineract.officeId;
export const ONEX_HYBX_WALLET_ID = onexShiva.fineract.hybxWalletId;

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from 'react';
import type { NasaIconName } from '../../components/icons/NasaIcon';
import NasaIcon from '../../components/icons/NasaIcon';
type Props = {
title: string;
subtitle?: string;
icon: NasaIconName;
defaultOpen?: boolean;
children: ReactNode;
};
export default function DashboardSectionFold({ title, subtitle, icon, defaultOpen = true, children }: Props) {
return (
<details className="dbis-section-fold" open={defaultOpen}>
<summary className="dbis-section-fold__summary">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-green-500/10 border border-green-500/25 text-green-400 shrink-0">
<NasaIcon name={icon} size={16} />
</span>
<span>
{title}
{subtitle && <span className="block text-xs font-normal text-[var(--nasa-muted)] mt-0.5">{subtitle}</span>}
</span>
<NasaIcon name="arrow-right" size={14} className="dbis-section-fold__chevron ml-auto text-[var(--nasa-muted)]" />
</summary>
<div className="dbis-section-fold__body">{children}</div>
</details>
);
}

View File

@@ -0,0 +1,128 @@
import { Link } from 'react-router-dom';
import NasaIcon, { type NasaIconName } from '../../components/icons/NasaIcon';
import type { EcosystemLink, EcosystemLinkAccent } from '../../config/ecosystemLinks';
import { ecosystemLinkUsesAnchor } from '../../config/ecosystemLinkRouting';
type Props = {
title: string;
subtitle?: string;
links: EcosystemLink[];
columns?: '2' | '3';
centered?: boolean;
sectionIcon?: NasaIconName;
variant?: 'grid' | 'list';
hideHeader?: boolean;
};
const ACCENT_STYLES: Record<EcosystemLinkAccent, { tile: string; icon: string }> = {
green: { tile: 'bg-green-500/15 border-green-500/30 shadow-[0_0_20px_rgba(34,197,94,0.1)]', icon: 'text-green-400' },
teal: { tile: 'bg-teal-500/15 border-teal-500/30 shadow-[0_0_20px_rgba(20,184,166,0.1)]', icon: 'text-teal-400' },
blue: { tile: 'bg-sky-500/15 border-sky-500/30 shadow-[0_0_20px_rgba(56,189,248,0.1)]', icon: 'text-sky-400' },
gold: { tile: 'bg-amber-500/15 border-amber-500/30 shadow-[0_0_20px_rgba(245,158,11,0.1)]', icon: 'text-amber-400' },
purple: { tile: 'bg-violet-500/15 border-violet-500/30 shadow-[0_0_20px_rgba(139,92,246,0.1)]', icon: 'text-violet-400' },
slate: { tile: 'bg-slate-500/15 border-slate-500/30', icon: 'text-slate-300' },
};
function LinkCard({ link, variant }: { link: EcosystemLink; variant: 'grid' | 'list' }) {
const accent = link.accent ?? 'green';
const styles = ACCENT_STYLES[accent];
const isExternal = link.external || link.href.startsWith('http');
const useAnchor = ecosystemLinkUsesAnchor(link);
const isList = variant === 'list';
const className = [
'hub-link-card group flex no-underline text-inherit rounded-xl border border-white/10 bg-[var(--nasa-panel)]',
'hover:border-green-500/40 hover:bg-[var(--nasa-panel-elevated)] active:scale-[0.98] transition-all duration-150 shadow-sm',
isList
? 'hub-link-card--list flex-row items-center text-left gap-3 p-3 min-h-[60px]'
: 'flex-col items-center text-center gap-3 p-4 min-h-[148px] hover:-translate-y-0.5',
].join(' ');
const inner = (
<>
<span
className={`hub-link-card__icon inline-flex items-center justify-center shrink-0 rounded-2xl border ${styles.tile} ${styles.icon} ${
isList ? 'w-10 h-10' : 'w-14 h-14'
}`}
>
<NasaIcon name={link.icon} size={isList ? 20 : 26} />
</span>
<div className={`hub-link-card__text ${isList ? 'flex-1 min-w-0' : 'flex-1 w-full'}`}>
<div className={`flex items-center gap-1.5 ${isList ? '' : 'justify-center'}`}>
<h3 className="text-sm font-semibold text-[var(--nasa-text)]">{link.label}</h3>
{isExternal && <NasaIcon name="globe" size={11} className="text-[var(--nasa-muted)] opacity-70 shrink-0" />}
</div>
<p className={`text-xs text-[var(--nasa-muted)] leading-relaxed ${isList ? 'mt-0.5 truncate' : 'mt-1.5 line-clamp-2'}`}>
{link.description}
</p>
</div>
{!isList && (
<span className="hub-link-card__open inline-flex items-center gap-1 text-xs font-medium text-green-500/80 group-hover:text-green-400">
Open
<NasaIcon name="arrow-right" size={12} />
</span>
)}
{isList && <NasaIcon name="arrow-right" size={14} className="text-[var(--nasa-muted)] shrink-0" />}
</>
);
if (isExternal) {
return (
<a href={link.href} target="_blank" rel="noopener noreferrer" className={className}>
{inner}
</a>
);
}
if (useAnchor) {
return (
<a href={link.href} className={className}>
{inner}
</a>
);
}
return (
<Link to={link.href} className={className}>
{inner}
</Link>
);
}
export default function EcosystemLinkGrid({
title,
subtitle,
links,
columns = '3',
centered = true,
sectionIcon = 'link',
variant = 'grid',
hideHeader = false,
}: Props) {
const gridClass =
variant === 'list'
? 'grid grid-cols-1 gap-2'
: columns === '2'
? 'grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4'
: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4';
return (
<section className="mb-6 md:mb-10">
{!hideHeader && (
<div className={`mb-4 md:mb-5 hidden md:block${centered ? ' text-center' : ''}`}>
<h2 className="dash-section-title flex items-center justify-center gap-2 text-base">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-green-500/10 border border-green-500/25 text-green-400">
<NasaIcon name={sectionIcon} size={16} />
</span>
{title}
</h2>
{subtitle && <p className="dash-section-subtitle mt-1">{subtitle}</p>}
</div>
)}
<div className={gridClass}>
{links.map((link) => (
<LinkCard key={link.id} link={link} variant={variant} />
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
import { Link } from 'react-router-dom';
import NasaIcon from '../../components/icons/NasaIcon';
import { FEATURED_APP_LINKS, type EcosystemLink, type EcosystemLinkAccent } from '../../config/ecosystemLinks';
import { ecosystemLinkUsesAnchor } from '../../config/ecosystemLinkRouting';
const ACCENT_TILE: Record<EcosystemLinkAccent, string> = {
green: 'bg-green-500/15 border-green-500/30 text-green-400',
teal: 'bg-teal-500/15 border-teal-500/30 text-teal-400',
blue: 'bg-sky-500/15 border-sky-500/30 text-sky-400',
gold: 'bg-amber-500/15 border-amber-500/30 text-amber-400',
purple: 'bg-violet-500/15 border-violet-500/30 text-violet-400',
slate: 'bg-slate-500/15 border-slate-500/30 text-slate-300',
};
function FeaturedItem({ link }: { link: EcosystemLink }) {
const accent = link.accent ?? 'green';
const tile = ACCENT_TILE[accent];
const short = link.label.replace('DBIS ', '').replace('Digital ', '');
const className = 'dbis-featured__item';
const inner = (
<>
<span className={`dbis-featured__icon border ${tile}`}>
<NasaIcon name={link.icon} size={22} />
</span>
<span className="dbis-featured__name">{short}</span>
</>
);
if (ecosystemLinkUsesAnchor(link)) {
return (
<a href={link.href} className={className}>
{inner}
</a>
);
}
return (
<Link to={link.href} className={className}>
{inner}
</Link>
);
}
export default function FeaturedAppsStrip() {
return (
<section className="dbis-featured" aria-label="Quick access">
<p className="dbis-featured__label">Quick access</p>
<div className="dbis-featured__scroll">
{FEATURED_APP_LINKS.map((link) => (
<FeaturedItem key={link.id} link={link} />
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,116 @@
import { useEffect, useState } from 'react';
import { fetchOmnlChainRegistry, type ChainRegistryResponse } from '../../config/supported-chains';
import NasaIcon, { StatusIcon } from '../../components/icons/NasaIcon';
import DashboardSection from '../omnl-dashboard/DashboardSection';
/** Office 24 Central Bank — 128-bank corridor status (settlement + SWIFT + M2 rails). */
export default function Office24BankCorridors() {
const [registry, setRegistry] = useState<ChainRegistryResponse | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchOmnlChainRegistry()
.then(setRegistry)
.finally(() => setLoading(false));
}, []);
const chains = registry?.chains ?? [];
const active = chains.filter((c) => c.status === 'active');
const openCorridors = active.length;
return (
<DashboardSection
title="128-bank corridor network"
subtitle="Open settlement lines · SWIFT · HYBX · M2 load · bridge · swap per chain"
icon="globe"
className="mb-8 lg:col-span-3"
action={
<span className="nasa-badge nasa-badge--nominal">
{loading ? 'Loading…' : `${openCorridors} / ${registry?.maxCapacity ?? 128} corridors open`}
</span>
}
>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
<div className="rounded border border-green-500/30 bg-green-500/5 p-3 text-center">
<p className="text-2xl font-bold text-[#3dff8a]">{registry?.stats?.active ?? '—'}</p>
<p className="text-xs text-[#7a9bb8]">Active chains</p>
</div>
<div className="rounded border border-white/10 p-3 text-center">
<p className="text-2xl font-bold text-[#e8f4ff]">{registry?.primaryChainId ?? 138}</p>
<p className="text-xs text-[#7a9bb8]">Settlement hub</p>
</div>
<div className="rounded border border-white/10 p-3 text-center">
<p className="text-2xl font-bold text-[#e8f4ff]">{registry?.mirrorChainId ?? 651940}</p>
<p className="text-xs text-[#7a9bb8]">Mirror chain</p>
</div>
<div className="rounded border border-white/10 p-3 text-center">
<p className="text-2xl font-bold text-[#ffb347]">{registry?.stats?.staged ?? '—'}</p>
<p className="text-xs text-[#7a9bb8]">Staged</p>
</div>
</div>
<p className="text-xs text-[#7a9bb8] mb-3 flex items-center gap-2">
<NasaIcon name="rail" size={12} />
All active chains have full OMNL integration: Fineract posting, SWIFT MT102/MT103, trade finance (SBLC/BG/LC), M2
token load, and bridge/swap when enabled in registry.
</p>
<div className="overflow-x-auto max-h-[280px] overflow-y-auto rounded border border-white/10">
<table className="w-full text-sm nasa-table">
<thead className="sticky top-0">
<tr className="text-left">
<th className="py-2 px-2">Chain</th>
<th className="py-2 px-2">Network</th>
<th className="py-2 px-2">Corridor</th>
<th className="py-2 px-2">SWIFT</th>
<th className="py-2 px-2">Settlement</th>
<th className="py-2 px-2">M2</th>
<th className="py-2 px-2">Swap</th>
<th className="py-2 px-2">Bridge</th>
</tr>
</thead>
<tbody>
{chains.slice(0, 128).map((c) => {
const open = c.status === 'active';
return (
<tr key={c.chainId}>
<td className="py-1.5 px-2 font-mono text-[#4ade80]">{c.chainId}</td>
<td className="py-1.5 px-2 text-[#e8f4ff]">{c.name}</td>
<td className="py-1.5 px-2">
<span
className={`text-xs font-mono uppercase ${open ? 'text-[#3dff8a]' : 'text-[#7a9bb8]'}`}
>
{open ? 'OPEN' : c.status}
</span>
</td>
<td className="py-1.5 px-2">
<StatusIcon ok={open} />
</td>
<td className="py-1.5 px-2">
<StatusIcon ok={open} />
</td>
<td className="py-1.5 px-2">
<StatusIcon ok={open && !!c.settlement?.m2LoadEnabled} />
</td>
<td className="py-1.5 px-2">
<StatusIcon ok={!!c.settlement?.swapEnabled} />
</td>
<td className="py-1.5 px-2">
<StatusIcon ok={!!c.settlement?.bridgeEnabled} />
</td>
</tr>
);
})}
{!chains.length && !loading && (
<tr>
<td colSpan={8} className="py-6 text-center text-[#7a9bb8]">
Chain registry unavailable settlement API /chains
</td>
</tr>
)}
</tbody>
</table>
</div>
</DashboardSection>
);
}

View File

@@ -0,0 +1,65 @@
import { Link } from 'react-router-dom';
import { FEATURED_APP_LINKS, IN_APP_LINKS, OPERATOR_CONSOLE_LINKS, PUBLIC_PORTAL_LINKS } from '../../config/ecosystemLinks';
import { ecosystemLinkUsesAnchor } from '../../config/ecosystemLinkRouting';
import NasaIcon, { StatusIcon } from '../../components/icons/NasaIcon';
import DashboardSection from '../omnl-dashboard/DashboardSection';
const SUBJECT_GROUPS = [
{ title: 'Core banking apps', links: FEATURED_APP_LINKS },
{ title: 'Ecosystem subjects', links: IN_APP_LINKS.filter((l) => !FEATURED_APP_LINKS.some((f) => f.id === l.id)) },
{ title: 'Operator consoles', links: OPERATOR_CONSOLE_LINKS },
{ title: 'Production portals', links: PUBLIC_PORTAL_LINKS },
];
export default function Office24EcosystemSubjects() {
return (
<DashboardSection
title="Ecosystem integration"
subtitle="All DBIS OMNL subjects · live corridors from Office 24 Central Bank"
icon="network"
className="mb-8"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{SUBJECT_GROUPS.map((group) => (
<div key={group.title}>
<h3 className="text-sm font-semibold text-[#e8f4ff] mb-3 flex items-center gap-2">
<NasaIcon name="check" size={12} className="nasa-icon--telemetry" />
{group.title}
</h3>
<ul className="space-y-2">
{group.links.map((link) => (
<li key={link.id}>
{ecosystemLinkUsesAnchor(link) ? (
<a
href={link.href}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noreferrer' : undefined}
className="flex items-center justify-between gap-2 rounded border border-white/10 px-3 py-2 hover:border-green-500/40 text-sm"
>
<span className="flex items-center gap-2 text-[#e8f4ff]">
<NasaIcon name={link.icon} size={14} />
{link.label}
</span>
<StatusIcon ok />
</a>
) : (
<Link
to={link.href}
className="flex items-center justify-between gap-2 rounded border border-white/10 px-3 py-2 hover:border-green-500/40 text-sm"
>
<span className="flex items-center gap-2 text-[#e8f4ff]">
<NasaIcon name={link.icon} size={14} />
{link.label}
</span>
<StatusIcon ok />
</Link>
)}
</li>
))}
</ul>
</div>
))}
</div>
</DashboardSection>
);
}

View File

@@ -0,0 +1,498 @@
/**
* App dashboard — standard dark UI (replaces NASA mission-control styling).
* Class names kept for compatibility; visuals use normal Inter typography and rounded cards.
*/
.omnl-app,
.app-shell,
.nasa-mission-root {
--nasa-bg: #141a16;
--nasa-bg-deep: #0f1412;
--nasa-panel: #1e2329;
--nasa-panel-elevated: #252830;
--nasa-panel-border: rgba(255, 255, 255, 0.1);
--nasa-telemetry: #22c55e;
--nasa-nominal: #22c55e;
--nasa-caution: #f59e0b;
--nasa-critical: #ef4444;
--nasa-text: #eaecef;
--nasa-muted: #848e9c;
--nasa-mono: Inter, system-ui, sans-serif;
}
html:has(.omnl-app),
html:has(.omnl-app) body {
background: var(--nasa-bg);
min-height: 100%;
}
.omnl-app {
background: var(--nasa-bg);
color: var(--nasa-text);
}
.nasa-icon--telemetry { color: var(--nasa-telemetry); }
.nasa-icon--nominal { color: var(--nasa-nominal); }
.nasa-icon--critical { color: var(--nasa-critical); }
.nasa-icon--caution { color: var(--nasa-caution); }
.nasa-icon--spin { animation: nasa-spin 1s linear infinite; }
@keyframes nasa-spin {
to { transform: rotate(360deg); }
}
.dash-page, .o24-page, .nasa-page, .cb-page {
min-height: calc(100vh - 56px);
font-family: Inter, system-ui, sans-serif;
}
.nasa-panel, .dash-card, .o24-card, .cb-card {
background: var(--nasa-panel);
border: 1px solid var(--nasa-panel-border);
border-radius: 12px;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.nasa-header {
border: none;
background: transparent;
padding: 0 0 1.5rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--nasa-panel-border);
}
.nasa-header__meta {
display: none;
}
.nasa-header__body {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
.nasa-header--centered .nasa-header__body {
justify-content: center;
align-items: center;
text-align: center;
}
.nasa-header--centered .nasa-header__title-row {
flex-direction: column;
align-items: center;
}
.nasa-header--centered .dash-page-subtitle {
margin-left: auto;
margin-right: auto;
}
.app-page-shell .dash-section-subtitle,
.hub-page .dash-section-subtitle {
margin-left: auto;
margin-right: auto;
}
.app-page-shell {
width: 100%;
max-width: 1100px;
margin-left: auto;
margin-right: auto;
}
.app-page-shell .dash-card,
.hub-page .dash-card {
width: 100%;
}
.nasa-header__title-row {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.nasa-header__icon-wrap {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
border: 1px solid var(--nasa-panel-border);
background: var(--nasa-panel-elevated);
color: var(--nasa-telemetry);
flex-shrink: 0;
}
.nasa-title, .dash-page-title {
font-family: Inter, system-ui, sans-serif;
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
text-transform: none;
color: var(--nasa-text);
line-height: 1.25;
}
.dash-page-subtitle {
margin-top: 0.35rem;
color: var(--nasa-muted);
font-size: 0.95rem;
max-width: 48rem;
line-height: 1.5;
}
.nasa-badge, .dash-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.65rem;
border-radius: 6px;
font-family: Inter, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
}
.nasa-badge--telemetry, .dash-badge--blue {
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.25);
}
.nasa-badge--nominal, .dash-badge--green {
background: rgba(34, 197, 94, 0.12);
color: #4ade80;
border: 1px solid rgba(34, 197, 94, 0.25);
}
.nasa-badge--caution, .dash-badge--gold {
background: rgba(245, 158, 11, 0.12);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.25);
}
.dash-banner {
border-radius: 10px;
padding: 1rem 1.15rem;
border: 1px solid var(--nasa-panel-border);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.75rem;
}
.dash-banner__title {
font-size: 0.95rem;
font-weight: 600;
font-family: Inter, system-ui, sans-serif;
letter-spacing: normal;
text-transform: none;
margin-bottom: 0.25rem;
}
.dash-banner__detail {
font-size: 0.875rem;
color: var(--nasa-muted);
line-height: 1.5;
}
.dash-banner--ok {
background: rgba(34, 197, 94, 0.08);
border-color: rgba(34, 197, 94, 0.25);
}
.dash-banner--ok .dash-banner__title { color: #4ade80; }
.dash-banner--warn {
background: rgba(245, 158, 11, 0.08);
border-color: rgba(245, 158, 11, 0.25);
}
.dash-banner--warn .dash-banner__title { color: #fbbf24; }
.dash-banner--error {
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.25);
}
.dash-banner--error .dash-banner__title { color: #f87171; }
.dash-banner--loading {
background: var(--nasa-panel);
}
.dash-stat-label {
font-family: Inter, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 500;
color: var(--nasa-muted);
text-transform: none;
letter-spacing: normal;
margin-bottom: 0.35rem;
text-align: center;
}
.dash-stat-value {
font-family: Inter, system-ui, sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--nasa-text);
font-variant-numeric: tabular-nums;
text-align: center;
}
.dash-stat-hint {
margin-top: 0.35rem;
font-size: 0.8rem;
color: var(--nasa-muted);
text-align: center;
}
.dash-section-title {
font-family: Inter, system-ui, sans-serif;
font-size: 1rem;
font-weight: 600;
letter-spacing: normal;
text-transform: none;
color: var(--nasa-text);
}
.dash-section-subtitle {
margin-top: 0.25rem;
font-size: 0.875rem;
color: var(--nasa-muted);
text-align: center;
}
.dash-kv {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.5rem 0;
font-size: 0.875rem;
border-bottom: 1px solid var(--nasa-panel-border);
}
.dash-kv:last-child { border-bottom: none; }
.dash-kv dt { color: var(--nasa-muted); flex-shrink: 0; }
.dash-kv dd { color: var(--nasa-text); text-align: right; word-break: break-word; }
.dash-kv .dash-kv-value--ok { color: #4ade80; font-weight: 500; }
.dash-quick-links__label {
font-family: Inter, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 500;
text-transform: none;
letter-spacing: normal;
color: var(--nasa-muted);
margin-bottom: 0.5rem;
}
.dash-quick-links__chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.85rem;
font-family: Inter, system-ui, sans-serif;
font-size: 0.8rem;
letter-spacing: normal;
color: var(--nasa-text);
background: var(--nasa-panel-elevated);
border: 1px solid var(--nasa-panel-border);
border-radius: 999px;
transition: background 0.15s, border-color 0.15s;
}
.dash-quick-links__chip:hover {
border-color: var(--nasa-telemetry);
color: var(--nasa-telemetry);
background: rgba(34, 197, 94, 0.08);
}
.dash-btn, .nasa-btn {
padding: 0.55rem 1rem;
border: 1px solid var(--nasa-panel-border);
border-radius: 8px;
font-family: Inter, system-ui, sans-serif;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
background: var(--nasa-panel-elevated);
color: var(--nasa-text);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
transition: background 0.15s, border-color 0.15s;
}
.dash-btn:hover, .nasa-btn:hover {
border-color: rgba(34, 197, 94, 0.4);
background: rgba(34, 197, 94, 0.08);
}
.nasa-btn--primary {
background: #16a34a;
border-color: #16a34a;
color: #fff;
}
.nasa-btn--primary:hover {
background: #22c55e;
border-color: #22c55e;
color: #fff;
}
.dash-btn:disabled, .nasa-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.nasa-input, .nasa-select {
width: 100%;
min-height: 44px;
padding: 0.5rem 0.75rem;
background: var(--nasa-panel-elevated);
border: 1px solid var(--nasa-panel-border);
border-radius: 8px;
color: var(--nasa-text);
font-family: Inter, system-ui, sans-serif;
font-size: 0.9rem;
}
.nasa-input:focus, .nasa-select:focus {
outline: none;
border-color: var(--nasa-telemetry);
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
.nasa-inset-panel {
background: var(--nasa-panel-elevated);
border: 1px solid var(--nasa-panel-border);
border-radius: 8px;
padding: 1rem;
}
.omnl-app-header {
background: #1a241c !important;
border-color: rgba(34, 197, 94, 0.2) !important;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
}
.nasa-nav-brand {
font-family: Inter, system-ui, sans-serif;
letter-spacing: normal;
text-transform: none;
color: var(--nasa-text) !important;
font-weight: 600;
}
.nasa-nav-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.75rem;
font-family: Inter, system-ui, sans-serif;
font-size: 0.875rem;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
color: var(--nasa-muted);
border-radius: 8px;
border: 1px solid transparent;
transition: color 0.15s, background 0.15s;
}
.nasa-nav-link:hover {
color: var(--nasa-text);
background: rgba(255, 255, 255, 0.05);
}
.nasa-nav-link--active {
color: #fff !important;
background: #16a34a !important;
border-color: transparent;
}
.nasa-table {
width: 100%;
border-collapse: collapse;
}
.nasa-table thead {
background: var(--nasa-panel-elevated);
font-family: Inter, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: normal;
text-transform: none;
color: var(--nasa-muted);
}
.nasa-table tbody tr { border-top: 1px solid var(--nasa-panel-border); }
.nasa-table tbody tr:hover { background: rgba(255, 255, 255, 0.03); }
.trade-layout {
display: grid;
grid-template-columns: 240px 1fr;
min-height: calc(100vh - 56px);
background: var(--nasa-bg);
}
.trade-markets, .trade-panel, .trade-pair-header, .trade-recent, .trade-book {
background: var(--nasa-panel) !important;
border-color: var(--nasa-panel-border) !important;
}
.trade-center {
min-height: 0;
background: var(--nasa-bg);
}
.trade-chart {
background: var(--nasa-panel) !important;
border-color: var(--nasa-panel-border) !important;
}
.trade-chart__canvas {
min-height: 240px;
border-radius: 8px;
border: 1px solid var(--nasa-panel-border);
background: var(--nasa-panel-elevated);
}
.trade-market-row:hover { background: rgba(255, 255, 255, 0.04); }
.trade-market-row--active {
background: rgba(34, 197, 94, 0.1);
border-left: 2px solid var(--nasa-telemetry) !important;
}
.trade-orderbook { max-height: 360px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.8rem; }
@media (max-width: 1023px) {
.trade-layout {
display: flex;
flex-direction: column;
min-height: calc(100dvh - 56px);
}
.trade-center { order: 2; }
.trade-panel {
order: 3;
width: 100% !important;
max-width: 100%;
border-left: none !important;
border-top: 1px solid var(--nasa-panel-border);
}
.trade-markets { display: none !important; }
}
@media (min-width: 1024px) {
.trade-layout { grid-template-columns: 240px 1fr 320px; }
}

View File

@@ -0,0 +1,369 @@
/**
* DBIS Dashboard — mobile-first hub styling
*/
.dbis-dashboard {
background:
radial-gradient(ellipse 120% 80% at 50% -20%, rgba(34, 197, 94, 0.12), transparent 55%),
var(--nasa-bg, #141a16);
}
.dbis-dashboard__inner {
width: 100%;
max-width: 56rem;
margin: 0 auto;
padding: 1rem 0.75rem 5.5rem;
}
@media (min-width: 640px) {
.dbis-dashboard__inner {
padding: 1.5rem 1rem 2rem;
}
}
.dbis-dashboard__hero {
text-align: center;
margin-bottom: 1.5rem;
}
@media (min-width: 640px) {
.dbis-dashboard__hero {
margin-bottom: 2rem;
}
}
.dbis-dashboard__logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 1rem;
background: linear-gradient(145deg, #16a34a, #065f46);
color: #fff;
border: 1px solid rgba(74, 222, 128, 0.35);
box-shadow: 0 8px 32px rgba(22, 163, 74, 0.35);
margin-bottom: 0.75rem;
}
@media (min-width: 640px) {
.dbis-dashboard__logo {
width: 4.5rem;
height: 4.5rem;
border-radius: 1.125rem;
}
}
.dbis-dashboard__eyebrow {
font-size: 0.65rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #22c55e;
margin-bottom: 0.35rem;
}
.dbis-dashboard__title {
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.03em;
color: var(--nasa-text, #eaecef);
line-height: 1.15;
}
@media (min-width: 640px) {
.dbis-dashboard__title {
font-size: 2.25rem;
}
}
.dbis-dashboard__subtitle {
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--nasa-muted, #848e9c);
max-width: 28rem;
margin-left: auto;
margin-right: auto;
line-height: 1.5;
}
.dbis-status-pills {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.dbis-status-pill {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.65rem;
border-radius: 999px;
font-size: 0.7rem;
font-weight: 500;
border: 1px solid rgba(34, 197, 94, 0.25);
background: rgba(34, 197, 94, 0.08);
color: #4ade80;
}
/* Featured horizontal strip */
.dbis-featured {
margin-bottom: 1.5rem;
}
.dbis-featured__label {
font-size: 0.75rem;
font-weight: 600;
color: var(--nasa-muted);
margin-bottom: 0.65rem;
padding-left: 0.15rem;
}
.dbis-featured__scroll {
display: flex;
gap: 0.65rem;
overflow-x: auto;
padding-bottom: 0.35rem;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.dbis-featured__scroll::-webkit-scrollbar {
display: none;
}
.dbis-featured__item {
flex: 0 0 auto;
scroll-snap-align: start;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
min-width: 5.25rem;
max-width: 5.5rem;
padding: 0.75rem 0.5rem;
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--nasa-panel);
text-decoration: none;
color: inherit;
transition: border-color 0.2s, transform 0.2s, background 0.2s;
min-height: 5.5rem;
}
.dbis-featured__item:active {
transform: scale(0.97);
}
.dbis-featured__item:hover {
border-color: rgba(34, 197, 94, 0.4);
background: var(--nasa-panel-elevated);
}
.dbis-featured__icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.875rem;
border: 1px solid transparent;
}
.dbis-featured__name {
font-size: 0.65rem;
font-weight: 600;
text-align: center;
line-height: 1.2;
color: var(--nasa-text);
}
@media (min-width: 768px) {
.dbis-featured__scroll {
display: grid;
grid-template-columns: repeat(5, 1fr);
overflow: visible;
gap: 0.75rem;
}
.dbis-featured__item {
min-width: 0;
max-width: none;
}
}
/* Collapsible sections on mobile */
.dbis-section-fold {
margin-bottom: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 0.875rem;
background: rgba(30, 35, 41, 0.5);
overflow: hidden;
}
.dbis-section-fold__summary {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.875rem 1rem;
cursor: pointer;
list-style: none;
min-height: 3rem;
font-weight: 600;
font-size: 0.9rem;
color: var(--nasa-text);
user-select: none;
}
.dbis-section-fold__summary::-webkit-details-marker {
display: none;
}
.dbis-section-fold__chevron {
margin-left: auto;
color: var(--nasa-muted);
transition: transform 0.2s;
}
.dbis-section-fold[open] .dbis-section-fold__chevron {
transform: rotate(180deg);
}
.dbis-section-fold__body {
padding: 0 0.75rem 0.875rem;
}
@media (min-width: 768px) {
.dbis-section-fold {
border: none;
background: transparent;
border-radius: 0;
margin-bottom: 0;
}
.dbis-section-fold__summary {
display: none;
}
.dbis-section-fold__body {
padding: 0;
}
}
/* Link cards */
.hub-link-card {
min-height: 8.5rem;
touch-action: manipulation;
}
@media (max-width: 639px) {
.hub-link-card {
min-height: 7.5rem;
padding: 0.875rem;
}
.hub-link-card__icon {
width: 3rem !important;
height: 3rem !important;
border-radius: 0.75rem !important;
}
}
.hub-link-card--list {
flex-direction: row !important;
align-items: center !important;
text-align: left !important;
min-height: 3.75rem !important;
padding: 0.75rem 1rem !important;
gap: 0.75rem !important;
}
.hub-link-card--list .hub-link-card__icon {
width: 2.5rem !important;
height: 2.5rem !important;
flex-shrink: 0;
}
.hub-link-card--list .hub-link-card__text {
flex: 1;
min-width: 0;
}
.hub-link-card--list .hub-link-card__open {
display: none;
}
.hub-link-card--list h3 {
text-align: left;
}
.hub-link-card--list p {
text-align: left;
margin-top: 0.15rem !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Mobile bottom nav */
.dbis-bottom-nav {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 60;
display: flex;
justify-content: space-around;
align-items: stretch;
min-height: 3.5rem;
padding-bottom: env(safe-area-inset-bottom, 0);
background: rgba(26, 36, 28, 0.96);
border-top: 1px solid rgba(34, 197, 94, 0.2);
backdrop-filter: blur(12px);
}
@media (min-width: 768px) {
.dbis-bottom-nav {
display: none;
}
}
.dbis-bottom-nav__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.15rem;
padding: 0.4rem 0.25rem;
min-height: 3.5rem;
font-size: 0.6rem;
font-weight: 500;
color: #848e9c;
text-decoration: none;
border: none;
background: transparent;
cursor: pointer;
touch-action: manipulation;
}
.dbis-bottom-nav__item--active {
color: #4ade80;
}
.dbis-bottom-nav__item:active {
background: rgba(255, 255, 255, 0.05);
}
.dbis-has-bottom-nav {
padding-bottom: calc(3.75rem + env(safe-area-inset-bottom, 0));
}
@media (min-width: 768px) {
.dbis-has-bottom-nav {
padding-bottom: 0;
}
}

View File

@@ -0,0 +1,13 @@
@layer components {
.ui-btn-primary {
@apply bg-green-600 text-white font-medium rounded-xl hover:bg-green-500 transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 focus:ring-offset-[#141a16] min-h-[44px] px-4 py-2;
}
.ui-btn-secondary {
@apply bg-[#1a241c] text-white font-medium rounded-xl border border-green-500/30 hover:bg-green-900/30 transition-colors min-h-[44px] px-4 py-2;
}
.ui-btn-icon {
@apply p-2 rounded-lg text-green-400 hover:bg-green-500/10 min-h-[44px] min-w-[44px];
}
}

View File

@@ -0,0 +1,8 @@
(function () {
/** auto → same-origin /api/v1 (vite proxy, portal-serve, or token-aggregation). */
window.resolveOmnlApiBase = function () {
const meta = document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content')?.trim();
if (meta && meta !== 'auto') return meta.replace(/\/$/, '');
return '/api/v1';
};
})();