Files
maestro/design/components/forms/Select.jsx
T
wangjia 43fa1f7dbb feat(ds): 表单输入/下拉文字改柔和灰——新增 --field-ink token 作单一真相源
输入/下拉文字此前用 --ink(近白)太刺眼且与正文耦合。新增 --field-ink token
(深 #a7b3a1 / 浅 #3c4838),Input/Select 组件引用之;改一个值即全改。
因项目无 DS 编译器,同步改 _ds_bundle.js(运行加载源)+ ambience.css 兜底,确保即时生效。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:30:17 +08:00

70 lines
3.8 KiB
React

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(--field-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(--field-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>
);
}