返回 DeepSeek-Reasonix
SoundSelect.tsx
根目录 / desktop / frontend / src / components / SoundSelect.tsx
1 import { useRef, useState } from "react";
2 import { Check, ChevronDown, Play } from "lucide-react";
3 import { AnchoredPopover } from "./AnchoredPopover";
4 import { useT } from "../lib/i18n";
5 import type { DictKey } from "../lib/i18n";
6 import type { SoundWavPref } from "../lib/sound";
7
8 type SoundOption = {
9 value: SoundWavPref;
10 labelKey: DictKey;
11 };
12
13 const OPTIONS: SoundOption[] = [
14 { value: "off", labelKey: "settings.notificationSound.off" },
15 { value: "synth", labelKey: "settings.notificationSound.synth" },
16 { value: "positive", labelKey: "settings.notificationSound.positive" },
17 { value: "correct", labelKey: "settings.notificationSound.correct" },
18 { value: "start", labelKey: "settings.notificationSound.start" },
19 { value: "back", labelKey: "settings.notificationSound.back" },
20 ];
21
22 export function SoundSelect({
23 value,
24 onChange,
25 onPreview,
26 previewDisabled,
27 }: {
28 value: SoundWavPref;
29 onChange: (v: SoundWavPref) => void;
30 onPreview: () => void;
31 previewDisabled?: boolean;
32 }) {
33 const t = useT();
34 const [open, setOpen] = useState(false);
35 const triggerRef = useRef<HTMLButtonElement>(null);
36 const selected = OPTIONS.find((o) => o.value === value) ?? OPTIONS[0];
37
38 return (
39 <div className="sound-select">
40 <button
41 ref={triggerRef}
42 className="sound-select__trigger"
43 type="button"
44 onClick={() => setOpen((v) => !v)}
45 >
46 <span className="sound-select__label">{t(selected.labelKey)}</span>
47 <ChevronDown
48 size={16}
49 className={`sound-select__chev${open ? " sound-select__chev--open" : ""}`}
50 />
51 </button>
52 {!previewDisabled && (
53 <button className="chip chip--icon" type="button" title={t("settings.notificationSoundPreview")} aria-label={t("settings.notificationSoundPreview")} onClick={onPreview}>
54 <Play size={13} aria-hidden="true" />
55 </button>
56 )}
57 <AnchoredPopover
58 open={open}
59 anchorRef={triggerRef}
60 onClose={() => setOpen(false)}
61 className="sound-select__menu"
62 placement="bottom"
63 >
64 <div className="sound-select__list" role="listbox">
65 {OPTIONS.map((opt) => (
66 <button
67 key={opt.value}
68 className={`sound-select__option${opt.value === value ? " sound-select__option--selected" : ""}`}
69 role="option"
70 aria-selected={opt.value === value}
71 type="button"
72 onClick={() => {
73 onChange(opt.value);
74 setOpen(false);
75 }}
76 >
77 <span>{t(opt.labelKey)}</span>
78 {opt.value === value && <Check size={14} className="sound-select__check" />}
79 </button>
80 ))}
81 </div>
82 </AnchoredPopover>
83 </div>
84 );
85 }
86
86 lines Plain Text