// 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}
))}
)
}
export function SectionCard({ title, subtitle, children }: { title: string; subtitle?: string; children: ReactNode }) {
return (
{title}
{subtitle && {subtitle}}
{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}
))}
)}
{examplePaths && (
示例: {examplePaths.join('、')}
)}
)}
)
}
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 />
) : (
onRename && setEditing(true)}>{name}
)}
)
}