diff --git a/web/src/App.tsx b/web/src/App.tsx
index 0fe0c1a..112041f 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
import { fetchChannels, fetchSounds, playSound, setVolumeLive } from './api';
import type { VoiceChannelInfo, Sound } from './types';
import { getCookie, setCookie } from './cookies';
@@ -103,15 +103,11 @@ export default function App() {
aria-label="Suche"
/>
-
-
-
+
Promise
) {
- return (s: Sound & { relativePath?: string }) => play(s.name, s.relativePath);
+type SelectProps = {
+ channels: VoiceChannelInfo[];
+ value: string;
+ onChange: (v: string) => void;
+};
+
+function CustomSelect({ channels, value, onChange }: SelectProps) {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ useEffect(() => {
+ const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); };
+ window.addEventListener('click', close);
+ return () => window.removeEventListener('click', close);
+ }, []);
+
+ const current = channels.find(c => `${c.guildId}:${c.channelId}` === value);
+
+ return (
+
+
+ {open && (
+
+ {channels.map((c) => {
+ const v = `${c.guildId}:${c.channelId}`;
+ const active = v === value;
+ return (
+
+ );
+ })}
+
+ )}
+
+ );
}
diff --git a/web/src/styles.css b/web/src/styles.css
index 390cd11..385634a 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -40,6 +40,37 @@ header p { opacity: .8; }
}
.control select option { background-color: #0f1530; color: #e7e7ee; }
.control select optgroup { background-color: #0f1530; color: #c8c8d8; }
+
+/* Custom Select */
+.custom-select { position: relative; }
+.select-trigger {
+ width: 100%;
+ text-align: left;
+ padding: 12px 14px;
+ border-radius: 14px;
+ border: 1px solid rgba(255,255,255,.25);
+ background: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,.06));
+ color: #e7e7ee;
+ backdrop-filter: blur(18px);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.2);
+}
+.select-trigger .chev { float: right; opacity: .8; }
+.select-menu {
+ position: absolute; inset: auto 0 auto 0; top: calc(100% + 6px);
+ border-radius: 12px;
+ overflow: hidden;
+ border: 1px solid rgba(255,255,255,.25);
+ background: rgba(15,21,48,.98);
+ box-shadow: 0 24px 48px rgba(0,0,0,.5);
+ max-height: 280px; overflow-y: auto;
+ z-index: 20;
+}
+.select-item {
+ width: 100%; text-align: left; padding: 10px 12px; color: #e7e7ee;
+ background: transparent; border: 0;
+}
+.select-item:hover { background: rgba(255,255,255,.08); color: #fff; }
+.select-item.active { background: rgba(255,255,255,.14); color: #fff; }
.control input::placeholder { color: #c8c8d8; }
.control.volume { display: grid; grid-template-columns: auto 1fr; gap: 10px; align-items: center; }