diff --git a/web/src/App.tsx b/web/src/App.tsx index c11438c..440fd90 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,65 +1,71 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, - adminStatus, adminLogin, adminLogout, adminDelete, adminRename, - playUrl, fetchCategories, createCategory, assignCategories, clearBadges, - updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents, + adminStatus, adminLogin, adminLogout, adminDelete, + fetchCategories, partyStart, partyStop, subscribeEvents, getSelectedChannels, setSelectedChannel, } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; -/* ── Category Color Palette ── */ +const THEMES = [ + { id: 'default', color: '#5865f2', label: 'Discord' }, + { id: 'purple', color: '#9b59b6', label: 'Midnight' }, + { id: 'forest', color: '#2ecc71', label: 'Forest' }, + { id: 'sunset', color: '#e67e22', label: 'Sunset' }, + { id: 'ocean', color: '#3498db', label: 'Ocean' }, +]; + const CAT_PALETTE = [ '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316', '#06b6d4', '#ef4444', '#a855f7', '#84cc16', '#d946ef', '#0ea5e9', '#f43f5e', '#10b981', ]; -const THEMES = [ - { id: 'midnight', label: 'Midnight' }, - { id: 'daylight', label: 'Daylight' }, - { id: 'neon', label: 'Neon' }, - { id: 'vapor', label: 'Vapor' }, - { id: 'matrix', label: 'Matrix' }, -]; - type Tab = 'all' | 'favorites' | 'recent'; -type BtnSize = 'S' | 'M' | 'L'; -const BTN_SIZES: { id: BtnSize; label: string }[] = [ - { id: 'S', label: 'S' }, - { id: 'M', label: 'M' }, - { id: 'L', label: 'L' }, -]; export default function App() { - /* ── State ── */ + /* ── Data ── */ const [sounds, setSounds] = useState([]); const [total, setTotal] = useState(0); const [folders, setFolders] = useState>([]); const [categories, setCategories] = useState([]); + /* ── Navigation ── */ const [activeTab, setActiveTab] = useState('all'); const [activeFolder, setActiveFolder] = useState(''); const [query, setQuery] = useState(''); + /* ── Channels ── */ const [channels, setChannels] = useState([]); const [selected, setSelected] = useState(''); const selectedRef = useRef(''); + const [channelOpen, setChannelOpen] = useState(false); + /* ── Playback ── */ const [volume, setVolume] = useState(1); - const [favs, setFavs] = useState>({}); - const [theme, setTheme] = useState(() => localStorage.getItem('jb-theme') || 'midnight'); - const [btnSize, setBtnSize] = useState(() => (localStorage.getItem('jb-btn-size') as BtnSize) || 'M'); - const [isAdmin, setIsAdmin] = useState(false); - const [showAdmin, setShowAdmin] = useState(false); + const [lastPlayed, setLastPlayed] = useState(''); + /* ── Preferences ── */ + const [favs, setFavs] = useState>({}); + const [theme, setTheme] = useState(() => localStorage.getItem('jb-theme') || 'default'); + const [cardSize, setCardSize] = useState(() => parseInt(localStorage.getItem('jb-card-size') || '110')); + + /* ── Party ── */ const [chaosMode, setChaosMode] = useState(false); const [partyActiveGuilds, setPartyActiveGuilds] = useState([]); const chaosModeRef = useRef(false); - const [lastPlayed, setLastPlayed] = useState(''); + /* ── Admin ── */ + const [isAdmin, setIsAdmin] = useState(false); + const [showAdmin, setShowAdmin] = useState(false); + const [adminPwd, setAdminPwd] = useState(''); + + /* ── UI ── */ const [notification, setNotification] = useState<{ msg: string; type: 'info' | 'error' } | null>(null); + const [clock, setClock] = useState(''); + const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; sound: Sound } | null>(null); + const [refreshKey, setRefreshKey] = useState(0); /* ── Refs ── */ useEffect(() => { chaosModeRef.current = chaosMode; }, [chaosMode]); @@ -74,6 +80,24 @@ export default function App() { const guildId = selected ? selected.split(':')[0] : ''; const channelId = selected ? selected.split(':')[1] : ''; + const selectedChannel = useMemo(() => + channels.find(c => `${c.guildId}:${c.channelId}` === selected), + [channels, selected]); + + /* ── Clock ── */ + useEffect(() => { + const update = () => { + const now = new Date(); + const h = String(now.getHours()).padStart(2, '0'); + const m = String(now.getMinutes()).padStart(2, '0'); + const s = String(now.getSeconds()).padStart(2, '0'); + setClock(`${h}:${m}:${s}`); + }; + update(); + const id = setInterval(update, 1000); + return () => clearInterval(id); + }, []); + /* ── Init ── */ useEffect(() => { (async () => { @@ -94,14 +118,20 @@ export default function App() { /* ── Theme ── */ useEffect(() => { - document.body.setAttribute('data-theme', theme); + if (theme === 'default') document.body.removeAttribute('data-theme'); + else document.body.setAttribute('data-theme', theme); localStorage.setItem('jb-theme', theme); }, [theme]); - /* ── Button Size ── */ + /* ── Card size ── */ useEffect(() => { - localStorage.setItem('jb-btn-size', btnSize); - }, [btnSize]); + const r = document.documentElement; + r.style.setProperty('--card-size', cardSize + 'px'); + const ratio = cardSize / 110; + r.style.setProperty('--card-emoji', Math.round(28 * ratio) + 'px'); + r.style.setProperty('--card-font', Math.max(9, Math.round(11 * ratio)) + 'px'); + localStorage.setItem('jb-card-size', String(cardSize)); + }, [cardSize]); /* ── SSE ── */ useEffect(() => { @@ -146,14 +176,13 @@ export default function App() { let folderParam = '__all__'; if (activeTab === 'recent') folderParam = '__recent__'; else if (activeFolder) folderParam = activeFolder; - const s = await fetchSounds(query, folderParam, undefined, false); setSounds(s.items); setTotal(s.total); setFolders(s.folders); } catch (e: any) { notify(e?.message || 'Sounds-Fehler', 'error'); } })(); - }, [activeTab, activeFolder, query]); + }, [activeTab, activeFolder, query, refreshKey]); /* ── Favs persistence ── */ useEffect(() => { @@ -174,6 +203,13 @@ export default function App() { } }, [selected]); + /* ── Close dropdowns on outside click ── */ + useEffect(() => { + const handler = () => { setChannelOpen(false); setCtxMenu(null); }; + document.addEventListener('click', handler); + return () => document.removeEventListener('click', handler); + }, []); + /* ── Actions ── */ async function handlePlay(s: Sound) { if (!selected) return notify('Bitte einen Voice-Channel auswählen', 'error'); @@ -190,8 +226,8 @@ export default function App() { } async function handleRandom() { - if (!sounds.length || !selected) return; - const rnd = sounds[Math.floor(Math.random() * sounds.length)]; + if (!displaySounds.length || !selected) return; + const rnd = displaySounds[Math.floor(Math.random() * displaySounds.length)]; handlePlay(rnd); } @@ -205,11 +241,32 @@ export default function App() { } } + async function handleChannelSelect(ch: VoiceChannelInfo) { + const v = `${ch.guildId}:${ch.channelId}`; + setSelected(v); + setChannelOpen(false); + try { await setSelectedChannel(ch.guildId, ch.channelId); } catch { } + } + + function toggleFav(key: string) { + setFavs(prev => ({ ...prev, [key]: !prev[key] })); + } + + async function handleAdminLogin() { + try { + const ok = await adminLogin(adminPwd); + if (ok) { setIsAdmin(true); setAdminPwd(''); notify('Admin eingeloggt'); } + else notify('Falsches Passwort', 'error'); + } catch { notify('Login fehlgeschlagen', 'error'); } + } + + async function handleAdminLogout() { + try { await adminLogout(); setIsAdmin(false); notify('Ausgeloggt'); } catch { } + } + /* ── Computed ── */ const displaySounds = useMemo(() => { - if (activeTab === 'favorites') { - return sounds.filter(s => favs[s.relativePath ?? s.fileName]); - } + if (activeTab === 'favorites') return sounds.filter(s => favs[s.relativePath ?? s.fileName]); return sounds; }, [sounds, activeTab, favs]); @@ -225,109 +282,180 @@ export default function App() { return m; }, [visibleFolders]); - /* ── Admin State ── */ - const [adminPwd, setAdminPwd] = useState(''); + const channelsByGuild = useMemo(() => { + const groups: Record = {}; + channels.forEach(c => { + if (!groups[c.guildName]) groups[c.guildName] = []; + groups[c.guildName].push(c); + }); + return groups; + }, [channels]); - async function handleAdminLogin() { - try { - const ok = await adminLogin(adminPwd); - if (ok) { setIsAdmin(true); setAdminPwd(''); notify('Admin eingeloggt'); } - else notify('Falsches Passwort', 'error'); - } catch { notify('Login fehlgeschlagen', 'error'); } - } + const clockMain = clock.slice(0, 5); + const clockSec = clock.slice(5); - async function handleAdminLogout() { - try { await adminLogout(); setIsAdmin(false); notify('Ausgeloggt'); } catch { } - } - - /* ── Render ── */ + /* ════════════════════════════════════════════ + RENDER + ════════════════════════════════════════════ */ return ( -
+
+ {chaosMode &&
} - {/* ════════ Header ════════ */} -
-
JUKEBOX
+ {/* ═══ TOPBAR ═══ */} +
+
+
+ music_note +
+ Soundboard -
+ {/* Channel Dropdown */} +
e.stopPropagation()}> + + {channelOpen && ( +
+ {Object.entries(channelsByGuild).map(([guild, chs]) => ( + +
{guild}
+ {chs.map(ch => ( +
handleChannelSelect(ch)} + > + volume_up + {ch.channelName} +
+ ))} +
+ ))} + {channels.length === 0 && ( +
+ Keine Channels verfügbar +
+ )} +
+ )} +
+
+ +
+
{clockMain}{clockSec}
+
+ +
+ {selected && ( +
+ + Verbunden +
+ )} + +
+
+ + {/* ═══ TOOLBAR ═══ */} +
+
+ + + +
+ +
search setQuery(e.target.value)} /> {query && ( )}
-
-
- {total} Sounds -
+
-
- {BTN_SIZES.map(s => ( - - ))} -
+ - + - + + +
+ grid_view + setCardSize(parseInt(e.target.value))} + />
-
- {/* ════════ Tab Bar ════════ */} - +
+ {THEMES.map(t => ( +
setTheme(t.id)} + /> + ))} +
+
- {/* ════════ Category Filter ════════ */} + {/* ═══ FOLDER CHIPS ═══ */} {activeTab === 'all' && visibleFolders.length > 0 && (
{visibleFolders.map(f => { @@ -338,169 +466,185 @@ export default function App() { key={f.key} className={`cat-chip ${isActive ? 'active' : ''}`} onClick={() => setActiveFolder(isActive ? '' : f.key)} - style={isActive ? { borderColor: color, color, background: `${color}12` } : undefined} + style={isActive ? { borderColor: color, color } : undefined} > {f.name.replace(/\s*\(\d+\)\s*$/, '')} - {f.count} + {f.count} ); })}
)} - {/* ════════ Sound Grid ════════ */} -
+ {/* ═══ MAIN ═══ */} +
{displaySounds.length === 0 ? ( -
- - {activeTab === 'favorites' ? 'star_border' : 'music_off'} - -

+

+
{activeTab === 'favorites' ? '⭐' : '🔇'}
+
{activeTab === 'favorites' - ? 'Noch keine Favorites — klick den Stern!' + ? 'Noch keine Favoriten' : query ? `Kein Sound für "${query}" gefunden` : 'Keine Sounds vorhanden'} -

+
+
+ {activeTab === 'favorites' + ? 'Klick den Stern auf einem Sound!' + : 'Hier gibt\'s noch nichts zu hören.'} +
) : ( -
+
{displaySounds.map((s, idx) => { const key = s.relativePath ?? s.fileName; const isFav = !!favs[key]; - const color = s.folder ? folderColorMap[s.folder] || '#555' : '#555'; - const isNew = s.badges?.includes('new'); - const isTop = s.badges?.includes('top'); const isPlaying = lastPlayed === s.name; + const isNew = s.isRecent || s.badges?.includes('new'); + const initial = s.name.charAt(0).toUpperCase(); + const folderColor = s.folder ? (folderColorMap[s.folder] || 'var(--accent)') : 'var(--accent)'; return ( - + {initial} + {s.name} + {s.folder && {s.folder}} +
+
+
+
+
); })}
)} +
+ + {/* ═══ BOTTOM BAR ═══ */} +
+
+
+
+
+
+ Spielt: + {lastPlayed || '—'} +
+
+ { + const newVol = volume > 0 ? 0 : 0.5; + setVolume(newVol); + if (guildId) setVolumeLive(guildId, newVol).catch(() => {}); + }} + > + {volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'} + + { + const v = parseFloat(e.target.value); + setVolume(v); + if (guildId) try { await setVolumeLive(guildId, v); } catch { } + }} + style={{ '--vol': `${Math.round(volume * 100)}%` } as React.CSSProperties} + /> + {Math.round(volume * 100)}% +
- {/* ════════ Control Bar ════════ */} -
-
-
- headset_mic - + {/* ═══ CONTEXT MENU ═══ */} + {ctxMenu && ( +
e.stopPropagation()} + > +
{ handlePlay(ctxMenu.sound); setCtxMenu(null); }}> + play_arrow + Abspielen
- - {lastPlayed && ( -
- play_arrow - {lastPlayed} -
+
{ + toggleFav(ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName); + setCtxMenu(null); + }}> + + {favs[ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName] ? 'star' : 'star_border'} + + Favorit +
+ {isAdmin && ( + <> +
+
{ + const path = ctxMenu.sound.relativePath ?? ctxMenu.sound.fileName; + try { + await adminDelete([path]); + notify('Sound gelöscht'); + setRefreshKey(k => k + 1); + } catch { notify('Löschen fehlgeschlagen', 'error'); } + setCtxMenu(null); + }}> + delete + Löschen +
+ )}
+ )} -
- - - - - -
- -
-
- { - const newVol = volume > 0 ? 0 : 0.5; - setVolume(newVol); - if (guildId) setVolumeLive(guildId, newVol).catch(() => {}); - }} - > - {volume === 0 ? 'volume_off' : volume < 0.5 ? 'volume_down' : 'volume_up'} - - { - const v = parseFloat(e.target.value); - setVolume(v); - if (guildId) try { await setVolumeLive(guildId, v); } catch { } - }} - style={{ '--fill': `${Math.round(volume * 100)}%` } as React.CSSProperties} - /> - {Math.round(volume * 100)}% -
-
-
- - {/* ════════ Notification Toast ════════ */} + {/* ═══ TOAST ═══ */} {notification && (
- + {notification.type === 'error' ? 'error_outline' : 'check_circle'} {notification.msg}
)} - {/* ════════ Admin Panel Overlay ════════ */} + {/* ═══ ADMIN PANEL ═══ */} {showAdmin && (
{ if (e.target === e.currentTarget) setShowAdmin(false); }}>
@@ -510,11 +654,10 @@ export default function App() { close - {!isAdmin ? (
- +
- +
) : (
-

- Eingeloggt als Admin -

- +

Eingeloggt als Admin

+
)}
diff --git a/web/src/styles.css b/web/src/styles.css index ea11fb8..92185be 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,193 +1,93 @@ /* ═══════════════════════════════════════════════════════════════ JUKEBOX — Discord Soundboard - Design: "DECK" — Cyberpunk Audio Console - Fonts: Syne (display) + Outfit (body) + Design: Discord-style dark theme with Blurple accent + Font: DM Sans (display + body) ═══════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Outfit:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap'); /* ──────────────────────────────────────────── - Theme: Midnight (default dark) + Theme Variables — Default (Discord Blurple) ──────────────────────────────────────────── */ :root { - --bg-base: #0b0b0f; - --bg-surface-0: #101016; - --bg-surface-1: #16161e; - --bg-surface-2: #1e1e28; - --bg-surface-3: #282834; + --bg-deep: #1a1b1e; + --bg-primary: #1e1f22; + --bg-secondary: #2b2d31; + --bg-tertiary: #313338; + --bg-modifier-hover: rgba(79, 84, 92, .16); + --bg-modifier-active: rgba(79, 84, 92, .24); + --bg-modifier-selected: rgba(79, 84, 92, .32); - --text-primary: #e4e4ec; - --text-secondary: #7a7a90; - --text-muted: #4a4a5e; + --text-normal: #dbdee1; + --text-muted: #949ba4; + --text-faint: #6d6f78; - --accent: #3b82f6; - --accent-hover: #2563eb; - --accent-glow: rgba(59, 130, 246, 0.15); - --accent-subtle: rgba(59, 130, 246, 0.08); + --accent: #5865f2; + --accent-hover: #4752c4; + --accent-glow: rgba(88, 101, 242, .45); - --danger: #ef4444; - --danger-glow: rgba(239, 68, 68, 0.15); - --success: #22c55e; - --warning: #f59e0b; + --green: #23a55a; + --red: #f23f42; + --yellow: #f0b232; + --white: #ffffff; - --border: rgba(255, 255, 255, 0.06); - --border-hover: rgba(255, 255, 255, 0.12); - --border-active: rgba(59, 130, 246, 0.4); + --font: 'DM Sans', 'Outfit', 'gg sans', 'Noto Sans', Whitney, 'Helvetica Neue', Helvetica, Arial, sans-serif; + --radius: 8px; + --radius-lg: 12px; + --shadow-low: 0 1px 3px rgba(0, 0, 0, .24); + --shadow-med: 0 4px 12px rgba(0, 0, 0, .32); + --shadow-high: 0 8px 24px rgba(0, 0, 0, .4); + --transition: 150ms cubic-bezier(.4, 0, .2, 1); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); - --shadow-glow: 0 0 20px var(--accent-glow); - - --grid-opacity: 1; - --grid-dot: rgba(255, 255, 255, 0.03); - - --header-h: 64px; - --tabs-h: 44px; - --cats-h: 44px; - --control-h: 72px; - --radius-sm: 6px; - --radius-md: 10px; - --radius-lg: 16px; - --radius-pill: 999px; + --card-size: 110px; + --card-emoji: 28px; + --card-font: 11px; color-scheme: dark; } -/* ──────────────────────────────────────────── - Theme: Daylight - ──────────────────────────────────────────── */ -[data-theme="daylight"] { - --bg-base: #f4f2ee; - --bg-surface-0: #eae8e3; - --bg-surface-1: #ffffff; - --bg-surface-2: #f0eee9; - --bg-surface-3: #e4e2dd; - - --text-primary: #1a1a2e; - --text-secondary: #6b6b80; - --text-muted: #a0a0b0; - - --accent: #2563eb; - --accent-hover: #1d4ed8; - --accent-glow: rgba(37, 99, 235, 0.1); - --accent-subtle: rgba(37, 99, 235, 0.05); - - --border: rgba(0, 0, 0, 0.08); - --border-hover: rgba(0, 0, 0, 0.14); - --border-active: rgba(37, 99, 235, 0.4); - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1); - --shadow-glow: 0 0 20px var(--accent-glow); - - --grid-opacity: 0; - --grid-dot: transparent; - - color-scheme: light; +/* ── Theme: Midnight Purple ── */ +[data-theme="purple"] { + --bg-deep: #13111c; + --bg-primary: #1a1726; + --bg-secondary: #241f35; + --bg-tertiary: #2e2845; + --accent: #9b59b6; + --accent-hover: #8e44ad; + --accent-glow: rgba(155, 89, 182, .45); } -/* ──────────────────────────────────────────── - Theme: Neon - ──────────────────────────────────────────── */ -[data-theme="neon"] { - --bg-base: #08080e; - --bg-surface-0: #0e0e18; - --bg-surface-1: #141422; - --bg-surface-2: #1c1c30; - --bg-surface-3: #24243c; - - --text-primary: #f0f0ff; - --text-secondary: #8080b0; - --text-muted: #505078; - - --accent: #e040fb; - --accent-hover: #d020eb; - --accent-glow: rgba(224, 64, 251, 0.2); - --accent-subtle: rgba(224, 64, 251, 0.08); - - --border: rgba(224, 64, 251, 0.08); - --border-hover: rgba(224, 64, 251, 0.18); - --border-active: rgba(224, 64, 251, 0.45); - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6); - --shadow-glow: 0 0 24px var(--accent-glow); - - --grid-opacity: 1; - --grid-dot: rgba(224, 64, 251, 0.025); - - color-scheme: dark; +/* ── Theme: Forest ── */ +[data-theme="forest"] { + --bg-deep: #0f1a14; + --bg-primary: #142119; + --bg-secondary: #1c2e22; + --bg-tertiary: #253a2c; + --accent: #2ecc71; + --accent-hover: #27ae60; + --accent-glow: rgba(46, 204, 113, .4); } -/* ──────────────────────────────────────────── - Theme: Vapor (Retrowave) - ──────────────────────────────────────────── */ -[data-theme="vapor"] { - --bg-base: #140a22; - --bg-surface-0: #1a0e2e; - --bg-surface-1: #22143a; - --bg-surface-2: #2c1c48; - --bg-surface-3: #362456; - - --text-primary: #e8daf8; - --text-secondary: #8a6aaa; - --text-muted: #5a4070; - - --accent: #06d6a0; - --accent-hover: #05c090; - --accent-glow: rgba(6, 214, 160, 0.18); - --accent-subtle: rgba(6, 214, 160, 0.07); - - --border: rgba(6, 214, 160, 0.07); - --border-hover: rgba(6, 214, 160, 0.15); - --border-active: rgba(6, 214, 160, 0.4); - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.6); - --shadow-glow: 0 0 24px var(--accent-glow); - - --grid-opacity: 1; - --grid-dot: rgba(6, 214, 160, 0.02); - - color-scheme: dark; +/* ── Theme: Sunset ── */ +[data-theme="sunset"] { + --bg-deep: #1a1210; + --bg-primary: #231815; + --bg-secondary: #2f201c; + --bg-tertiary: #3d2a24; + --accent: #e67e22; + --accent-hover: #d35400; + --accent-glow: rgba(230, 126, 34, .4); } -/* ──────────────────────────────────────────── - Theme: Matrix - ──────────────────────────────────────────── */ -[data-theme="matrix"] { - --bg-base: #050a05; - --bg-surface-0: #0a120a; - --bg-surface-1: #0f1a0f; - --bg-surface-2: #162216; - --bg-surface-3: #1e2e1e; - - --text-primary: #c0ecc0; - --text-secondary: #5a8a5a; - --text-muted: #2e5a2e; - - --accent: #22c55e; - --accent-hover: #16a34a; - --accent-glow: rgba(34, 197, 94, 0.18); - --accent-subtle: rgba(34, 197, 94, 0.06); - - --border: rgba(34, 197, 94, 0.07); - --border-hover: rgba(34, 197, 94, 0.15); - --border-active: rgba(34, 197, 94, 0.4); - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.5); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.6); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.7); - --shadow-glow: 0 0 24px var(--accent-glow); - - --grid-opacity: 1; - --grid-dot: rgba(34, 197, 94, 0.025); - - color-scheme: dark; +/* ── Theme: Ocean ── */ +[data-theme="ocean"] { + --bg-deep: #0a1628; + --bg-primary: #0f1e33; + --bg-secondary: #162a42; + --bg-tertiary: #1e3652; + --accent: #3498db; + --accent-hover: #2980b9; + --accent-glow: rgba(52, 152, 219, .4); } /* ──────────────────────────────────────────── @@ -199,15 +99,17 @@ padding: 0; } -body { - font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; - background: var(--bg-base); - color: var(--text-primary); +html, body { + height: 100%; + overflow: hidden; + background: var(--bg-deep); + color: var(--text-normal); + font-family: var(--font); + font-size: 14px; + line-height: 1.4; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - overflow: hidden; - height: 100dvh; - width: 100vw; + transition: background .4s ease, color .4s ease; } button { @@ -223,20 +125,10 @@ input, select { color: inherit; } -::-webkit-scrollbar { - width: 6px; - height: 6px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: var(--text-muted); - border-radius: 3px; -} -::-webkit-scrollbar-thumb:hover { - background: var(--text-secondary); -} +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--bg-tertiary); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #3f4147; } ::selection { background: var(--accent); @@ -244,281 +136,528 @@ input, select { } /* ──────────────────────────────────────────── - App Shell + App Layout ──────────────────────────────────────────── */ -.app-shell { +.app { display: flex; flex-direction: column; - height: 100dvh; - width: 100vw; - overflow: hidden; + height: 100vh; position: relative; } -/* Dot grid pattern overlay */ -.app-shell::after { - content: ''; - position: fixed; - inset: 0; - opacity: var(--grid-opacity); - background-image: radial-gradient( - circle, var(--grid-dot) 1px, transparent 1px - ); - background-size: 28px 28px; - pointer-events: none; - z-index: 0; -} - /* ──────────────────────────────────────────── - Header + Top Bar ──────────────────────────────────────────── */ -.header { - height: var(--header-h); - min-height: var(--header-h); +.topbar { display: flex; align-items: center; - gap: 16px; - padding: 0 24px; - background: var(--bg-surface-0); - border-bottom: 1px solid var(--border); - position: relative; - z-index: 20; -} - -.logo { - font-family: 'Syne', sans-serif; - font-weight: 800; - font-size: 22px; - letter-spacing: -0.5px; - background: linear-gradient(135deg, var(--accent), var(--text-primary)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + padding: 0 20px; + height: 52px; + background: var(--bg-secondary); + border-bottom: 1px solid rgba(0, 0, 0, .24); + z-index: 10; flex-shrink: 0; - user-select: none; + gap: 16px; + transition: background .4s ease; } -.header-search { - flex: 1; - max-width: 420px; - position: relative; +.topbar-left { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; } -.header-search .search-icon { - position: absolute; - left: 12px; - top: 50%; - transform: translateY(-50%); - font-size: 18px; - color: var(--text-muted); - pointer-events: none; -} - -.header-search input { - width: 100%; - background: var(--bg-surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-md); - padding: 8px 36px 8px 38px; - font-size: 13.5px; - font-weight: 400; - color: var(--text-primary); - transition: all 0.2s ease; -} - -.header-search input::placeholder { - color: var(--text-muted); -} - -.header-search input:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); - background: var(--bg-surface-1); -} - -.header-search .search-clear { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - width: 22px; - height: 22px; - border-radius: 50%; +.app-logo { + width: 28px; + height: 28px; + background: var(--accent); + border-radius: 8px; display: flex; align-items: center; justify-content: center; - color: var(--text-muted); - transition: all 0.15s; + transition: background .4s ease; } -.header-search .search-clear:hover { - background: var(--bg-surface-3); - color: var(--text-primary); +.app-title { + font-size: 16px; + font-weight: 700; + color: var(--white); + letter-spacing: -.02em; } -.header-meta { +/* ── Clock ── */ +.clock-wrap { + flex: 1; + display: flex; + justify-content: center; +} + +.clock { + font-size: 22px; + font-weight: 700; + color: var(--text-normal); + letter-spacing: .02em; + font-variant-numeric: tabular-nums; + opacity: .9; +} + +.clock-seconds { + font-size: 14px; + color: var(--text-faint); + font-weight: 500; +} + +.topbar-right { display: flex; align-items: center; - gap: 16px; - margin-left: auto; + gap: 6px; flex-shrink: 0; } -.sound-count { - font-size: 12px; +/* ── Channel Dropdown ── */ +.channel-dropdown { + position: relative; + flex-shrink: 0; +} + +.channel-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 12px 5px 10px; + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-normal); + font-family: var(--font); + font-size: 13px; font-weight: 600; - color: var(--text-secondary); - font-variant-numeric: tabular-nums; + cursor: pointer; + transition: all var(--transition); white-space: nowrap; } -.sound-count strong { - color: var(--accent); - font-weight: 700; +.channel-btn:hover { + background: var(--bg-modifier-selected); + border-color: rgba(255, 255, 255, .12); } -/* ── Size Toggle (S / M / L) ── */ -.size-toggle { - display: inline-flex; - background: var(--bg-surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - overflow: hidden; +.channel-btn.open { + border-color: var(--accent); +} + +.channel-btn .cb-icon { + font-size: 16px; + color: var(--text-muted); +} + +.channel-btn .chevron { + font-size: 12px; + color: var(--text-faint); + transition: transform var(--transition); + margin-left: 2px; +} + +.channel-btn.open .chevron { + transform: rotate(180deg); +} + +.channel-status { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--green); flex-shrink: 0; } -.size-opt { - padding: 4px 10px; - font-size: 11px; +.channel-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 220px; + background: var(--bg-deep); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: var(--radius); + box-shadow: var(--shadow-high); + padding: 6px; + z-index: 100; + animation: ctx-in 100ms ease-out; +} + +.channel-menu-header { + padding: 6px 8px 4px; + font-size: 10px; font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--text-faint); +} + +.channel-option { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: 4px; + font-size: 13px; color: var(--text-muted); - transition: all 0.15s; - border-right: 1px solid var(--border); - line-height: 1; -} - -.size-opt:last-child { - border-right: none; -} - -.size-opt:hover { - color: var(--text-primary); - background: var(--bg-surface-3); -} - -.size-opt.active { - color: var(--accent); - background: var(--accent-subtle); -} - -/* Theme & Channel selects */ -.select-clean { - appearance: none; - background: var(--bg-surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 6px 28px 6px 10px; - font-size: 12.5px; - font-weight: 500; - color: var(--text-primary); cursor: pointer; - transition: all 0.15s; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%237a7a90' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; + transition: all var(--transition); } -.select-clean:hover { - border-color: var(--border-hover); - background-color: var(--bg-surface-3); +.channel-option:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); } -.select-clean:focus { +.channel-option.active { + background: var(--accent); + color: var(--white); +} + +.channel-option .co-icon { + font-size: 16px; + opacity: .7; +} + +/* ── Connection Indicator ── */ +.connection { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 20px; + background: rgba(35, 165, 90, .12); + font-size: 12px; + color: var(--green); + font-weight: 600; +} + +.conn-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--green); + box-shadow: 0 0 6px rgba(35, 165, 90, .6); + animation: pulse-dot 2s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { box-shadow: 0 0 6px rgba(35, 165, 90, .5); } + 50% { box-shadow: 0 0 12px rgba(35, 165, 90, .8); } +} + +/* ── Admin Icon Button ── */ +.admin-btn-icon { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-faint); + transition: all var(--transition); + font-size: 18px; +} + +.admin-btn-icon:hover { + background: var(--bg-modifier-hover); + color: var(--text-normal); +} + +.admin-btn-icon.active { + color: var(--accent); +} + +/* ──────────────────────────────────────────── + Toolbar + ──────────────────────────────────────────── */ +.toolbar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 20px; + background: var(--bg-primary); + border-bottom: 1px solid rgba(0, 0, 0, .12); + flex-shrink: 0; + flex-wrap: wrap; + transition: background .4s ease; +} + +/* ── Category Tabs ── */ +.cat-tabs { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.cat-tab { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-muted); + font-family: var(--font); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} + +.cat-tab:hover { + background: var(--bg-modifier-selected); + color: var(--text-normal); +} + +.cat-tab.active { + background: var(--accent); + color: var(--white); +} + +.tab-count { + font-size: 10px; + font-weight: 700; + background: rgba(255, 255, 255, .15); + padding: 0 6px; + border-radius: 8px; + line-height: 1.6; +} + +/* ── Search ── */ +.search-wrap { + position: relative; + flex: 1; + max-width: 280px; + min-width: 140px; +} + +.search-wrap .search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 15px; + color: var(--text-faint); + pointer-events: none; +} + +.search-input { + width: 100%; + height: 32px; + padding: 0 28px 0 32px; + border: 1px solid rgba(255, 255, 255, .06); + border-radius: 20px; + background: var(--bg-secondary); + color: var(--text-normal); + font-family: var(--font); + font-size: 13px; outline: none; + transition: all var(--transition); +} + +.search-input::placeholder { + color: var(--text-faint); +} + +.search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); } -.select-clean option { - background: var(--bg-surface-1); - color: var(--text-primary); -} - -/* ──────────────────────────────────────────── - Tab Bar - ──────────────────────────────────────────── */ -.tab-bar { - height: var(--tabs-h); - min-height: var(--tabs-h); +.search-clear { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + width: 20px; + height: 20px; + border-radius: 50%; display: flex; - align-items: stretch; - gap: 0; - padding: 0 24px; - background: var(--bg-surface-0); - border-bottom: 1px solid var(--border); - position: relative; - z-index: 15; + align-items: center; + justify-content: center; + color: var(--text-faint); + transition: all var(--transition); } -.tab-btn { - position: relative; - padding: 0 18px; - font-size: 13px; - font-weight: 600; - color: var(--text-secondary); - transition: color 0.2s; +.search-clear:hover { + background: var(--bg-tertiary); + color: var(--text-normal); +} + +.toolbar-spacer { + flex: 1; +} + +/* ── Toolbar Buttons ── */ +.tb-btn { display: flex; align-items: center; gap: 6px; + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 20px; + background: var(--bg-tertiary); + color: var(--text-muted); + font-family: var(--font); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); white-space: nowrap; } -.tab-btn:hover { - color: var(--text-primary); +.tb-btn:hover { + background: var(--bg-modifier-selected); + color: var(--text-normal); + border-color: rgba(255, 255, 255, .12); } -.tab-btn.active { +.tb-btn .tb-icon { + font-size: 15px; +} + +.tb-btn.random { + border-color: rgba(88, 101, 242, .3); color: var(--accent); } -.tab-btn.active::after { - content: ''; - position: absolute; - bottom: 0; - left: 12px; - right: 12px; - height: 2px; +.tb-btn.random:hover { background: var(--accent); - border-radius: 2px 2px 0 0; + color: var(--white); + border-color: var(--accent); } -.tab-badge { - font-size: 10.5px; - font-weight: 700; - background: var(--accent-subtle); - color: var(--accent); - padding: 1px 6px; - border-radius: var(--radius-pill); - font-variant-numeric: tabular-nums; +.tb-btn.party { + border-color: rgba(240, 178, 50, .3); + color: var(--yellow); } -/* ──────────────────────────────────────────── - Category Filter Strip - ──────────────────────────────────────────── */ -.category-strip { - min-height: var(--cats-h); +.tb-btn.party:hover { + background: var(--yellow); + color: #1a1b1e; + border-color: var(--yellow); +} + +.tb-btn.party.active { + background: var(--yellow); + color: #1a1b1e; + border-color: var(--yellow); + animation: party-btn 600ms ease-in-out infinite alternate; +} + +@keyframes party-btn { + from { box-shadow: 0 0 8px rgba(240, 178, 50, .4); } + to { box-shadow: 0 0 20px rgba(240, 178, 50, .7); } +} + +.tb-btn.stop { + border-color: rgba(242, 63, 66, .3); + color: var(--red); +} + +.tb-btn.stop:hover { + background: var(--red); + color: var(--white); + border-color: var(--red); +} + +/* ── Size Slider ── */ +.size-control { display: flex; align-items: center; gap: 6px; - padding: 0 24px; - background: var(--bg-surface-0); - border-bottom: 1px solid var(--border); + padding: 4px 10px; + border-radius: 20px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .06); +} + +.size-control .sc-icon { + font-size: 14px; + color: var(--text-faint); +} + +.size-slider { + -webkit-appearance: none; + appearance: none; + width: 70px; + height: 3px; + border-radius: 2px; + background: var(--bg-modifier-selected); + outline: none; + cursor: pointer; +} + +.size-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + cursor: pointer; + transition: transform var(--transition); +} + +.size-slider::-webkit-slider-thumb:hover { + transform: scale(1.3); +} + +.size-slider::-moz-range-thumb { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + border: none; + cursor: pointer; +} + +/* ── Theme Selector ── */ +.theme-selector { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 20px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .06); +} + +.theme-dot { + width: 16px; + height: 16px; + border-radius: 50%; + cursor: pointer; + transition: all var(--transition); + border: 2px solid transparent; +} + +.theme-dot:hover { + transform: scale(1.2); +} + +.theme-dot.active { + border-color: var(--white); + box-shadow: 0 0 6px rgba(255, 255, 255, .3); +} + +/* ──────────────────────────────────────────── + Category / Folder Strip + ──────────────────────────────────────────── */ +.category-strip { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 20px; + background: var(--bg-primary); + border-bottom: 1px solid rgba(0, 0, 0, .12); overflow-x: auto; - overflow-y: hidden; - position: relative; - z-index: 14; + flex-shrink: 0; scrollbar-width: none; + transition: background .4s ease; } .category-strip::-webkit-scrollbar { @@ -529,28 +668,27 @@ input, select { display: inline-flex; align-items: center; gap: 6px; - padding: 5px 12px; - border-radius: var(--radius-pill); + padding: 4px 12px; + border-radius: 20px; font-size: 12px; font-weight: 600; - color: var(--text-secondary); - background: var(--bg-surface-2); - border: 1px solid var(--border); + color: var(--text-muted); + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .06); white-space: nowrap; - transition: all 0.15s ease; + cursor: pointer; + transition: all var(--transition); flex-shrink: 0; } .cat-chip:hover { - border-color: var(--border-hover); - color: var(--text-primary); - background: var(--bg-surface-3); + border-color: rgba(255, 255, 255, .12); + color: var(--text-normal); + background: var(--bg-tertiary); } .cat-chip.active { - background: var(--accent-subtle); - border-color: var(--accent); - color: var(--accent); + background: rgba(88, 101, 242, .1); } .cat-dot { @@ -560,541 +698,560 @@ input, select { flex-shrink: 0; } +.cat-count { + font-size: 10px; + font-weight: 700; + opacity: .5; +} + /* ──────────────────────────────────────────── - Sounds Area & Grid + Main Grid Area ──────────────────────────────────────────── */ -.sounds-area { +.main { flex: 1; overflow-y: auto; - overflow-x: hidden; + padding: 16px 20px; + background: var(--bg-primary); + transition: background .4s ease; +} + +.sound-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--card-size), 1fr)); + gap: 8px; +} + +/* ──────────────────────────────────────────── + Sound Card + ──────────────────────────────────────────── */ +.sound-card { position: relative; - z-index: 1; -} - -.sounds-grid { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 16px 24px; - padding-bottom: calc(var(--control-h) + 24px); - align-content: flex-start; -} - -.sounds-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 80px 24px; - color: var(--text-muted); - text-align: center; - width: 100%; -} - -.sounds-empty .material-icons { - font-size: 48px; - margin-bottom: 12px; - opacity: 0.4; -} - -.sounds-empty p { - font-size: 14px; - font-weight: 500; -} - -/* ── Sound Button (Compact Pill) ── */ -.sound-btn { - display: inline-flex; - align-items: center; - gap: 0; - height: 36px; - padding: 0 12px 0 0; - border-radius: var(--radius-sm); - background: var(--bg-surface-1); - border: 1px solid var(--border); - font-size: 12.5px; - font-weight: 500; - color: var(--text-primary); + gap: 3px; + padding: 12px 6px 8px; + background: var(--bg-secondary); + border-radius: var(--radius-lg); cursor: pointer; - transition: all 0.12s ease; - position: relative; + transition: all var(--transition); + border: 2px solid transparent; + user-select: none; overflow: hidden; - max-width: 220px; - flex-shrink: 0; + aspect-ratio: 1; + opacity: 0; + animation: card-enter 350ms ease-out forwards; } -.sound-btn .cat-bar { - width: 3px; - height: 100%; - flex-shrink: 0; - border-radius: var(--radius-sm) 0 0 var(--radius-sm); +.sound-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + opacity: 0; + transition: opacity var(--transition); + background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 70%); + pointer-events: none; } -.sound-btn .sound-label { - padding: 0 10px; - white-space: nowrap; +.sound-card:hover { + background: var(--bg-tertiary); + transform: translateY(-3px); + box-shadow: var(--shadow-med), 0 0 20px var(--accent-glow); + border-color: rgba(88, 101, 242, .2); +} + +.sound-card:hover::before { + opacity: 1; +} + +.sound-card:active { + transform: translateY(0); + transition-duration: 50ms; +} + +.sound-card.playing { + border-color: var(--accent); + animation: card-enter 350ms ease-out forwards, playing-glow 1.2s ease-in-out infinite alternate; +} + +@keyframes playing-glow { + from { box-shadow: 0 0 4px var(--accent-glow); } + to { box-shadow: 0 0 16px var(--accent-glow); } +} + +@keyframes card-enter { + from { opacity: 0; transform: translateY(10px) scale(.97); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +/* ── Ripple Effect ── */ +.ripple { + position: absolute; + border-radius: 50%; + background: rgba(88, 101, 242, .3); + transform: scale(0); + animation: ripple-expand 500ms ease-out forwards; + pointer-events: none; +} + +@keyframes ripple-expand { + to { transform: scale(3); opacity: 0; } +} + +/* ── Sound Card Content ── */ +.sound-emoji { + font-size: var(--card-emoji); + font-weight: 800; + line-height: 1; + z-index: 1; + transition: transform var(--transition); + opacity: .7; + font-family: 'Syne', 'DM Sans', sans-serif; +} + +.sound-card:hover .sound-emoji { + transform: scale(1.15); + opacity: 1; +} + +.sound-card.playing .sound-emoji { + animation: emoji-bounce 400ms ease; + opacity: 1; +} + +@keyframes emoji-bounce { + 0%, 100% { transform: scale(1); } + 40% { transform: scale(1.3); } + 70% { transform: scale(.95); } +} + +.sound-name { + font-size: var(--card-font); + font-weight: 600; + text-align: center; + color: var(--text-normal); + z-index: 1; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; - line-height: 36px; + white-space: nowrap; + padding: 0 4px; } -.sound-btn:hover { - background: var(--bg-surface-2); - border-color: var(--border-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-md); +.sound-duration { + font-size: 9px; + color: var(--text-faint); + z-index: 1; + font-weight: 500; } -.sound-btn:active { - transform: scale(0.96); - box-shadow: none; - transition-duration: 0.05s; -} - -.sound-btn .fav-star { +/* ── Favorite Star ── */ +.fav-star { position: absolute; - right: 3px; - top: 50%; - transform: translateY(-50%); - font-size: 14px; + top: 4px; + right: 4px; opacity: 0; - color: var(--text-muted); - transition: all 0.15s; + transition: all var(--transition); + cursor: pointer; + z-index: 2; + color: var(--text-faint); padding: 2px; line-height: 1; } -.sound-btn:hover .fav-star { - opacity: 0.6; +.fav-star .fav-icon { + font-size: 14px; } -.sound-btn .fav-star.is-fav { - opacity: 1; - color: var(--warning); +.sound-card:hover .fav-star { + opacity: .6; } -.sound-btn:hover .fav-star.is-fav { - opacity: 1; +.fav-star:hover { + opacity: 1 !important; + color: var(--yellow); + transform: scale(1.2); } -/* Playing animation */ -.sound-btn.is-playing { - border-color: var(--accent); - box-shadow: var(--shadow-glow); +.fav-star.active { + opacity: 1 !important; + color: var(--yellow); } -.sound-btn.is-playing::after { - content: ''; +/* ── "New" Badge ── */ +.new-badge { position: absolute; - inset: 0; - background: var(--accent-glow); - animation: pulse-bg 1s ease infinite; - pointer-events: none; -} - -/* Badge indicators */ -.sound-btn .badge-dot { - position: absolute; - top: 3px; - right: 3px; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--accent); -} - -.sound-btn .badge-dot.new { - background: var(--success); -} - -.sound-btn .badge-dot.top { - background: var(--warning); -} - -/* ── Button Size Variants (S / M / L) ── */ -/* S = default (36px) */ - -[data-btn-size="M"] .sound-btn { - height: 46px; - font-size: 13.5px; - max-width: 260px; - padding: 0 16px 0 0; -} - -[data-btn-size="M"] .sound-btn .sound-label { - line-height: 46px; - padding: 0 12px; -} - -[data-btn-size="M"] .sound-btn .cat-bar { - width: 4px; -} - -[data-btn-size="M"] .sound-btn .fav-star { - right: 4px; -} - -[data-btn-size="M"] .sound-btn .fav-star .material-icons { - font-size: 16px; -} - -[data-btn-size="M"] .sound-btn .badge-dot { - width: 7px; - height: 7px; top: 4px; - right: 4px; + left: 4px; + font-size: 8px; + font-weight: 700; + background: var(--green); + color: white; + padding: 1px 5px; + border-radius: 6px; + text-transform: uppercase; + letter-spacing: .03em; + z-index: 2; } -[data-btn-size="L"] .sound-btn { - height: 58px; - font-size: 14.5px; - max-width: 300px; - padding: 0 20px 0 0; +/* ── Playing Wave Indicator ── */ +.playing-indicator { + position: absolute; + bottom: 3px; + left: 50%; + transform: translateX(-50%); + display: none; + gap: 2px; + align-items: flex-end; + height: 10px; } -[data-btn-size="L"] .sound-btn .sound-label { - line-height: 58px; - padding: 0 14px; +.sound-card.playing .playing-indicator { + display: flex; } -[data-btn-size="L"] .sound-btn .cat-bar { - width: 5px; +.wave-bar { + width: 2px; + background: var(--accent); + border-radius: 1px; + animation: wave 600ms ease-in-out infinite alternate; } -[data-btn-size="L"] .sound-btn .fav-star { - right: 6px; -} +.wave-bar:nth-child(1) { height: 3px; animation-delay: 0ms; } +.wave-bar:nth-child(2) { height: 7px; animation-delay: 150ms; } +.wave-bar:nth-child(3) { height: 5px; animation-delay: 300ms; } +.wave-bar:nth-child(4) { height: 9px; animation-delay: 100ms; } -[data-btn-size="L"] .sound-btn .fav-star .material-icons { - font-size: 18px; -} - -[data-btn-size="L"] .sound-btn .badge-dot { - width: 8px; - height: 8px; - top: 5px; - right: 5px; +@keyframes wave { + from { height: 2px; } + to { height: 10px; } } /* ──────────────────────────────────────────── - Control Bar (Bottom) + Empty State ──────────────────────────────────────────── */ -.control-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: var(--control-h); - background: var(--bg-surface-0); - border-top: 1px solid var(--border); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - display: flex; +.empty-state { + display: none; + flex-direction: column; align-items: center; - padding: 0 24px; - gap: 12px; - z-index: 30; + justify-content: center; + gap: 10px; + padding: 60px 20px; + text-align: center; } -.ctrl-section { +.empty-state.visible { + display: flex; +} + +.empty-emoji { + font-size: 42px; +} + +.empty-title { + font-size: 15px; + font-weight: 700; + color: var(--text-normal); +} + +.empty-desc { + font-size: 13px; + color: var(--text-muted); + max-width: 260px; +} + +/* ──────────────────────────────────────────── + Bottom Bar + ──────────────────────────────────────────── */ +.bottombar { + display: flex; + align-items: center; + gap: 14px; + padding: 0 20px; + height: 48px; + background: var(--bg-secondary); + border-top: 1px solid rgba(0, 0, 0, .24); + z-index: 10; + flex-shrink: 0; + transition: background .4s ease; +} + +.now-playing { display: flex; align-items: center; gap: 8px; -} - -.ctrl-section.left { - flex: 1; - min-width: 0; -} - -.ctrl-section.center { - flex-shrink: 0; -} - -.ctrl-section.right { - flex: 1; - justify-content: flex-end; - min-width: 0; -} - -/* Channel select in control bar */ -.channel-wrap { - display: flex; - align-items: center; - gap: 6px; - min-width: 0; -} - -.channel-wrap .material-icons { - font-size: 18px; + font-size: 13px; color: var(--text-muted); - flex-shrink: 0; + min-width: 0; + flex: 1; } -.channel-select { - appearance: none; - background: var(--bg-surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 6px 28px 6px 10px; +.np-label { + color: var(--text-faint); font-size: 12px; - font-weight: 500; - color: var(--text-primary); - cursor: pointer; - min-width: 160px; - max-width: 280px; - transition: all 0.15s; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%237a7a90' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; -} - -.channel-select:hover { - border-color: var(--border-hover); -} - -.channel-select:focus { - outline: none; - border-color: var(--accent); - box-shadow: 0 0 0 2px var(--accent-glow); -} - -.channel-select option { - background: var(--bg-surface-1); -} - -/* ── Control Buttons ── */ -.ctrl-btn { - height: 38px; - padding: 0 16px; - border-radius: var(--radius-pill); - font-size: 12.5px; - font-weight: 600; - display: flex; - align-items: center; - gap: 6px; - transition: all 0.15s ease; white-space: nowrap; } -.ctrl-btn .material-icons { - font-size: 18px; +.np-name { + color: var(--accent); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -/* Stop */ -.ctrl-btn.stop { - background: var(--danger-glow); - color: var(--danger); -} -.ctrl-btn.stop:hover { - background: var(--danger); - color: white; - box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3); +.np-waves { + display: none; + gap: 1.5px; + align-items: flex-end; + height: 14px; } -/* Random / Shuffle */ -.ctrl-btn.shuffle { +.np-waves.active { + display: flex; +} + +.np-wave-bar { + width: 2.5px; background: var(--accent); - color: white; -} -.ctrl-btn.shuffle:hover { - background: var(--accent-hover); - box-shadow: 0 4px 16px var(--accent-glow); - transform: scale(1.03); + border-radius: 1px; + animation: wave 500ms ease-in-out infinite alternate; } -/* Party Mode */ -.ctrl-btn.party { - background: var(--bg-surface-2); - color: var(--text-secondary); - border: 1px solid var(--border); -} -.ctrl-btn.party:hover { - border-color: var(--border-hover); - color: var(--text-primary); - background: var(--bg-surface-3); -} - -.ctrl-btn.party.active { - background: linear-gradient(135deg, #ec4899, #8b5cf6, #3b82f6, #06b6d4); - background-size: 300% 300%; - animation: party-gradient 4s ease infinite; - color: white; - border: none; - box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4); -} - -.ctrl-btn.party.active:hover { - transform: scale(1.03); -} +.np-wave-bar:nth-child(1) { height: 4px; animation-delay: 0ms; } +.np-wave-bar:nth-child(2) { height: 9px; animation-delay: 120ms; } +.np-wave-bar:nth-child(3) { height: 6px; animation-delay: 240ms; } +.np-wave-bar:nth-child(4) { height: 11px; animation-delay: 80ms; } /* ── Volume ── */ -.volume-wrap { +.volume-section { display: flex; align-items: center; gap: 8px; - width: 150px; - flex-shrink: 0; + margin-left: auto; } -.volume-wrap .material-icons { - font-size: 16px; +.volume-icon { + font-size: 18px; color: var(--text-muted); cursor: pointer; - flex-shrink: 0; + transition: color var(--transition); } -.volume-wrap .material-icons:hover { - color: var(--text-secondary); +.volume-icon:hover { + color: var(--text-normal); } .volume-slider { -webkit-appearance: none; appearance: none; - width: 100%; - height: 4px; - background: var(--bg-surface-3); + width: 90px; + height: 3px; border-radius: 2px; + background: linear-gradient(to right, var(--accent) 0%, var(--accent) var(--vol, 80%), var(--bg-tertiary) var(--vol, 80%)); outline: none; - position: relative; - background-image: linear-gradient(var(--accent), var(--accent)); - background-size: var(--fill, 100%) 100%; - background-repeat: no-repeat; + cursor: pointer; } .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; - width: 14px; - height: 14px; + width: 12px; + height: 12px; border-radius: 50%; - background: var(--text-primary); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); + background: var(--white); + box-shadow: 0 1px 4px rgba(0, 0, 0, .3); cursor: pointer; - transition: transform 0.1s; -} - -.volume-slider::-webkit-slider-thumb:hover { - transform: scale(1.2); } .volume-slider::-moz-range-thumb { - width: 14px; - height: 14px; + width: 12px; + height: 12px; border-radius: 50%; - background: var(--text-primary); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4); - cursor: pointer; + background: var(--white); + box-shadow: 0 1px 4px rgba(0, 0, 0, .3); border: none; + cursor: pointer; } .volume-pct { font-size: 11px; - font-weight: 600; - color: var(--text-muted); - min-width: 30px; + color: var(--text-faint); + min-width: 28px; text-align: right; font-variant-numeric: tabular-nums; } -/* ── Now Playing ── */ -.now-playing { - font-size: 11px; - font-weight: 500; - color: var(--text-muted); - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +/* ──────────────────────────────────────────── + Party Mode Overlay + ──────────────────────────────────────────── */ +.party-overlay { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 50; + opacity: 0; + transition: opacity .3s ease; } -.now-playing span { - color: var(--accent); +.party-overlay.active { + opacity: 1; + animation: party-hue 2s linear infinite; +} + +.party-overlay::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(45deg, + rgba(255, 0, 0, .04), + rgba(0, 255, 0, .04), + rgba(0, 0, 255, .04), + rgba(255, 255, 0, .04) + ); + background-size: 400% 400%; + animation: party-grad 3s ease infinite; +} + +@keyframes party-grad { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +@keyframes party-hue { + to { filter: hue-rotate(360deg); } } /* ──────────────────────────────────────────── - Notifications / Toast + Context Menu + ──────────────────────────────────────────── */ +.ctx-menu { + position: fixed; + min-width: 160px; + background: var(--bg-deep); + border: 1px solid rgba(255, 255, 255, .06); + border-radius: var(--radius); + box-shadow: var(--shadow-high); + padding: 4px; + z-index: 1000; + animation: ctx-in 100ms ease-out; +} + +@keyframes ctx-in { + from { opacity: 0; transform: scale(.96) translateY(-4px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.ctx-item { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 4px; + font-size: 13px; + color: var(--text-normal); + cursor: pointer; + transition: all var(--transition); +} + +.ctx-item:hover { + background: var(--accent); + color: var(--white); +} + +.ctx-item.danger { + color: var(--red); +} + +.ctx-item.danger:hover { + background: var(--red); + color: var(--white); +} + +.ctx-item .ctx-icon { + font-size: 15px; +} + +.ctx-sep { + height: 1px; + background: rgba(255, 255, 255, .06); + margin: 3px 8px; +} + +/* ──────────────────────────────────────────── + Toast Notification ──────────────────────────────────────────── */ .toast { position: fixed; - bottom: calc(var(--control-h) + 16px); + bottom: 64px; left: 50%; transform: translateX(-50%); padding: 10px 20px; - border-radius: var(--radius-pill); + border-radius: 20px; font-size: 13px; font-weight: 600; z-index: 100; display: flex; align-items: center; gap: 8px; - box-shadow: var(--shadow-lg); + box-shadow: var(--shadow-high); animation: toast-in 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); pointer-events: none; } -.toast .material-icons { +.toast .toast-icon { font-size: 16px; } .toast.error { - background: var(--danger); + background: var(--red); color: white; } .toast.info { - background: var(--success); + background: var(--green); color: white; } +@keyframes toast-in { + from { transform: translate(-50%, 16px); opacity: 0; } + to { transform: translate(-50%, 0); opacity: 1; } +} + /* ──────────────────────────────────────────── - Admin Panel (Overlay) + Admin Panel Overlay ──────────────────────────────────────────── */ -.admin-toggle { - width: 32px; - height: 32px; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); - transition: all 0.15s; -} - -.admin-toggle:hover { - background: var(--bg-surface-2); - color: var(--text-primary); -} - -.admin-toggle.is-admin { - color: var(--accent); -} - .admin-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, .6); backdrop-filter: blur(8px); - z-index: 50; + -webkit-backdrop-filter: blur(8px); + z-index: 60; display: flex; align-items: center; justify-content: center; - animation: fade-in 0.2s ease; + animation: fade-in 200ms ease; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } } .admin-panel { - background: var(--bg-surface-1); - border: 1px solid var(--border); + background: var(--bg-secondary); + border: 1px solid rgba(255, 255, 255, .08); border-radius: var(--radius-lg); padding: 28px; width: 90%; - max-width: 480px; - max-height: 80vh; - overflow-y: auto; - box-shadow: var(--shadow-lg); + max-width: 400px; + box-shadow: var(--shadow-high); } .admin-panel h3 { - font-family: 'Syne', sans-serif; font-size: 18px; font-weight: 700; margin-bottom: 20px; @@ -1103,20 +1260,20 @@ input, select { justify-content: space-between; } -.admin-panel .admin-close { +.admin-close { width: 28px; height: 28px; - border-radius: var(--radius-sm); + border-radius: 6px; display: flex; align-items: center; justify-content: center; - color: var(--text-secondary); - transition: all 0.15s; + color: var(--text-muted); + transition: all var(--transition); } -.admin-panel .admin-close:hover { - background: var(--bg-surface-3); - color: var(--text-primary); +.admin-close:hover { + background: var(--bg-tertiary); + color: var(--text-normal); } .admin-field { @@ -1127,20 +1284,22 @@ input, select { display: block; font-size: 12px; font-weight: 600; - color: var(--text-secondary); + color: var(--text-muted); margin-bottom: 6px; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: .5px; } .admin-field input { width: 100%; - background: var(--bg-surface-2); - border: 1px solid var(--border); - border-radius: var(--radius-sm); + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, .08); + border-radius: 8px; padding: 10px 12px; font-size: 14px; - transition: all 0.15s; + color: var(--text-normal); + font-family: var(--font); + transition: all var(--transition); } .admin-field input:focus { @@ -1149,242 +1308,110 @@ input, select { box-shadow: 0 0 0 2px var(--accent-glow); } -.admin-btn { +.admin-btn-action { padding: 10px 20px; - border-radius: var(--radius-sm); + border-radius: 8px; font-size: 13px; font-weight: 600; - transition: all 0.15s; + font-family: var(--font); + cursor: pointer; + transition: all var(--transition); } -.admin-btn.primary { +.admin-btn-action.primary { background: var(--accent); color: white; + border: none; } -.admin-btn.primary:hover { + +.admin-btn-action.primary:hover { background: var(--accent-hover); } -.admin-btn.outline { +.admin-btn-action.outline { background: transparent; - border: 1px solid var(--border); - color: var(--text-secondary); -} -.admin-btn.outline:hover { - border-color: var(--border-hover); - color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, .08); + color: var(--text-muted); } -.admin-btn.danger { - background: var(--danger-glow); - color: var(--danger); -} -.admin-btn.danger:hover { - background: var(--danger); - color: white; -} - -/* ──────────────────────────────────────────── - Party Mode Global Effects - ──────────────────────────────────────────── */ -.app-shell.party-active .header { - border-bottom-color: transparent; - background: linear-gradient(90deg, - rgba(236, 72, 153, 0.08), - rgba(139, 92, 246, 0.08), - rgba(59, 130, 246, 0.08), - rgba(6, 182, 212, 0.08) - ); - background-size: 400% 100%; - animation: party-gradient 6s ease infinite; -} - -.app-shell.party-active .control-bar { - border-top-color: transparent; - box-shadow: 0 -2px 20px rgba(139, 92, 246, 0.15); -} - -/* ──────────────────────────────────────────── - Animations & Keyframes - ──────────────────────────────────────────── */ -@keyframes party-gradient { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } -} - -@keyframes pulse-bg { - 0%, 100% { opacity: 0.3; } - 50% { opacity: 0.6; } -} - -@keyframes toast-in { - from { - transform: translate(-50%, 16px); - opacity: 0; - } - to { - transform: translate(-50%, 0); - opacity: 1; - } -} - -@keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Staggered entrance for sound buttons */ -.sounds-grid .sound-btn { - animation: sound-enter 0.2s ease both; -} - -@keyframes sound-enter { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); - } +.admin-btn-action.outline:hover { + border-color: rgba(255, 255, 255, .12); + color: var(--text-normal); } /* ──────────────────────────────────────────── Responsive ──────────────────────────────────────────── */ -@media (max-width: 768px) { - .header { - padding: 0 16px; - gap: 10px; - height: auto; - min-height: 56px; - flex-wrap: wrap; - padding-top: 10px; - padding-bottom: 10px; +@media (max-width: 700px) { + .toolbar { + gap: 6px; + padding: 8px 12px; } - .logo { - font-size: 18px; - } - - .header-search { - order: 10; - max-width: 100%; - flex-basis: 100%; - } - - .header-meta { - gap: 8px; - } - - .sound-count { - display: none; - } - - .tab-bar { - padding: 0 16px; + .cat-tabs { overflow-x: auto; scrollbar-width: none; } - .tab-bar::-webkit-scrollbar { + .cat-tabs::-webkit-scrollbar { display: none; } - .category-strip { - padding: 0 16px; + .search-wrap { + max-width: 100%; + min-width: 100%; + order: -1; } - .sounds-grid { - padding: 12px 16px; - gap: 5px; + .size-control, + .theme-selector { + display: none; } - .sound-btn { - font-size: 11.5px; - height: 32px; - max-width: 180px; + .main { + padding: 12px; } - [data-btn-size="M"] .sound-btn { - height: 40px; - font-size: 12.5px; - max-width: 220px; - } - [data-btn-size="M"] .sound-btn .sound-label { - line-height: 40px; - } - - [data-btn-size="L"] .sound-btn { - height: 50px; - font-size: 13.5px; - max-width: 260px; - } - [data-btn-size="L"] .sound-btn .sound-label { - line-height: 50px; - } - - .control-bar { + .topbar { padding: 0 12px; - height: auto; - min-height: var(--control-h); - flex-wrap: wrap; - padding-top: 10px; - padding-bottom: 10px; gap: 8px; } - .ctrl-section.left { - order: 1; - flex-basis: 100%; + .channel-label { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; } - .ctrl-section.center { - order: 2; + .clock { + font-size: 16px; } - .ctrl-section.right { - order: 3; + .clock-seconds { + font-size: 11px; } - .volume-wrap { - width: 120px; + .bottombar { + padding: 0 12px; } - .channel-select { - min-width: 120px; - } - - .sounds-grid { - padding-bottom: calc(140px + 24px); - } - - .toast { - bottom: 150px; + .tb-btn span:not(.tb-icon) { + display: none; } } @media (max-width: 480px) { - .ctrl-btn span:not(.material-icons) { + .connection { display: none; } - .ctrl-btn { - padding: 0 10px; - } - - .volume-wrap { - width: 100px; - } - - .now-playing { + .app-title { display: none; } + + .toolbar .tb-btn { + padding: 6px 8px; + } } /* ──────────────────────────────────────────── @@ -1401,17 +1428,3 @@ input, select { white-space: nowrap; border-width: 0; } - -/* Vapor theme gradient overlay */ -[data-theme="vapor"] .app-shell::before { - content: ''; - position: fixed; - inset: 0; - background: linear-gradient(180deg, - rgba(120, 40, 200, 0.06) 0%, - transparent 40%, - rgba(6, 214, 160, 0.04) 100% - ); - pointer-events: none; - z-index: 0; -}