// Shared UI primitives extracted from ConfigPage.tsx import { useState, type ReactNode } from 'react'; import { X, Plus, Trash2 } from 'lucide-react'; import { inputCls } from './constants'; import type { KnownSource } from './types'; export function Field({ label, children, hint, }: { label: string; children: ReactNode; hint?: string; }) { return (
{children} {hint &&

{hint}

}
); } export function Toggle({ checked, onChange, }: { checked: boolean; onChange: (v: boolean) => void; }) { return ( ); } export function TagEditor({ tags, onChange }: { tags: string[]; onChange: (t: string[]) => void }) { const [input, setInput] = useState(''); const add = () => { const v = input.trim(); if (v && !tags.includes(v)) { onChange([...tags, v]); setInput(''); } }; return (
{tags.map((t, i) => ( {t} ))}
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())} placeholder="输入后按 Enter" className={inputCls + ' !text-xs'} />
); } export function SectionCard({ title, subtitle, children, }: { title: string; subtitle?: string; children: ReactNode; }) { return (

{title}

{subtitle && ( {subtitle} )}
{children}
); } /** 模态框标题栏:图标 + 标题 + X 关闭按钮,与项目模态框惯例一致 */ export function ModalHeader({ icon, title, onClose, }: { icon: ReactNode; title: string; onClose: () => void; }) { return (
{icon} {title}
); } /** 模态框底部按钮区,与项目模态框惯例一致 */ export function ModalFooter({ children }: { children: ReactNode }) { return (
{children}
); } export function SourceEditor({ sources, onChange, knownSources, examplePaths, showCustom = true, }: { sources: string[]; onChange: (s: string[]) => void; knownSources: KnownSource[]; examplePaths?: string[]; showCustom?: boolean; }) { const [customInput, setCustomInput] = useState(''); const knownKeys = new Set(knownSources.map((k) => k.key)); const customPaths = sources.filter((s) => !knownKeys.has(s)); const toggleKnown = (key: string) => { if (sources.includes(key)) { onChange(sources.filter((s) => s !== key)); } else { onChange([...sources, key]); } }; const addCustom = () => { const v = customInput.trim(); if (v && !sources.includes(v)) { onChange([...sources, v]); setCustomInput(''); } }; const removeCustom = (path: string) => { onChange(sources.filter((s) => s !== path)); }; return (
{/* Known sources as toggles */}
{knownSources.map((src) => (
{src.label}
{src.description}
toggleKnown(src.key)} />
))}
{/* Custom paths (only shown when showCustom is true) */} {showCustom && (
自定义路径
{customPaths.length > 0 && (
{customPaths.map((p) => ( {p} ))}
)}
setCustomInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), addCustom())} placeholder="输入绝对路径,如 D:\my-skills" className={inputCls + ' !text-xs font-mono'} />
{examplePaths && (

示例: {examplePaths.join('、')}

)}
)}
); } export interface CheckboxListOption { key: string; label: string; description?: string; /** 可选分组标识,配合 groupBy 使用 */ group?: string; } interface CheckboxListProps { options: CheckboxListOption[]; selected: string[]; onChange: (selected: string[]) => void; /** 未在 options 中出现但已选中的值(legacy 数据),以可移除标签形式展示 */ extraSelected?: string[]; emptyHint?: string; /** 可选:按返回的分组名分组展示(如 "builtin" / "mcp:xxx") */ groupBy?: (option: CheckboxListOption) => string; } /** * 通用勾选列表组件:用 Toggle 切换每个预设选项;额外的 legacy 已选值以可移除标签展示。 */ export function CheckboxList({ options, selected, onChange, extraSelected = [], emptyHint, groupBy, }: CheckboxListProps) { const toggle = (key: string) => onChange(selected.includes(key) ? selected.filter((k) => k !== key) : [...selected, key]); const selectedSet = new Set(selected); const optionsInList = new Set(options.map((o) => o.key)); // 仅展示未出现在 options 中的额外已选值 const extra = extraSelected.filter((k) => !optionsInList.has(k)); // 分组渲染 const renderOptions = (opts: CheckboxListOption[]) => (
{opts.map((option) => (
{option.label}
{option.description && (
{option.description}
)}
toggle(option.key)} />
))}
); let body: ReactNode; if (options.length === 0) { body =

{emptyHint || '无可用选项'}

; } else if (groupBy) { const groups = new Map(); for (const opt of options) { const g = groupBy(opt); const arr = groups.get(g) ?? []; arr.push(opt); groups.set(g, arr); } body = (
{Array.from(groups.entries()).map(([g, opts]) => (
{g}
{renderOptions(opts)}
))}
); } else { body = renderOptions(options); } return (
{body} {extra.length > 0 && (
其他已选(不在当前选项中)
{extra.map((key) => ( {key} ))}
)}
); } export function MapEntryHeader({ name, onDelete, onRename, }: { name: string; onDelete: () => void; onRename?: (n: string) => void; }) { const [editing, setEditing] = useState(false); const [val, setVal] = useState(name); return (
{editing ? ( setVal(e.target.value)} onBlur={() => { setEditing(false); onRename?.(val.trim() || name); }} onKeyDown={(e) => e.key === 'Enter' && (setEditing(false), onRename?.(val.trim() || name)) } className={inputCls + ' !py-1 !text-xs max-w-[200px]'} autoFocus /> ) : ( { if (onRename) { setVal(name); setEditing(true); } }} > {name} )}
); }