- 材质统一边光: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
117 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|