911ceca079
Select 组件(用户拍板:下拉框全端共用一个真相源): - design/components/forms/Select.jsx:自绘下拉——触发器同 Input (control-sm/radius-sm/border-1 + chevronDown),弹层同菜单 (radius-md/shadow-menu、选中项前置对勾、hover surface-2), 外点/Esc 收起;登记簿加卡;icons.js 新增 chevronDown 并登记 icon-map - 原型 SettingsTray 与桌面 SettingsApp 同步换用(替换原生 select—— 其系统弹层样式/位置不可控,即 12A 截图问题) 原型组件运行时可再生(修复单源缺口): - design-pipeline/bundle-components.mjs:用 desktop 现成 esbuild 重建 _ds_bundle.js(此前为 Claude 导出产物、仓库内无法再生,组件一改即 过期);import 'react' 经全局 shim,命名空间不变;已实测原型页正常 - _ds_bundle.js 重建(15 个组件,含 Select/Icon) 设置窗口无边框: - tauri.conf settings 窗口 titleBarStyle=Overlay + hiddenTitle, 系统红绿灯悬浮左上;SettingsApp 自绘顶条(surface-2 + 下边框 + data-tauri-drag-region 可拖拽),对齐原型 MacWindow 规格 合并 feat/design-truth-source(真相源与闸体系)进本分支 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
4.4 KiB
React
111 lines
4.4 KiB
React
// Select:自绘下拉选择器(真相源组件,全端外观基准)。
|
||
// 原生 <select> 的弹层是系统样式且位置不可控——设计语言要求:触发器同 Input
|
||
// (control-sm / radius-sm / border-1),弹层同菜单(radius-md / shadow-menu),
|
||
// 选中项前置对勾。iOS/Android 以本组件登记规格 + 各自 DuduTheme 镜像实现。
|
||
import { useEffect, useRef, useState } from 'react';
|
||
import { Icon } from '../core/Icon';
|
||
|
||
const ddSelectCss = `
|
||
.dd-select { position: relative; display: inline-block; font-family: var(--font-sans); }
|
||
.dd-select__trigger {
|
||
display: inline-flex; align-items: center; gap: 6px;
|
||
height: var(--control-sm); padding: 0 26px 0 10px;
|
||
border-radius: var(--radius-sm); border: 1px solid var(--border-1);
|
||
background: var(--surface-card); color: var(--text-1);
|
||
font-size: var(--text-sm); font-family: inherit; cursor: pointer;
|
||
transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
|
||
white-space: nowrap;
|
||
}
|
||
.dd-select__trigger:hover { background: var(--surface-2); }
|
||
.dd-select__trigger:focus-visible { outline: none; box-shadow: var(--focus-ring); }
|
||
.dd-select__chev {
|
||
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
||
pointer-events: none; color: var(--text-3); display: inline-flex;
|
||
}
|
||
.dd-select__menu {
|
||
position: absolute; top: calc(100% + 4px); right: 0; z-index: 60;
|
||
min-width: 100%; max-width: 300px; padding: 4px; margin: 0;
|
||
background: var(--surface-card); border: 1px solid var(--border-1);
|
||
border-radius: var(--radius-md); box-shadow: var(--shadow-menu);
|
||
list-style: none;
|
||
}
|
||
.dd-select__opt {
|
||
display: flex; align-items: center; gap: 7px; width: 100%;
|
||
height: 28px; padding: 0 10px 0 8px; border: none; border-radius: var(--radius-sm);
|
||
background: transparent; color: var(--text-1);
|
||
font-size: var(--text-sm); font-family: inherit; cursor: pointer;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left;
|
||
}
|
||
.dd-select__opt:hover { background: var(--surface-2); }
|
||
.dd-select__opt .dd-select__tick { width: 14px; flex: none; display: inline-flex; color: var(--accent-text); visibility: hidden; }
|
||
.dd-select__opt[aria-selected="true"] .dd-select__tick { visibility: visible; }
|
||
`;
|
||
|
||
if (typeof document !== 'undefined' && !document.getElementById('dd-select-css')) {
|
||
const s = document.createElement('style');
|
||
s.id = 'dd-select-css';
|
||
s.textContent = ddSelectCss;
|
||
document.head.appendChild(s);
|
||
}
|
||
|
||
/**
|
||
* options: Array<string | { value, label }>;value/onChange 受控。
|
||
* placeholder 在 value 无匹配项时显示。
|
||
*/
|
||
export function Select({ value, onChange, options = [], placeholder = '请选择', ...rest }) {
|
||
const [open, setOpen] = useState(false);
|
||
const rootRef = useRef(null);
|
||
|
||
const items = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
||
const current = items.find((o) => o.value === value);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onDown = (e) => {
|
||
if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
|
||
};
|
||
const onKey = (e) => {
|
||
if (e.key === 'Escape') setOpen(false);
|
||
};
|
||
document.addEventListener('mousedown', onDown);
|
||
document.addEventListener('keydown', onKey);
|
||
return () => {
|
||
document.removeEventListener('mousedown', onDown);
|
||
document.removeEventListener('keydown', onKey);
|
||
};
|
||
}, [open]);
|
||
|
||
return (
|
||
<span className="dd-select" ref={rootRef} {...rest}>
|
||
<button
|
||
type="button"
|
||
className="dd-select__trigger"
|
||
aria-haspopup="listbox"
|
||
aria-expanded={open}
|
||
onClick={() => setOpen((v) => !v)}
|
||
>
|
||
{current ? current.label : placeholder}
|
||
</button>
|
||
<span className="dd-select__chev"><Icon name="chevronDown" size={13} /></span>
|
||
{open && (
|
||
<ul className="dd-select__menu" role="listbox">
|
||
{items.map((o) => (
|
||
<li key={o.value}>
|
||
<button
|
||
type="button"
|
||
className="dd-select__opt"
|
||
role="option"
|
||
aria-selected={o.value === value}
|
||
onClick={() => { onChange?.(o.value); setOpen(false); }}
|
||
>
|
||
<span className="dd-select__tick"><Icon name="check" size={12} sw={2.5} /></span>
|
||
{o.label}
|
||
</button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</span>
|
||
);
|
||
}
|