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
+16
View File
@@ -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;
+32
View File
@@ -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="智能" />
```
+17
View File
@@ -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;
+28
View File
@@ -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>
);
}
+6
View File
@@ -0,0 +1,6 @@
输入框:bg-deep 底、直角、focus 时绿 dim 描边+ring。占位文案给真实示例(如 `/path/to/repo`)。
```jsx
<Input label="名称" required placeholder="my-project" />
<Input label="校验命令" placeholder="npm test(可空)" />
```
+13
View File
@@ -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;
+69
View File
@@ -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>
);
}
+8
View File
@@ -0,0 +1,8 @@
下拉选择,与 Input 同形制。选项标签遵循「英文值 · 中文说明」惯例。
```jsx
<Select label="工作模式" options={[
{ value: 'manual', label: 'manual · 手动' },
{ value: 'auto-easy', label: 'auto-easy · 自动执行 Easy' },
]} />
```
+16
View File
@@ -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;
+17
View File
@@ -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} />
```
+27
View File
@@ -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>