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>
This commit is contained in:
wangjia
2026-06-13 14:29:07 +08:00
parent 0b3ebbd569
commit 5a1e185b1a
102 changed files with 10323 additions and 0 deletions
@@ -0,0 +1,93 @@
// 移动版 · 头部 + 底部 Tab 栏
const mIcon = { fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', viewBox: '0 0 24 24' };
function MIconTask({ s = 22 }) {
return <svg {...mIcon} width={s} height={s}><rect x="4" y="4" width="16" height="16" rx="2.5" /><polyline points="8 12 11 15 16 9" /></svg>;
}
function MIconGate({ s = 22 }) {
return <svg {...mIcon} width={s} height={s}><path d="M12 3 L22 21 L2 21 Z" /><line x1="12" y1="10" x2="12" y2="14" /><line x1="12" y1="17.5" x2="12" y2="17.51" /></svg>;
}
function MIconEvent({ s = 22 }) {
return <svg {...mIcon} width={s} height={s}><polyline points="2 12 7 12 10 5 14 19 17 12 22 12" /></svg>;
}
function MIconGit({ s = 22 }) {
return <svg {...mIcon} width={s} height={s}><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>;
}
function MobileMark({ scale = 1.6 }) {
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 5px rgba(95,221,125,.4))' }} />;
}
function MobileHeader({ project, counts }) {
const { CountBadge } = window.MaestroDesignSystem_a6a290;
return (
<header style={{
display: 'flex', alignItems: 'center', gap: 10, padding: '12px 14px',
borderBottom: '1px solid var(--line)', background: 'var(--bg-deep)',
position: 'sticky', top: 0, zIndex: 20,
}}>
<MobileMark />
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 700, letterSpacing: '.04em', whiteSpace: 'nowrap' }}>{project.name}</div>
<div style={{ fontSize: 10, color: 'var(--faint)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{project.branch} · {project.autonomy}</div>
</div>
<div style={{ marginLeft: 'auto', display: 'flex', gap: 6 }}>
<CountBadge kind="gate" count={counts.gate} />
<CountBadge kind="run" count={counts.run} />
</div>
</header>
);
}
function MobileTabBar({ tab, setTab, gateCount }) {
const tabs = [
{ id: 'tasks', label: '任务', icon: MIconTask },
{ id: 'gates', label: '审批', icon: MIconGate, badge: gateCount },
{ id: 'events', label: '事件', icon: MIconEvent },
{ id: 'projects', label: '项目', icon: MIconGit },
];
return (
<nav style={{
display: 'grid', gridTemplateColumns: 'repeat(4,1fr)',
borderTop: '1px solid var(--line)', background: 'var(--bg-deep)',
position: 'sticky', bottom: 0, zIndex: 20,
}}>
{tabs.map((t) => {
const active = tab === t.id;
const Icon = t.icon;
return (
<button key={t.id} onClick={() => setTab(t.id)} style={{
fontFamily: 'var(--mono)', background: 'transparent', border: 'none', cursor: 'pointer',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
padding: '9px 0 10px', minHeight: 56, position: 'relative',
color: active ? 'var(--green)' : 'var(--faint)',
textShadow: active ? '0 0 10px rgba(95,221,125,.45)' : 'none',
}}>
<span style={{ position: 'relative', filter: active ? 'drop-shadow(0 0 6px rgba(95,221,125,.5))' : 'none' }}>
<Icon />
{t.badge ? (
<span style={{
position: 'absolute', top: -4, right: -8, minWidth: 15, height: 15,
fontSize: 9.5, fontWeight: 700, lineHeight: '15px', textAlign: 'center',
background: 'var(--violet)', color: 'var(--bg-deep)', borderRadius: 8, padding: '0 3px',
}}>{t.badge}</span>
) : null}
</span>
<span style={{ fontSize: 10, letterSpacing: '.12em' }}>{t.label}</span>
</button>
);
})}
</nav>
);
}
Object.assign(window, { MaestroMobHeader: MobileHeader, MaestroMobTabBar: MobileTabBar });
@@ -0,0 +1,198 @@
// 移动版 · 四个 Tab 视图:任务 / 审批 / 事件 / 项目
function MobTaskRow({ task, depth = 0, expandedId, setExpandedId }) {
const { ComplexityBadge, StatusChip } = window.MaestroDesignSystem_a6a290;
const kids = task.children || [];
const expanded = expandedId === task.id;
const isGate = ['plan_review', 'spec_review', 'exec_review'].includes(task.status);
return (
<React.Fragment>
<div onClick={() => setExpandedId(expanded ? null : task.id)} style={{
display: 'flex', flexDirection: 'column', gap: 6,
padding: '12px 14px', minHeight: 44, cursor: 'pointer',
paddingLeft: 14 + depth * 16,
borderBottom: '1px solid var(--line-soft)',
borderLeft: depth ? '1px solid var(--line-soft)' : 'none',
background: expanded ? 'var(--panel-2)' : isGate ? 'rgba(184,142,245,.05)' : 'transparent',
}}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ fontWeight: 500, fontSize: 14, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{task.title}</span>
<span style={{ color: 'var(--faint)', fontSize: 10.5, flex: 'none' }}>{task.id}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<ComplexityBadge complexity={task.cplx} />
<StatusChip status={task.status} />
<span style={{ marginLeft: 'auto', fontSize: 10.5, color: task.prio === 'P0' ? 'var(--amber)' : 'var(--faint)' }}>{task.prio}</span>
</div>
</div>
{expanded ? (
<div style={{ background: 'var(--panel)', borderBottom: '1px solid var(--line)', borderLeft: '2px solid var(--green-dim)', padding: '12px 14px', animation: 'maestro-rise .2s ease both' }}>
{(task.doc || task.ops) ? (
<React.Fragment>
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 4 }}>{task.doc ? '改动方案(SPEC' : '将执行的操作(OPERATIONS'}</div>
<pre style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '10px 12px', fontFamily: 'var(--mono)', fontSize: 12, whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0, color: 'var(--ink)', lineHeight: 1.6 }}>{task.doc || task.ops}</pre>
</React.Fragment>
) : (
<div style={{ padding: '10px 12px', border: '1px dashed var(--line)', borderRadius: 6, color: 'var(--faint)', fontStyle: 'italic', fontSize: 12 }}> Claude Code 产出 MCP 写入并提交评审</div>
)}
</div>
) : null}
{kids.map((k) => <MobTaskRow key={k.id} task={k} depth={depth + 1} expandedId={expandedId} setExpandedId={setExpandedId} />)}
</React.Fragment>
);
}
function MobTasks({ tasks, agents }) {
const { SectionHead } = window.MaestroDesignSystem_a6a290;
const [expandedId, setExpandedId] = React.useState(null);
return (
<div>
{agents.length ? (
<div style={{ margin: '12px 14px 0', display: 'flex', gap: 10, alignItems: 'center', background: 'var(--panel)', border: '1px solid var(--cyan-dim)', borderRadius: 6, padding: '10px 12px' }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--cyan)', boxShadow: '0 0 8px var(--cyan)', animation: 'maestro-pulse .9s infinite', flex: 'none' }}></span>
<span style={{ fontSize: 12, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{agents[0].title}</span>
<span style={{ marginLeft: 'auto', flex: 'none', fontSize: 10, letterSpacing: '.1em', color: 'var(--cyan)', border: '1px solid var(--cyan-dim)', borderRadius: 3, padding: '0 6px' }}>{agents[0].kind}</span>
</div>
) : null}
<SectionHead title="任务树" style={{ padding: '14px 14px 8px' }} />
<div>
{tasks.map((t) => <MobTaskRow key={t.id} task={t} expandedId={expandedId} setExpandedId={setExpandedId} />)}
</div>
</div>
);
}
function MobGatePreview({ item, onDecide, onClose }) {
const { ComplexityBadge, Button, Textarea } = window.MaestroDesignSystem_a6a290;
const [rejecting, setRejecting] = React.useState(false);
const [reason, setReason] = React.useState('');
if (!item) return null;
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 1300, display: 'flex', flexDirection: 'column', background: 'var(--bg)', animation: 'maestro-rise .18s ease both' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '12px 14px', borderBottom: '1px solid var(--line)', background: 'var(--panel-2)', flex: 'none' }}>
<span style={{ fontSize: 10.5, fontWeight: 700, letterSpacing: '.18em', color: 'var(--bg)', background: 'var(--violet)', padding: '2px 8px', borderRadius: 3, flex: 'none' }}>{item.gateLabel}</span>
<span style={{ fontWeight: 700, fontSize: 14, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.title}</span>
<span style={{ flex: 1 }}></span>
<button onClick={onClose} title="关闭" style={{ fontFamily: 'var(--mono)', fontSize: 15, background: 'transparent', color: 'var(--muted)', border: 'none', cursor: 'pointer', padding: '8px 10px', minHeight: 44 }}></button>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '14px 16px 24px' }}>
<div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 10 }}>{item.meta}</div>
{item.docLabel ? <div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', margin: '0 0 6px' }}>{item.docLabel}</div> : null}
<div style={{ background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderLeft: '2px solid var(--green-dim)', borderRadius: 4, padding: '12px 14px', fontFamily: 'var(--mono)', fontSize: 13.5, lineHeight: 1.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: 'var(--ink)' }}>{item.doc}</div>
</div>
<div style={{ flex: 'none', padding: '12px 14px calc(12px + env(safe-area-inset-bottom))', borderTop: '1px solid var(--line)', background: 'var(--panel-2)', display: 'grid', gap: 10 }}>
{rejecting ? (
<React.Fragment>
<Textarea danger placeholder="改进意见(必填)" minHeight={56} value={reason} onChange={setReason} />
<div style={{ display: 'flex', gap: 10 }}>
<Button variant="reject" disabled={!reason.trim()} style={{ flex: 1, minHeight: 44 }}
onClick={() => { onDecide(item, 'reject', reason); onClose(); }}>确认驳回</Button>
<Button style={{ flex: 1, minHeight: 44 }} onClick={() => { setRejecting(false); setReason(''); }}>取消</Button>
</div>
</React.Fragment>
) : (
<div style={{ display: 'flex', gap: 10 }}>
<Button variant="accept" style={{ flex: 1, minHeight: 44 }} onClick={() => { onDecide(item, 'accept'); onClose(); }}> 通过</Button>
<Button variant="reject" style={{ flex: 1, minHeight: 44 }} onClick={() => setRejecting(true)}> 驳回</Button>
</div>
)}
</div>
</div>
);
}
function MobGates({ approvals, onDecide }) {
const { GateCard, GateStripe, Button, Textarea, SectionHead } = window.MaestroDesignSystem_a6a290;
const [rejecting, setRejecting] = React.useState(null);
const [reason, setReason] = React.useState('');
const [preview, setPreview] = React.useState(null);
if (!approvals.length) {
return <div style={{ margin: '24px 14px', padding: '38px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, fontSize: 12 }}>无待裁决审批</div>;
}
return (
<div style={{ padding: '12px 14px' }}>
<GateStripe />
<SectionHead mark="violet" title="审批闸 · 等待裁决" style={{ padding: '10px 0 8px', color: 'var(--violet)' }} />
{approvals.map((a) => (
<div key={a.id} style={{ marginBottom: 12 }}>
<GateCard gate={a.gate} title={a.title} meta={a.meta} docLabel={a.docLabel} doc={a.doc}
actions={
<React.Fragment>
<Button variant="accept" style={{ flex: 1, minHeight: 44 }} onClick={() => onDecide(a, 'accept')}> 通过</Button>
<Button variant="reject" style={{ flex: 1, minHeight: 44 }} onClick={() => setRejecting(rejecting === a.id ? null : a.id)}> 驳回</Button>
<Button variant="ghost" style={{ minHeight: 44 }} title="全屏阅读"
onClick={() => setPreview({ ...a, gateLabel: { plan: '拆解评审', spec: '方案评审', exec: '结果评审' }[a.gate] || a.gate })}></Button>
</React.Fragment>
}>
{rejecting === a.id ? (
<div style={{ display: 'grid', gap: 10, marginTop: 10 }}>
<Textarea danger placeholder="改进意见(必填)" minHeight={56} value={reason} onChange={setReason} />
<Button variant="reject" disabled={!reason.trim()} style={{ minHeight: 44 }}
onClick={() => { onDecide(a, 'reject', reason); setRejecting(null); setReason(''); }}>
确认驳回
</Button>
</div>
) : null}
</GateCard>
</div>
))}
<MobGatePreview item={preview} onDecide={onDecide} onClose={() => setPreview(null)} />
</div>
);
}
function MobEvents({ events }) {
const { EventItem, SectionHead } = window.MaestroDesignSystem_a6a290;
return (
<div>
<SectionHead title="事件流" style={{ padding: '14px 14px 8px' }} />
<ul style={{ listStyle: 'none', padding: '0 14px', margin: 0 }}>
{events.map((e, i) => <EventItem key={i} type={e.type} time={e.time} detail={e.detail} />)}
</ul>
</div>
);
}
function MobProjects({ projects, currentId, onSelect, project }) {
const { SectionHead, Select, Input, Button } = window.MaestroDesignSystem_a6a290;
return (
<div>
<SectionHead title="项目" style={{ padding: '14px 14px 8px' }} />
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{projects.map((p) => {
const active = p.id === currentId;
return (
<li key={p.id} onClick={() => onSelect(p.id)} style={{
display: 'flex', alignItems: 'center', gap: 10, minHeight: 52,
padding: '10px 14px', cursor: 'pointer',
borderBottom: '1px solid var(--line-soft)',
borderLeft: '2px solid ' + (active ? 'var(--green)' : 'transparent'),
background: active ? 'var(--panel-2)' : 'transparent',
color: active ? 'var(--green)' : 'var(--muted)',
}}>
<svg width="16" height="16" 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>
<span style={{ display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }}>
<span style={{ fontWeight: 600, fontSize: 13, color: active ? 'var(--green)' : 'var(--ink)' }}>{p.name}</span>
<span style={{ fontSize: 10.5, color: 'var(--faint)' }}>{p.path}</span>
</span>
{active ? <span style={{ marginLeft: 'auto', fontSize: 10, letterSpacing: '.12em', color: 'var(--green)' }}>当前</span> : null}
</li>
);
})}
</ul>
<SectionHead title="项目配置" style={{ padding: '18px 14px 8px' }} />
<div style={{ margin: '0 14px', background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: 14, display: 'grid', gap: 12 }}>
<Input label="最大并发" type="number" defaultValue={String(project.concurrency)} />
<Select label="工作模式" defaultValue={project.autonomy} options={[
{ value: 'manual', label: 'manual · 手动' },
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
{ value: 'auto-approved', label: 'auto-approved · 自动执行已批准' },
]} />
<Button variant="solid" style={{ minHeight: 44 }}>保存配置</Button>
</div>
</div>
);
}
Object.assign(window, { MaestroMobTasks: MobTasks, MaestroMobGates: MobGates, MaestroMobEvents: MobEvents, MaestroMobProjects: MobProjects });
+90
View File
@@ -0,0 +1,90 @@
<!-- @dsCard group="Console" viewport="390x760" name="移动版调度台" subtitle="单栏 + 底部 Tab(任务/审批/事件/项目)· 触控目标 ≥44px · 可点击" -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MAESTRO · 移动版调度台</title>
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<script src="../console/data.js"></script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { background: var(--bg); color: var(--ink); font-family: var(--mono); font-size: 13px; line-height: 1.55; display: flex; justify-content: center; }
body::before {
content: ''; position: fixed; inset: 0; z-index: 999; pointer-events: none;
background:
repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px),
radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.5));
opacity: .5;
}
::selection { background: var(--green-dim); color: #fff; }
#root { width: 100%; max-width: 430px; height: 100vh; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="MobileChrome.jsx"></script>
<script type="text/babel" src="MobileViews.jsx"></script>
<script type="text/babel">
const { Toast } = window.MaestroDesignSystem_a6a290;
const M = window.MAESTRO_MOCK;
function App() {
const [tab, setTab] = React.useState('tasks');
const [currentId, setCurrentId] = React.useState('p1');
const [approvals, setApprovals] = React.useState(M.approvals);
const [events, setEvents] = React.useState(M.events);
const [toasts, setToasts] = React.useState([]);
const project = M.projects.find((p) => p.id === currentId);
const isMain = currentId === 'p1';
const tasks = isMain ? M.tasks : [];
const agents = isMain ? M.agents : [];
const gates = isMain ? approvals : [];
const toast = (kind, text) => {
const id = Date.now();
setToasts((t) => [...t, { id, kind, text }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2600);
};
const now = () => new Date().toTimeString().slice(0, 8);
const decide = (a, action, reason) => {
setApprovals((list) => list.filter((x) => x.id !== a.id));
if (action === 'accept') {
setEvents((ev) => [{ type: 'approval.granted', time: now(), detail: a.title + ' · ' + a.gate + '_review 通过' }, ...ev]);
toast('ok', '已通过 · ' + a.title);
} else {
setEvents((ev) => [{ type: 'approval.rejected', time: now(), detail: a.title + ' ↳ ' + reason }, ...ev]);
toast('warn', '已驳回 · 退回重做');
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', borderLeft: '1px solid var(--line-soft)', borderRight: '1px solid var(--line-soft)', background: 'var(--bg)' }}>
<window.MaestroMobHeader project={project} counts={{ gate: gates.length, run: agents.length }} />
<main style={{ flex: 1, overflowY: 'auto', paddingBottom: 16 }}>
{tab === 'tasks' ? (
tasks.length
? <window.MaestroMobTasks tasks={tasks} agents={agents} />
: <div style={{ margin: '24px 14px', padding: '38px 0', textAlign: 'center', color: 'var(--faint)', border: '1px dashed var(--line)', borderRadius: 6, fontSize: 12 }}>该项目暂无任务</div>
) : null}
{tab === 'gates' ? <window.MaestroMobGates approvals={gates} onDecide={decide} /> : null}
{tab === 'events' ? <window.MaestroMobEvents events={events} /> : null}
{tab === 'projects' ? <window.MaestroMobProjects projects={M.projects} currentId={currentId} project={project}
onSelect={(id) => { setCurrentId(id); toast('ok', '已切换 · ' + M.projects.find((p) => p.id === id).name); }} /> : null}
</main>
<window.MaestroMobTabBar tab={tab} setTab={setTab} gateCount={gates.length} />
<div style={{ position: 'fixed', bottom: 72, left: '50%', transform: 'translateX(-50%)', zIndex: 1200, display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', width: 'max-content', maxWidth: '90vw' }}>
{toasts.map((t) => <Toast key={t.id} kind={t.kind}>{t.text}</Toast>)}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>