feat: 自制下拉列表——文件夹分组标题(📁+小字)与1px细分隔线,弃用原生select/optgroup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from 'xterm'
|
||||
import 'xterm/css/xterm.css'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
@@ -1245,6 +1245,51 @@ function TextViewer(props: { path: string; fileUrl: string; isMd: boolean }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 自制下拉列表(原生 select 无法美化):文件夹分组标题 + 1px 细分隔线 + 选项列表 */
|
||||
function SelectPop(props: {
|
||||
value: string | null
|
||||
groups: { title: string; items: { id: string; label: string; isCurrent?: boolean }[] }[]
|
||||
placeholder?: string
|
||||
onChange: (id: string) => void
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const flat = props.groups.flatMap((g) => g.items)
|
||||
const selected = flat.find((i) => i.id === props.value)
|
||||
return (
|
||||
<div className="dsh-wt_select">
|
||||
<button type="button" className="dsh-wt_selectBtn" onClick={() => setOpen((v) => !v)}>
|
||||
<span className={'dsh-wt_selectVal' + (selected ? '' : ' dsh-wt_selectPh')}>{selected?.label ?? props.placeholder ?? ''}</span>
|
||||
<span className="dsh-wt_selectCaret" aria-hidden>▾</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="dsh-wt_selectList">
|
||||
{props.groups.map((g, gi) => (
|
||||
<Fragment key={g.title || 'g' + gi}>
|
||||
{g.title && (
|
||||
<>
|
||||
<div className="dsh-wt_selectDivider" />
|
||||
<div className="dsh-wt_selectGroup">📁 {g.title}</div>
|
||||
</>
|
||||
)}
|
||||
{g.items.map((it) => (
|
||||
<button
|
||||
key={it.id}
|
||||
type="button"
|
||||
className={'dsh-wt_selectItem' + (it.id === props.value ? ' dsh-wt_selectItemOn' : '')}
|
||||
onClick={() => { props.onChange(it.id); setOpen(false) }}
|
||||
>
|
||||
<span className="dsh-wt_selectItemTitle">{it.label}</span>
|
||||
{it.isCurrent && <span className="dsh-wt_selectCurrent">{T('custom.sessionCurrent')}</span>}
|
||||
</button>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 自定义窗口:居中对话框。两种模式:新建专属会话 / 发送到已有会话(默认当前会话)。 */
|
||||
function CustomPane() {
|
||||
const custom = splitEnv?.custom
|
||||
@@ -1320,37 +1365,25 @@ function CustomPane() {
|
||||
{mode === 'existing' && (
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.session')}</span>
|
||||
<select
|
||||
className="dsh-wt_customSelect"
|
||||
value={sessionId ?? ''}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
>
|
||||
{sessionGroups.map((g) => g.title
|
||||
? (
|
||||
<optgroup key={g.title} label={g.title}>
|
||||
{g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)
|
||||
: g.sessions.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.title}{s.isCurrent ? ' ' + T('custom.sessionCurrent') : ''}</option>
|
||||
)),
|
||||
)}
|
||||
</select>
|
||||
<SelectPop
|
||||
value={sessionId}
|
||||
groups={sessionGroups.map((g) => ({
|
||||
title: g.title,
|
||||
items: g.sessions.map((s) => ({ id: s.id, label: s.title, isCurrent: s.isCurrent })),
|
||||
}))}
|
||||
placeholder={T('custom.session')}
|
||||
onChange={setSessionId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="dsh-wt_customRow">
|
||||
<span className="dsh-wt_customLabel">{T('custom.project')}</span>
|
||||
<select
|
||||
className="dsh-wt_customSelect"
|
||||
value={projectId ?? ''}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<SelectPop
|
||||
value={projectId}
|
||||
groups={[{ title: '', items: projects.map((p) => ({ id: p.id, label: p.name })) }]}
|
||||
placeholder={T('custom.project')}
|
||||
onChange={setProjectId}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -205,6 +205,21 @@ export const css = [
|
||||
'.dsh-wt_customLabel{flex:none;font-size:11.5px;color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
||||
'.dsh-wt_customSelect{flex:1;min-width:0;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;outline:none}',
|
||||
'.dsh-wt_customSelect option{background-color:var(--dsw-alias-bg-base,#0b0e14);color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 自制下拉(文件夹分组 + 细分隔线)
|
||||
'.dsh-wt_select{position:relative;flex:1;min-width:0}',
|
||||
'.dsh-wt_selectBtn{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;cursor:pointer}',
|
||||
'.dsh-wt_selectBtn:hover{border-color:var(--dsw-alias-border-l2,#3a4150)}',
|
||||
'.dsh-wt_selectVal{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_selectPh{color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||
'.dsh-wt_selectCaret{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:10px;line-height:1}',
|
||||
'.dsh-wt_selectList{margin-top:4px;max-height:180px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);padding:4px;box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}',
|
||||
'.dsh-wt_selectItem{display:flex;align-items:center;gap:6px;width:100%;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:17px;cursor:pointer;text-align:left}',
|
||||
'.dsh-wt_selectItem:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
|
||||
'.dsh-wt_selectItemOn{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_selectItemTitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
||||
'.dsh-wt_selectCurrent{flex:none;font-size:10px;color:var(--dsw-alias-label-tertiary,#6e7683)}',
|
||||
'.dsh-wt_selectDivider{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}',
|
||||
'.dsh-wt_selectGroup{display:flex;align-items:center;gap:5px;padding:2px 8px 1px;font-size:10px;letter-spacing:.04em;color:var(--dsw-alias-label-tertiary,#6e7683);user-select:none}',
|
||||
'.dsh-wt_customSend{align-self:flex-end;padding:6px 14px;border:none;border-radius:6px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);color:#fff;font:inherit;font-size:12px;cursor:pointer}',
|
||||
'.dsh-wt_customSend:disabled{opacity:.55;cursor:default}',
|
||||
'.dsh-wt_customFail{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d);margin:0}',
|
||||
|
||||
Reference in New Issue
Block a user