feat(frontend): Stitch-Layouts (Light/Dark/Rainbow) 1:1 übernommen; JSX migriert; Funktionen erhalten
This commit is contained in:
parent
a8602700b3
commit
7b8cb11819
2 changed files with 128 additions and 222 deletions
268
web/src/App.tsx
268
web/src/App.tsx
|
|
@ -137,234 +137,106 @@ export default function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className="container">
|
<div className="container mx-auto">
|
||||||
<header>
|
<header className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8">
|
||||||
<div className="header-row">
|
<div>
|
||||||
<h1>Einmal mit Soundboard -Profis</h1>
|
<h1 className="text-4xl sm:text-5xl font-black gradient-text">Soundboard Profis</h1>
|
||||||
<div className="clock">{clock}</div>
|
<p className="text-6xl sm:text-8xl font-bold mt-1" style={{color:'var(--text-primary)'}}>{clock}</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display:'flex', gap:12, alignItems:'center' }}>
|
<div className="flex items-center space-x-3 mt-4 sm:mt-0">
|
||||||
<div className="badge">Geladene Sounds: {total}</div>
|
<div className="text-right">
|
||||||
<div className="badge">Gesamt abgespielt: {totalPlays}</div>
|
<span className="text-sm block" style={{color:'var(--text-secondary)'}}>Geladene Sounds</span>
|
||||||
<button type="button" className="tab" style={{ background:'#b91c1c', borderColor:'transparent', color:'#fff' }} onClick={async () => {
|
<span className="text-xl font-bold" style={{color:'var(--text-primary)'}}>{total}</span>
|
||||||
if (!selected) return;
|
<span className="text-xs block" style={{color:'var(--text-secondary)'}}>Gesamt abgespielt: {totalPlays}</span>
|
||||||
const [guildId] = selected.split(':');
|
</div>
|
||||||
try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method:'POST' }); } catch {}
|
<button className="bg-[var(--accent-blue)] text-white hover:bg-opacity-90 font-semibold py-2 px-5 rounded-full transition-all" onClick={async () => {
|
||||||
}}>Panik</button>
|
try { const res = await fetch('/api/sounds'); const data = await res.json(); const items = data?.items || []; if (!items.length || !selected) return; const rnd = items[Math.floor(Math.random()*items.length)]; const [guildId, channelId] = selected.split(':'); await playSound(rnd.name, guildId, channelId, volume, rnd.relativePath);} catch {}
|
||||||
<button type="button" className="tab" onClick={async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/sounds');
|
|
||||||
const data = await res.json();
|
|
||||||
const items = data?.items || [];
|
|
||||||
if (!items.length || !selected) return;
|
|
||||||
const rnd = items[Math.floor(Math.random() * items.length)];
|
|
||||||
const [guildId, channelId] = selected.split(':');
|
|
||||||
await playSound(rnd.name, guildId, channelId, volume, rnd.relativePath);
|
|
||||||
} catch {}
|
|
||||||
}}>Random</button>
|
}}>Random</button>
|
||||||
|
<button className="bg-red-600 text-white hover:bg-red-700 font-semibold py-2 px-5 rounded-full transition-all" onClick={async () => { if (!selected) return; const [guildId] = selected.split(':'); await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method:'POST' }); }}>Panik</button>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin && (
|
|
||||||
<div className="badge">Admin-Modus</div>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="controls glass row1">
|
<div className="control-panel rounded-xl p-6 mb-8">
|
||||||
<div className="control search">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 items-center">
|
||||||
<input
|
<div className="relative">
|
||||||
value={query}
|
<input className="input-field pl-10" placeholder="Nach Sounds suchen..." value={query} onChange={(e)=>setQuery(e.target.value)} />
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>search</span>
|
||||||
placeholder="Nach Sounds suchen..."
|
|
||||||
aria-label="Suche"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<CustomSelect
|
<div className="relative">
|
||||||
channels={channels}
|
<CustomSelect channels={channels} value={selected} onChange={setSelected} />
|
||||||
value={selected}
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>folder_special</span>
|
||||||
onChange={setSelected}
|
|
||||||
/>
|
|
||||||
<div className="control volume">
|
|
||||||
<label>🔊 {Math.round(volume * 100)}%</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
value={volume}
|
|
||||||
onChange={async (e) => {
|
|
||||||
const v = parseFloat(e.target.value);
|
|
||||||
setVolume(v);
|
|
||||||
if (selected) {
|
|
||||||
const [guildId] = selected.split(':');
|
|
||||||
try { await setVolumeLive(guildId, v); } catch {}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-label="Lautstärke"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="control theme">
|
<div className="flex items-center space-x-3">
|
||||||
<select value={theme} onChange={(e) => setTheme(e.target.value)} aria-label="Theme">
|
<span className="material-icons" style={{color:'var(--text-secondary)'}}>volume_up</span>
|
||||||
|
<input className="w-full h-2 rounded-lg appearance-none cursor-pointer" style={{background:'var(--bg-tertiary)'}} type="range" min={0} max={1} step={0.01} value={volume} onChange={async (e)=>{ const v=parseFloat(e.target.value); setVolume(v); if(selected){ const [guildId]=selected.split(':'); try{ await setVolumeLive(guildId, v);}catch{} }}} />
|
||||||
|
<span className="text-sm font-semibold w-8 text-center" style={{color:'var(--text-secondary)'}}>{Math.round(volume*100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative md:col-span-2 lg:col-span-1">
|
||||||
|
<input className="input-field pl-10" placeholder="MP3 URL..." value={mediaUrl} onChange={(e)=>setMediaUrl(e.target.value)} onKeyDown={async (e)=>{ if(e.key==='Enter'){ if(!selected){ setError('Bitte Voice-Channel wählen'); setInfo(null); return;} const [guildId,channelId]=selected.split(':'); try{ await playUrl(mediaUrl,guildId,channelId,volume); setError(null); setInfo('MP3 heruntergeladen und abgespielt.'); }catch(err:any){ setInfo(null); setError(err?.message||'Download fehlgeschlagen'); } } }} />
|
||||||
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>link</span>
|
||||||
|
<button className="absolute right-0 top-0 h-full px-4 text-white flex items-center rounded-r-lg transition-all font-semibold" style={{background:'var(--accent-green)'}} onClick={async ()=>{ if(!selected){ setError('Bitte Voice-Channel wählen'); setInfo(null); return;} const [guildId,channelId]=selected.split(':'); try{ await playUrl(mediaUrl,guildId,channelId,volume); setError(null); setInfo('MP3 heruntergeladen und abgespielt.'); }catch(e:any){ setInfo(null); setError(e?.message||'Download fehlgeschlagen'); } }}>
|
||||||
|
<span className="material-icons text-sm mr-1">file_download</span>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-3 lg:col-span-2">
|
||||||
|
<div className="relative flex-grow">
|
||||||
|
<select className="input-field appearance-none pl-10" value={theme} onChange={(e)=>setTheme(e.target.value)}>
|
||||||
|
<option value="system">System</option>
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
<option value="light">Light</option>
|
|
||||||
<option value="rainbow">Rainbow Chaos</option>
|
<option value="rainbow">Rainbow Chaos</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>palette</span>
|
||||||
|
<span className="material-icons absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" style={{color:'var(--text-secondary)'}}>unfold_more</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="controls glass row2">
|
|
||||||
<div className="control" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 8 }}>
|
|
||||||
<input
|
|
||||||
value={mediaUrl}
|
|
||||||
onChange={(e) => setMediaUrl(e.target.value)}
|
|
||||||
onKeyDown={async (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
if (!selected) { setError('Bitte Voice-Channel wählen'); return; }
|
|
||||||
const [guildId, channelId] = selected.split(':');
|
|
||||||
try { await playUrl(mediaUrl, guildId, channelId, volume); }
|
|
||||||
catch (err: any) { setError(err?.message || 'Play-URL fehlgeschlagen'); }
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="MP3 URL..."
|
|
||||||
/>
|
|
||||||
<button type="button" className="tab" onClick={async () => {
|
|
||||||
if (!selected) { setError('Bitte Voice-Channel wählen'); setInfo(null); return; }
|
|
||||||
const [guildId, channelId] = selected.split(':');
|
|
||||||
try { await playUrl(mediaUrl, guildId, channelId, volume); setError(null); setInfo('MP3 heruntergeladen und abgespielt.'); }
|
|
||||||
catch (e: any) { setInfo(null); setError(e?.message || 'Download fehlgeschlagen'); }
|
|
||||||
}}>⬇ Download</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
<div className="mt-6" style={{borderTop:'1px solid var(--border-color)', paddingTop:'1.5rem'}}>
|
||||||
|
<div className="flex items-center gap-4 justify-end">
|
||||||
{!isAdmin && (
|
{!isAdmin && (
|
||||||
<section className="controls glass row3">
|
<>
|
||||||
<div className="control" style={{ width: 280 }}>
|
<div className="relative w-full sm:w-auto" style={{maxWidth:'15%'}}>
|
||||||
<input type="password" value={adminPwd} onChange={(e) => setAdminPwd(e.target.value)} placeholder="Admin Passwort" />
|
<input className="input-field pl-10" placeholder="Admin Passwort" type="password" value={adminPwd} onChange={(e)=>setAdminPwd(e.target.value)} />
|
||||||
|
<span className="material-icons absolute left-3 top-1/2 -translate-y-1/2" style={{color:'var(--text-secondary)'}}>lock</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="control" style={{ width: 120 }}>
|
<button className="bg-gray-800 text-white hover:bg-black font-semibold py-2 px-5 rounded-lg transition-all w-full sm:w-auto" style={{maxWidth:'15%'}} onClick={async ()=>{ const ok=await adminLogin(adminPwd); if(ok){ setIsAdmin(true); setAdminPwd(''); } else alert('Login fehlgeschlagen'); }}>Login</button>
|
||||||
<button type="button" className="tab" style={{ width: '100%' }} onClick={async () => {
|
</>
|
||||||
const ok = await adminLogin(adminPwd);
|
|
||||||
if (ok) { setIsAdmin(true); setAdminPwd(''); }
|
|
||||||
else alert('Login fehlgeschlagen');
|
|
||||||
}}>Login</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Admin Toolbar */}
|
<div className="bg-transparent mb-8">
|
||||||
{isAdmin && (
|
<div className="flex flex-wrap gap-3 text-sm">
|
||||||
<section className="controls glass" style={{ marginTop: -8 }}>
|
<button className={`tag-btn ${activeFolder==='__favs__'?'active':''}`} onClick={()=>setActiveFolder('__favs__')}>Favoriten ({favCount})</button>
|
||||||
<div className="control" style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
{folders.map(f=> (
|
||||||
<button type="button" className="tab" onClick={async () => {
|
<button key={f.key} className={`tag-btn ${activeFolder===f.key?'active':''}`} onClick={async ()=>{ setActiveFolder(f.key); const resp=await fetchSounds(undefined, f.key); setSounds(resp.items); setTotal(resp.total); setFolders(resp.folders); }}>{f.name} ({f.count})</button>
|
||||||
const toDelete = Object.entries(selectedSet).filter(([, v]) => v).map(([k]) => k);
|
|
||||||
if (toDelete.length === 0) return;
|
|
||||||
if (!confirm(`Wirklich ${toDelete.length} Datei(en) löschen?`)) return;
|
|
||||||
try { await adminDelete(toDelete); } catch (e: any) { alert(e?.message || 'Löschen fehlgeschlagen'); }
|
|
||||||
// refresh
|
|
||||||
const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder;
|
|
||||||
const s = await fetchSounds(query, folderParam);
|
|
||||||
setSounds(s.items);
|
|
||||||
setTotal(s.total);
|
|
||||||
setFolders(s.folders);
|
|
||||||
setSelectedSet({});
|
|
||||||
}}>🗑️ Löschen</button>
|
|
||||||
{selectedCount === 1 && (
|
|
||||||
<RenameInline onSubmit={async (newName) => {
|
|
||||||
const from = Object.keys(selectedSet).find((k) => selectedSet[k]);
|
|
||||||
if (!from) return;
|
|
||||||
try { await adminRename(from, newName); } catch (e: any) { alert(e?.message || 'Umbenennen fehlgeschlagen'); return; }
|
|
||||||
const folderParam = activeFolder === '__favs__' ? '__all__' : activeFolder;
|
|
||||||
const s = await fetchSounds(query, folderParam);
|
|
||||||
setSounds(s.items);
|
|
||||||
setTotal(s.total);
|
|
||||||
setFolders(s.folders);
|
|
||||||
setSelectedSet({});
|
|
||||||
}} />
|
|
||||||
)}
|
|
||||||
<button type="button" className="tab" onClick={async () => { await adminLogout(); setIsAdmin(false); }}>Logout</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{folders.length > 0 && (
|
|
||||||
<nav className="tabs glass">
|
|
||||||
{/* Favoriten Tab */}
|
|
||||||
<button
|
|
||||||
key="__favs__"
|
|
||||||
className={`tab ${activeFolder === '__favs__' ? 'active' : ''}`}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setActiveFolder('__favs__')}
|
|
||||||
>
|
|
||||||
Favoriten ({favCount})
|
|
||||||
</button>
|
|
||||||
{folders.map((f) => (
|
|
||||||
<button
|
|
||||||
key={f.key}
|
|
||||||
className={`tab ${activeFolder === f.key ? 'active' : ''}`}
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
setActiveFolder(f.key);
|
|
||||||
const resp = await fetchSounds(undefined, f.key);
|
|
||||||
setSounds(resp.items);
|
|
||||||
setTotal(resp.total);
|
|
||||||
setFolders(resp.folders);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{f.name} ({f.count})
|
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</nav>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{error && <div className="error">{error}</div>}
|
{error && <div className="error">{error}</div>}
|
||||||
{info && <div className="badge" style={{ background:'rgba(34,197,94,.18)', borderColor:'rgba(34,197,94,.35)' }}>{info}</div>}
|
{info && <div className="badge" style={{ background:'rgba(34,197,94,.18)', borderColor:'rgba(34,197,94,.35)' }}>{info}</div>}
|
||||||
|
|
||||||
<section className="grid">
|
<main className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||||
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
|
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
|
||||||
const key = `${s.relativePath ?? s.fileName}`;
|
const key = `${s.relativePath ?? s.fileName}`;
|
||||||
const isFav = !!favs[key];
|
const isFav = !!favs[key];
|
||||||
return (
|
return (
|
||||||
<div key={`${s.fileName}-${s.name}`} className="sound-wrap row">
|
<div key={`${s.fileName}-${s.name}`} className="sound-btn group rounded-xl flex items-center justify-between p-3 cursor-pointer" onClick={()=>handlePlay(s.name, s.relativePath)}>
|
||||||
{isAdmin && (
|
<span className="text-sm font-medium truncate pr-2">{s.name}</span>
|
||||||
<input
|
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
className="row-check"
|
<button className="text-gray-400 hover:text-[var(--accent-green)]" onClick={(e)=>{e.stopPropagation(); handlePlay(s.name, s.relativePath);}}><span className="material-icons text-xl">add_circle_outline</span></button>
|
||||||
type="checkbox"
|
<button className="text-gray-400 hover:text-[var(--accent-blue)]" onClick={(e)=>{e.stopPropagation(); setFavs(prev=>({ ...prev, [key]: !prev[key] }));}}><span className="material-icons text-xl">{isFav?'star':'star_border'}</span></button>
|
||||||
checked={!!selectedSet[key]}
|
</div>
|
||||||
onClick={(e) => { try { e.stopPropagation(); } catch {} }}
|
|
||||||
onChange={(e) => {
|
|
||||||
try {
|
|
||||||
setSelectedSet((prev) => ({ ...prev, [key]: e.target.checked }));
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Checkbox change error:', err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<button className="sound" type="button" onClick={(e) => { e.stopPropagation(); handlePlay(s.name, s.relativePath); }} disabled={loading}>
|
|
||||||
{s.isRecent ? '🆕 ' : ''}{s.name}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`fav ${isFav ? 'active' : ''}`}
|
|
||||||
aria-label={isFav ? 'Favorit entfernen' : 'Als Favorit speichern'}
|
|
||||||
title={isFav ? 'Favorit entfernen' : 'Als Favorit speichern'}
|
|
||||||
onClick={() => setFavs((prev) => ({ ...prev, [key]: !prev[key] }))}
|
|
||||||
>
|
|
||||||
★
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{filtered.length === 0 && <div className="hint">Keine Sounds gefunden.</div>}
|
</main>
|
||||||
</section>
|
|
||||||
{/* footer counter entfällt, da oben sichtbar */}
|
|
||||||
</div>
|
</div>
|
||||||
{showTop && (
|
{showTop && (
|
||||||
<button
|
<button type="button" className="back-to-top" aria-label="Nach oben" onClick={()=>window.scrollTo({top:0, behavior:'smooth'})}>↑ Top</button>
|
||||||
type="button"
|
|
||||||
className="back-to-top"
|
|
||||||
aria-label="Nach oben"
|
|
||||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
|
||||||
>
|
|
||||||
↑ Top
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,17 @@ body {
|
||||||
[data-theme="light"] .tab.active { background: linear-gradient(135deg, rgba(59,130,246,.25), rgba(99,102,241,.25)); }
|
[data-theme="light"] .tab.active { background: linear-gradient(135deg, rgba(59,130,246,.25), rgba(99,102,241,.25)); }
|
||||||
[data-theme="light"] .badge { background: rgba(15,23,42,.06); border-color: rgba(15,23,42,.1); color: #0f172a; }
|
[data-theme="light"] .badge { background: rgba(15,23,42,.06); border-color: rgba(15,23,42,.1); color: #0f172a; }
|
||||||
|
|
||||||
|
/* Stitch utility classes (Light) */
|
||||||
|
[data-theme="light"] .control-panel { background-color: #ffffff; border: 1px solid #e8e8ed; }
|
||||||
|
[data-theme="light"] .tag-btn { padding: 8px 16px; border-radius: 9999px; font-size: 0.875rem; font-weight: 500; background: #e8e8ed; color: #6e6e73; border: 1px solid transparent; transition: all .2s ease; cursor: pointer; }
|
||||||
|
[data-theme="light"] .tag-btn:hover { background: #dcdce1; color: #1d1d1f; }
|
||||||
|
[data-theme="light"] .tag-btn.active { background: #007aff; color: #fff; font-weight: 600; }
|
||||||
|
[data-theme="light"] .input-field { width: 100%; background: #f2f2f6; border: 1px solid #dcdce1; border-radius: .5rem; padding: .5rem 1rem; color: #1d1d1f; outline: none; }
|
||||||
|
[data-theme="light"] .input-field:focus { box-shadow: 0 0 0 3px rgba(0,122,255,.25); border-color: transparent; }
|
||||||
|
[data-theme="light"] .sound-btn { background: #ffffff; border: 1px solid #e8e8ed; box-shadow: 0 1px 2px rgba(0,0,0,.05); transition: all .2s ease; }
|
||||||
|
[data-theme="light"] .sound-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.1); border-color: #007aff; }
|
||||||
|
.gradient-text { background: -webkit-linear-gradient(45deg, #333, #555); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||||
|
|
||||||
/* Rainbow Chaos Theme */
|
/* Rainbow Chaos Theme */
|
||||||
[data-theme="rainbow"] body {
|
[data-theme="rainbow"] body {
|
||||||
background:
|
background:
|
||||||
|
|
@ -69,6 +80,29 @@ body {
|
||||||
[data-theme="rainbow"] .tab.active { background: linear-gradient(90deg, #ff6384AA, #36a2ebAA, #ffce56AA, #4bc0c0AA, #9966ffAA); }
|
[data-theme="rainbow"] .tab.active { background: linear-gradient(90deg, #ff6384AA, #36a2ebAA, #ffce56AA, #4bc0c0AA, #9966ffAA); }
|
||||||
[data-theme="rainbow"] .tabs.glass { border: none; background: transparent; box-shadow: none; }
|
[data-theme="rainbow"] .tabs.glass { border: none; background: transparent; box-shadow: none; }
|
||||||
|
|
||||||
|
/* Rainbow Chaos (Stitch) */
|
||||||
|
@keyframes rainbow-bg { 0%{background-position:0% 50%} 50%{background-position:100% 50%} 100%{background-position:0% 50%} }
|
||||||
|
[data-theme="rainbow"] body { background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-size: 400% 400%; animation: rainbow-bg 15s ease infinite; }
|
||||||
|
[data-theme="rainbow"] .control-panel { background-color: rgba(30,30,30,.75); border: 1px solid #3a3a3c; backdrop-filter: blur(10px); }
|
||||||
|
[data-theme="rainbow"] .tag-btn { padding: 8px 16px; border-radius: 9999px; font-size: .875rem; font-weight: 500; background: rgba(44,44,44,.8); color: #a0a0a0; border:1px solid transparent; transition: transform .3s; cursor: pointer; text-shadow: 0 1px 2px rgba(0,0,0,.5) }
|
||||||
|
[data-theme="rainbow"] .tag-btn:hover { background: rgba(58,58,58,.9); color: #fff; transform: scale(1.1); }
|
||||||
|
[data-theme="rainbow"] .tag-btn.active { background: linear-gradient(45deg,#ff00ff,#00ffff); color: #fff; font-weight:700; border:1px solid #fff; box-shadow: 0 0 15px rgba(255,0,255,.7), 0 0 15px rgba(0,255,255,.7); }
|
||||||
|
[data-theme="rainbow"] .input-field { width:100%; background: rgba(44,44,44,.8); border:1px solid #3a3a3c; border-radius:.5rem; padding:.5rem 1rem; color:#e0e0e0; outline:none; }
|
||||||
|
[data-theme="rainbow"] .input-field:focus { box-shadow: 0 0 10px #23a6d5, 0 0 5px #e73c7e; border-color:#fff; }
|
||||||
|
[data-theme="rainbow"] .sound-btn { background: rgba(30,30,30,.75); border:1px solid #3a3a3c; box-shadow: 0 1px 2px rgba(0,0,0,.2); backdrop-filter: blur(10px); transition: transform .2s ease, box-shadow .2s; }
|
||||||
|
[data-theme="rainbow"] .sound-btn:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 8px 30px rgba(0,0,0,.5); border-color:#fff; }
|
||||||
|
[data-theme="rainbow"] .gradient-text { background: -webkit-linear-gradient(45deg,#ff8a00,#e52e71,#9c27b0); -webkit-background-clip:text; -webkit-text-fill-color:transparent; text-shadow: 0 0 10px rgba(255,255,255,.2) }
|
||||||
|
|
||||||
|
/* Dark (Stitch) */
|
||||||
|
[data-theme="dark"] .control-panel { background-color:#1e1e1e; border:1px solid #3a3a3c }
|
||||||
|
[data-theme="dark"] .tag-btn { padding:8px 16px; border-radius:9999px; font-size:.875rem; font-weight:500; background:#2c2c2c; color:#a0a0a0; border:1px solid transparent; }
|
||||||
|
[data-theme="dark"] .tag-btn:hover { background:#3a3a3a; color:#e0e0e0 }
|
||||||
|
[data-theme="dark"] .tag-btn.active { background:#0a84ff; color:#fff; font-weight:600 }
|
||||||
|
[data-theme="dark"] .input-field { width:100%; background:#2c2c2c; border:1px solid #3a3a3c; border-radius:.5rem; padding:.5rem 1rem; color:#e0e0e0 }
|
||||||
|
[data-theme="dark"] .sound-btn { background:#1e1e1e; border:1px solid #3a3a3c; box-shadow:0 1px 2px rgba(0,0,0,.2) }
|
||||||
|
[data-theme="dark"] .sound-btn:hover { transform: translateY(-1px); box-shadow:0 4px 12px rgba(0,0,0,.4); border-color:#0a84ff }
|
||||||
|
[data-theme="dark"] .gradient-text { background: -webkit-linear-gradient(45deg,#e0e0e0,#a0a0a0); -webkit-background-clip:text; -webkit-text-fill-color:transparent }
|
||||||
|
|
||||||
.container { width: 90vw; max-width: 1800px; margin: 0 auto; padding: 28px; }
|
.container { width: 90vw; max-width: 1800px; margin: 0 auto; padding: 28px; }
|
||||||
header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
|
header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
|
||||||
.
|
.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue