feat(console): 任务树筛选/折叠/统一标题 + 侧栏全局Agent卡片/拖拽排序/全局设置弹框/记忆项目 + 状态点真实化

任务树:四维多选下拉筛选(状态/复杂度/优先级/执行者)、已选置顶、新建任务按钮移右上角(0任务也能建)。
四块主区(Agent/审批闸/任务树/归档)统一标题组件(图标+名称+数量徽标)+ 折叠按钮(localStorage 记忆)。
侧栏:全局 Agent 弹层改常驻卡片(项目默认3+折叠+按活跃排序)、顶部项目拖拽排序、全局设置移入用户菜单弹框(保存/取消,落库)、记忆当前项目。
adaptProject 用后端 summary 的 running/attention 判状态点颜色(青/琥珀/灰)+ 紫色关注数徽标;agent 标题不再越界;i18n 5 语言补全。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 11:30:32 +08:00
parent 43fa1f7dbb
commit 8765e61498
9 changed files with 430 additions and 194 deletions
+170 -84
View File
@@ -87,8 +87,84 @@ const sIco = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke
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 }) {
// 全局设置弹框:用户级默认配置(新建项目套用)。保存 / 取消,落库经 onSave。
function SettingsModal({ settings, onSave, onClose, t }) {
const { Button, Input, Select } = window.MaestroDesignSystem_a6a290;
const s = settings || {};
const [autonomy, setAutonomy] = React.useState(s.autonomy || 'manual');
const [concurrency, setConcurrency] = React.useState(String(s.concurrency ?? 1));
const [maxRetries, setMaxRetries] = React.useState(String(s.maxRetries ?? 2));
const [timeoutMin, setTimeoutMin] = React.useState(String(Math.round((s.timeoutMs ?? 1800000) / 60000)));
const [autoPlan, setAutoPlan] = React.useState(!!s.autoApprovePlan);
const [autoExec, setAutoExec] = React.useState(!!s.autoApproveExec);
const [budgetUsd, setBudgetUsd] = React.useState(s.budgetUsd != null ? String(s.budgetUsd) : '');
const [budgetPeriod, setBudgetPeriod] = React.useState(s.budgetPeriod || 'month');
const [model, setModel] = React.useState(s.model || '');
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
const submit = () => {
onSave({
autonomy, concurrency: Number(concurrency) || 1, maxRetries: Number(maxRetries) || 0,
timeoutMs: Math.max(1, Number(timeoutMin) || 30) * 60000,
autoApprovePlan: autoPlan, autoApproveExec: autoExec,
budgetUsd: budgetUsd.trim() === '' ? null : Number(budgetUsd),
budgetPeriod, model: model.trim() === '' ? null : model.trim(),
});
onClose();
};
const Toggle = ({ on, set }) => (
<button type="button" onClick={() => set(!on)} style={{
alignSelf: 'flex-start', fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 700, letterSpacing: '.08em',
background: on ? 'rgba(95,221,125,.1)' : 'var(--bg-deep)', color: on ? 'var(--green)' : 'var(--faint)',
border: '1px solid ' + (on ? 'var(--green-dim)' : 'var(--line)'), borderRadius: 'var(--radius-sm,4px)',
padding: '6px 14px', cursor: 'pointer', transition: 'all .1s',
}}>{on ? 'ON' : 'OFF'}</button>
);
const field = (label, node) => (
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11, color: 'var(--muted)', letterSpacing: '.05em' }}>
<span>{label}</span>{node}
</label>
);
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 2000, display: 'grid', placeItems: 'center' }}>
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(4,6,5,.86)', backdropFilter: 'blur(3px)' }}></div>
<div style={{
position: 'relative', width: 460, maxWidth: '92vw', maxHeight: '88vh', overflowY: 'auto',
background: 'var(--bg-deep)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
boxShadow: '0 18px 50px rgba(0,0,0,.7)', padding: '18px 20px', animation: 'maestro-rise .14s ease both',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: 700, letterSpacing: '.16em', color: 'var(--muted)', textTransform: 'uppercase', marginBottom: 3 }}>
<span style={{ color: 'var(--green)' }}></span>{t.gGlobalSettings}
</div>
<div style={{ fontSize: 11, color: 'var(--faint)', marginBottom: 15 }}>{t.gSettingsHint}</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 13 }}>
{field(t.workMode, <Select value={autonomy} onChange={setAutonomy} options={Object.entries(t.autonomy).map(([value, label]) => ({ value, label }))} />)}
{field(t.maxConcurrency, <Input type="number" value={concurrency} onChange={setConcurrency} style={{ width: '100%' }} />)}
{field(t.gMaxRetries, <Input type="number" value={maxRetries} onChange={setMaxRetries} style={{ width: '100%' }} />)}
{field(t.gTimeoutMin, <Input type="number" value={timeoutMin} onChange={setTimeoutMin} style={{ width: '100%' }} />)}
{field(t.gAutoPlan, <Toggle on={autoPlan} set={setAutoPlan} />)}
{field(t.gAutoExec, <Toggle on={autoExec} set={setAutoExec} />)}
{field(t.gBudget, <Input type="number" placeholder="∞" value={budgetUsd} onChange={setBudgetUsd} style={{ width: '100%' }} />)}
{field(t.gBudgetPeriod, <Select value={budgetPeriod} onChange={setBudgetPeriod} options={[{ value: 'day', label: t.gPeriodDay }, { value: 'month', label: t.gPeriodMonth }]} />)}
</div>
<div style={{ marginTop: 13 }}>
{field(t.model, <Input placeholder={t.modelPh} value={model} onChange={setModel} style={{ width: '100%' }} />)}
</div>
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 18 }}>
<Button onClick={onClose}>{t.cancel}</Button>
<Button variant="solid" onClick={submit}>{t.save}</Button>
</div>
</div>
</div>
);
}
function UserRow({ user, t, collapsed, settings, onSaveSettings }) {
const [open, setOpen] = React.useState(false);
const [showSettings, setShowSettings] = React.useState(false);
const ref = React.useRef(null);
React.useEffect(() => {
if (!open) return;
@@ -133,13 +209,18 @@ function UserRow({ user, t, collapsed }) {
<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 }}
{[
{ label: t.gGlobalSettings, onClick: () => { setShowSettings(true); setOpen(false); }, red: false },
{ label: t.gUserSettings, onClick: null, red: false },
{ label: t.gUserSignOut, onClick: null, red: true },
].map((it) => (
<div key={it.label} onClick={it.onClick || undefined} style={{ padding: '6px 10px', fontSize: 12, color: it.red ? '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>
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>{it.label}</div>
))}
</div>
) : null}
{showSettings ? <SettingsModal settings={settings} onSave={onSaveSettings} onClose={() => setShowSettings(false)} t={t} /> : null}
</div>
);
}
@@ -150,83 +231,70 @@ function fmtTokens(n) {
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);
// 全局 AGENT 卡片(内联常驻;按项目默认 3 个、多余折叠、按最近活跃排序)
function AgentCard({ global, summary, t, collapsed }) {
const [showAll, setShowAll] = React.useState(false);
if (collapsed) return null;
// 「最近活跃」排序:运行中优先,其次本周 token 用量(无活跃时间戳时的代理)
const projs = [...summary.byProject].sort((a, b) => (b.active - a.active) || (b.tokens - a.tokens));
const maxTok = Math.max(...projs.map((p) => p.tokens), 1);
const shown = showAll ? projs : projs.slice(0, 3);
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 style={{
margin: '10px 12px 4px', border: '1px solid var(--line)', borderRadius: 'var(--radius-md,6px)',
background: 'var(--bg-deep)', padding: '11px 12px',
}}>
<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)', display: 'inline-flex' }}><IcoAgents /></span>{t.gAgents.toUpperCase()}
<span style={{ marginLeft: 'auto', fontWeight: 400, letterSpacing: '.04em', color: 'var(--faint)' }}>{t.apWeek}</span>
</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 style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1, background: 'var(--line-soft)', border: '1px solid var(--line-soft)', borderRadius: 4, overflow: 'hidden', marginBottom: 11 }}>
{[
[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: '7px 9px' }}>
<div style={{ fontSize: 15, fontWeight: 700, color: c, letterSpacing: '.02em' }}>{v}</div>
<div style={{ fontSize: 9.5, color: 'var(--muted)', letterSpacing: '.06em', marginTop: 1 }}>{k}</div>
</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>
{/* 按项目(默认 3,多余折叠,按活跃排序)*/}
<div style={{ fontSize: 9.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--faint)', marginBottom: 7 }}>{t.apPerProj}</div>
<div style={{ display: 'grid', gap: 8 }}>
{shown.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 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>
))}
</div>
{projs.length > 3 ? (
<button type="button" onClick={() => setShowAll((s) => !s)} style={{
marginTop: 9, width: '100%', background: 'transparent', border: '1px dashed var(--line)', color: 'var(--faint)',
borderRadius: 4, padding: '4px 0', fontSize: 10.5, fontFamily: 'var(--mono)', cursor: 'pointer', letterSpacing: '.05em',
}}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--muted)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--faint)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
{showAll ? t.apCollapse : t.apMore.replace('{n}', projs.length - 3)}
</button>
) : null}
</div>
);
@@ -299,8 +367,19 @@ function GlobalConfigRow({ global, t, collapsed }) {
);
}
function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig }) {
function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onToggleCollapse, t, global, user, summary, onGlobalConfig, onReorder, settings, onSaveSettings }) {
const { Button } = window.MaestroDesignSystem_a6a290;
const [dragId, setDragId] = React.useState(null);
const [overId, setOverId] = React.useState(null);
const onDrop = (targetId) => {
if (dragId && dragId !== targetId && onReorder) {
const ids = projects.map((x) => x.id);
const from = ids.indexOf(dragId);
const to = ids.indexOf(targetId);
if (from > -1 && to > -1) { ids.splice(to, 0, ids.splice(from, 1)[0]); onReorder(ids); }
}
setDragId(null); setOverId(null);
};
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' }}>
@@ -328,16 +407,24 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog
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) : '')}
draggable={!collapsed}
onDragStart={(e) => { setDragId(p.id); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => { if (dragId) { e.preventDefault(); if (p.id !== dragId && overId !== p.id) setOverId(p.id); } }}
onDragLeave={() => setOverId((o) => (o === p.id ? null : o))}
onDrop={(e) => { e.preventDefault(); onDrop(p.id); }}
onDragEnd={() => { setDragId(null); setOverId(null); }}
style={{
position: 'relative',
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: 'pointer',
padding: collapsed ? '10px 0' : '9px 14px 9px 12px', cursor: dragId ? 'grabbing' : '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)',
opacity: dragId === p.id ? 0.4 : 1,
boxShadow: overId === p.id ? 'inset 0 2px 0 var(--green)' : 'none',
}}
onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel)'; }}
onMouseEnter={(e) => { if (!active && !dragId) e.currentTarget.style.background = 'var(--panel)'; }}
onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}>
<ProjectLogo project={p} size={collapsed ? 26 : 24} />
{!collapsed ? (
@@ -366,12 +453,11 @@ function Sidebar({ projects, currentId, onSelect, onNewProject, collapsed, onTog
);
})}
</ul>
{/* ── 底部:全局 agent · 全局配置 · 用户 ── */}
{/* ── 全局 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} />
<AgentCard global={global} summary={summary} t={t} collapsed={collapsed} />
</div>
<UserRow user={user} t={t} collapsed={collapsed} />
<UserRow user={user} t={t} collapsed={collapsed} settings={settings} onSaveSettings={onSaveSettings} />
<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>