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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user