style(select): Custom Glass Select mit dunklem Hover (besserer Kontrast)

This commit is contained in:
vibe-bot 2025-08-08 13:14:27 +02:00
parent 196f473b01
commit 57a06570ef
2 changed files with 80 additions and 12 deletions

View file

@ -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"
/>
</div>
<div className="control select">
<select value={selected} onChange={(e) => setSelected(e.target.value)} aria-label="Voice-Channel">
{channels.map((c) => (
<option key={`${c.guildId}:${c.channelId}`} value={`${c.guildId}:${c.channelId}`}>
{c.guildName} {c.channelName}
</option>
))}
</select>
</div>
<CustomSelect
channels={channels}
value={selected}
onChange={setSelected}
/>
<div className="control volume">
<label>🔊 {Math.round(volume * 100)}%</label>
<input
@ -192,8 +188,49 @@ export default function App() {
);
}
function handlePlayWithPathFactory(play: (name: string, rel?: string) => Promise<void>) {
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<HTMLDivElement | null>(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 (
<div className="control select custom-select" ref={ref}>
<button type="button" className="select-trigger" onClick={() => setOpen(v => !v)}>
{current ? `${current.guildName} ${current.channelName}` : 'Channel wählen'}
<span className="chev"></span>
</button>
{open && (
<div className="select-menu">
{channels.map((c) => {
const v = `${c.guildId}:${c.channelId}`;
const active = v === value;
return (
<button
type="button"
key={v}
className={`select-item ${active ? 'active' : ''}`}
onClick={() => { onChange(v); setOpen(false); }}
>
{c.guildName} {c.channelName}
</button>
);
})}
</div>
)}
</div>
);
}

View file

@ -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; }