mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: enhance VaultPage and App layout with new UI components and styles
This commit is contained in:
+151
-122
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { Link, Route, Switch, useLocation } from 'wouter';
|
||||
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 ConfirmDialog from '@/components/ConfirmDialog';
|
||||
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({
|
||||
queryKey: ['ciphers', session?.accessToken],
|
||||
queryFn: () => getCiphers(authedFetch),
|
||||
@@ -638,129 +645,151 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<div className="brand">NodeWarden</div>
|
||||
<nav className="nav">
|
||||
<Link href="/vault" className={`nav-link ${location === '/vault' ? 'active' : ''}`}>
|
||||
Vault
|
||||
</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
|
||||
<div className="app-page">
|
||||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<div className="brand">
|
||||
<Shield size={20} className="brand-icon" />
|
||||
<span>NodeWarden</span>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<div className="user-chip">
|
||||
<ShieldUser size={16} />
|
||||
<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 type="button" className="btn btn-secondary small" onClick={handleLogout}>
|
||||
<LogOut size={14} className="btn-icon" /> Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
<Link href="/help" className={`nav-link ${location === '/help' ? 'active' : ''}`}>
|
||||
Help
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="topbar-actions">
|
||||
<span className="user-email">{profile?.email}</span>
|
||||
<button type="button" className="btn btn-secondary small" onClick={handleLock}>
|
||||
<Lock size={14} className="btn-icon" /> Lock
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary small" onClick={handleLogout}>
|
||||
<LogOut size={14} className="btn-icon" /> Log Out
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="content">
|
||||
<Switch>
|
||||
<Route path="/vault">
|
||||
<VaultPage
|
||||
ciphers={decryptedCiphers}
|
||||
folders={decryptedFolders}
|
||||
loading={ciphersQuery.isFetching || foldersQuery.isFetching}
|
||||
emailForReprompt={profile?.email || session?.email || ''}
|
||||
onRefresh={refreshVault}
|
||||
onCreate={createVaultItem}
|
||||
onUpdate={updateVaultItem}
|
||||
onDelete={deleteVaultItem}
|
||||
onBulkDelete={bulkDeleteVaultItems}
|
||||
onBulkMove={bulkMoveVaultItems}
|
||||
onVerifyMasterPassword={verifyMasterPasswordAction}
|
||||
onNotify={pushToast}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
{profile && (
|
||||
<SettingsPage
|
||||
profile={profile}
|
||||
totpEnabled={!!totpStatusQuery.data?.enabled}
|
||||
onSaveProfile={saveProfileAction}
|
||||
onChangePassword={changePasswordAction}
|
||||
onEnableTotp={async (secret, token) => {
|
||||
await enableTotpAction(secret, token);
|
||||
await totpStatusQuery.refetch();
|
||||
}}
|
||||
onOpenDisableTotp={() => setDisableTotpOpen(true)}
|
||||
/>
|
||||
{profile?.role === 'admin' && (
|
||||
<Link href="/admin" className={`side-link ${location === '/admin' ? 'active' : ''}`}>
|
||||
<ShieldUser size={16} />
|
||||
<span>Admin Panel</span>
|
||||
</Link>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/admin">
|
||||
<AdminPage
|
||||
currentUserId={profile?.id || ''}
|
||||
users={usersQuery.data || []}
|
||||
invites={invitesQuery.data || []}
|
||||
onRefresh={() => {
|
||||
void usersQuery.refetch();
|
||||
void invitesQuery.refetch();
|
||||
}}
|
||||
onCreateInvite={async (hours) => {
|
||||
await createInvite(authedFetch, hours);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'Invite created');
|
||||
}}
|
||||
onDeleteAllInvites={async () => {
|
||||
setConfirm({
|
||||
title: 'Delete all invites',
|
||||
message: 'Delete all invite codes (active/inactive)?',
|
||||
danger: true,
|
||||
onConfirm: () => {
|
||||
setConfirm(null);
|
||||
void (async () => {
|
||||
await deleteAllInvites(authedFetch);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'All invites deleted');
|
||||
})();
|
||||
},
|
||||
});
|
||||
}}
|
||||
onToggleUserStatus={async (userId, status) => {
|
||||
await setUserStatus(authedFetch, userId, status === 'active' ? 'banned' : 'active');
|
||||
await usersQuery.refetch();
|
||||
pushToast('success', 'User status updated');
|
||||
}}
|
||||
onDeleteUser={async (userId) => {
|
||||
setConfirm({
|
||||
title: 'Delete user',
|
||||
message: 'Delete this user and all user data?',
|
||||
danger: true,
|
||||
onConfirm: () => {
|
||||
setConfirm(null);
|
||||
void (async () => {
|
||||
await deleteUser(authedFetch, userId);
|
||||
await usersQuery.refetch();
|
||||
pushToast('success', 'User deleted');
|
||||
})();
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRevokeInvite={async (code) => {
|
||||
await revokeInvite(authedFetch, code);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'Invite revoked');
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/help">
|
||||
<HelpPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
</main>
|
||||
<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">
|
||||
<Switch>
|
||||
<Route path="/vault">
|
||||
<VaultPage
|
||||
ciphers={decryptedCiphers}
|
||||
folders={decryptedFolders}
|
||||
loading={ciphersQuery.isFetching || foldersQuery.isFetching}
|
||||
emailForReprompt={profile?.email || session?.email || ''}
|
||||
onRefresh={refreshVault}
|
||||
onCreate={createVaultItem}
|
||||
onUpdate={updateVaultItem}
|
||||
onDelete={deleteVaultItem}
|
||||
onBulkDelete={bulkDeleteVaultItems}
|
||||
onBulkMove={bulkMoveVaultItems}
|
||||
onVerifyMasterPassword={verifyMasterPasswordAction}
|
||||
onNotify={pushToast}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
{profile && (
|
||||
<SettingsPage
|
||||
profile={profile}
|
||||
totpEnabled={!!totpStatusQuery.data?.enabled}
|
||||
onSaveProfile={saveProfileAction}
|
||||
onChangePassword={changePasswordAction}
|
||||
onEnableTotp={async (secret, token) => {
|
||||
await enableTotpAction(secret, token);
|
||||
await totpStatusQuery.refetch();
|
||||
}}
|
||||
onOpenDisableTotp={() => setDisableTotpOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</Route>
|
||||
<Route path="/admin">
|
||||
<AdminPage
|
||||
currentUserId={profile?.id || ''}
|
||||
users={usersQuery.data || []}
|
||||
invites={invitesQuery.data || []}
|
||||
onRefresh={() => {
|
||||
void usersQuery.refetch();
|
||||
void invitesQuery.refetch();
|
||||
}}
|
||||
onCreateInvite={async (hours) => {
|
||||
await createInvite(authedFetch, hours);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'Invite created');
|
||||
}}
|
||||
onDeleteAllInvites={async () => {
|
||||
setConfirm({
|
||||
title: 'Delete all invites',
|
||||
message: 'Delete all invite codes (active/inactive)?',
|
||||
danger: true,
|
||||
onConfirm: () => {
|
||||
setConfirm(null);
|
||||
void (async () => {
|
||||
await deleteAllInvites(authedFetch);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'All invites deleted');
|
||||
})();
|
||||
},
|
||||
});
|
||||
}}
|
||||
onToggleUserStatus={async (userId, status) => {
|
||||
await setUserStatus(authedFetch, userId, status === 'active' ? 'banned' : 'active');
|
||||
await usersQuery.refetch();
|
||||
pushToast('success', 'User status updated');
|
||||
}}
|
||||
onDeleteUser={async (userId) => {
|
||||
setConfirm({
|
||||
title: 'Delete user',
|
||||
message: 'Delete this user and all user data?',
|
||||
danger: true,
|
||||
onConfirm: () => {
|
||||
setConfirm(null);
|
||||
void (async () => {
|
||||
await deleteUser(authedFetch, userId);
|
||||
await usersQuery.refetch();
|
||||
pushToast('success', 'User deleted');
|
||||
})();
|
||||
},
|
||||
});
|
||||
}}
|
||||
onRevokeInvite={async (code) => {
|
||||
await revokeInvite(authedFetch, code);
|
||||
await invitesQuery.refetch();
|
||||
pushToast('success', 'Invite revoked');
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/help">
|
||||
<HelpPage />
|
||||
</Route>
|
||||
</Switch>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -9,9 +9,13 @@ import {
|
||||
EyeOff,
|
||||
ExternalLink,
|
||||
FileKey2,
|
||||
Folder as FolderIcon,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
FolderInput,
|
||||
Globe,
|
||||
KeyRound,
|
||||
LayoutGrid,
|
||||
Pencil,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
@@ -304,6 +308,14 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
const [repromptPassword, setRepromptPassword] = useState('');
|
||||
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(() => {
|
||||
setRepromptApprovedCipherId(null);
|
||||
setRepromptPassword('');
|
||||
@@ -554,35 +566,35 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
<div className="sidebar-block">
|
||||
<div className="sidebar-title">Types</div>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'all' ? 'active' : ''}`} onClick={() => setTypeFilter('all')}>
|
||||
All Items
|
||||
<LayoutGrid size={14} className="tree-icon" /> All Items
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'favorite' ? 'active' : ''}`} onClick={() => setTypeFilter('favorite')}>
|
||||
Favorites
|
||||
<Star size={14} className="tree-icon" /> Favorites
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'login' ? 'active' : ''}`} onClick={() => setTypeFilter('login')}>
|
||||
Login
|
||||
<Globe size={14} className="tree-icon" /> Login
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'card' ? 'active' : ''}`} onClick={() => setTypeFilter('card')}>
|
||||
Card
|
||||
<CreditCard size={14} className="tree-icon" /> Card
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'identity' ? 'active' : ''}`} onClick={() => setTypeFilter('identity')}>
|
||||
Identity
|
||||
<ShieldUser size={14} className="tree-icon" /> Identity
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'note' ? 'active' : ''}`} onClick={() => setTypeFilter('note')}>
|
||||
Note
|
||||
<StickyNote size={14} className="tree-icon" /> Note
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'ssh' ? 'active' : ''}`} onClick={() => setTypeFilter('ssh')}>
|
||||
SSH Key
|
||||
<KeyRound size={14} className="tree-icon" /> SSH Key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-block">
|
||||
<div className="sidebar-title">Folders</div>
|
||||
<button type="button" className={`tree-btn ${folderFilter === 'all' ? 'active' : ''}`} onClick={() => setFolderFilter('all')}>
|
||||
All
|
||||
<FolderOpen size={14} className="tree-icon" /> All
|
||||
</button>
|
||||
<button type="button" className={`tree-btn ${folderFilter === 'none' ? 'active' : ''}`} onClick={() => setFolderFilter('none')}>
|
||||
No Folder
|
||||
<FolderX size={14} className="tree-icon" /> No Folder
|
||||
</button>
|
||||
{props.folders.map((folder) => (
|
||||
<button
|
||||
@@ -591,7 +603,7 @@ export default function VaultPage(props: VaultPageProps) {
|
||||
className={`tree-btn ${folderFilter === folder.id ? 'active' : ''}`}
|
||||
onClick={() => setFolderFilter(folder.id)}
|
||||
>
|
||||
{folder.decName || folder.name || folder.id}
|
||||
<FolderIcon size={14} className="tree-icon" /> {folder.decName || folder.name || folder.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+150
-41
@@ -197,44 +197,47 @@ body,
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.app-page {
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
background: #e9edf3;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
height: 64px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #d9e0ea;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
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);
|
||||
.brand-icon {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
@@ -243,59 +246,121 @@ body,
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
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 {
|
||||
flex: 1;
|
||||
padding: 14px;
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
width: min(1540px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.vault-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(360px, 43%) 1fr;
|
||||
grid-template-columns: 260px minmax(420px, 1fr) 400px;
|
||||
gap: 12px;
|
||||
height: calc(100vh - 64px - 28px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.list-panel,
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border: 1px solid #d8dee8;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebar-block {
|
||||
border: 1px solid var(--line);
|
||||
border: 1px solid #dbe2ed;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #f9fbfe;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #475467;
|
||||
color: #344054;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border: 1px solid var(--primary);
|
||||
height: 40px;
|
||||
border: 1px solid #c9d4e4;
|
||||
border-radius: 10px;
|
||||
padding: 0 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tree-btn {
|
||||
@@ -307,6 +372,9 @@ body,
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-btn.active {
|
||||
@@ -315,10 +383,15 @@ body,
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
@@ -328,24 +401,30 @@ body,
|
||||
.list-panel {
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 12px;
|
||||
border: 1px solid #e1e6ef;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.list-item:hover {
|
||||
background: #f8fbff;
|
||||
border-color: #cdd9ea;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -810,6 +889,36 @@ body,
|
||||
}
|
||||
|
||||
@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 {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
|
||||
Reference in New Issue
Block a user