✨ feat(vscode): 用自定义 SelectMenu 替换原生 select 修复暗色样式
This commit is contained in:
@@ -12,6 +12,8 @@ import type { ManagedSession, ManagedSessionsData } from '../types'
|
|||||||
import { Download, Plus, Terminal, Trash2, X } from 'lucide-react'
|
import { Download, Plus, Terminal, Trash2, X } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { SelectMenu } from './ui/select-menu'
|
||||||
|
|
||||||
interface ManagedSessionsPanelProps {
|
interface ManagedSessionsPanelProps {
|
||||||
data: ManagedSessionsData
|
data: ManagedSessionsData
|
||||||
profiles: ClaudeProfile[]
|
profiles: ClaudeProfile[]
|
||||||
@@ -143,18 +145,15 @@ export function ManagedSessionsPanel({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-12 shrink-0 text-xs text-[var(--vscode-descriptionForeground)]">Profile</span>
|
<span className="w-12 shrink-0 text-xs text-[var(--vscode-descriptionForeground)]">Profile</span>
|
||||||
<select
|
<SelectMenu
|
||||||
value={selectedProfile}
|
value={selectedProfile}
|
||||||
onChange={e => setSelectedProfile(e.target.value)}
|
onChange={setSelectedProfile}
|
||||||
className="min-w-0 flex-1 rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-input-background)] px-2 py-1 text-xs text-[var(--vscode-input-foreground)] outline-none focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]"
|
options={profiles.length === 0
|
||||||
>
|
? [{ value: '', label: '(无可用 profile)' }]
|
||||||
{profiles.length === 0 && (
|
: profiles.map(p => ({ value: p.path, label: p.name }))}
|
||||||
<option value="">(无可用 profile)</option>
|
variant="input"
|
||||||
)}
|
className="min-w-0 flex-1"
|
||||||
{profiles.map(p => (
|
/>
|
||||||
<option key={p.path} value={p.path}>{p.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-12 shrink-0 text-xs text-[var(--vscode-descriptionForeground)]">名字</span>
|
<span className="w-12 shrink-0 text-xs text-[var(--vscode-descriptionForeground)]">名字</span>
|
||||||
@@ -385,24 +384,18 @@ function SessionRow({ session, profiles, onRename, onResume, onDelete, onCloseTa
|
|||||||
<span className="truncate">{basename(session.profilePath)}</span>
|
<span className="truncate">{basename(session.profilePath)}</span>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<select
|
<SelectMenu
|
||||||
value={pickedProfile}
|
value={pickedProfile}
|
||||||
|
onChange={setUserPicked}
|
||||||
title="换 profile 后点行打开;已打开的终端下次才生效"
|
title="换 profile 后点行打开;已打开的终端下次才生效"
|
||||||
onClick={e => e.stopPropagation()}
|
variant="ghost"
|
||||||
onMouseDown={e => e.stopPropagation()}
|
options={[
|
||||||
onChange={(e) => {
|
...(storedMissing
|
||||||
e.stopPropagation()
|
? [{ value: pickedProfile, label: basename(pickedProfile) || '(未设置)' }]
|
||||||
setUserPicked(e.target.value)
|
: []),
|
||||||
}}
|
...profiles.map(p => ({ value: p.path, label: p.name })),
|
||||||
className="min-w-0 bg-transparent py-0 pr-1 text-[10px] text-[var(--vscode-descriptionForeground)] outline-none focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]"
|
]}
|
||||||
>
|
/>
|
||||||
{storedMissing && (
|
|
||||||
<option value={pickedProfile}>{basename(pickedProfile) || '(未设置)'}</option>
|
|
||||||
)}
|
|
||||||
{profiles.map(p => (
|
|
||||||
<option key={p.path} value={p.path}>{p.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
)}
|
||||||
{created ? <span className="shrink-0">{(profiles.length > 0 || Boolean(basename(session.profilePath))) ? ' · ' : ''}{created}</span> : null}
|
{created ? <span className="shrink-0">{(profiles.length > 0 || Boolean(basename(session.profilePath))) ? ' · ' : ''}{created}</span> : null}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { ReactElement } from 'react'
|
|||||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { SelectMenu } from './ui/select-menu'
|
||||||
|
|
||||||
type GroupKey = 'auth' | 'network' | 'youtrack' | 'prompts' | 'hooks'
|
type GroupKey = 'auth' | 'network' | 'youtrack' | 'prompts' | 'hooks'
|
||||||
|
|
||||||
@@ -487,22 +488,20 @@ export function SettingsModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select
|
<SelectMenu
|
||||||
value={conflictResolutionProfilePath}
|
value={conflictResolutionProfilePath}
|
||||||
onChange={e => setConflictResolutionProfilePath(e.target.value)}
|
onChange={setConflictResolutionProfilePath}
|
||||||
|
variant="input"
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
>
|
options={[
|
||||||
<option value="">(默认)</option>
|
{ value: '', label: '(默认)' },
|
||||||
{profiles.map(p => (
|
...profiles.map(p => ({ value: p.path, label: p.name })),
|
||||||
<option key={p.path} value={p.path}>{p.name}</option>
|
...(conflictResolutionProfilePath
|
||||||
))}
|
&& !profiles.some(p => p.path === conflictResolutionProfilePath)
|
||||||
{conflictResolutionProfilePath
|
? [{ value: conflictResolutionProfilePath, label: `自定义:${conflictResolutionProfilePath}` }]
|
||||||
&& !profiles.some(p => p.path === conflictResolutionProfilePath) && (
|
: []),
|
||||||
<option value={conflictResolutionProfilePath}>
|
]}
|
||||||
{`自定义:${conflictResolutionProfilePath}`}
|
/>
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
@@ -513,22 +512,20 @@ export function SettingsModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select
|
<SelectMenu
|
||||||
value={commitProfilePath}
|
value={commitProfilePath}
|
||||||
onChange={e => setCommitProfilePath(e.target.value)}
|
onChange={setCommitProfilePath}
|
||||||
|
variant="input"
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
>
|
options={[
|
||||||
<option value="">(未配置)</option>
|
{ value: '', label: '(未配置)' },
|
||||||
{profiles.map(p => (
|
...profiles.map(p => ({ value: p.path, label: p.name })),
|
||||||
<option key={p.path} value={p.path}>{p.name}</option>
|
...(commitProfilePath
|
||||||
))}
|
&& !profiles.some(p => p.path === commitProfilePath)
|
||||||
{commitProfilePath
|
? [{ value: commitProfilePath, label: `自定义:${commitProfilePath}` }]
|
||||||
&& !profiles.some(p => p.path === commitProfilePath) && (
|
: []),
|
||||||
<option value={commitProfilePath}>
|
]}
|
||||||
{`自定义:${commitProfilePath}`}
|
/>
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
@@ -728,20 +725,19 @@ export function SettingsModal({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{youtrackProjects.status === 'ready' && youtrackProjects.projects.length > 0 && (
|
{youtrackProjects.status === 'ready' && youtrackProjects.projects.length > 0 && (
|
||||||
<select
|
<SelectMenu
|
||||||
value={youtrackProjectShortName}
|
value={youtrackProjectShortName}
|
||||||
onChange={e => setYoutrackProjectShortName(e.target.value)}
|
onChange={setYoutrackProjectShortName}
|
||||||
className={`${inputClass} mt-2`}
|
className={`${inputClass} mt-2`}
|
||||||
>
|
variant="input"
|
||||||
<option value="">— 选择项目 —</option>
|
options={[
|
||||||
{youtrackProjects.projects.map(p => (
|
{ value: '', label: '— 选择项目 —' },
|
||||||
<option key={p.id} value={p.shortName}>
|
...youtrackProjects.projects.map(p => ({
|
||||||
{p.shortName}
|
value: p.shortName,
|
||||||
{' · '}
|
label: `${p.shortName} · ${p.name}`,
|
||||||
{p.name}
|
})),
|
||||||
</option>
|
]}
|
||||||
))}
|
/>
|
||||||
</select>
|
|
||||||
)}
|
)}
|
||||||
{youtrackProjects.status === 'error' && (
|
{youtrackProjects.status === 'error' && (
|
||||||
<span className="mt-1 block text-[10px] text-state-red">
|
<span className="mt-1 block text-[10px] text-state-red">
|
||||||
@@ -891,14 +887,16 @@ export function SettingsModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<select
|
<SelectMenu
|
||||||
value={worktreeOpenWith}
|
value={worktreeOpenWith}
|
||||||
onChange={e => setWorktreeOpenWith(e.target.value as 'vscode' | 'pycharm')}
|
onChange={v => setWorktreeOpenWith(v as 'vscode' | 'pycharm')}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
>
|
variant="input"
|
||||||
<option value="vscode">VS Code</option>
|
options={[
|
||||||
<option value="pycharm">PyCharm</option>
|
{ value: 'vscode', label: 'VS Code' },
|
||||||
</select>
|
{ value: 'pycharm', label: 'PyCharm' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{worktreeOpenWith === 'pycharm' && (
|
{worktreeOpenWith === 'pycharm' && (
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
* 背景色,依赖 VS Code 主题 CSS 变量。
|
* 背景色,依赖 VS Code 主题 CSS 变量。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -24,6 +23,8 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { SelectMenu } from './ui/select-menu'
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
|
|
||||||
@@ -193,22 +194,16 @@ function PropertyRow({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<select
|
<SelectMenu
|
||||||
value={String(value ?? '')}
|
value={String(value ?? '')}
|
||||||
onChange={e => onChange(propDef.key, e.target.value)}
|
onChange={v => onChange(propDef.key, v)}
|
||||||
className={`${inputClass} appearance-none`}
|
className={inputClass}
|
||||||
style={{ backgroundColor: 'var(--vscode-dropdown-background)', color: 'var(--vscode-dropdown-foreground)' }}
|
variant="input"
|
||||||
>
|
options={(propDef.options ?? []).map(o => ({
|
||||||
{(propDef.options ?? []).map(o => (
|
value: String(o.value),
|
||||||
<option
|
label: o.label,
|
||||||
key={String(o.value)}
|
}))}
|
||||||
value={String(o.value)}
|
/>
|
||||||
style={{ backgroundColor: 'var(--vscode-dropdown-background)', color: 'var(--vscode-dropdown-foreground)' }}
|
|
||||||
>
|
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
case 'multiline':
|
case 'multiline':
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import type { KeyboardEvent, MouseEvent, ReactElement } from 'react'
|
||||||
|
import { Check, ChevronDown } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export interface SelectMenuOption {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
hint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectMenuProps {
|
||||||
|
value: string
|
||||||
|
options: SelectMenuOption[]
|
||||||
|
onChange: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
title?: string
|
||||||
|
disabled?: boolean
|
||||||
|
variant?: 'input' | 'ghost'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_MENU_HEIGHT = 240
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义下拉组件,替代原生 select。
|
||||||
|
*
|
||||||
|
* 原生 select 的展开弹层是 Chromium 系统层,VS Code 暗色主题下样式脱节,
|
||||||
|
* 这里用手写弹层 + createPortal 渲染到 body,避免被 overflow 容器裁掉。
|
||||||
|
*/
|
||||||
|
export function SelectMenu({
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
placeholder = '请选择',
|
||||||
|
title,
|
||||||
|
disabled = false,
|
||||||
|
variant = 'input',
|
||||||
|
className,
|
||||||
|
}: SelectMenuProps): ReactElement {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [highlight, setHighlight] = useState(-1)
|
||||||
|
const [pos, setPos] = useState<{ top: number, left: number, width: number } | null>(null)
|
||||||
|
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
||||||
|
const menuRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const optionRefs = useRef<Array<HTMLButtonElement | null>>([])
|
||||||
|
|
||||||
|
const selected = options.find(o => o.value === value) ?? null
|
||||||
|
|
||||||
|
const computePos = useCallback((): void => {
|
||||||
|
const el = triggerRef.current
|
||||||
|
if (!el)
|
||||||
|
return
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const menuHeight = Math.min(options.length * 32, MAX_MENU_HEIGHT)
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom
|
||||||
|
const spaceAbove = rect.top
|
||||||
|
const openUp = spaceBelow < menuHeight && spaceAbove > spaceBelow
|
||||||
|
setPos({
|
||||||
|
top: openUp ? Math.max(8, rect.top - menuHeight - 4) : rect.bottom + 4,
|
||||||
|
left: rect.left,
|
||||||
|
width: Math.max(rect.width, 120),
|
||||||
|
})
|
||||||
|
}, [options.length])
|
||||||
|
|
||||||
|
const openMenu = useCallback((): void => {
|
||||||
|
computePos()
|
||||||
|
setOpen(true)
|
||||||
|
setHighlight(options.findIndex(o => o.value === value))
|
||||||
|
}, [computePos, options, value])
|
||||||
|
|
||||||
|
const closeMenu = useCallback((): void => {
|
||||||
|
setOpen(false)
|
||||||
|
setHighlight(-1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const selectOption = useCallback((index: number): void => {
|
||||||
|
const opt = options[index]
|
||||||
|
if (opt)
|
||||||
|
onChange(opt.value)
|
||||||
|
closeMenu()
|
||||||
|
}, [options, onChange, closeMenu])
|
||||||
|
|
||||||
|
const moveHighlight = useCallback((dir: 1 | -1): void => {
|
||||||
|
if (options.length === 0)
|
||||||
|
return
|
||||||
|
setHighlight((prev) => {
|
||||||
|
if (prev < 0)
|
||||||
|
return dir === 1 ? 0 : options.length - 1
|
||||||
|
return (prev + dir + options.length) % options.length
|
||||||
|
})
|
||||||
|
}, [options.length])
|
||||||
|
|
||||||
|
// 弹层打开时把高亮项滚进视野(初始定位 + 键盘移动都靠它)。
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!open)
|
||||||
|
return
|
||||||
|
const el = optionRefs.current[highlight]
|
||||||
|
el?.scrollIntoView({ block: 'nearest' })
|
||||||
|
}, [open, highlight])
|
||||||
|
|
||||||
|
// 外点击关闭:mousedown 捕获阶段,落在触发器/弹层内不关。
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open)
|
||||||
|
return
|
||||||
|
function onPointerDown(e: globalThis.MouseEvent): void {
|
||||||
|
const t = e.target
|
||||||
|
if (t instanceof Node && !triggerRef.current?.contains(t) && !menuRef.current?.contains(t))
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onPointerDown, true)
|
||||||
|
return () => document.removeEventListener('mousedown', onPointerDown, true)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
// 窗口 resize / 容器滚动时重算位置,让弹层始终贴住触发器。
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open)
|
||||||
|
return
|
||||||
|
function onScroll(): void {
|
||||||
|
computePos()
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', onScroll)
|
||||||
|
document.addEventListener('scroll', onScroll, true)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onScroll)
|
||||||
|
document.removeEventListener('scroll', onScroll, true)
|
||||||
|
}
|
||||||
|
}, [open, computePos])
|
||||||
|
|
||||||
|
function onTriggerClick(e: MouseEvent<HTMLButtonElement>): void {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (disabled)
|
||||||
|
return
|
||||||
|
if (open)
|
||||||
|
closeMenu()
|
||||||
|
else
|
||||||
|
openMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTriggerMouseDown(e: MouseEvent): void {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTriggerKeyDown(e: KeyboardEvent<HTMLButtonElement>): void {
|
||||||
|
if (disabled)
|
||||||
|
return
|
||||||
|
const key = e.key
|
||||||
|
if (!open) {
|
||||||
|
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
openMenu()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
moveHighlight(1)
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
moveHighlight(-1)
|
||||||
|
break
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (highlight >= 0)
|
||||||
|
selectOption(highlight)
|
||||||
|
break
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
closeMenu()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerClasses
|
||||||
|
= variant === 'input'
|
||||||
|
? 'rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-input-background)] px-2 py-1 text-xs text-[var(--vscode-input-foreground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]'
|
||||||
|
: 'rounded bg-transparent px-1 py-0 text-[10px] text-[var(--vscode-descriptionForeground)] hover:bg-[var(--vscode-toolbar-hoverBackground)]'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={onTriggerClick}
|
||||||
|
onMouseDown={onTriggerMouseDown}
|
||||||
|
onKeyDown={onTriggerKeyDown}
|
||||||
|
className={cn(
|
||||||
|
'flex min-w-0 items-center justify-between gap-1',
|
||||||
|
triggerClasses,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn('min-w-0 truncate', selected ? '' : 'opacity-50')}>
|
||||||
|
{selected?.label ?? placeholder}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className={cn('size-3.5 shrink-0', variant === 'ghost' && 'size-3')} />
|
||||||
|
</button>
|
||||||
|
{open && pos && createPortal(
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
role="listbox"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="fixed z-50 overflow-auto rounded border border-[var(--vscode-panel-border)] bg-[var(--vscode-editorWidget-background,var(--vscode-editor-background))] shadow-md"
|
||||||
|
style={{ top: pos.top, left: pos.left, width: pos.width, maxHeight: MAX_MENU_HEIGHT }}
|
||||||
|
>
|
||||||
|
{options.length === 0
|
||||||
|
? (
|
||||||
|
<div className="px-2 py-3 text-center text-xs text-[var(--vscode-descriptionForeground)]">
|
||||||
|
无选项
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: options.map((opt, i) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
ref={(el) => { optionRefs.current[i] = el }}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={opt.value === value}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
selectOption(i)
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex w-full items-center gap-1.5 px-2 py-1 text-left text-xs',
|
||||||
|
i === highlight
|
||||||
|
? 'bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]'
|
||||||
|
: 'text-[var(--vscode-foreground)] hover:bg-[var(--vscode-list-hoverBackground)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn('grid size-3.5 shrink-0 place-items-center', opt.value === value ? 'opacity-100' : 'opacity-0')}>
|
||||||
|
<Check className="size-3.5" />
|
||||||
|
</span>
|
||||||
|
<span className="flex min-w-0 flex-col">
|
||||||
|
<span className="truncate">{opt.label}</span>
|
||||||
|
{opt.hint && <span className="truncate text-[10px] opacity-70">{opt.hint}</span>}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user