feat(admin-next): add collapsible icon-only sidebar

master
moonrailgun 19 hours ago
parent 6e2fb8d829
commit 3f932c55d6

@ -64,3 +64,16 @@ test('keeps inactive navigation neutral and groups user actions in a dropdown',
assert.match(styles, /grid-template-columns:\s*24px 1fr/);
assert.match(styles, /column-gap:\s*12px/);
});
test('collapses the sidebar to icons on desktop', () => {
const components = readFileSync(`${__dirname}/components.tsx`, 'utf8');
const styles = readFileSync(`${__dirname}/styles.css`, 'utf8');
assert.match(components, /className="icon-button sidebar-toggle"/);
assert.match(components, /aria-expanded=\{!collapsed\}/);
assert.match(styles, /\.sidebar-collapsed \.sidebar \{ width: 68px; \}/);
assert.match(
styles,
/\.sidebar-collapsed \.nav button span[^}]*display: none/
);
});

@ -223,6 +223,8 @@ const sections: { label: string; routes: { id: RouteId; icon: IconName }[] }[] =
},
];
const SIDEBAR_STORAGE_KEY = 'tailchat:admin-next:sidebar';
export function AppShell({
route,
username,
@ -237,6 +239,9 @@ export function AppShell({
}>) {
const { t, language, setLanguage } = useI18n();
const [drawer, setDrawer] = useState(false);
const [collapsed, setCollapsed] = useState(
() => window.localStorage.getItem(SIDEBAR_STORAGE_KEY) === 'collapsed'
);
const [palette, setPalette] = useState(false);
const [query, setQuery] = useState('');
useEffect(() => {
@ -265,8 +270,16 @@ export function AppShell({
setDrawer(false);
setPalette(false);
};
const toggleSidebar = () => {
const next = !collapsed;
window.localStorage.setItem(
SIDEBAR_STORAGE_KEY,
next ? 'collapsed' : 'expanded'
);
setCollapsed(next);
};
return (
<div className="app-shell">
<div className={`app-shell ${collapsed ? 'sidebar-collapsed' : ''}`}>
{drawer && (
<ArcoButton
className="drawer-backdrop"
@ -291,6 +304,7 @@ export function AppShell({
type="text"
key={item.id}
className={route === item.id ? 'active' : ''}
title={collapsed ? t(`route.${item.id}`) : undefined}
onClick={() => go(item.id)}
>
<Icon name={item.icon} />
@ -300,7 +314,18 @@ export function AppShell({
</div>
))}
</nav>
<div className="sidebar-footer">{t('app.footer')}</div>
<div className="sidebar-footer">
<span>{t('app.footer')}</span>
<ArcoButton
type="text"
className="icon-button sidebar-toggle"
onClick={toggleSidebar}
aria-expanded={!collapsed}
aria-label={t(collapsed ? 'shell.expand' : 'shell.collapse')}
title={t(collapsed ? 'shell.expand' : 'shell.collapse')}
icon={<Icon name="chevron" />}
/>
</div>
</aside>
<div className="workspace">
<header className="topbar">

@ -54,6 +54,8 @@ const commonZh: Record<string, string> = {
'auth.expired': '登录已过期,请重新登录',
'auth.logout': '退出登录',
'shell.menu': '打开导航',
'shell.collapse': '收起侧边栏',
'shell.expand': '展开侧边栏',
'shell.command': '快速跳转',
'shell.commandHint': '按 ⌘K 快速跳转',
'shell.commandPlaceholder': '搜索页面…',
@ -224,6 +226,8 @@ const commonEn: Record<string, string> = {
'auth.expired': 'Your session expired. Sign in again.',
'auth.logout': 'Sign out',
'shell.menu': 'Open navigation',
'shell.collapse': 'Collapse sidebar',
'shell.expand': 'Expand sidebar',
'shell.command': 'Quick navigation',
'shell.commandHint': 'Press ⌘K to jump',
'shell.commandPlaceholder': 'Search pages…',

@ -71,7 +71,7 @@ code, kbd, pre { font-family: "SFMono-Regular", Consolas, monospace; }
.icon-button:hover { background: var(--surface); }
.app-shell { min-height: 100vh; }
.sidebar { position: fixed; inset: 0 auto 0 0; width: 260px; z-index: 30; display: flex; flex-direction: column; background: #0e1118; border-right: 1px solid var(--border-soft); }
.sidebar { position: fixed; inset: 0 auto 0 0; width: 260px; z-index: 30; display: flex; flex-direction: column; background: #0e1118; border-right: 1px solid var(--border-soft); transition: width .2s ease; }
.brand { height: 76px; padding: 0 22px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid var(--border-soft); }
.brand img { width: 38px; height: 38px; border-radius: 10px; }
.brand div { min-width: 0; display: flex; flex-direction: column; }
@ -84,8 +84,10 @@ code, kbd, pre { font-family: "SFMono-Regular", Consolas, monospace; }
.sidebar .nav .arco-btn-text:not(.active) { color: #9aa4b5; }
.sidebar .nav .arco-btn-text:not(.active):hover { color: var(--text); background: rgba(255,255,255,.035); }
.nav button.active { background: var(--primary-soft); color: #72b8ff; }
.sidebar-footer { padding: 14px 22px; border-top: 1px solid var(--border-soft); color: #596476; font-size: 11px; }
.workspace { min-height: 100vh; margin-left: 260px; }
.sidebar-footer { padding: 8px 8px 8px 22px; display: flex; align-items: center; justify-content: space-between; gap: 8px; border-top: 1px solid var(--border-soft); color: #596476; font-size: 11px; }
.sidebar-toggle { flex: 0 0 auto; color: #697488; }
.sidebar-toggle .icon { transform: rotate(180deg); transition: transform .2s ease; }
.workspace { min-height: 100vh; margin-left: 260px; transition: margin-left .2s ease; }
.topbar { height: 62px; position: sticky; top: 0; z-index: 20; display: flex; align-items: center; padding: 0 28px; background: rgba(11, 14, 20, .88); border-bottom: 1px solid var(--border-soft); backdrop-filter: blur(14px); }
.command-trigger { height: 36px; min-width: 236px; padding: 0 10px; display: flex; align-items: center; gap: 9px; color: var(--muted); background: var(--panel); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; }
.command-trigger span { flex: 1; text-align: left; font-size: 12px; }
@ -262,6 +264,18 @@ label > span { color: #a7b0bf; font-size: 12px; font-weight: 600; }
.login-panel footer { margin-top: 28px; color: #596476; text-align: center; font-size: 10px; }
.drawer-backdrop { display: none; }
@media (min-width: 941px) {
.sidebar-collapsed .sidebar { width: 68px; }
.sidebar-collapsed .workspace { margin-left: 68px; }
.sidebar-collapsed .brand { padding: 0; justify-content: center; }
.sidebar-collapsed .brand div, .sidebar-collapsed .nav-label, .sidebar-collapsed .nav button span, .sidebar-collapsed .sidebar-footer span { display: none; }
.sidebar-collapsed .nav { padding: 14px 10px 24px; }
.sidebar-collapsed .nav-section + .nav-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-soft); }
.sidebar-collapsed .nav button { padding: 0; justify-content: center; }
.sidebar-collapsed .sidebar-footer { padding: 8px; justify-content: center; }
.sidebar-collapsed .sidebar-toggle .icon { transform: none; }
}
@media (max-width: 1120px) {
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
.network-detail-grid { grid-template-columns: 1fr; }

Loading…
Cancel
Save