Files
dudu/design/components/forms/Select.jsx
T
wangjia 911ceca079
ci / server (push) Failing after 11s
ci / design-system (push) Failing after 12s
feat(design+desktop): Select 真相源组件 + 设置窗口无边框(12A 实测反馈)
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>
2026-07-11 01:00:29 +08:00

111 lines
4.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}