feat: enhance VaultPage and App layout with new UI components and styles

This commit is contained in:
shuaiplus
2026-02-28 23:55:25 +08:00
committed by Shuai
parent 651eb69bd6
commit 32c695c81f
4 changed files with 325 additions and 175 deletions
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NodeWarden</title> <title>NodeWarden</title>
<script type="module" crossorigin src="/assets/index-C-ko-NHm.js"></script> <script type="module" crossorigin src="/assets/index-CfeJfWbB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BKQdQWYk.css"> <link rel="stylesheet" crossorigin href="/assets/index-BNxoWS2-.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+51 -22
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'preact/hooks'; import { useEffect, useMemo, useState } from 'preact/hooks';
import { Link, Route, Switch, useLocation } from 'wouter'; import { Link, Route, Switch, useLocation } from 'wouter';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Lock, LogOut } from 'lucide-preact'; import { CircleHelp, LogOut, Plus, Settings as SettingsIcon, Shield, ShieldUser, Vault } from 'lucide-preact';
import AuthViews from '@/components/AuthViews'; import AuthViews from '@/components/AuthViews';
import ConfirmDialog from '@/components/ConfirmDialog'; import ConfirmDialog from '@/components/ConfirmDialog';
import ToastHost from '@/components/ToastHost'; import ToastHost from '@/components/ToastHost';
@@ -291,6 +291,13 @@ export default function App() {
}); });
} }
function handleQuickAdd() {
navigate('/vault');
window.setTimeout(() => {
window.dispatchEvent(new Event('nodewarden:add-item'));
}, 0);
}
const ciphersQuery = useQuery({ const ciphersQuery = useQuery({
queryKey: ['ciphers', session?.accessToken], queryKey: ['ciphers', session?.accessToken],
queryFn: () => getCiphers(authedFetch), queryFn: () => getCiphers(authedFetch),
@@ -638,35 +645,55 @@ export default function App() {
return ( return (
<> <>
<div className="app-page">
<div className="app-shell"> <div className="app-shell">
<header className="topbar"> <header className="topbar">
<div className="brand">NodeWarden</div> <div className="brand">
<nav className="nav"> <Shield size={20} className="brand-icon" />
<Link href="/vault" className={`nav-link ${location === '/vault' ? 'active' : ''}`}> <span>NodeWarden</span>
Vault </div>
</Link>
<Link href="/settings" className={`nav-link ${location === '/settings' ? 'active' : ''}`}>
Settings
</Link>
{profile?.role === 'admin' && (
<Link href="/admin" className={`nav-link ${location === '/admin' ? 'active' : ''}`}>
Admin
</Link>
)}
<Link href="/help" className={`nav-link ${location === '/help' ? 'active' : ''}`}>
Help
</Link>
</nav>
<div className="topbar-actions"> <div className="topbar-actions">
<span className="user-email">{profile?.email}</span> <div className="user-chip">
<button type="button" className="btn btn-secondary small" onClick={handleLock}> <ShieldUser size={16} />
<Lock size={14} className="btn-icon" /> Lock <span>{profile?.email}</span>
</div>
<button type="button" className="btn btn-secondary small" onClick={() => navigate('/settings')}>
<Shield size={14} className="btn-icon" /> Account Security
</button> </button>
<button type="button" className="btn btn-secondary small" onClick={handleLogout}> <button type="button" className="btn btn-secondary small" onClick={handleLogout}>
<LogOut size={14} className="btn-icon" /> Log Out <LogOut size={14} className="btn-icon" /> Sign Out
</button> </button>
</div> </div>
</header> </header>
<div className="app-main">
<aside className="app-side">
<Link href="/vault" className={`side-link ${location === '/vault' ? 'active' : ''}`}>
<Vault size={16} />
<span>My Vault</span>
</Link>
{profile?.role === 'admin' && (
<Link href="/admin" className={`side-link ${location === '/admin' ? 'active' : ''}`}>
<ShieldUser size={16} />
<span>Admin Panel</span>
</Link>
)}
<Link href="/settings" className={`side-link ${location === '/settings' ? 'active' : ''}`}>
<SettingsIcon size={16} />
<span>System Settings</span>
</Link>
<Link href="/help" className={`side-link ${location === '/help' ? 'active' : ''}`}>
<CircleHelp size={16} />
<span>Support Center</span>
</Link>
<div className="side-spacer" />
<button type="button" className="btn btn-primary side-add-btn" onClick={handleQuickAdd}>
<Plus size={16} className="btn-icon" /> Add New Item
</button>
<button type="button" className="btn btn-secondary side-lock-btn" onClick={handleLock}>
Lock
</button>
</aside>
<main className="content"> <main className="content">
<Switch> <Switch>
<Route path="/vault"> <Route path="/vault">
@@ -762,6 +789,8 @@ export default function App() {
</Switch> </Switch>
</main> </main>
</div> </div>
</div>
</div>
<ConfirmDialog <ConfirmDialog
open={!!confirm} open={!!confirm}
+22 -10
View File
@@ -9,9 +9,13 @@ import {
EyeOff, EyeOff,
ExternalLink, ExternalLink,
FileKey2, FileKey2,
Folder as FolderIcon,
FolderOpen,
FolderX,
FolderInput, FolderInput,
Globe, Globe,
KeyRound, KeyRound,
LayoutGrid,
Pencil, Pencil,
Plus, Plus,
RefreshCw, RefreshCw,
@@ -304,6 +308,14 @@ export default function VaultPage(props: VaultPageProps) {
const [repromptPassword, setRepromptPassword] = useState(''); const [repromptPassword, setRepromptPassword] = useState('');
const [repromptApprovedCipherId, setRepromptApprovedCipherId] = useState<string | null>(null); const [repromptApprovedCipherId, setRepromptApprovedCipherId] = useState<string | null>(null);
useEffect(() => {
const onQuickAdd = () => {
startCreate(1);
};
window.addEventListener('nodewarden:add-item', onQuickAdd);
return () => window.removeEventListener('nodewarden:add-item', onQuickAdd);
}, []);
useEffect(() => { useEffect(() => {
setRepromptApprovedCipherId(null); setRepromptApprovedCipherId(null);
setRepromptPassword(''); setRepromptPassword('');
@@ -554,35 +566,35 @@ export default function VaultPage(props: VaultPageProps) {
<div className="sidebar-block"> <div className="sidebar-block">
<div className="sidebar-title">Types</div> <div className="sidebar-title">Types</div>
<button type="button" className={`tree-btn ${typeFilter === 'all' ? 'active' : ''}`} onClick={() => setTypeFilter('all')}> <button type="button" className={`tree-btn ${typeFilter === 'all' ? 'active' : ''}`} onClick={() => setTypeFilter('all')}>
All Items <LayoutGrid size={14} className="tree-icon" /> All Items
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'favorite' ? 'active' : ''}`} onClick={() => setTypeFilter('favorite')}> <button type="button" className={`tree-btn ${typeFilter === 'favorite' ? 'active' : ''}`} onClick={() => setTypeFilter('favorite')}>
Favorites <Star size={14} className="tree-icon" /> Favorites
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'login' ? 'active' : ''}`} onClick={() => setTypeFilter('login')}> <button type="button" className={`tree-btn ${typeFilter === 'login' ? 'active' : ''}`} onClick={() => setTypeFilter('login')}>
Login <Globe size={14} className="tree-icon" /> Login
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'card' ? 'active' : ''}`} onClick={() => setTypeFilter('card')}> <button type="button" className={`tree-btn ${typeFilter === 'card' ? 'active' : ''}`} onClick={() => setTypeFilter('card')}>
Card <CreditCard size={14} className="tree-icon" /> Card
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'identity' ? 'active' : ''}`} onClick={() => setTypeFilter('identity')}> <button type="button" className={`tree-btn ${typeFilter === 'identity' ? 'active' : ''}`} onClick={() => setTypeFilter('identity')}>
Identity <ShieldUser size={14} className="tree-icon" /> Identity
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'note' ? 'active' : ''}`} onClick={() => setTypeFilter('note')}> <button type="button" className={`tree-btn ${typeFilter === 'note' ? 'active' : ''}`} onClick={() => setTypeFilter('note')}>
Note <StickyNote size={14} className="tree-icon" /> Note
</button> </button>
<button type="button" className={`tree-btn ${typeFilter === 'ssh' ? 'active' : ''}`} onClick={() => setTypeFilter('ssh')}> <button type="button" className={`tree-btn ${typeFilter === 'ssh' ? 'active' : ''}`} onClick={() => setTypeFilter('ssh')}>
SSH Key <KeyRound size={14} className="tree-icon" /> SSH Key
</button> </button>
</div> </div>
<div className="sidebar-block"> <div className="sidebar-block">
<div className="sidebar-title">Folders</div> <div className="sidebar-title">Folders</div>
<button type="button" className={`tree-btn ${folderFilter === 'all' ? 'active' : ''}`} onClick={() => setFolderFilter('all')}> <button type="button" className={`tree-btn ${folderFilter === 'all' ? 'active' : ''}`} onClick={() => setFolderFilter('all')}>
All <FolderOpen size={14} className="tree-icon" /> All
</button> </button>
<button type="button" className={`tree-btn ${folderFilter === 'none' ? 'active' : ''}`} onClick={() => setFolderFilter('none')}> <button type="button" className={`tree-btn ${folderFilter === 'none' ? 'active' : ''}`} onClick={() => setFolderFilter('none')}>
No Folder <FolderX size={14} className="tree-icon" /> No Folder
</button> </button>
{props.folders.map((folder) => ( {props.folders.map((folder) => (
<button <button
@@ -591,7 +603,7 @@ export default function VaultPage(props: VaultPageProps) {
className={`tree-btn ${folderFilter === folder.id ? 'active' : ''}`} className={`tree-btn ${folderFilter === folder.id ? 'active' : ''}`}
onClick={() => setFolderFilter(folder.id)} onClick={() => setFolderFilter(folder.id)}
> >
{folder.decName || folder.name || folder.id} <FolderIcon size={14} className="tree-icon" /> {folder.decName || folder.name || folder.id}
</button> </button>
))} ))}
</div> </div>
+150 -41
View File
@@ -197,44 +197,47 @@ body,
color: #334155; color: #334155;
} }
.app-page {
min-height: 100%;
padding: 20px;
background: #e9edf3;
}
.app-shell { .app-shell {
height: 100%; height: calc(100vh - 40px);
max-width: 1800px;
margin: 0 auto;
background: #f5f7fb;
border: 1px solid #d5dce7;
border-radius: 18px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
} }
.topbar { .topbar {
height: 64px; height: 64px;
background: var(--primary); background: #f8fafc;
color: #fff; border-bottom: 1px solid #d9e0ea;
color: #0f172a;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 0 16px; padding: 0 14px;
} }
.brand { .brand {
font-size: 20px; display: inline-flex;
font-weight: 800; align-items: center;
gap: 10px;
font-size: 32px;
font-weight: 900;
color: #1e293b;
} }
.nav { .brand-icon {
display: flex; color: #334155;
gap: 8px;
}
.nav-link {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
padding: 8px 14px;
border-radius: 10px;
font-weight: 600;
}
.nav-link.active,
.nav-link:hover {
color: #fff;
background: rgba(255, 255, 255, 0.16);
} }
.topbar-actions { .topbar-actions {
@@ -243,59 +246,121 @@ body,
gap: 8px; gap: 8px;
} }
.user-email { .user-chip {
font-size: 18px; display: inline-flex;
opacity: 0.9; align-items: center;
gap: 6px;
height: 34px;
border-radius: 999px;
padding: 0 12px;
border: 1px solid #d5deea;
background: #fff;
color: #334155;
font-size: 14px;
font-weight: 600;
}
.app-main {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 220px 1fr;
}
.app-side {
border-right: 1px solid #d9e0ea;
background: #eef3f9;
padding: 14px 10px;
display: flex;
flex-direction: column;
gap: 6px;
}
.side-link {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 10px;
border-radius: 9px;
color: #0f172a;
text-decoration: none;
border: 1px solid transparent;
font-weight: 600;
}
.side-link:hover {
background: #e4ecf8;
}
.side-link.active {
background: #d7e6fb;
border-color: #bcd3f6;
color: #0f3f95;
}
.side-spacer {
flex: 1;
}
.side-add-btn,
.side-lock-btn {
width: 100%;
}
.side-add-btn {
justify-content: flex-start;
} }
.content { .content {
flex: 1; min-height: 0;
padding: 14px; padding: 10px;
overflow: auto; overflow: auto;
width: min(1540px, 100%);
margin: 0 auto;
} }
.vault-grid { .vault-grid {
display: grid; display: grid;
grid-template-columns: 280px minmax(360px, 43%) 1fr; grid-template-columns: 260px minmax(420px, 1fr) 400px;
gap: 12px; gap: 12px;
height: calc(100vh - 64px - 28px); height: 100%;
min-height: 0;
} }
.sidebar, .sidebar,
.list-panel, .list-panel,
.card { .card {
background: #fff; background: #fff;
border: 1px solid var(--line); border: 1px solid #d8dee8;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
} }
.sidebar { .sidebar {
padding: 10px; padding: 8px;
overflow: auto; overflow: auto;
} }
.sidebar-block { .sidebar-block {
border: 1px solid var(--line); border: 1px solid #dbe2ed;
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
margin-bottom: 10px; margin-bottom: 10px;
background: #f9fbfe;
} }
.sidebar-title { .sidebar-title {
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
color: #475467; color: #344054;
margin-bottom: 8px; margin-bottom: 8px;
} }
.search-input { .search-input {
width: 100%; width: 100%;
height: 42px; height: 40px;
border: 1px solid var(--primary); border: 1px solid #c9d4e4;
border-radius: 10px; border-radius: 10px;
padding: 0 12px; padding: 0 12px;
background: #fff;
} }
.tree-btn { .tree-btn {
@@ -307,6 +372,9 @@ body,
padding: 8px 10px; padding: 8px 10px;
margin-bottom: 4px; margin-bottom: 4px;
cursor: pointer; cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
} }
.tree-btn.active { .tree-btn.active {
@@ -315,10 +383,15 @@ body,
font-weight: 700; font-weight: 700;
} }
.tree-icon {
flex-shrink: 0;
}
.list-col { .list-col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
min-height: 0;
} }
.toolbar { .toolbar {
@@ -328,24 +401,30 @@ body,
.list-panel { .list-panel {
overflow: auto; overflow: auto;
min-height: 0; min-height: 0;
padding: 8px;
} }
.list-item { .list-item {
width: 100%; width: 100%;
background: #fff; background: #fff;
border-bottom: 1px solid var(--line); border: 1px solid #e1e6ef;
padding: 12px; border-radius: 10px;
padding: 10px 12px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 10px;
margin-bottom: 8px;
} }
.list-item:hover { .list-item:hover {
background: #f8fbff; background: #f8fbff;
border-color: #cdd9ea;
} }
.list-item.active { .list-item.active {
background: #edf4ff; background: linear-gradient(180deg, #e6f0ff, #d9e9ff);
border-color: #9dbbec;
box-shadow: inset 0 0 0 1px rgba(52, 93, 171, 0.08);
} }
.row-check { .row-check {
@@ -810,6 +889,36 @@ body,
} }
@media (max-width: 1180px) { @media (max-width: 1180px) {
.app-page {
padding: 8px;
}
.app-shell {
height: calc(100vh - 16px);
border-radius: 12px;
}
.app-main {
grid-template-columns: 1fr;
}
.app-side {
border-right: none;
border-bottom: 1px solid #d9e0ea;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.side-spacer {
display: none;
}
.side-add-btn,
.side-lock-btn {
grid-column: span 1;
}
.vault-grid { .vault-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
height: auto; height: auto;