Files
UHD_Media_Tool/apps/web/app/components/settings/settings-search.tsx
T
rootandClaude Opus 5.5 09a2adb637 feat(web): 设计规范与 uhd_web 同步(截至 2026-10-08)
- 材质统一边光:slab / float / pill / pill-selected / seg-thumb / chrome-side 改为左上亮右下暗的 1px 边光 + 外侧暗线;新增 lens / field / chip 参数与 pg-float-modal
- 主按钮:转动光谱环改为静态左上光谱高光,悬停沿边扫过一小段;次按钮同为 lens 玻璃
- Input / Textarea / SelectTrigger 挂 .field-glass,原生下拉与实体选择器同步;对话框按钮行 gap-3
- 提示条改 pg-float(状态色只在图标、展开不叠放);新增 GlassTooltip 接管 title 提示
- 触屏判断 input-modality.ts、touch / hover 自定义变体;animate-pg-pop 改 backwards;scroll-fade-end
- 控制台外壳:侧栏无硬分隔线、折叠开关进标志行;顶栏去磨砂,右侧控件收进玻璃胶囊
- 登录页居中单卡 auth-card;详情头图「订阅」改回普通主按钮,删除 pill-solid;发现页海报角标改 glass-chip-media
- 引入 simple-liquid-glass(liquid-surface.tsx);更新 docs/design 三份规范与 CHANGELOG

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 02:29:19 +08:00

117 lines
3.9 KiB
TypeScript

import { Search, X } from "lucide-react";
import { useId, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router";
import { Input } from "~/components/ui/input.tsx";
import { cn } from "~/lib/utils.ts";
import { type SettingsSearchHit, searchSettings } from "./search-index.ts";
import { settingsHash } from "./sections.tsx";
/** 设置页搜索框:按设置名、节名找位置;选中后跳到该节,字段会被聚焦 */
export function SettingsSearch({ className }: { className?: string }) {
const navigate = useNavigate();
const listId = useId();
const [q, setQ] = useState("");
const [open, setOpen] = useState(false);
const [active, setActive] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const hits = useMemo(() => searchSettings(q), [q]);
const show = open && q.trim() !== "";
const pick = (h: SettingsSearchHit) => {
setOpen(false);
setQ("");
navigate({ hash: settingsHash(h.section, h.anchor) }, { state: h.field ? { field: h.field } : null });
};
return (
<div className={cn("relative", className)}>
<Search
className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
aria-hidden
/>
<Input
ref={inputRef}
type="text"
enterKeyHint="search"
role="combobox"
aria-expanded={show}
aria-controls={listId}
aria-autocomplete="list"
aria-label="搜索设置"
placeholder="搜索设置"
value={q}
className="pr-8 pl-8"
onChange={(e) => {
setQ(e.target.value);
setActive(0);
setOpen(true);
}}
onFocus={() => setOpen(true)}
onBlur={() => window.setTimeout(() => setOpen(false), 120)}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActive((i) => Math.min(i + 1, hits.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter") {
const h = hits[active];
if (h) {
e.preventDefault();
pick(h);
}
} else if (e.key === "Escape") {
setQ("");
setOpen(false);
}
}}
/>
{q ? (
<button
type="button"
aria-label="清除搜索"
className="absolute top-1/2 right-2 -translate-y-1/2 rounded-full p-0.5 text-muted-foreground hover:text-foreground"
onClick={() => {
setQ("");
inputRef.current?.focus();
}}
>
<X className="size-3.5" />
</button>
) : null}
{show ? (
<div
id={listId}
role="listbox"
aria-label="搜索结果"
className="pg-float absolute inset-x-0 top-full z-30 mt-1.5 max-h-80 animate-pg-pop overflow-y-auto rounded-2xl p-1"
>
{hits.length === 0 ? (
<p className="px-3 py-2 text-caption text-muted-foreground">没有匹配的设置</p>
) : (
hits.map((h, i) => (
<button
key={h.key}
type="button"
role="option"
aria-selected={i === active}
onMouseDown={(e) => e.preventDefault()}
onMouseEnter={() => setActive(i)}
onClick={() => pick(h)}
className={cn(
"block w-full rounded-lg px-3 py-1.5 text-left transition-colors duration-micro",
i === active ? "bg-foreground/[0.06]" : "",
)}
>
<span className="block truncate text-sm text-foreground">{h.label}</span>
<span className="block truncate text-meta text-muted-foreground">{h.where}</span>
</button>
))
)}
</div>
) : null}
</div>
);
}