Files
maestro/design/ui_kits/console/Sidebar.jsx
T
wangjia 5a1e185b1a docs(design): 解压 Maestro Design System 到 design/(重构参照材料)
phosphor console v1.1 设计系统(从现网 web/style.css 演化):tokens(colors/effects/
typography/fonts) + components(core/forms/surfaces .jsx) + ui_kits(console + console_mobile)
+ assets/icons(15 双色 SVG) + guidelines + CLAUDE/SKILL/readme。供 B1–B6 重构任务对照实现。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:29:07 +08:00

398 lines
23 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 左栏:logo + 项目列表(git 图标)+ WS 状态脚 · 可折叠(折叠后仅图标)
// 项目 logo:图片优先(原型无图),回退首字母色块 hsl(h 45% 60%)
function ProjectLogo({ project, size = 26 }) {
return (
<span style={{
flex: 'none', width: size, height: size, borderRadius: 5,
display: 'grid', placeItems: 'center',
fontSize: size * 0.5, fontWeight: 700, color: '#0a0d0b',
background: 'hsl(' + (project.hue || 140) + ' 45% 60%)',
}}>{(project.name || '?')[0].toUpperCase()}</span>
);
}
function MaestroGitIcon({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none' }}>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</svg>
);
}
// 像素章鱼 mark(折叠态 logo
function MaestroMark({ scale = 2.2 }) {
const ref = React.useRef(null);
React.useEffect(() => {
const MAP = ['.....LLLLL.....','...LLLLLLLLL...','..GGGGGGGGGGG..','..GGEEGGGEEGG..','..GGEEGGGEEGG..','..GGGGGGGGGGG..','...GGGGGGGGG...','...G..G.G..G...','...G..G.G..G...','..G...G.G...G..','..D...D.D...D..','.D...D...D...D.'];
const INK = { L: '#79ec94', G: '#5fdd7d', D: '#2e6b3d', E: '#070908' };
const ctx = ref.current.getContext('2d');
MAP.forEach((row, y) => [...row].forEach((ch, x) => {
if (INK[ch]) { ctx.fillStyle = INK[ch]; ctx.fillRect(x, y, 1, 1); }
}));
}, []);
return <canvas ref={ref} width="15" height="12"
style={{ width: 15 * scale, height: 12 * scale, imageRendering: 'pixelated', filter: 'drop-shadow(0 0 6px rgba(95,221,125,.4))' }} />;
}
// 项目状态:运行中(青脉冲) / 暂停(faint) / 阻塞(琥珀) / 空闲(muted)
function projStateMeta(state, t) {
return ({
running: { color: 'var(--cyan)', pulse: true, label: t.projRunning },
paused: { color: 'var(--faint)', pulse: false, label: t.projPaused },
blocked: { color: 'var(--amber)', pulse: true, label: t.projBlocked },
idle: { color: 'var(--muted)', pulse: false, label: t.projIdle },
})[state] || { color: 'var(--muted)', pulse: false, label: t.projIdle };
}
function ProjStatusDot({ meta, size = 7 }) {
return (
<span style={{
flex: 'none', width: size, height: size, borderRadius: '50%', background: meta.color,
boxShadow: '0 0 7px ' + meta.color,
animation: meta.pulse ? 'maestro-pulse 1.4s infinite' : 'none',
}}></span>
);
}
// 侧栏底部小行:图标 + 主文 + 副文(折叠时仅图标,hover 高亮)
function SideRow({ icon, label, detail, onClick, collapsed, title, accent }) {
const interactive = !!onClick;
return (
<div onClick={onClick} title={title || label}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: collapsed ? '9px 0' : '8px 12px', minHeight: 40,
justifyContent: collapsed ? 'center' : 'flex-start',
cursor: interactive ? 'pointer' : 'default', transition: 'background .1s',
}}
onMouseEnter={interactive ? (e) => { e.currentTarget.style.background = 'var(--panel)'; } : undefined}
onMouseLeave={interactive ? (e) => { e.currentTarget.style.background = 'transparent'; } : undefined}>
<span style={{ flex: 'none', color: accent || 'var(--muted)', display: 'inline-flex' }}>{icon}</span>
{!collapsed ? (
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{label}</span>
{detail ? <span style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{detail}</span> : null}
</span>
) : null}
{!collapsed && interactive ? <span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11 }}></span> : null}
</div>
);
}
const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none' } };
function IcoAgents() { return <svg {...sIco}><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none" /></svg>; }
function IcoGlobalCfg() { return <svg {...sIco}><circle cx="12" cy="12" r="9" /><line x1="3" y1="12" x2="21" y2="12" /><path d="M12 3a14 14 0 0 0 0 18a14 14 0 0 0 0-18" /></svg>; }
function UserRow({ user, t, collapsed }) {
const [open, setOpen] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const avatar = (
<span style={{
flex: 'none', width: 26, height: 26, borderRadius: '50%', display: 'grid', placeItems: 'center',
fontSize: 12, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + (user.hue || 200) + ' 50% 62%)',
}}>{user.initial}</span>
);
return (
<div ref={ref} style={{ position: 'relative', borderTop: '1px solid var(--line-soft)' }}>
<div onClick={() => setOpen(!open)} title={user.name + ' · ' + user.plan}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: collapsed ? '10px 0' : '10px 12px', justifyContent: collapsed ? 'center' : 'flex-start', cursor: 'pointer', transition: 'background .1s' }}
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
{avatar}
{!collapsed ? (
<React.Fragment>
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--ink)', whiteSpace: 'nowrap' }}>{user.name}</span>
<span style={{ fontSize: 10, color: 'var(--green)', whiteSpace: 'nowrap' }}>{user.plan}</span>
</span>
<span style={{ flex: 'none', color: 'var(--faint)', fontSize: 11, transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}></span>
</React.Fragment>
) : null}
</div>
{open ? (
<div style={{
position: collapsed ? 'fixed' : 'absolute',
...(collapsed
? { left: 58, bottom: 14, width: 180 }
: { bottom: 'calc(100% + 4px)', left: 12, right: 12, minWidth: 160 }),
zIndex: 80,
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
boxShadow: '0 10px 30px rgba(0,0,0,.65)', padding: 6, animation: 'maestro-rise .12s ease both',
}}>
<div style={{ padding: '6px 10px', borderBottom: '1px solid var(--line-soft)', marginBottom: 4 }}>
<div style={{ fontSize: 12, fontWeight: 600 }}>{user.name} <span style={{ color: 'var(--faint)', fontWeight: 400 }}>{user.handle}</span></div>
<div style={{ fontSize: 10.5, color: 'var(--green)', marginTop: 2 }}>{t.gUserPlan} · {user.plan}</div>
</div>
{[t.gUserSettings, t.gUserSignOut].map((label, i) => (
<div key={label} style={{ padding: '6px 10px', fontSize: 12, color: i === 1 ? 'var(--red)' : 'var(--ink)', cursor: 'pointer', borderRadius: 3 }}
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-2)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{label}</div>
))}
</div>
) : null}
</div>
);
}
function fmtTokens(n) {
if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
if (n >= 1e3) return Math.round(n / 1e3) + 'K';
return String(n);
}
function AgentsRow({ global, summary, t, collapsed }) {
const [open, setOpen] = React.useState(false);
const [pos, setPos] = React.useState(null);
const ref = React.useRef(null);
const rowRef = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const toggle = () => {
if (!open && rowRef.current) {
const r = rowRef.current.getBoundingClientRect();
const W = 286;
const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8);
setPos({ left, bottom: window.innerHeight - r.top + 6, width: W });
}
setOpen((o) => !o);
};
const maxTok = Math.max(...summary.byProject.map((p) => p.tokens), 1);
const detail = t.gAgentsDetail.replace('{p}', global.runningProjects).replace('{P}', global.projectCount).replace('{a}', global.runningAgents);
return (
<div style={{ position: 'relative' }}>
<div ref={rowRef}>
<SideRow collapsed={collapsed} icon={<IcoAgents />} accent="var(--cyan)" onClick={toggle}
label={t.gAgents} detail={detail} title={t.gAgents + ' · ' + detail} />
</div>
{open && pos ? (
<div ref={ref} style={{
position: 'fixed', left: pos.left, bottom: pos.bottom, width: pos.width, zIndex: 1000,
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
boxShadow: '0 12px 34px rgba(0,0,0,.7)', padding: '12px 14px', animation: 'maestro-rise .12s ease both',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 10 }}>
<span style={{ color: 'var(--cyan)' }}></span>{t.gAgents.toUpperCase()}
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
</div>
{/* 总结 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 12 }}>
{[
[fmtTokens(summary.tokensWeek), t.apTokens, 'var(--cyan)'],
['$' + summary.costWeek.toFixed(1), t.apCost, 'var(--green)'],
[summary.runsWeek, t.apRuns, 'var(--ink)'],
[summary.activeNow + ' / ' + global.maxAgents, t.apActive, summary.activeNow ? 'var(--cyan)' : 'var(--faint)'],
].map(([v, k, c], i) => (
<div key={i} style={{ background: 'var(--bg-deep)', padding: '8px 10px' }}>
<div style={{ fontSize: 16, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
<div style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
</div>
))}
</div>
{/* 按项目 */}
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '.18em', color: 'var(--faint)', marginBottom: 7 }}>{t.apPerProj}</div>
<div style={{ display: 'grid', gap: 9 }}>
{summary.byProject.map((p) => (
<div key={p.id}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
<span style={{ flex: 'none', width: 16, height: 16, borderRadius: 4, display: 'grid', placeItems: 'center', fontSize: 9, fontWeight: 700, color: '#0a0d0b', background: 'hsl(' + p.hue + ' 45% 60%)' }}>{p.name[0].toUpperCase()}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
{p.active > 0 ? (
<span style={{ flex: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 9.5, color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 5px' }}>
<span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 6px var(--cyan)', animation: 'maestro-pulse .9s infinite' }}></span>{p.active}
</span>
) : <span style={{ flex: 'none', fontSize: 9.5, color: 'var(--faint)' }}>{t.apIdle}</span>}
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 11, fontWeight: 600, color: 'var(--cyan)' }}>{fmtTokens(p.tokens)}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingLeft: 24 }}>
<span style={{ flex: 1, height: 4, background: 'var(--panel-2)', borderRadius: 2, overflow: 'hidden' }}>
<span style={{ display: 'block', height: '100%', width: (p.tokens / maxTok * 100) + '%', background: 'var(--cyan)', boxShadow: '0 0 6px rgba(89,200,216,.5)' }}></span>
</span>
<span style={{ flex: 'none', fontSize: 10, color: 'var(--faint)' }}>{p.runs} {t.apRuns}</span>
</div>
</div>
))}
</div>
</div>
) : null}
</div>
);
}
function GlobalConfigRow({ global, t, collapsed }) {
const { Select } = window.MaestroDesignSystem_a6a290;
const [open, setOpen] = React.useState(false);
const [pos, setPos] = React.useState(null);
const ref = React.useRef(null);
const rowRef = React.useRef(null);
React.useEffect(() => {
if (!open) return;
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target) && rowRef.current && !rowRef.current.contains(e.target)) setOpen(false); };
document.addEventListener('mousedown', onDoc);
return () => document.removeEventListener('mousedown', onDoc);
}, [open]);
const toggle = () => {
if (!open && rowRef.current) {
const r = rowRef.current.getBoundingClientRect();
const W = 250;
// 优先在行上方对齐左缘;折叠态贴侧栏右侧
const left = collapsed ? r.right + 6 : Math.min(r.left, window.innerWidth - W - 8);
setPos({ left, bottom: window.innerHeight - r.top + 6, width: W });
}
setOpen((o) => !o);
};
const stat = (k, v, accent) => (
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, padding: '5px 0', borderBottom: '1px solid var(--line-soft)' }}>
<span style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.06em' }}>{k}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: accent || 'var(--ink)' }}>{v}</span>
</div>
);
return (
<div style={{ position: 'relative' }}>
<div ref={rowRef}>
<SideRow collapsed={collapsed} icon={<IcoGlobalCfg />} onClick={toggle}
label={t.gConfig}
detail={t.gConfigDetail.replace('{n}', global.maxAgents).replace('{v}', global.daemon)}
title={t.gConfig} />
</div>
{open && pos ? (
<div ref={ref} style={{
position: 'fixed', left: pos.left, bottom: pos.bottom, width: pos.width,
zIndex: 1000, background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
boxShadow: '0 12px 34px rgba(0,0,0,.7)', padding: '12px 14px', animation: 'maestro-rise .12s ease both',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--muted)', marginBottom: 8 }}>
<span style={{ color: 'var(--cyan)' }}></span>{t.gConfig.toUpperCase()}
</div>
<div style={{ marginBottom: 12 }}>
{stat('daemon', global.daemon, 'var(--green)')}
{stat('port', ':' + global.port)}
{stat('uptime', global.uptime)}
{stat(t.gAgents, global.runningAgents + ' / ' + global.maxAgents, 'var(--cyan)')}
</div>
<div style={{ display: 'grid', gap: 10 }}>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
<span>{t.maxConcurrency}</span>
<Select defaultValue={String(global.maxAgents)} style={{ width: '100%' }} options={['1', '2', '4', '6', '8'].map((n) => ({ value: n, label: n }))} />
</label>
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em' }}>
<span>{t.workMode}</span>
<Select defaultValue={global.autonomy} style={{ width: '100%' }} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />
</label>
</div>
</div>
) : null}
</div>
);
}
function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig }) {
const { Button } = window.MaestroDesignSystem_a6a290;
return (
<aside style={{ background: 'var(--bg-deep)', borderRight: '1px solid var(--line)', display: 'flex', flexDirection: 'column', overflowY: 'auto', overflowX: 'hidden' }}>
<div style={{ padding: collapsed ? '16px 0 12px' : '20px 16px 14px', borderBottom: '1px solid var(--line-soft)', display: 'flex', flexDirection: 'column', alignItems: collapsed ? 'center' : 'flex-start' }}>
{collapsed ? <MaestroMark /> : (
<React.Fragment>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<MaestroMark scale={1.8} />
<span style={{ fontSize: 19, fontWeight: 700, letterSpacing: '.28em', color: 'var(--green)', textShadow: '0 0 12px rgba(95,221,125,.45)' }}>
MAESTRO<span style={{ marginLeft: 1, animation: 'maestro-blink 1.1s steps(1) infinite' }}></span>
</span>
</div>
<div style={{ marginTop: 5, fontSize: 9, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.2em', lineHeight: 1.7, textTransform: 'uppercase' }}>{t.logoSub}</div>
</React.Fragment>
)}
</div>
{!collapsed ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, fontWeight: 600, letterSpacing: '.22em', color: 'var(--muted)', padding: '16px 16px 8px' }}>
<span style={{ color: 'var(--green)' }}></span>{t.projects}
<span style={{ marginLeft: 'auto' }}><Button variant="ghost" size="xs" onClick={onNewProject}>{t.newProject}</Button></span>
</div>
) : <div style={{ height: 12 }}></div>}
<ul style={{ listStyle: 'none', flex: 1, margin: 0, padding: 0 }}>
{projects.map((p) => {
const active = p.id === currentId;
const meta = projStateMeta(p.state, t);
return (
<li key={p.id} onClick={() => onSelect(p.id)} title={p.name + ' · ' + meta.label + (p.pending ? ' · ' + t.projPendingTip.replace('{n}', p.pending) : '')}
style={{
position: 'relative',
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 9,
justifyContent: collapsed ? 'center' : 'flex-start',
borderLeft: '2px solid ' + (active ? 'var(--green)' : 'transparent'),
background: active ? 'var(--panel-2)' : 'transparent',
color: active ? 'var(--green)' : 'var(--muted)',
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
<ProjectLogo project={p} size={collapsed ? 26 : 24} />
{!collapsed ? (
<React.Fragment>
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0, flex: 1 }}>
<span style={{ fontWeight: 600, fontSize: 13, color: active ? 'var(--green)' : 'var(--ink)' }}>{p.name}</span>
<span style={{ fontSize: 10.5, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.path}</span>
</span>
<span style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
{p.pending > 0 ? (
<span title={t.projPendingTip.replace('{n}', p.pending)} style={{
fontFamily: 'var(--mono)', fontSize: 9.5, fontWeight: 700, lineHeight: '15px',
minWidth: 15, height: 15, textAlign: 'center', padding: '0 4px',
color: 'var(--bg-deep)', background: 'var(--violet)', borderRadius: 8,
}}>{p.pending}</span>
) : null}
<ProjStatusDot meta={meta} />
</span>
</React.Fragment>
) : (
<span style={{ position: 'absolute', top: 7, right: 9, display: 'flex', alignItems: 'center' }}>
<ProjStatusDot meta={meta} size={p.pending > 0 ? 8 : 6} />
</span>
)}
</li>
);
})}
</ul>
{/* ── 底部:全局 agent · 全局配置 · 用户 ── */}
<div style={{ borderTop: '1px solid var(--line-soft)' }}>
<AgentsRow global={global} summary={summary} t={t} collapsed={collapsed} />
<GlobalConfigRow global={global} t={t} collapsed={collapsed} />
</div>
<UserRow user={user} t={t} collapsed={collapsed} />
<div style={{ borderTop: '1px solid var(--line-soft)', padding: collapsed ? '10px 0' : '10px 12px', fontSize: 11, color: 'var(--muted)', display: 'flex', alignItems: 'center', gap: 7, justifyContent: collapsed ? 'center' : 'flex-start' }}>
{!collapsed ? (
<React.Fragment>
<span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--green)', boxShadow: '0 0 8px var(--green)', display: 'inline-block', flex: 'none' }}></span>
<span style={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>{t.connected}</span>
</React.Fragment>
) : null}
<button onClick={onToggleCollapse} title={collapsed ? t.expandSide : t.collapseSide}
style={{
marginLeft: collapsed ? 0 : 'auto', flex: 'none', cursor: 'pointer',
fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1,
background: 'transparent', color: 'var(--muted)',
border: '1px solid var(--line)', borderRadius: 'var(--radius-sm, 4px)', padding: '4px 7px',
}}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--green)'; e.currentTarget.style.borderColor = 'var(--green-dim)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
{collapsed ? '»' : '«'}
</button>
</div>
</aside>
);
}
window.MaestroKitSidebar = Sidebar;