diff --git a/src/renderer/src/ui/Input.tsx b/src/renderer/src/ui/Input.tsx
index 86c286e..ec8b8e7 100644
--- a/src/renderer/src/ui/Input.tsx
+++ b/src/renderer/src/ui/Input.tsx
@@ -33,6 +33,7 @@ export function Input({
wrapperClassName,
className,
disabled,
+ readOnly,
...inputProps
}: InputProps): React.JSX.Element {
const wrapperClass = classNames(
@@ -45,9 +46,10 @@ export function Input({
)
return (
-
+
{prefix ? {prefix} : null}
-
+
{suffix ? {suffix} : null}
)
diff --git a/src/renderer/src/ui/Select.tsx b/src/renderer/src/ui/Select.tsx
new file mode 100644
index 0000000..06890f3
--- /dev/null
+++ b/src/renderer/src/ui/Select.tsx
@@ -0,0 +1,82 @@
+/**
+ * YCode UI Select(P3)。
+ *
+ * 外观壳与行为复用 `components/source-select/SourceSelect.tsx`(RecoveredSelect),
+ * 不重写闭合态标记、弹层与键盘行为;这里只做尺寸命名映射与只读入口。
+ *
+ * 契约见 `docs/界面基础组件规范-2026-10-18.md` 的 Select 章节:
+ * 对外尺寸 sm|md|lg,内部映射 small|medium|large,几何沿用恢复样式。
+ */
+import SourceSelect, { type SourceSelectProps } from '../components/source-select/SourceSelect'
+import './select.css'
+
+export type SelectSize = 'sm' | 'md' | 'lg'
+
+const SOURCE_SIZE: Record
= {
+ sm: 'small',
+ md: 'medium',
+ lg: 'large'
+}
+
+export interface SelectProps extends Omit {
+ size?: SelectSize
+}
+
+function classNames(...values: Array): string {
+ return values.filter(Boolean).join(' ')
+}
+
+/** 统一下拉:透传 source-select 的全部现有 props,仅替换 size 命名。 */
+export function Select({ size = 'md', className, ...props }: SelectProps): React.JSX.Element {
+ return (
+
+ )
+}
+
+export interface StaticSelectProps {
+ /** 闭合态显示值(原样渲染,不做截断/格式化)。 */
+ value: string
+ 'aria-label': string
+ /** 外层不可用提示文案,例如「暂未接入」。 */
+ hint?: string
+ size?: SelectSize
+ /** 落到闭合态按钮上,保留页面级 `.xxx.wb-select` 布局覆盖的选择器命中。 */
+ className?: string
+}
+
+const noop = (): void => {}
+
+/**
+ * 只读/不可用的静态下拉:复用同一个 Select 闭合态壳,但停在闭合态。
+ *
+ * - `disabled` 让 RecoveredSelect/Popover 都拒绝展开,同时把按钮移出 Tab 顺序;
+ * - 恢复样式的 `.wb-select--disabled` 会降低前景对比度,`select.css` 按
+ * --yc-select-* token 把它还原成普通闭合态(不可用行要求保持原版外观);
+ * - 外层保留 data-settings-unavailable / aria-disabled / title 语义。
+ */
+export function StaticSelect({
+ value,
+ hint,
+ size = 'md',
+ className,
+ 'aria-label': ariaLabel
+}: StaticSelectProps): React.JSX.Element {
+ return (
+
+
+
+ )
+}
diff --git a/src/renderer/src/ui/Switch.tsx b/src/renderer/src/ui/Switch.tsx
index 1e54371..71ed300 100644
--- a/src/renderer/src/ui/Switch.tsx
+++ b/src/renderer/src/ui/Switch.tsx
@@ -16,6 +16,7 @@ export interface SwitchProps {
onCheckedChange?(checked: boolean): void
disabled?: boolean
readOnly?: boolean
+ tabIndex?: number
size?: SwitchSize
'aria-label': string
className?: string
@@ -30,6 +31,7 @@ export function Switch({
onCheckedChange,
disabled = false,
readOnly = false,
+ tabIndex,
size = 'md',
className,
'aria-label': ariaLabel
@@ -54,6 +56,7 @@ export function Switch({
aria-readonly={readOnly || undefined}
checked={checked}
disabled={disabled}
+ tabIndex={tabIndex}
onChange={handleChange}
/>
diff --git a/src/renderer/src/ui/input.css b/src/renderer/src/ui/input.css
index b1d8ce6..23eba63 100644
--- a/src/renderer/src/ui/input.css
+++ b/src/renderer/src/ui/input.css
@@ -38,6 +38,11 @@
outline: none;
}
+.yc-input[data-readonly='true'],
+.yc-input[data-readonly='true'] .yc-input__inner {
+ cursor: default;
+}
+
.yc-input__inner::placeholder {
color: var(--yc-input-placeholder);
}
diff --git a/src/renderer/src/ui/select.css b/src/renderer/src/ui/select.css
new file mode 100644
index 0000000..b296870
--- /dev/null
+++ b/src/renderer/src/ui/select.css
@@ -0,0 +1,32 @@
+/**
+ * YCode UI Select 样式(P3)。
+ *
+ * 常规 `Select` 直接复用 source-select 的恢复样式(闭合态几何与状态一律不重写),
+ * 因此本文件不定义基础尺寸/边框/宽度,避免盖掉页面级
+ * `.general-settings-panel .wb-select`、`.power-save-blocker-settings__select.wb-select`
+ * 这类同元素(0,2,0)覆盖。
+ *
+ * 本文件只处理 `StaticSelect`:恢复样式用 `.wb-select--disabled` 表达不可用,
+ * 而设置页不可用行必须保持「普通闭合态」外观(不允许 disabled 降低前景对比度),
+ * 所以按 --yc-select-* token 把颜色与光标还原回闭合态。
+ * 选择器权重 (0,3,0)/(0,4,0) 高于 `.source-select-host .wb-select--disabled`
+ * 的 (0,2,0)/(0,3,0),不依赖样式表先后顺序。
+ */
+
+.yc-select-static {
+ display: inline-flex;
+ min-width: 0;
+}
+
+.source-select-host .yc-select--static.wb-select--disabled {
+ color: var(--yc-select-fg-static);
+ cursor: default;
+}
+
+.source-select-host .yc-select--static.wb-select--disabled:hover {
+ border-color: var(--yc-select-border-hover);
+}
+
+.source-select-host .yc-select--static.wb-select--disabled .wb-select__caret {
+ color: var(--yc-select-icon-static);
+}
diff --git a/src/renderer/src/ui/tokens.css b/src/renderer/src/ui/tokens.css
index 5b3bdfb..09833fc 100644
--- a/src/renderer/src/ui/tokens.css
+++ b/src/renderer/src/ui/tokens.css
@@ -160,6 +160,10 @@
--yc-select-bg-disabled: #f7f7f7;
--yc-select-fg-disabled: rgba(0, 0, 0, 0.3);
+ /* Select 静态只读闭合态:等价恢复样式 .wb-select 的非 disabled 外观,深色见下方覆盖 */
+ --yc-select-fg-static: #000;
+ --yc-select-icon-static: rgba(0, 0, 0, 0.5);
+
/* Dialog */
--yc-dialog-bg: #ffffff;
--yc-dialog-border: #dedede;
@@ -274,6 +278,9 @@ html[data-theme='dark'] {
--yc-select-bg-disabled: #171717;
--yc-select-fg-disabled: rgba(255, 255, 255, 0.3);
+ --yc-select-fg-static: #fff;
+ --yc-select-icon-static: rgba(255, 255, 255, 0.5);
+
--yc-dialog-bg: #1f1f1f;
--yc-dialog-border: #3d3d3d;
--yc-dialog-overlay: rgba(0, 0, 0, 0.4);