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:
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Maestro 按钮。直角、等宽字体、hover 抬升或辉光。
|
||||
* @startingPoint section="Components" subtitle="五种变体的等宽直角按钮" viewport="700x180"
|
||||
*/
|
||||
export interface ButtonProps {
|
||||
/** 'default' 描边 · 'solid' 实心绿(主操作) · 'ghost' 无边框 · 'accept' 绿描边(通过) · 'reject' 红描边(驳回) */
|
||||
variant?: 'default' | 'solid' | 'ghost' | 'accept' | 'reject';
|
||||
/** 'xs' 紧凑尺寸(区块头内联动作) */
|
||||
size?: 'xs';
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit';
|
||||
onClick?: () => void;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export declare function Button(props: ButtonProps): JSX.Element;
|
||||
@@ -0,0 +1,31 @@
|
||||
const css = `
|
||||
.m-btn{font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.08em;background:transparent;color:var(--ink);border:1px solid var(--line);padding:6px 14px;cursor:pointer;transition:all .12s;border-radius:var(--radius-sm,4px);white-space:nowrap}
|
||||
.m-btn:hover{border-color:var(--muted);background:var(--panel-2)}
|
||||
.m-btn--xs{padding:2px 8px;font-size:11px}
|
||||
.m-btn--ghost{border-color:transparent;color:var(--muted)}
|
||||
.m-btn--ghost:hover{color:var(--green);border-color:var(--green-dim);background:transparent}
|
||||
.m-btn--solid{background:var(--green);color:var(--bg-deep);border-color:var(--green)}
|
||||
.m-btn--solid:hover{background:#79ec94;border-color:#79ec94;box-shadow:0 0 14px rgba(95,221,125,.35)}
|
||||
.m-btn--accept{border-color:var(--green-dim);color:var(--green)}
|
||||
.m-btn--accept:hover{background:var(--green);color:var(--bg-deep);border-color:var(--green);box-shadow:0 0 14px rgba(95,221,125,.3)}
|
||||
.m-btn--reject{border-color:var(--red-dim);color:var(--red)}
|
||||
.m-btn--reject:hover{background:var(--red);color:var(--bg-deep);border-color:var(--red);box-shadow:0 0 14px rgba(255,93,93,.3)}
|
||||
.m-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-btn-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-btn-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Button({ variant = 'default', size, disabled, onClick, type = 'button', style, children }) {
|
||||
ensureCss();
|
||||
const cls = ['m-btn'];
|
||||
if (variant !== 'default') cls.push('m-btn--' + variant);
|
||||
if (size === 'xs') cls.push('m-btn--xs');
|
||||
return (
|
||||
<button type={type} className={cls.join(' ')} disabled={disabled} onClick={onClick} style={style}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
Maestro 的按钮:直角等宽,描边为默认形态,实心绿仅用于每屏最多一个主操作;accept/reject 成对出现在审批闸。
|
||||
|
||||
```jsx
|
||||
<Button variant="solid">创建任务</Button>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
<Button variant="ghost" size="xs">+ 新建</Button>
|
||||
```
|
||||
|
||||
变体:`default`(描边)· `solid`(主操作)· `ghost`(低调内联)· `accept` / `reject`(审批对,hover 填充信号色+辉光)。`size="xs"` 用于区块头内的小动作。disabled = opacity .4。
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 复杂度徽章:HARD(红)/ MED(琥珀)/ EASY(绿),信号色 7% 透明底。
|
||||
*/
|
||||
export interface ComplexityBadgeProps {
|
||||
complexity?: 'hard' | 'medium' | 'easy';
|
||||
}
|
||||
export declare function ComplexityBadge(props: ComplexityBadgeProps): JSX.Element;
|
||||
@@ -0,0 +1,17 @@
|
||||
const css = `
|
||||
.m-cplx{flex:none;display:inline-block;font-family:var(--mono);font-size:10px;font-weight:700;letter-spacing:.14em;padding:1px 7px;border:1px solid;border-radius:var(--radius-xs,3px)}
|
||||
.m-cplx--hard{color:var(--red);border-color:var(--red-dim);background:rgba(255,93,93,.07)}
|
||||
.m-cplx--medium{color:var(--amber);border-color:var(--amber-dim);background:rgba(240,180,41,.07)}
|
||||
.m-cplx--easy{color:var(--green);border-color:var(--green-dim);background:rgba(95,221,125,.07)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-cplx-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-cplx-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const LABEL = { hard: 'HARD', medium: 'MED', easy: 'EASY' };
|
||||
|
||||
export function ComplexityBadge({ complexity = 'medium' }) {
|
||||
ensureCss();
|
||||
return <span className={'m-cplx m-cplx--' + complexity}>{LABEL[complexity] || complexity}</span>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
复杂度徽章:任务行右侧的 HARD / MED / EASY 三级标签,红/琥珀/绿 + 7% 透明信号底。
|
||||
|
||||
```jsx
|
||||
<ComplexityBadge complexity="hard" />
|
||||
<ComplexityBadge complexity="easy" />
|
||||
```
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 顶栏计数徽章:⚠ 待审批(紫脉冲)· ▸ 待执行(绿)· ◉ 执行中(青)· ⛓ 被阻塞(琥珀虚线边)· Σ 总量(中性)。hover 展开文字标签;count 为 0 时灰化。
|
||||
*/
|
||||
export interface CountBadgeProps {
|
||||
kind?: 'gate' | 'ready' | 'run' | 'blocked' | 'total';
|
||||
count?: number;
|
||||
/** 覆盖默认中文标签 */
|
||||
label?: string;
|
||||
title?: string;
|
||||
}
|
||||
export declare function CountBadge(props: CountBadgeProps): JSX.Element;
|
||||
@@ -0,0 +1,40 @@
|
||||
const css = `
|
||||
.m-bdg{display:inline-flex;align-items:center;font-family:var(--mono);font-size:11.5px;line-height:1;letter-spacing:.08em;height:28px;padding:0 10px;cursor:default;white-space:nowrap;border-radius:var(--radius-sm,4px)}
|
||||
.m-bdg .m-bdg-ico{margin-right:5px;font-size:12px;line-height:1;font-family:var(--mono)}
|
||||
.m-bdg .m-bdg-n{font-weight:700}
|
||||
.m-bdg .m-bdg-label{max-width:0;opacity:0;overflow:hidden;transition:max-width .28s ease,opacity .22s ease,margin-left .28s ease}
|
||||
.m-bdg:hover .m-bdg-label{max-width:8em;opacity:1;margin-left:5px}
|
||||
.m-bdg--gate{color:var(--violet);border:1px solid var(--violet-dim)}
|
||||
.m-bdg--gate:not(.m-bdg--zero){animation:maestro-pulse 2.2s infinite}
|
||||
.m-bdg--ready{color:var(--green);border:1px solid var(--green-dim)}
|
||||
.m-bdg--run{color:var(--cyan);border:1px solid var(--cyan-dim)}
|
||||
.m-bdg--run:not(.m-bdg--zero) .m-bdg-ico{animation:maestro-pulse .9s infinite}
|
||||
.m-bdg--blocked{color:var(--amber);border:1px dashed var(--amber-dim)}
|
||||
.m-bdg--total{color:var(--ink);border:1px solid var(--line)}
|
||||
.m-bdg--zero{color:var(--faint);border-color:var(--line-soft);animation:none}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-bdg-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-bdg-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const META = {
|
||||
gate: { ico: '⚠', label: '项待审批' },
|
||||
ready: { ico: '▸', label: '待执行' },
|
||||
run: { ico: '◉', label: '执行中' },
|
||||
blocked: { ico: '⛓', label: '被阻塞' },
|
||||
total: { ico: 'Σ', label: '总量' },
|
||||
};
|
||||
|
||||
export function CountBadge({ kind = 'ready', count = 0, label, title }) {
|
||||
ensureCss();
|
||||
const m = META[kind] || META.ready;
|
||||
const cls = 'm-bdg m-bdg--' + kind + (count === 0 ? ' m-bdg--zero' : '');
|
||||
return (
|
||||
<span className={cls} title={title}>
|
||||
<span className="m-bdg-ico">{m.ico}</span>
|
||||
<span className="m-bdg-n">{count}</span>
|
||||
<span className="m-bdg-label">{label || m.label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
顶栏徽章:图标 + 数字,hover 横向展开中文标签。三种语义固定:gate ⚠ 琥珀 / ready ▸ 绿 / run ◉ 青。
|
||||
|
||||
```jsx
|
||||
<CountBadge kind="gate" count={2} />
|
||||
<CountBadge kind="ready" count={5} />
|
||||
<CountBadge kind="run" count={1} />
|
||||
```
|
||||
|
||||
count=0 自动灰化并停掉脉冲动画。
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 额度/用量计量条:LED 分段小灯 + 同色加粗百分比,用量分级自动变色(≤80% 青 / >80% 琥珀 / >95% 红),已用格带辉光。
|
||||
*/
|
||||
export interface QuotaMeterProps {
|
||||
/** 左侧小标签,如 "5h" / "周" */
|
||||
label?: string;
|
||||
/** 用量百分比 0–100 */
|
||||
pct?: number;
|
||||
/** 右侧弱化说明,如 "3h 8m 后重置" */
|
||||
detail?: string;
|
||||
/** 分段格数,默认 16 */
|
||||
cells?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function QuotaMeter(props: QuotaMeterProps): JSX.Element;
|
||||
@@ -0,0 +1,24 @@
|
||||
// 额度计量条:LED 分段 + 用量分级变色(≤80% 青 / >80% 琥珀 / >95% 红,与产品 .agent-usage 阈值对齐)
|
||||
export function QuotaMeter({ label, pct = 0, detail, cells = 16, style }) {
|
||||
const p = Math.max(0, Math.min(100, pct));
|
||||
const filled = Math.round((p / 100) * cells);
|
||||
const color = p > 95 ? 'var(--red)' : p > 80 ? 'var(--amber)' : 'var(--cyan)';
|
||||
const glow = p > 95 ? 'rgba(255,93,93,.4)' : p > 80 ? 'rgba(240,180,41,.35)' : 'rgba(89,200,216,.4)';
|
||||
return (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: 'var(--mono)', ...style }}
|
||||
title={(label ? label + ' · ' : '') + p + '%' + (detail ? ' · ' + detail : '')}>
|
||||
{label ? <span style={{ fontSize: 11, color: 'var(--muted)', letterSpacing: '.08em', flex: 'none' }}>{label}</span> : null}
|
||||
<span style={{ display: 'inline-flex', gap: 2, padding: 3, background: 'var(--bg-deep)', border: '1px solid var(--line-soft)', borderRadius: 3 }}>
|
||||
{Array.from({ length: cells }, (_, i) => (
|
||||
<span key={i} style={{
|
||||
width: 4, height: 9, borderRadius: 1,
|
||||
background: i < filled ? color : 'var(--panel-2)',
|
||||
boxShadow: i < filled ? '0 0 5px ' + glow : 'none',
|
||||
}}></span>
|
||||
))}
|
||||
</span>
|
||||
<b style={{ fontSize: 12, color }}>{p}%</b>
|
||||
{detail ? <span style={{ fontSize: 10.5, color: 'var(--faint)' }}>{detail}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
额度计量条:agent 用量/配额的 LED 分段显示,按用量自动变色(青→琥珀→红),已用格带辉光。
|
||||
|
||||
```jsx
|
||||
<QuotaMeter label="5h" pct={50} detail="3h 8m 后重置" />
|
||||
<QuotaMeter label="周" pct={78} detail="16h 38m 后重置" />
|
||||
<QuotaMeter pct={94} cells={24} />
|
||||
```
|
||||
|
||||
`pct` 决定填充与颜色(≥70% 琥珀、≥90% 红);`detail` 放重置时间等弱化说明;`cells` 调分段密度。
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 区块标题:▍色块标记 + 全大写宽字距标题,可挂右侧动作。
|
||||
*/
|
||||
export interface SectionHeadProps {
|
||||
/** ▍标记颜色:绿(默认)/ 紫(审批)/ 琥珀(警示)/ 青(agent) */
|
||||
mark?: 'green' | 'violet' | 'amber' | 'cyan';
|
||||
title?: React.ReactNode;
|
||||
/** 右侧动作(通常是 ghost xs 按钮) */
|
||||
action?: React.ReactNode;
|
||||
/** 滚动容器内吸顶 + 底缘淡出 */
|
||||
sticky?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function SectionHead(props: SectionHeadProps): JSX.Element;
|
||||
@@ -0,0 +1,17 @@
|
||||
export function SectionHead({ mark = 'green', title, action, sticky, style }) {
|
||||
const markColor = { green: 'var(--green)', amber: 'var(--amber)', cyan: 'var(--cyan)', violet: 'var(--violet)' }[mark] || 'var(--green)';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.22em',
|
||||
color: 'var(--muted)', textTransform: 'uppercase',
|
||||
padding: '18px 0 10px',
|
||||
...(sticky ? { position: 'sticky', top: 0, zIndex: 5, background: 'linear-gradient(var(--bg) 75%, transparent)' } : {}),
|
||||
...style,
|
||||
}}>
|
||||
<span style={{ color: markColor, flex: 'none' }}>▍</span>
|
||||
<span style={{ whiteSpace: 'nowrap', display: 'inline-flex', alignItems: 'center', minWidth: 0 }}>{title}</span>
|
||||
{action ? <span style={{ marginLeft: 'auto', flex: 'none' }}>{action}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
区块标题:▍ + 全大写 .22em 字距小标题,Maestro 每个区块的开头。mark 颜色表达区块语义(amber=审批闸、cyan=agent)。
|
||||
|
||||
```jsx
|
||||
<SectionHead title="任务树" action={<Button variant="ghost" size="xs">+ 新建任务</Button>} />
|
||||
<SectionHead mark="amber" title="审批闸 · 等待裁决" />
|
||||
```
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 任务状态 chip:圆点 + 压缩状态词,按状态机分组上色(等待类脉冲动画)。
|
||||
*/
|
||||
export interface StatusChipProps {
|
||||
/** 状态机 16 态之一;决定颜色组与动画 */
|
||||
status?: 'init' | 'analyzing' | 'plan_review' | 'decomposed' | 'speccing' | 'spec_review'
|
||||
| 'ready' | 'blocked' | 'queued' | 'executing' | 'exec_review' | 'failed'
|
||||
| 'needs_attention' | 'done' | 'paused' | 'cancelled';
|
||||
/** 覆盖默认中文标签 */
|
||||
label?: string;
|
||||
}
|
||||
export declare function StatusChip(props: StatusChipProps): JSX.Element;
|
||||
@@ -0,0 +1,46 @@
|
||||
const STATUS_LABEL = {
|
||||
init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解',
|
||||
decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案',
|
||||
ready: '可执行', blocked: '被依赖阻塞', queued: '排队中',
|
||||
executing: '执行中', exec_review: '待审/合', failed: '失败',
|
||||
needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消',
|
||||
};
|
||||
const STATUS_GROUP = {
|
||||
init: 'idle', analyzing: 'work', speccing: 'work',
|
||||
plan_review: 'gate', spec_review: 'gate', exec_review: 'gate',
|
||||
decomposed: 'container', ready: 'go', queued: 'go', executing: 'run',
|
||||
blocked: 'hold', paused: 'hold', failed: 'bad', needs_attention: 'bad',
|
||||
done: 'done', cancelled: 'dead',
|
||||
};
|
||||
const css = `
|
||||
.m-chip{flex:none;display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:11px;padding:1px 8px;border:1px solid var(--line);color:var(--muted);white-space:nowrap;border-radius:var(--radius-xs,3px)}
|
||||
.m-chip::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||
.m-chip--work{color:var(--cyan);border-color:var(--cyan-dim)}
|
||||
.m-chip--gate{color:var(--violet);border-color:var(--violet-dim)}
|
||||
.m-chip--gate::before{animation:maestro-pulse 1.4s infinite}
|
||||
.m-chip--container{color:#9bb4c8;border-color:#2c3c4a}
|
||||
.m-chip--go{color:var(--green);border-color:var(--green-dim)}
|
||||
.m-chip--run{color:var(--cyan);border-color:var(--cyan);box-shadow:0 0 10px rgba(89,200,216,.25)}
|
||||
.m-chip--run::before{animation:maestro-pulse .8s infinite}
|
||||
.m-chip--hold{color:var(--faint)}
|
||||
.m-chip--bad{color:var(--red);border-color:var(--red-dim)}
|
||||
.m-chip--bad::before{animation:maestro-pulse 1s infinite}
|
||||
.m-chip--done{color:var(--bg-deep);background:var(--green);border-color:var(--green);font-weight:700}
|
||||
.m-chip--dead{color:var(--faint);text-decoration:line-through}
|
||||
.m-chip--dead::before{background:var(--faint)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-chip-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-chip-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function StatusChip({ status = 'init', label }) {
|
||||
ensureCss();
|
||||
const group = STATUS_GROUP[status] || 'idle';
|
||||
return (
|
||||
<span className={'m-chip m-chip--' + group}>
|
||||
{label || STATUS_LABEL[status] || status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
状态 chip:把 16 个状态机状态渲染成「圆点 + 中文短词」,颜色按组(gate=琥珀脉冲、run=青辉光、done=实心绿、bad=红脉冲)。
|
||||
|
||||
```jsx
|
||||
<StatusChip status="executing" />
|
||||
<StatusChip status="exec_review" />
|
||||
<StatusChip status="done" />
|
||||
```
|
||||
|
||||
传 `status` 即可,标签与颜色自动对应;`label` 可覆盖文字。等待审批/执行中/失败的圆点会脉冲。
|
||||
@@ -0,0 +1,53 @@
|
||||
<!-- @dsCard group="Components" viewport="700x280" name="Core · 按钮与徽章" subtitle="Button 五变体 · StatusChip · ComplexityBadge · CountBadge · QuotaMeter · SectionHead" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<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>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Button, StatusChip, ComplexityBadge, CountBadge, SectionHead, QuotaMeter } = window.MaestroDesignSystem_a6a290;
|
||||
const row = { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' };
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<div style={row}>
|
||||
<Button>默认</Button>
|
||||
<Button variant="solid">创建任务</Button>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
<Button variant="ghost">+ 新建</Button>
|
||||
<Button size="xs">⟳ 同步 todo</Button>
|
||||
<Button disabled>禁用</Button>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<StatusChip status="ready" />
|
||||
<StatusChip status="executing" />
|
||||
<StatusChip status="exec_review" />
|
||||
<StatusChip status="blocked" />
|
||||
<StatusChip status="failed" />
|
||||
<StatusChip status="done" />
|
||||
<StatusChip status="cancelled" />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<ComplexityBadge complexity="hard" />
|
||||
<ComplexityBadge complexity="medium" />
|
||||
<ComplexityBadge complexity="easy" />
|
||||
<span style={{ width: 10 }}></span>
|
||||
<CountBadge kind="gate" count={2} />
|
||||
<CountBadge kind="ready" count={5} />
|
||||
<CountBadge kind="run" count={1} />
|
||||
<CountBadge kind="blocked" count={1} />
|
||||
<CountBadge kind="total" count={5} />
|
||||
<CountBadge kind="run" count={0} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<QuotaMeter label="5h" pct={50} detail="3h 8m 后重置" />
|
||||
<QuotaMeter label="周" pct={78} detail="16h 38m 后重置" />
|
||||
<QuotaMeter pct={94} />
|
||||
</div>
|
||||
<SectionHead title="任务树" style={{ padding: '4px 0 0' }} action={<Button variant="ghost" size="xs">+ 新建任务</Button>} />
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 复杂度分段选择:HARD / MEDIUM / EASY 三段,选中段填充对应信号色 dim 底。
|
||||
* 可选第四段 auto(智能 · 由模型决定复杂度),青色信号。
|
||||
*/
|
||||
export interface ComplexitySegProps {
|
||||
value?: 'hard' | 'medium' | 'easy' | 'auto';
|
||||
defaultValue?: 'hard' | 'medium' | 'easy' | 'auto';
|
||||
onChange?: (value: 'hard' | 'medium' | 'easy' | 'auto') => void;
|
||||
/** 追加 auto 段(青色):由模型决定复杂度 */
|
||||
includeAuto?: boolean;
|
||||
/** auto 段文字,默认 'AUTO'(可传本地化「智能」) */
|
||||
autoLabel?: string;
|
||||
/** 覆盖各段文字:{ hard, medium, easy, auto } 局部即可 */
|
||||
labels?: Partial<Record<'hard' | 'medium' | 'easy' | 'auto', string>>;
|
||||
}
|
||||
export declare function ComplexitySeg(props: ComplexitySegProps): JSX.Element;
|
||||
@@ -0,0 +1,32 @@
|
||||
const css = `
|
||||
.m-seg{display:inline-flex;border:1px solid var(--line);font-family:var(--mono);border-radius:var(--radius-sm,4px);overflow:hidden}
|
||||
.m-seg button{font-family:var(--mono);padding:7px 13px;cursor:pointer;font-size:11px;font-weight:700;letter-spacing:.12em;color:var(--faint);background:transparent;border:none;border-radius:0}
|
||||
.m-seg button + button{border-left:1px solid var(--line)}
|
||||
.m-seg button.on-hard{background:var(--red-dim);color:var(--red)}
|
||||
.m-seg button.on-medium{background:var(--amber-dim);color:var(--amber)}
|
||||
.m-seg button.on-easy{background:var(--green-dim);color:var(--green)}
|
||||
.m-seg button.on-auto{background:var(--cyan-dim);color:var(--cyan)}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-seg-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-seg-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const BASE_OPTS = [['hard', 'HARD'], ['medium', 'MEDIUM'], ['easy', 'EASY']];
|
||||
|
||||
export function ComplexitySeg({ value, defaultValue = 'medium', onChange, includeAuto, autoLabel = 'AUTO', labels }) {
|
||||
ensureCss();
|
||||
const [inner, setInner] = React.useState(defaultValue);
|
||||
const cur = value !== undefined ? value : inner;
|
||||
const opts = includeAuto ? [...BASE_OPTS, ['auto', autoLabel]] : BASE_OPTS;
|
||||
return (
|
||||
<span className="m-seg">
|
||||
{opts.map(([v, lab]) => (
|
||||
<button key={v} type="button" className={cur === v ? 'on-' + v : ''}
|
||||
onClick={() => { setInner(v); if (onChange) onChange(v); }}>
|
||||
{(labels && labels[v]) || lab}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
复杂度三段选择器,建任务表单用。选中段填充信号色 dim 底(红/琥珀/绿)。
|
||||
|
||||
```jsx
|
||||
<ComplexitySeg defaultValue="medium" onChange={(v) => setCplx(v)} />
|
||||
```
|
||||
|
||||
可加第四段 `auto`(青色,「智能」= 由模型决定复杂度)。`autoLabel` 传本地化文字,`labels` 覆盖任意段:
|
||||
|
||||
```jsx
|
||||
<ComplexitySeg defaultValue="auto" includeAuto autoLabel="智能" />
|
||||
```
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 文本/数字输入框:深底直角,focus 绿 dim 边框 + 1px ring。可带顶部小标签与红色必填星。
|
||||
*/
|
||||
export interface InputProps {
|
||||
/** 顶部 11px 字距标签 */
|
||||
label?: string;
|
||||
/** 标签后追加红色 * */
|
||||
required?: boolean;
|
||||
type?: 'text' | 'number';
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
width?: number | string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Input(props: InputProps): JSX.Element;
|
||||
@@ -0,0 +1,28 @@
|
||||
const css = `
|
||||
.m-label{display:flex;flex-direction:column;gap:4px;font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:.08em}
|
||||
.m-input{font-family:var(--mono);font-size:13px;background:var(--bg-deep);color:var(--ink);border:1px solid var(--line);padding:7px 10px;outline:none;transition:border-color .12s;border-radius:var(--radius-sm,4px)}
|
||||
.m-input:focus{border-color:var(--green-dim);box-shadow:0 0 0 1px var(--green-dim)}
|
||||
.m-input::placeholder{color:var(--faint)}
|
||||
.m-req{color:var(--red)}
|
||||
`;
|
||||
export function ensureFieldCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-field-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-field-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Input({ label, required, type = 'text', placeholder, value, defaultValue, onChange, width, style }) {
|
||||
ensureFieldCss();
|
||||
const ctrl = (
|
||||
<input className="m-input" type={type} placeholder={placeholder} value={value}
|
||||
defaultValue={defaultValue} style={{ width, ...style }} autoComplete="off"
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined} readOnly={value !== undefined && !onChange} />
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label">
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
输入框:bg-deep 底、直角、focus 时绿 dim 描边+ring。占位文案给真实示例(如 `/path/to/repo`)。
|
||||
|
||||
```jsx
|
||||
<Input label="名称" required placeholder="my-project" />
|
||||
<Input label="校验命令" placeholder="npm test(可空)" />
|
||||
```
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 下拉选择(自绘弹层,非原生 select):触发器与 Input 同形制,弹出磷光风格选项菜单——当前项绿色 ▍ 标记 + 抬升底。选项文案用「值 · 中文」格式(如 "manual · 手动")。
|
||||
*/
|
||||
export interface SelectProps {
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
options?: Array<string | { value: string; label: string }>;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Select(props: SelectProps): JSX.Element;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ensureFieldCss } from './Input.jsx';
|
||||
|
||||
const css = `
|
||||
.m-select{position:relative;display:inline-flex;flex-direction:column;min-width:120px}
|
||||
.m-select-trigger{font-family:var(--mono);font-size:13px;background:var(--bg-deep);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-sm,4px);padding:7px 10px;cursor:pointer;outline:none;transition:border-color .12s;display:flex;align-items:center;gap:10px;text-align:left;width:100%}
|
||||
.m-select-trigger:hover{border-color:var(--muted)}
|
||||
.m-select-trigger.open,.m-select-trigger:focus{border-color:var(--green-dim);box-shadow:0 0 0 1px var(--green-dim)}
|
||||
.m-select-value{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.m-select-caret{flex:none;font-size:8px;color:var(--faint);transition:transform .12s}
|
||||
.m-select-trigger.open .m-select-caret{transform:rotate(180deg)}
|
||||
.m-select-menu{position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:80;display:grid;background:var(--bg-deep);border:1px solid var(--line);border-radius:var(--radius-md,6px);padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.65);animation:maestro-rise .12s ease both;max-height:260px;overflow-y:auto}
|
||||
.m-select-opt{font-family:var(--mono);font-size:12.5px;text-align:left;background:transparent;color:var(--ink);border:none;border-radius:4px;padding:7px 10px 7px 8px;cursor:pointer;display:flex;align-items:center;gap:7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.m-select-opt:hover{background:var(--panel)}
|
||||
.m-select-opt.on{background:var(--panel-2);color:var(--green);font-weight:700}
|
||||
.m-select-mark{flex:none;width:8px;color:var(--green);font-size:10px}
|
||||
`;
|
||||
function ensureSelectCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-select-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-select-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Select({ label, required, options = [], value, defaultValue, onChange, style }) {
|
||||
ensureFieldCss();
|
||||
ensureSelectCss();
|
||||
const opts = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
||||
const [inner, setInner] = React.useState(defaultValue !== undefined ? defaultValue : (opts[0] ? opts[0].value : ''));
|
||||
const cur = value !== undefined ? value : inner;
|
||||
const curOpt = opts.find((o) => o.value === cur);
|
||||
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); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
|
||||
}, [open]);
|
||||
const pick = (v) => {
|
||||
setInner(v); setOpen(false);
|
||||
if (onChange) onChange(v);
|
||||
};
|
||||
const ctrl = (
|
||||
<span className="m-select" ref={ref} style={style}>
|
||||
<button type="button" className={'m-select-trigger' + (open ? ' open' : '')} onClick={() => setOpen(!open)}>
|
||||
<span className="m-select-value">{curOpt ? curOpt.label : cur}</span>
|
||||
<span className="m-select-caret">▼</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<span className="m-select-menu">
|
||||
{opts.map((o) => (
|
||||
<button type="button" key={o.value} className={'m-select-opt' + (o.value === cur ? ' on' : '')}
|
||||
onClick={() => pick(o.value)}>
|
||||
<span className="m-select-mark">{o.value === cur ? '▍' : ''}</span>{o.label}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label">
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
下拉选择,与 Input 同形制。选项标签遵循「英文值 · 中文说明」惯例。
|
||||
|
||||
```jsx
|
||||
<Select label="工作模式" options={[
|
||||
{ value: 'manual', label: 'manual · 手动' },
|
||||
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
|
||||
]} />
|
||||
```
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 多行文本:纵向可拉伸。danger 形态(红 dim 边)用于驳回意见(reject 必填理由)。
|
||||
*/
|
||||
export interface TextareaProps {
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
onChange?: (value: string) => void;
|
||||
minHeight?: number;
|
||||
/** 红 dim 边框 — 驳回意见输入 */
|
||||
danger?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Textarea(props: TextareaProps): JSX.Element;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ensureFieldCss } from './Input.jsx';
|
||||
|
||||
export function Textarea({ label, required, placeholder, value, defaultValue, onChange, minHeight = 72, danger, style }) {
|
||||
ensureFieldCss();
|
||||
const ctrl = (
|
||||
<textarea className="m-input" placeholder={placeholder} value={value} defaultValue={defaultValue}
|
||||
style={{ resize: 'vertical', minHeight, width: '100%', ...(danger ? { borderColor: 'var(--red-dim)' } : {}), ...style }}
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined} readOnly={value !== undefined && !onChange} />
|
||||
);
|
||||
if (!label) return ctrl;
|
||||
return (
|
||||
<label className="m-label" style={{ width: '100%' }}>
|
||||
<span>{label}{required ? <span className="m-req"> *</span> : null}</span>
|
||||
{ctrl}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
多行文本框。`danger` 形态用于驳回意见——Maestro 规定 reject 必须带改进意见。
|
||||
|
||||
```jsx
|
||||
<Textarea label="改动方案" placeholder="改动内容 + 为什么这么做" />
|
||||
<Textarea danger placeholder="驳回理由(必填)" minHeight={56} />
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
<!-- @dsCard group="Components" viewport="700x240" name="Forms · 表单控件" subtitle="Input · Select · Textarea(danger) · ComplexitySeg(含智能段)" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<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>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { Input, Select, Textarea, ComplexitySeg } = window.MaestroDesignSystem_a6a290;
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gap: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<Input label="名称" required placeholder="my-project" />
|
||||
<Input label="仓库路径" required placeholder="/path/to/repo" width={180} />
|
||||
<Select label="工作模式" options={[
|
||||
{ value: 'manual', label: 'manual · 手动' },
|
||||
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
|
||||
{ value: 'auto-approved', label: 'auto-approved · 自动执行已批准' },
|
||||
]} />
|
||||
<ComplexitySeg defaultValue="auto" includeAuto autoLabel="智能" />
|
||||
</div>
|
||||
<Textarea danger placeholder="驳回理由(必填)" minHeight={56} />
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 事件流条目:左侧 5px 方形色标 + 事件类型 + 时间 + 详情,类型决定颜色。放在 ul 里使用。
|
||||
*/
|
||||
export interface EventItemProps {
|
||||
/** 事件类型 → 中文标签 + 色标颜色 */
|
||||
type?: 'task.created' | 'task.updated' | 'status.changed' | 'approval.requested'
|
||||
| 'approval.granted' | 'approval.rejected' | 'run.started' | 'run.finished' | 'project.synced';
|
||||
/** 右对齐时间文字,如 "14:02:31" */
|
||||
time?: string;
|
||||
detail?: React.ReactNode;
|
||||
/** 覆盖默认类型标签 */
|
||||
label?: string;
|
||||
}
|
||||
export declare function EventItem(props: EventItemProps): JSX.Element;
|
||||
@@ -0,0 +1,38 @@
|
||||
const css = `
|
||||
.m-ev{font-family:var(--mono);padding:7px 0 7px 12px;border-bottom:1px solid var(--line-soft);position:relative;font-size:11.5px;color:var(--ink);animation:maestro-rise .25s ease both;list-style:none}
|
||||
.m-ev::before{content:'';position:absolute;left:0;top:13px;width:5px;height:5px;background:var(--m-ev-c,var(--muted));border-radius:1.5px}
|
||||
.m-ev-head{display:flex;gap:8px;align-items:baseline}
|
||||
.m-ev-type{font-weight:600;color:var(--m-ev-c,var(--ink));letter-spacing:.04em}
|
||||
.m-ev-time{color:var(--faint);font-size:10.5px;margin-left:auto;flex:none}
|
||||
.m-ev-detail{color:var(--muted);margin-top:1px;word-break:break-word}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-ev-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-ev-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const TYPE_META = {
|
||||
'task.created': ['任务创建', 'var(--green)'],
|
||||
'task.updated': ['任务更新', 'var(--muted)'],
|
||||
'status.changed': ['状态变更', 'var(--cyan)'],
|
||||
'approval.requested': ['请求审批', 'var(--violet)'],
|
||||
'approval.granted': ['审批通过', 'var(--green)'],
|
||||
'approval.rejected': ['审批驳回', 'var(--red)'],
|
||||
'run.started': ['运行开始', 'var(--cyan)'],
|
||||
'run.finished': ['运行结束', 'var(--cyan)'],
|
||||
'project.synced': ['todo 同步', 'var(--green)'],
|
||||
};
|
||||
|
||||
export function EventItem({ type = 'task.updated', time, detail, label }) {
|
||||
ensureCss();
|
||||
const [lab, color] = TYPE_META[type] || [type, 'var(--muted)'];
|
||||
return (
|
||||
<li className="m-ev" style={{ '--m-ev-c': color }}>
|
||||
<div className="m-ev-head">
|
||||
<span className="m-ev-type">{label || lab}</span>
|
||||
{time ? <span className="m-ev-time">{time}</span> : null}
|
||||
</div>
|
||||
{detail ? <div className="m-ev-detail">{detail}</div> : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
事件流条目:右栏审计流的一行——方形色标(注意:方的,不是圆点)+ 类型 + 时间 + 详情。
|
||||
|
||||
```jsx
|
||||
<ul style={{ padding: 0 }}>
|
||||
<EventItem type="approval.rejected" time="14:02" detail="重构 sync 模块 ↳ 缺少回滚方案" />
|
||||
<EventItem type="run.started" time="14:05" detail="executor · worktree wt-127" />
|
||||
</ul>
|
||||
```
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 审批闸卡片:琥珀 dim 边 + 实心琥珀闸标签 + 产出文档块 + accept/reject 动作脚。配套 GateStripe 斜纹警示条。
|
||||
*/
|
||||
export interface GateCardProps {
|
||||
/** 闸类型颜色:plan 拆解评审 · spec 方案评审 · exec 结果评审 */
|
||||
gate?: 'plan' | 'spec' | 'exec';
|
||||
/** 覆盖闸标签文字(多语言场景) */
|
||||
kindLabel?: string;
|
||||
title?: React.ReactNode;
|
||||
/** 头部右侧 meta 文字 */
|
||||
meta?: React.ReactNode;
|
||||
/** 文档块上方 .18em 字距小标签,如 "改动方案(SPEC)" */
|
||||
docLabel?: string;
|
||||
/** 产出文本,渲染进 pre.doc(左侧绿 dim 强调边) */
|
||||
doc?: string;
|
||||
/** 底部动作行(accept / reject 按钮对) */
|
||||
actions?: React.ReactNode;
|
||||
/** 折叠时隐藏正文与动作脚,仅留头部 */
|
||||
collapsed?: boolean;
|
||||
/** 显示头部右上的折叠/展开 ▾▸ 按钮 */
|
||||
foldable?: boolean;
|
||||
onToggleFold?: () => void;
|
||||
foldTitle?: string;
|
||||
/** 头部右侧自定义元素(折叠按钮左侧) */
|
||||
headerExtra?: React.ReactNode;
|
||||
/** 头部双击回调(如全屏阅读) */
|
||||
onHeaderDoubleClick?: () => void;
|
||||
/** 头部/整卡 title 提示(如「双击全屏」) */
|
||||
headerTitle?: string;
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function GateCard(props: GateCardProps): JSX.Element;
|
||||
@@ -0,0 +1,61 @@
|
||||
const css = `
|
||||
.m-gate{background:var(--panel);border:1px solid var(--violet-dim);font-family:var(--mono);color:var(--ink);animation:maestro-rise .25s ease both;border-radius:var(--radius-md,6px);overflow:hidden}
|
||||
.m-gate-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line-soft)}
|
||||
.m-gate--collapsed .m-gate-head{border-bottom:none}
|
||||
.m-gate-hx{margin-left:auto;display:flex;align-items:center;gap:6px;flex:none}
|
||||
.m-gate-fold{font-family:var(--mono);font-size:12px;line-height:1;cursor:pointer;background:transparent;color:var(--muted);border:1px solid var(--line);border-radius:var(--radius-sm,4px);padding:3px 7px;transition:color .12s,border-color .12s}
|
||||
.m-gate-fold:hover{color:var(--violet);border-color:var(--violet-dim)}
|
||||
.m-gate-kind{font-size:10.5px;font-weight:700;letter-spacing:.18em;color:var(--bg);background:var(--violet);padding:2px 8px;border-radius:var(--radius-xs,3px)}
|
||||
.m-gate-title{font-weight:600;font-size:13px}
|
||||
.m-gate-body{padding:12px 14px;font-size:12.5px}
|
||||
.m-gate-actions{display:flex;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--line-soft)}
|
||||
.m-gate-doclabel{font-size:10.5px;color:var(--muted);letter-spacing:.18em;margin:8px 0 4px}
|
||||
.m-gate-doclabel:first-child{margin-top:0}
|
||||
.m-gate-doc{background:var(--bg-deep);border:1px solid var(--line-soft);border-left:2px solid var(--green-dim);padding:10px 12px;font-family:var(--mono);font-size:12.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;max-height:280px;overflow-y:auto;color:var(--ink);margin:0;border-radius:var(--radius-sm,4px)}
|
||||
.m-gate-stripe{height:5px;background:repeating-linear-gradient(-45deg,var(--violet) 0 9px,transparent 9px 18px);opacity:.8;border-radius:3px}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-gate-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-gate-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
const GATE_LABEL = { plan: '拆解评审', spec: '方案评审', exec: '结果评审' };
|
||||
|
||||
export function GateCard({ gate = 'spec', kindLabel, title, meta, docLabel, doc, actions, children, style, collapsed, foldable, onToggleFold, foldTitle, headerExtra, onHeaderDoubleClick, headerTitle }) {
|
||||
ensureCss();
|
||||
return (
|
||||
<div className={'m-gate' + (collapsed ? ' m-gate--collapsed' : '')} style={style} onDoubleClick={onHeaderDoubleClick}>
|
||||
<div className="m-gate-head" title={headerTitle}>
|
||||
<span className="m-gate-kind">{kindLabel || GATE_LABEL[gate] || gate}</span>
|
||||
<span className="m-gate-title">{title}</span>
|
||||
{meta ? <span style={{ fontSize: 11, color: 'var(--muted)' }}>{meta}</span> : null}
|
||||
{(headerExtra || foldable) ? (
|
||||
<span className="m-gate-hx">
|
||||
{headerExtra}
|
||||
{foldable ? (
|
||||
<button type="button" className="m-gate-fold" title={foldTitle}
|
||||
onClick={(e) => { e.stopPropagation(); onToggleFold && onToggleFold(); }}>
|
||||
{collapsed ? '▸' : '▾'}
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{!collapsed ? (
|
||||
<React.Fragment>
|
||||
<div className="m-gate-body">
|
||||
{docLabel ? <div className="m-gate-doclabel">{docLabel}</div> : null}
|
||||
{doc ? <pre className="m-gate-doc">{doc}</pre> : null}
|
||||
{children}
|
||||
</div>
|
||||
{actions ? <div className="m-gate-actions">{actions}</div> : null}
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GateStripe() {
|
||||
ensureCss();
|
||||
return <div className="m-gate-stripe"></div>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
审批闸卡片——Maestro 的核心交互单元。琥珀边框 + 实心琥珀闸标签,体内放 agent 产出文档,脚部放 accept/reject 按钮对(reject 必填理由)。
|
||||
|
||||
```jsx
|
||||
<GateStripe />
|
||||
<GateCard gate="spec" title="重构 sync 模块" meta="maestro · MED"
|
||||
docLabel="改动方案(SPEC)" doc={specText}
|
||||
actions={<><Button variant="accept">✓ 通过</Button><Button variant="reject">✕ 驳回</Button></>} />
|
||||
```
|
||||
|
||||
`GateStripe` 是审批区顶部的琥珀斜纹警示条,整区一条即可。
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 基础面板:--panel 底 + 1px --line 边,直角无阴影。表单、配置区的容器。
|
||||
*/
|
||||
export interface PanelProps {
|
||||
children?: React.ReactNode;
|
||||
padding?: string | number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Panel(props: PanelProps): JSX.Element;
|
||||
@@ -0,0 +1,10 @@
|
||||
export function Panel({ children, padding = '14px', style }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 'var(--radius-md, 6px)',
|
||||
fontFamily: 'var(--mono)', color: 'var(--ink)', padding, ...style,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
基础面板容器:panel 底 + line 边 + 直角。无阴影——阴影只属于浮层。
|
||||
|
||||
```jsx
|
||||
<Panel><SectionHead title="新建项目" style={{ padding: 0 }} /> …表单… </Panel>
|
||||
```
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 状态流转时间线:左侧 2px 竖线 + 圆点的只读事件序列,用于归档详情/任务历史。
|
||||
*/
|
||||
export interface TimelineItem {
|
||||
/** 时间文字,如 "2026-06-12 14:02:31" */
|
||||
time: string;
|
||||
/** 事件文字,如 "执行中 → 待审/合" */
|
||||
text: string;
|
||||
/** 操作者/来源,如 "编排器 · 依赖满足自动放行" */
|
||||
who?: string;
|
||||
/** 圆点颜色(CSS 值),默认 --muted */
|
||||
color?: string;
|
||||
}
|
||||
export interface TimelineProps {
|
||||
items?: TimelineItem[];
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Timeline(props: TimelineProps): JSX.Element;
|
||||
@@ -0,0 +1,28 @@
|
||||
const css = `
|
||||
.m-tl{display:grid;font-family:var(--mono)}
|
||||
.m-tl-item{display:flex;gap:12px;align-items:baseline;padding:5px 0 5px 14px;font-size:12px;border-left:2px solid var(--line);position:relative;color:var(--ink)}
|
||||
.m-tl-item::before{content:'';position:absolute;left:-4px;top:11px;width:6px;height:6px;border-radius:50%;background:var(--m-tl-c,var(--muted))}
|
||||
.m-tl-time{flex:none;color:var(--faint);font-size:11px;min-width:130px}
|
||||
.m-tl-text{color:var(--ink)}
|
||||
.m-tl-who{color:var(--muted);font-size:11px}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-tl-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-tl-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Timeline({ items = [], style }) {
|
||||
ensureCss();
|
||||
return (
|
||||
<div className="m-tl" style={style}>
|
||||
{items.map((it, i) => (
|
||||
<div key={i} className="m-tl-item" style={it.color ? { '--m-tl-c': it.color } : null}>
|
||||
<span className="m-tl-time">{it.time}</span>
|
||||
<span className="m-tl-text">{it.text}</span>
|
||||
{it.who ? <span className="m-tl-who">{it.who}</span> : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
状态流转时间线:归档详情/任务历史的只读事件序列——左竖线 + 圆点 + 时间/文字/操作者三段。
|
||||
|
||||
```jsx
|
||||
<Timeline items={[
|
||||
{ time: '06-12 05:58', text: '审批驳回(方案评审):缺少回滚方案', who: '审批人 you', color: 'var(--red)' },
|
||||
{ time: '06-12 06:10', text: '写方案中 → 待确认方案', who: '编排器' },
|
||||
{ time: '06-12 06:31', text: '审批通过(方案评审)', color: 'var(--green)' },
|
||||
]} />
|
||||
```
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Toast 提示:深底 + 信号色边框与文字(ok/err/warn),底部居中堆叠,8px 上移入场。
|
||||
*/
|
||||
export interface ToastProps {
|
||||
/** 无 kind = 中性 ink;ok 绿 · err 红 · warn 琥珀 */
|
||||
kind?: 'ok' | 'err' | 'warn';
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
export declare function Toast(props: ToastProps): JSX.Element;
|
||||
@@ -0,0 +1,17 @@
|
||||
const css = `
|
||||
.m-toast{font-family:var(--mono);font-size:12.5px;background:var(--bg-deep);border:1px solid var(--line);color:var(--ink);padding:9px 16px;max-width:70vw;animation:m-toast-in .18s ease both;box-shadow:0 8px 30px rgba(0,0,0,.55);border-radius:var(--radius-md,6px)}
|
||||
.m-toast--err{border-color:var(--red);color:var(--red)}
|
||||
.m-toast--ok{border-color:var(--green-dim);color:var(--green)}
|
||||
.m-toast--warn{border-color:var(--amber-dim);color:var(--amber)}
|
||||
@keyframes m-toast-in{from{opacity:0;transform:translateY(8px)}}
|
||||
`;
|
||||
function ensureCss() {
|
||||
if (typeof document === 'undefined' || document.getElementById('m-toast-css')) return;
|
||||
const s = document.createElement('style'); s.id = 'm-toast-css'; s.textContent = css;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Toast({ kind, children, style }) {
|
||||
ensureCss();
|
||||
return <div className={'m-toast' + (kind ? ' m-toast--' + kind : '')} style={style}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Toast:屏幕底部居中堆叠的提示条,深底 + 信号色描边同色文字。文案短促:「已通过」「驳回需填理由」。
|
||||
|
||||
```jsx
|
||||
<Toast kind="ok">已通过 · 合并到 main</Toast>
|
||||
<Toast kind="err">驳回必须填写改进意见</Toast>
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- @dsCard group="Components" viewport="700x400" name="Surfaces · 面板与反馈" subtitle="GateCard 审批闸 · Toast · EventItem 事件流 · Timeline 时间线 · Panel" -->
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||||
<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>
|
||||
<style>body{margin:0;background:var(--bg);font-family:var(--mono);color:var(--ink);padding:14px 16px}</style>
|
||||
</head><body><div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { GateCard, GateStripe, Toast, EventItem, Button, Timeline } = window.MaestroDesignSystem_a6a290;
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 240px', gap: 14 }}>
|
||||
<div>
|
||||
<GateStripe />
|
||||
<GateCard gate="spec" title="重构 sync 模块" meta="maestro · MED" style={{ marginTop: 10 }}
|
||||
docLabel="改动方案(SPEC)"
|
||||
doc={"把轮询改为 WS 推送:\n1. store 层加 events 订阅\n2. web 端断线重连 + 指数退避"}
|
||||
actions={<>
|
||||
<Button variant="accept">✓ 通过</Button>
|
||||
<Button variant="reject">✕ 驳回</Button>
|
||||
</>} />
|
||||
<div style={{ display: 'grid', gap: 8, marginTop: 12, justifyItems: 'start' }}>
|
||||
<Toast kind="ok">已通过 · 进入 ready</Toast>
|
||||
<Toast kind="err">驳回必须填写改进意见</Toast>
|
||||
<Toast kind="warn">daemon 重连中…</Toast>
|
||||
</div>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--muted)', letterSpacing: '.18em', marginBottom: 6 }}>状态流转时间线</div>
|
||||
<Timeline items={[
|
||||
{ time: '06-12 05:58', text: '审批驳回(方案评审):缺少回滚方案', who: 'you', color: 'var(--red)' },
|
||||
{ time: '06-12 06:10', text: '写方案中 → 待确认方案', who: '编排器' },
|
||||
{ time: '06-12 06:31', text: '审批通过(方案评审)', who: 'you', color: 'var(--green)' },
|
||||
]} />
|
||||
</div>
|
||||
</div>
|
||||
<ul style={{ padding: 0, margin: 0 }}>
|
||||
<EventItem type="task.created" time="13:58" detail="重构 sync 模块" />
|
||||
<EventItem type="approval.requested" time="14:00" detail="spec_review · 等待裁决" />
|
||||
<EventItem type="approval.rejected" time="14:02" detail="↳ 缺少回滚方案" />
|
||||
<EventItem type="run.started" time="14:05" detail="executor · worktree wt-127" />
|
||||
<EventItem type="status.changed" time="14:09" detail="executing → exec_review" />
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user