feat(wallet): model RabbyHub MVP and modern dashboard
test / workspace (push) Successful in 13m26s

This commit is contained in:
Tom You
2026-07-08 23:41:07 -05:00
parent eb8540bb71
commit 480d686c41
7 changed files with 1243 additions and 45 deletions
+128 -14
View File
@@ -1,15 +1,129 @@
async function loadFeatures(){
const fallback = [
["Terminal", "VT-style surface, tabs, split panes, Unicode"],
["Connections", "Local shell, SSH, Telnet, Serial profile model"],
["UX", "Command palette, shortcuts, quake window, restored sessions"],
["Footprint", "Rust core + Tauri shell instead of Electron"]
];
let rows = fallback;
try {
const { invoke } = window.__TAURI__.core;
rows = (await invoke('workspace_summary')).map(x => [x.feature, x.detail]);
} catch (_) {}
document.getElementById('features').innerHTML = rows.map(([f,d]) => `<li><b>${f}</b>: ${d}</li>`).join('');
const fallbackDashboard = {
active_account: {
name: 'Main Wallet',
address: '0x1111111111111111111111111111111111111111',
source: 'SeedPhrase'
},
chains: [
{ id: 1, name: 'Ethereum' },
{ id: 8453, name: 'Base' }
],
balances: [
{ chain_id: 1, symbol: 'ETH', amount: 1.24, usd_value: 4200 },
{ chain_id: 8453, symbol: 'USDC', amount: 1280, usd_value: 1280 }
],
approvals: [
{ chain_id: 1, asset_symbol: 'USDC', spender: '0x2222222222222222222222222222222222222222', allowance: 'unlimited', risk: 'High' }
],
activities: [
{ label: 'Swap ETH → USDC', status: 'Confirmed', chain_id: 1 }
],
settings: {
theme: 'System',
language: 'en',
currency: 'USD',
auto_lock_minutes: 15,
prefer_rabby_over_metamask: true
}
};
const fallbackFeatures = [
['Create/unlock wallet', 'Password, lock/unlock, encrypted local state', 'Modeled'],
['Import/create accounts', 'Seed phrase, private key, JSON, watch-only', 'Modeled'],
['Multi-chain networks', 'Built-in, custom RPC, testnet, offline chain flags', 'Modeled'],
['Portfolio dashboard', 'Balances, NFTs, DeFi positions, activity', 'UiPrototype'],
['Dapp provider and permissions', 'Origin/account/chain permissions', 'Modeled'],
['Signing and security previews', 'Typed data/message/transaction previews and risk', 'Modeled'],
['Settings and customization', 'Theme, language, currency, auto-lock, default wallet mode', 'UiPrototype']
];
function money(value) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value || 0);
}
loadFeatures();
function shortAddress(address) {
if (!address || address.length < 12) return address || '';
return `${address.slice(0, 6)}${address.slice(-4)}`;
}
function statusLabel(value) {
if (typeof value === 'string') return value;
if (value && typeof value === 'object') return Object.keys(value)[0] || 'Modeled';
return 'Modeled';
}
async function invokeOrFallback(command, fallback) {
try {
const tauri = window.__TAURI__?.core;
if (!tauri) return fallback;
return await tauri.invoke(command);
} catch (_) {
return fallback;
}
}
function renderDashboard(dashboard) {
const total = dashboard.balances.reduce((sum, token) => sum + token.usd_value, 0);
const highRisk = dashboard.approvals.filter((approval) => ['High', 'Critical'].includes(statusLabel(approval.risk))).length;
document.getElementById('account-address').textContent = shortAddress(dashboard.active_account.address);
document.getElementById('portfolio-total').textContent = money(total);
document.getElementById('hero-total').textContent = money(total);
document.getElementById('chain-count').textContent = dashboard.chains.length;
document.getElementById('risk-count').textContent = highRisk;
document.getElementById('approval-risk').textContent = highRisk;
document.getElementById('token-list').innerHTML = dashboard.balances.map((token) => `
<div class="asset-row">
<div class="coin-mark">${token.symbol.slice(0, 1)}</div>
<div><strong>${token.symbol}</strong><span>Chain ${token.chain_id}</span></div>
<div class="asset-value"><strong>${money(token.usd_value)}</strong><span>${token.amount}</span></div>
</div>
`).join('');
document.getElementById('approval-list').innerHTML = dashboard.approvals.map((approval) => `
<div class="approval-row">
<div><strong>${approval.asset_symbol} allowance</strong><span>${shortAddress(approval.spender)}</span></div>
<span class="risk ${statusLabel(approval.risk).toLowerCase()}">${statusLabel(approval.risk)}</span>
</div>
`).join('');
document.getElementById('activity-list').innerHTML = dashboard.activities.map((activity) => `
<div class="activity-row"><span>↗</span><div><strong>${activity.label}</strong><small>${statusLabel(activity.status)} · Chain ${activity.chain_id}</small></div></div>
`).join('');
const settings = dashboard.settings;
document.getElementById('settings-summary').innerHTML = [
['Theme', statusLabel(settings.theme)],
['Language', settings.language],
['Currency', settings.currency],
['Auto-lock', `${settings.auto_lock_minutes} min`],
['Default wallet', settings.prefer_rabby_over_metamask ? 'Rabby' : 'Browser default']
].map(([label, value]) => `<div><span>${label}</span><strong>${value}</strong></div>`).join('');
}
function renderFeatures(features) {
document.getElementById('wallet-features').innerHTML = features.map((feature) => {
const title = Array.isArray(feature) ? feature[0] : feature.feature;
const acceptance = Array.isArray(feature) ? feature[1] : feature.acceptance;
const status = Array.isArray(feature) ? feature[2] : statusLabel(feature.status);
return `
<article class="feature-card">
<span class="feature-status">${status}</span>
<strong>${title}</strong>
<p>${acceptance}</p>
</article>
`;
}).join('');
}
async function boot() {
const [dashboard, features] = await Promise.all([
invokeOrFallback('wallet_dashboard', fallbackDashboard),
invokeOrFallback('wallet_mvp_summary', fallbackFeatures)
]);
renderDashboard(dashboard);
renderFeatures(features);
}
boot();
+127 -28
View File
@@ -7,35 +7,134 @@
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<aside class="sidebar">
<div class="brand"><span class="logo"></span><div><strong>Rabby</strong><small>Rust + Tauri terminal</small></div></div>
<button class="primary">+ New tab</button>
<nav>
<a class="active">⌘ Local Shell</a>
<a>🔐 SSH: production</a>
<a>☎ Telnet lab</a>
<a>🔌 Serial /dev/ttyUSB0</a>
</nav>
<section><h3>Features</h3><ul id="features"><li>Loading Rust backend…</li></ul></section>
</aside>
<main>
<header class="tabs">
<div class="tab active">Local Shell <span></span></div><div class="tab">Prod SSH</div><div class="spacer"></div><button>Command palette</button><button>Quake mode</button>
</header>
<section class="workspace">
<div class="pane large"><div class="chrome">bash — rabby-core tests</div><pre>$ cargo test -p rabby-core
running 6 tests
test slug_normalizes_profile_ids ... ok
test profile_validation_rejects_empty_command ... ok
test split_panes_count_nested_leaves ... ok
test split_ratio_has_sane_bounds ... ok
test workspace_adds_tabs_for_known_profiles ... ok
test feature_matrix_covers_tabby_readme_capabilities ... ok
<div class="app-shell">
<aside class="rail" aria-label="Primary navigation">
<div class="brand-mark" aria-label="Rabby">R</div>
<button class="rail-icon active" title="Dashboard"></button>
<button class="rail-icon" title="Approvals"></button>
<button class="rail-icon" title="Activity"></button>
<button class="rail-icon" title="Settings"></button>
</aside>
Rabby keeps Tabby-style UX while moving the app shell to Rust/Tauri.</pre></div>
<div class="pane"><div class="chrome">profile inspector</div><div class="cards"><article><b>Session restore</b><p>Tabs and split tree are represented in Rust.</p></article><article><b>Small footprint target</b><p>No Electron runtime; static HTML/CSS plus Tauri backend.</p></article></div></div>
</section>
</main>
<aside class="wallet-panel">
<header class="brand-row">
<div>
<p class="eyebrow">Rabby Wallet MVP</p>
<h1>Clean multi-chain wallet</h1>
</div>
<span class="status-dot">Modeled</span>
</header>
<section class="account-card">
<div class="account-topline">
<span>Main Wallet</span>
<button class="ghost-button">•••</button>
</div>
<strong id="account-address">0x1111…1111</strong>
<p>Seed phrase · Ethereum + Base</p>
<div class="account-actions">
<button>Send</button>
<button>Receive</button>
<button>Swap</button>
</div>
</section>
<nav class="side-list">
<a class="active"><span>Portfolio</span><b id="portfolio-total">$0.00</b></a>
<a><span>Dapp permissions</span><b id="dapp-count">MVP</b></a>
<a><span>Approvals at risk</span><b id="risk-count">0</b></a>
<a><span>Customization</span><b>Theme</b></a>
</nav>
</aside>
<main class="content">
<header class="topbar">
<div>
<p class="eyebrow">RabbyHub/Rabby inspired UI</p>
<h2>Portfolio dashboard</h2>
</div>
<div class="top-actions">
<button class="secondary">Lock</button>
<button class="primary">Connect dapp</button>
</div>
</header>
<section class="hero-grid">
<article class="balance-card">
<p>Total balance</p>
<strong id="hero-total">$0.00</strong>
<span>Across modeled token balances</span>
</article>
<article class="metric-card">
<p>Chains</p>
<strong id="chain-count">0</strong>
<span>Custom RPC ready</span>
</article>
<article class="metric-card warning">
<p>High-risk approvals</p>
<strong id="approval-risk">0</strong>
<span>Review before signing</span>
</article>
</section>
<section class="two-column">
<article class="panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Assets</p>
<h3>Token balances</h3>
</div>
<button class="ghost-button">Refresh</button>
</div>
<div id="token-list" class="asset-list"></div>
</article>
<article class="panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Security</p>
<h3>Approvals</h3>
</div>
<button class="ghost-button">Revoke</button>
</div>
<div id="approval-list" class="approval-list"></div>
</article>
</section>
<section class="two-column lower">
<article class="panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Activity</p>
<h3>Recent wallet events</h3>
</div>
</div>
<div id="activity-list" class="activity-list"></div>
</article>
<article class="panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Settings</p>
<h3>Customization</h3>
</div>
</div>
<div id="settings-summary" class="settings-grid"></div>
</article>
</section>
<section class="panel feature-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">Feature record</p>
<h3>RabbyHub MVP coverage</h3>
</div>
<a class="doc-link" href="../docs/rabbyhub-mvp-feature-inventory.md">docs/rabbyhub-mvp-feature-inventory.md</a>
</div>
<div id="wallet-features" class="feature-grid"></div>
</section>
</main>
</div>
<script src="app.js"></script>
</body>
</html>
+280 -1
View File
@@ -1 +1,280 @@
:root{font-family:Inter,ui-sans-serif,system-ui;color:#dce7ff;background:#090d18}*{box-sizing:border-box}body{margin:0;display:flex;min-height:100vh;background:radial-gradient(circle at 70% 10%,#203d7a55,transparent 38%),#090d18}.sidebar{width:290px;padding:22px;border-right:1px solid #263149;background:#0d1324cc;backdrop-filter:blur(14px)}.brand{display:flex;gap:12px;align-items:center;margin-bottom:24px}.logo{display:grid;place-items:center;width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,#7c5cff,#00d4ff);box-shadow:0 0 34px #3b82f6}.brand small{display:block;color:#8ea0c5}.primary,button{border:1px solid #34425f;background:#141d33;color:#e8f0ff;border-radius:10px;padding:10px 13px}button.primary{width:100%;background:linear-gradient(135deg,#5d5fef,#16c5d9);border:0;font-weight:700}nav{display:grid;gap:8px;margin:22px 0}nav a{padding:11px 12px;border-radius:10px;color:#aebfe4}.active,nav a:hover{background:#1d2a49;color:white}h3{font-size:12px;text-transform:uppercase;letter-spacing:.14em;color:#6f82aa}ul{padding-left:20px;color:#aebfe4;line-height:1.5}main{flex:1;display:flex;flex-direction:column}.tabs{height:62px;display:flex;gap:8px;align-items:center;padding:0 18px;border-bottom:1px solid #263149;background:#0b1020}.tab{padding:12px 16px;border-radius:12px 12px 0 0;background:#10182d;color:#aebfe4}.tab.active{background:#18233d;color:#fff}.tab span{color:#20e3b2}.spacer{flex:1}.workspace{flex:1;display:grid;grid-template-columns:1.35fr .65fr;gap:16px;padding:18px}.pane{border:1px solid #273753;border-radius:18px;background:#0c1222;overflow:hidden;box-shadow:0 24px 70px #0008}.chrome{height:42px;display:flex;align-items:center;padding:0 16px;background:#111a30;border-bottom:1px solid #273753;color:#8fa4d0;font-size:13px}pre{margin:0;padding:22px;color:#8df7c8;font:15px/1.6 'SFMono-Regular',Consolas,monospace}.cards{display:grid;gap:14px;padding:16px}article{padding:16px;border-radius:14px;background:#121d34;border:1px solid #283957}article p{color:#9fb0d4}
:root {
color: #19213d;
background: #eef3ff;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 12% 8%, rgba(112, 139, 255, 0.26), transparent 26%),
radial-gradient(circle at 88% 14%, rgba(85, 213, 190, 0.24), transparent 30%),
linear-gradient(135deg, #f8fbff 0%, #edf3ff 48%, #e8f1ff 100%);
}
button, a { font: inherit; }
button { cursor: pointer; }
.app-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 76px 320px minmax(0, 1fr);
}
.rail {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
padding: 22px 12px;
background: #ffffffcc;
border-right: 1px solid rgba(111, 130, 180, 0.18);
backdrop-filter: blur(18px);
}
.brand-mark {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 16px;
color: white;
font-weight: 900;
letter-spacing: -0.05em;
background: linear-gradient(135deg, #7b61ff, #36d2b6);
box-shadow: 0 18px 42px rgba(90, 99, 255, 0.28);
}
.rail-icon {
width: 44px;
height: 44px;
border: 0;
border-radius: 15px;
color: #72809f;
background: transparent;
}
.rail-icon.active,
.rail-icon:hover {
color: #3154ff;
background: #eef2ff;
}
.wallet-panel {
padding: 26px;
background: rgba(255, 255, 255, 0.72);
border-right: 1px solid rgba(111, 130, 180, 0.18);
backdrop-filter: blur(22px);
}
.brand-row,
.topbar,
.panel-heading,
.account-topline,
.top-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.eyebrow {
margin: 0 0 6px;
color: #7a88a8;
text-transform: uppercase;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.14em;
}
h1, h2, h3, p { margin-top: 0; }
h1 { margin-bottom: 0; font-size: 24px; line-height: 1.1; }
h2 { margin-bottom: 0; font-size: 30px; letter-spacing: -0.04em; }
h3 { margin-bottom: 0; font-size: 18px; }
.status-dot,
.feature-status,
.risk {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
}
.status-dot,
.feature-status { color: #3154ff; background: #edf1ff; }
.account-card,
.panel,
.balance-card,
.metric-card {
border: 1px solid rgba(111, 130, 180, 0.18);
border-radius: 28px;
background: rgba(255, 255, 255, 0.82);
box-shadow: 0 20px 60px rgba(65, 78, 128, 0.12);
}
.account-card {
margin: 24px 0;
padding: 22px;
background: linear-gradient(145deg, #22305b, #11172c);
color: white;
box-shadow: 0 22px 60px rgba(24, 35, 78, 0.28);
}
.account-card p { color: #aebbe2; }
.account-card strong { font-size: 21px; }
.account-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 18px;
}
.account-actions button,
.primary,
.secondary,
.ghost-button {
border: 0;
border-radius: 14px;
padding: 10px 14px;
font-weight: 800;
}
.account-actions button,
.primary {
color: white;
background: linear-gradient(135deg, #6f64ff, #35ceb4);
}
.secondary { color: #3154ff; background: #eef2ff; }
.ghost-button { color: #647194; background: #f3f6ff; }
.account-card .ghost-button { color: white; background: rgba(255, 255, 255, 0.12); }
.side-list { display: grid; gap: 10px; }
.side-list a {
display: flex;
justify-content: space-between;
padding: 14px 16px;
color: #62708f;
text-decoration: none;
border-radius: 16px;
}
.side-list a.active,
.side-list a:hover { color: #24345f; background: #f2f5ff; }
.content {
min-width: 0;
padding: 30px;
overflow: auto;
}
.topbar { margin-bottom: 24px; }
.hero-grid {
display: grid;
grid-template-columns: 1.5fr repeat(2, minmax(180px, 0.7fr));
gap: 16px;
margin-bottom: 16px;
}
.balance-card,
.metric-card,
.panel { padding: 22px; }
.balance-card { color: white; background: linear-gradient(145deg, #4e64ff, #8c62ff); }
.balance-card p,
.balance-card span { color: rgba(255, 255, 255, 0.76); }
.balance-card strong { display: block; font-size: 42px; letter-spacing: -0.05em; }
.metric-card strong { display: block; font-size: 36px; letter-spacing: -0.04em; }
.metric-card p,
.metric-card span { color: #7a88a8; }
.metric-card.warning strong { color: #ff7a45; }
.two-column {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
gap: 16px;
margin-bottom: 16px;
}
.asset-list,
.approval-list,
.activity-list,
.settings-grid,
.feature-grid { margin-top: 18px; }
.asset-row,
.approval-row,
.activity-row {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0;
border-top: 1px solid #edf1fb;
}
.asset-row:first-child,
.approval-row:first-child,
.activity-row:first-child { border-top: 0; }
.coin-mark {
width: 42px;
height: 42px;
display: grid;
place-items: center;
color: white;
font-weight: 900;
border-radius: 50%;
background: linear-gradient(135deg, #35ceb4, #3154ff);
}
.asset-row span,
.approval-row span,
.activity-row small { display: block; color: #8390ad; }
.asset-value { margin-left: auto; text-align: right; }
.risk.high,
.risk.critical { color: #b94b18; background: #fff0e8; }
.risk.medium { color: #8a6500; background: #fff6d8; }
.risk.low { color: #147a58; background: #e5fbf3; }
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.settings-grid div {
padding: 14px;
border-radius: 16px;
background: #f7f9ff;
}
.settings-grid span { display: block; color: #7d89a5; font-size: 12px; }
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.feature-card {
padding: 16px;
border-radius: 20px;
background: #f8faff;
border: 1px solid #edf1fb;
}
.feature-card strong { display: block; margin: 12px 0 8px; }
.feature-card p { color: #6c7895; margin-bottom: 0; }
.doc-link { color: #3154ff; text-decoration: none; font-weight: 800; }
@media (max-width: 1080px) {
.app-shell { grid-template-columns: 68px 1fr; }
.wallet-panel { display: none; }
.hero-grid,
.two-column { grid-template-columns: 1fr; }
}