5a1e185b1a
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>
398 lines
23 KiB
React
398 lines
23 KiB
React
// 左栏: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;
|