feat: runtime accent color picker with 6 presets
Replace all hardcoded #c8ff00 / rgba(200,255,0,...) with CSS custom properties (--accent, --accent-rgb) so the accent color updates live. Add themeStore (Zustand + localStorage) with 6 presets (Лайм, Синий, Розовый, Фиолет, Оранж, Минт). Add ThemeApplier component that syncs CSS vars on load. Add color picker UI section in ExtraTab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { getPublicPlaylists } from '@/lib/authApi'
|
||||
import type { PublicPlaylist, PlaylistTrack } from '@/types'
|
||||
import Header from '@/components/Header'
|
||||
|
||||
const TAG_PALETTE = ['#c8ff00', '#ff6b9d', '#6bcdff', '#ffb86b', '#b86bff', '#6bffb8']
|
||||
const TAG_PALETTE = ['var(--accent)', '#ff6b9d', '#6bcdff', '#ffb86b', '#b86bff', '#6bffb8']
|
||||
|
||||
function tagColor(tag: string): string {
|
||||
let h = 0
|
||||
@@ -47,7 +47,7 @@ function PlaylistCard({
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<div
|
||||
className="w-9 h-9 rounded-[10px] shrink-0 flex items-center justify-center font-display font-extrabold text-[15px] select-none"
|
||||
style={{ background: 'rgba(200,255,0,0.1)', color: '#c8ff00' }}
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.1)', color: 'var(--accent)' }}
|
||||
>
|
||||
{pl.username[0].toUpperCase()}
|
||||
</div>
|
||||
@@ -90,8 +90,8 @@ function PlaylistCard({
|
||||
disabled={!trackCount}
|
||||
className="text-[12px] font-display font-bold px-3 py-1.5 rounded-[9px] transition-all duration-200 cursor-pointer whitespace-nowrap shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: isLaunched ? '#c8ff00' : 'rgba(200,255,0,0.12)',
|
||||
color: isLaunched ? '#0a0a0f' : '#c8ff00',
|
||||
background: isLaunched ? 'var(--accent)' : 'rgba(var(--accent-rgb),0.12)',
|
||||
color: isLaunched ? '#0a0a0f' : 'var(--accent)',
|
||||
}}
|
||||
>
|
||||
{isLaunched ? '▶ Играет' : '▶ Play'}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:root {
|
||||
--border: rgba(255, 255, 255, 0.07);
|
||||
--accent: #c8ff00;
|
||||
--accent-rgb: 200,255,0;
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -50,7 +51,7 @@ audio {
|
||||
.eq-bar {
|
||||
width: 2.5px;
|
||||
border-radius: 2px;
|
||||
background: #c8ff00;
|
||||
background: var(--accent);
|
||||
animation: eqAnim 0.7s ease-in-out infinite alternate;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -83,7 +84,7 @@ audio {
|
||||
.queue-bar {
|
||||
width: 2.5px;
|
||||
border-radius: 1px;
|
||||
background: #c8ff00;
|
||||
background: var(--accent);
|
||||
animation: barAnim 0.7s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -138,7 +139,7 @@ audio {
|
||||
|
||||
/* ── Drag & drop ── */
|
||||
.drag-over {
|
||||
border-top: 2px solid #c8ff00 !important;
|
||||
border-top: 2px solid var(--accent) !important;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Syne, DM_Sans } from 'next/font/google'
|
||||
import AuthHydrator from '@/components/AuthHydrator'
|
||||
import AudioBackground from '@/components/AudioBackground'
|
||||
import GlobalPlayer from '@/components/GlobalPlayer'
|
||||
import ThemeApplier from '@/components/ThemeApplier'
|
||||
import './globals.css'
|
||||
|
||||
const syne = Syne({
|
||||
@@ -32,6 +33,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
return (
|
||||
<html lang="ru" className={`${syne.variable} ${dmSans.variable}`}>
|
||||
<body className="font-sans bg-bg text-app-text min-h-screen pb-[72px] px-4 pt-5 sm:px-4">
|
||||
<ThemeApplier />
|
||||
<AudioBackground />
|
||||
<div className="relative" style={{ zIndex: 1 }}>
|
||||
<AuthHydrator />
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function LandingPage() {
|
||||
<div className="flex items-center gap-3 flex-wrap justify-center">
|
||||
<Link
|
||||
href="/app"
|
||||
className="px-8 py-3 bg-accent text-bg font-display font-bold text-sm rounded-xl hover:brightness-110 active:scale-[0.97] transition-all duration-150 shadow-[0_0_24px_rgba(200,255,0,0.25)]"
|
||||
className="px-8 py-3 bg-accent text-bg font-display font-bold text-sm rounded-xl hover:brightness-110 active:scale-[0.97] transition-all duration-150 shadow-[0_0_24px_rgba(var(--accent-rgb),0.25)]"
|
||||
>
|
||||
Начать вечеринку →
|
||||
</Link>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { searchTracks, proxyImgUrl, fetchYandexPlaylist } from '@/lib/api'
|
||||
import type { Playlist, SearchResult } from '@/types'
|
||||
import Header from '@/components/Header'
|
||||
|
||||
const TAG_PALETTE = ['#c8ff00', '#ff6b9d', '#6bcdff', '#ffb86b', '#b86bff', '#6bffb8']
|
||||
const TAG_PALETTE = ['var(--accent)', '#ff6b9d', '#6bcdff', '#ffb86b', '#b86bff', '#6bffb8']
|
||||
|
||||
function tagColor(tag: string): string {
|
||||
let h = 0
|
||||
@@ -61,18 +61,18 @@ function TrackVersionPicker({ title, onPlay }: { title: string; onPlay: (r: Sear
|
||||
onClick={() => saved ? removeVersion(title) : saveVersion(title, r)}
|
||||
title={saved ? 'Забыть версию' : 'Запомнить эту версию'}
|
||||
className="w-6 h-6 rounded-md border flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ borderColor: saved ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(200,255,0,0.08)' : 'transparent' }}
|
||||
style={{ borderColor: saved ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(var(--accent-rgb),0.08)' : 'transparent' }}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? '#c8ff00' : 'none'} stroke={saved ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? 'var(--accent)' : 'none'} stroke={saved ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onPlay(r)}
|
||||
className="w-6 h-6 rounded-md flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ background: 'rgba(200,255,0,0.12)', border: '1px solid rgba(200,255,0,0.2)' }}
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.12)', border: '1px solid rgba(var(--accent-rgb),0.2)' }}
|
||||
>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="#c8ff00"><path d="M8 5v14l11-7z" /></svg>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="var(--accent)"><path d="M8 5v14l11-7z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -87,13 +87,13 @@ function Toggle({ value, onChange, label }: { value: boolean; onChange: (v: bool
|
||||
<div
|
||||
className="relative w-9 h-5 rounded-full transition-colors duration-200 shrink-0"
|
||||
style={{
|
||||
background: value ? 'rgba(200,255,0,0.25)' : 'rgba(255,255,255,0.06)',
|
||||
border: `1px solid ${value ? 'rgba(200,255,0,0.45)' : 'rgba(255,255,255,0.1)'}`,
|
||||
background: value ? 'rgba(var(--accent-rgb),0.25)' : 'rgba(255,255,255,0.06)',
|
||||
border: `1px solid ${value ? 'rgba(var(--accent-rgb),0.45)' : 'rgba(255,255,255,0.1)'}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute top-[2px] w-4 h-4 rounded-full transition-all duration-200"
|
||||
style={{ left: value ? 'calc(100% - 18px)' : '2px', background: value ? '#c8ff00' : 'rgba(255,255,255,0.25)' }}
|
||||
style={{ left: value ? 'calc(100% - 18px)' : '2px', background: value ? 'var(--accent)' : 'rgba(255,255,255,0.25)' }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[12px] text-muted group-hover:text-app-text transition-colors">{label}</span>
|
||||
@@ -196,7 +196,7 @@ function PlaylistCard({
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-display text-[14px] font-bold text-app-text">{pl.name}</span>
|
||||
{pl.is_public && (
|
||||
<span className="text-[9px] font-display font-bold tracking-[1px] uppercase px-1.5 py-px rounded-md" style={{ background: 'rgba(200,255,0,0.1)', color: '#c8ff00' }}>
|
||||
<span className="text-[9px] font-display font-bold tracking-[1px] uppercase px-1.5 py-px rounded-md" style={{ background: 'rgba(var(--accent-rgb),0.1)', color: 'var(--accent)' }}>
|
||||
публичный
|
||||
</span>
|
||||
)}
|
||||
@@ -225,7 +225,7 @@ function PlaylistCard({
|
||||
{trackCount > 0 && (
|
||||
<button onClick={playAll}
|
||||
className="text-[11px] font-display font-bold px-2.5 py-1.5 rounded-[9px] cursor-pointer transition-all"
|
||||
style={{ background: launched ? '#c8ff00' : 'rgba(200,255,0,0.12)', color: launched ? '#0a0a0f' : '#c8ff00' }}>
|
||||
style={{ background: launched ? 'var(--accent)' : 'rgba(var(--accent-rgb),0.12)', color: launched ? '#0a0a0f' : 'var(--accent)' }}>
|
||||
▶ Play
|
||||
</button>
|
||||
)}
|
||||
@@ -264,8 +264,8 @@ function PlaylistCard({
|
||||
onClick={() => setVersionsFor(versionsFor === track.title ? null : track.title)}
|
||||
title="Версии трека"
|
||||
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-md border flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ borderColor: versionsFor === track.title ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.1)', background: versionsFor === track.title ? 'rgba(200,255,0,0.08)' : 'transparent' }}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={versionsFor === track.title ? '#c8ff00' : '#777'} strokeWidth="2">
|
||||
style={{ borderColor: versionsFor === track.title ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.1)', background: versionsFor === track.title ? 'rgba(var(--accent-rgb),0.08)' : 'transparent' }}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={versionsFor === track.title ? 'var(--accent)' : '#777'} strokeWidth="2">
|
||||
<path d="M9 18V5l12-2v13" strokeLinecap="round" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -273,8 +273,8 @@ function PlaylistCard({
|
||||
onClick={() => playTrack(track.title)}
|
||||
title="Воспроизвести"
|
||||
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-md flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ background: 'rgba(200,255,0,0.12)', border: '1px solid rgba(200,255,0,0.2)' }}>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="#c8ff00"><path d="M8 5v14l11-7z" /></svg>
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.12)', border: '1px solid rgba(var(--accent-rgb),0.2)' }}>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="var(--accent)"><path d="M8 5v14l11-7z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
{versionsFor === track.title && (
|
||||
@@ -371,17 +371,17 @@ function FavoritesCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-surface border rounded-app overflow-hidden mb-2.5" style={{ borderColor: 'rgba(200,255,0,0.2)' }}>
|
||||
<div className="bg-surface border rounded-app overflow-hidden mb-2.5" style={{ borderColor: 'rgba(var(--accent-rgb),0.2)' }}>
|
||||
<div className="flex items-center gap-3 px-4 py-3.5">
|
||||
<div className="w-9 h-9 rounded-[10px] shrink-0 flex items-center justify-center" style={{ background: 'rgba(200,255,0,0.1)' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#c8ff00">
|
||||
<div className="w-9 h-9 rounded-[10px] shrink-0 flex items-center justify-center" style={{ background: 'rgba(var(--accent-rgb),0.1)' }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="var(--accent)">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-display text-[14px] font-bold text-app-text">Избранное</span>
|
||||
<span className="text-[9px] font-display font-bold tracking-[1px] uppercase px-1.5 py-px rounded-md" style={{ background: 'rgba(200,255,0,0.1)', color: '#c8ff00' }}>
|
||||
<span className="text-[9px] font-display font-bold tracking-[1px] uppercase px-1.5 py-px rounded-md" style={{ background: 'rgba(var(--accent-rgb),0.1)', color: 'var(--accent)' }}>
|
||||
локальный
|
||||
</span>
|
||||
</div>
|
||||
@@ -404,7 +404,7 @@ function FavoritesCard() {
|
||||
onClick={handlePlay}
|
||||
disabled={!favorites.length}
|
||||
className="text-[12px] font-display font-bold px-3 py-1.5 rounded-[9px] transition-all duration-200 cursor-pointer whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{ background: launched ? '#c8ff00' : 'rgba(200,255,0,0.12)', color: launched ? '#0a0a0f' : '#c8ff00' }}
|
||||
style={{ background: launched ? 'var(--accent)' : 'rgba(var(--accent-rgb),0.12)', color: launched ? '#0a0a0f' : 'var(--accent)' }}
|
||||
>
|
||||
{launched ? '▶ Играет' : '▶ Play'}
|
||||
</button>
|
||||
@@ -420,16 +420,16 @@ function FavoritesCard() {
|
||||
<button
|
||||
onClick={() => setVersionsFor(versionsFor === title ? null : title)}
|
||||
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-md border flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ borderColor: versionsFor === title ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.1)', background: versionsFor === title ? 'rgba(200,255,0,0.08)' : 'transparent' }}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={versionsFor === title ? '#c8ff00' : '#777'} strokeWidth="2">
|
||||
style={{ borderColor: versionsFor === title ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.1)', background: versionsFor === title ? 'rgba(var(--accent-rgb),0.08)' : 'transparent' }}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke={versionsFor === title ? 'var(--accent)' : '#777'} strokeWidth="2">
|
||||
<path d="M9 18V5l12-2v13" strokeLinecap="round" /><circle cx="6" cy="18" r="3" /><circle cx="18" cy="16" r="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => playTrack(title)}
|
||||
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-md flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ background: 'rgba(200,255,0,0.12)', border: '1px solid rgba(200,255,0,0.2)' }}>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="#c8ff00"><path d="M8 5v14l11-7z" /></svg>
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.12)', border: '1px solid rgba(var(--accent-rgb),0.2)' }}>
|
||||
<svg width="8" height="8" viewBox="0 0 24 24" fill="var(--accent)"><path d="M8 5v14l11-7z" /></svg>
|
||||
</button>
|
||||
<button onClick={() => removeFavorite(title)}
|
||||
className="opacity-0 group-hover:opacity-100 w-6 h-6 flex items-center justify-center text-muted hover:text-[#ff6b6b] transition-all cursor-pointer">
|
||||
@@ -534,7 +534,7 @@ function YandexImportForm({ onImport, onClose }: {
|
||||
<div className="bg-surface2 border border-white/[0.06] rounded-[9px] p-3 mb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[11px] text-muted">Найдено треков:</span>
|
||||
<span className="text-[11px] font-display font-bold" style={{ color: '#c8ff00' }}>{preview.tracks.length}</span>
|
||||
<span className="text-[11px] font-display font-bold" style={{ color: 'var(--accent)' }}>{preview.tracks.length}</span>
|
||||
</div>
|
||||
<div className="max-h-[140px] overflow-y-auto flex flex-col gap-0.5">
|
||||
{preview.tracks.map((t, i) => (
|
||||
@@ -673,7 +673,7 @@ export default function PlaylistsPage() {
|
||||
className="flex items-center gap-1.5 text-[12px] font-display font-bold px-3.5 py-2 rounded-xl transition-all duration-150 cursor-pointer"
|
||||
style={showForm
|
||||
? { background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--color-muted)' }
|
||||
: { background: '#c8ff00', color: '#0a0a0f' }
|
||||
: { background: 'var(--accent)', color: '#0a0a0f' }
|
||||
}
|
||||
>
|
||||
{showForm ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client'
|
||||
|
||||
import { use, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@@ -145,14 +145,14 @@ export default function RemotePage({ params }: { params: Promise<{ id: string }>
|
||||
<button
|
||||
onClick={() => setTab('player')}
|
||||
className="px-3 py-1 text-[11px] font-display font-bold rounded-[6px] transition-all cursor-pointer"
|
||||
style={{ background: tab === 'player' ? 'rgba(200,255,0,0.1)' : 'transparent', color: tab === 'player' ? '#c8ff00' : '#555' }}
|
||||
style={{ background: tab === 'player' ? 'rgba(var(--accent-rgb),0.1)' : 'transparent', color: tab === 'player' ? 'var(--accent)' : '#555' }}
|
||||
>
|
||||
Плеер
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('queue')}
|
||||
className="px-3 py-1 text-[11px] font-display font-bold rounded-[6px] transition-all cursor-pointer flex items-center gap-1"
|
||||
style={{ background: tab === 'queue' ? 'rgba(200,255,0,0.1)' : 'transparent', color: tab === 'queue' ? '#c8ff00' : '#555' }}
|
||||
style={{ background: tab === 'queue' ? 'rgba(var(--accent-rgb),0.1)' : 'transparent', color: tab === 'queue' ? 'var(--accent)' : '#555' }}
|
||||
>
|
||||
Очередь
|
||||
{queue.length > 0 && <span className="text-[10px] opacity-60">{queue.length}</span>}
|
||||
@@ -263,7 +263,7 @@ export default function RemotePage({ params }: { params: Promise<{ id: string }>
|
||||
volumeDebounce.current = setTimeout(() => cmd(id, 'volume', v), 150)
|
||||
}}
|
||||
className="flex-1 cursor-pointer h-1.5"
|
||||
style={{ accentColor: '#c8ff00' }}
|
||||
style={{ accentColor: 'var(--accent)' }}
|
||||
/>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-muted shrink-0">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
||||
@@ -295,7 +295,7 @@ export default function RemotePage({ params }: { params: Promise<{ id: string }>
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 border-b border-white/[0.05] last:border-b-0 transition-colors"
|
||||
style={{ background: active ? 'rgba(200,255,0,0.04)' : undefined }}
|
||||
style={{ background: active ? 'rgba(var(--accent-rgb),0.04)' : undefined }}
|
||||
>
|
||||
<span className="text-[11px] text-muted w-4 text-right shrink-0 font-display">{i + 1}</span>
|
||||
{v.img ? (
|
||||
@@ -312,9 +312,9 @@ export default function RemotePage({ params }: { params: Promise<{ id: string }>
|
||||
onClick={(e) => { e.stopPropagation(); handleSaveVersion(i) }}
|
||||
title={saved ? 'Забыть версию' : 'Запомнить версию'}
|
||||
className="w-7 h-7 rounded-full border flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{ borderColor: saved ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(200,255,0,0.08)' : 'transparent' }}
|
||||
style={{ borderColor: saved ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(var(--accent-rgb),0.08)' : 'transparent' }}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={saved ? '#c8ff00' : 'none'} stroke={saved ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={saved ? 'var(--accent)' : 'none'} stroke={saved ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -378,7 +378,7 @@ export default function RemotePage({ params }: { params: Promise<{ id: string }>
|
||||
key={i}
|
||||
onClick={() => cmd(id, 'goto', i)}
|
||||
className="flex items-center gap-2.5 px-1 py-2.5 border-b border-white/[0.05] last:border-b-0 cursor-pointer active:bg-surface2 transition-colors rounded-[6px]"
|
||||
style={{ background: active ? 'rgba(200,255,0,0.04)' : undefined }}
|
||||
style={{ background: active ? 'rgba(var(--accent-rgb),0.04)' : undefined }}
|
||||
>
|
||||
{active ? (
|
||||
<div className="flex items-end gap-[1.5px] w-3.5 h-3.5 shrink-0 ml-0.5">
|
||||
|
||||
@@ -58,11 +58,11 @@ function ResultCard({ result, onPlay }: { result: SearchResult; onPlay: (r: Sear
|
||||
title={saved ? 'Забыть версию' : 'Запомнить эту версию'}
|
||||
className="w-7 h-7 rounded-[7px] border flex items-center justify-center transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
borderColor: saved ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: saved ? 'rgba(200,255,0,0.08)' : 'transparent',
|
||||
borderColor: saved ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: saved ? 'rgba(var(--accent-rgb),0.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={saved ? '#c8ff00' : 'none'} stroke={saved ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={saved ? 'var(--accent)' : 'none'} stroke={saved ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -75,11 +75,11 @@ function ResultCard({ result, onPlay }: { result: SearchResult; onPlay: (r: Sear
|
||||
title="Добавить в плейлист"
|
||||
className="w-7 h-7 rounded-[7px] border flex items-center justify-center transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
borderColor: playlistOpen ? 'rgba(200,255,0,0.35)' : 'rgba(255,255,255,0.07)',
|
||||
background: playlistOpen ? 'rgba(200,255,0,0.06)' : 'transparent',
|
||||
borderColor: playlistOpen ? 'rgba(var(--accent-rgb),0.35)' : 'rgba(255,255,255,0.07)',
|
||||
background: playlistOpen ? 'rgba(var(--accent-rgb),0.06)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={playlistOpen ? '#c8ff00' : '#666'} strokeWidth="2.5" strokeLinecap="round">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke={playlistOpen ? 'var(--accent)' : '#666'} strokeWidth="2.5" strokeLinecap="round">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -98,11 +98,11 @@ function ResultCard({ result, onPlay }: { result: SearchResult; onPlay: (r: Sear
|
||||
title={favorited ? 'Убрать из избранного' : 'В избранное'}
|
||||
className="w-7 h-7 rounded-[7px] border flex items-center justify-center transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
borderColor: favorited ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: favorited ? 'rgba(200,255,0,0.08)' : 'transparent',
|
||||
borderColor: favorited ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: favorited ? 'rgba(var(--accent-rgb),0.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={favorited ? '#c8ff00' : 'none'} stroke={favorited ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill={favorited ? 'var(--accent)' : 'none'} stroke={favorited ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -112,9 +112,9 @@ function ResultCard({ result, onPlay }: { result: SearchResult; onPlay: (r: Sear
|
||||
onClick={() => onPlay(result)}
|
||||
title="Воспроизвести"
|
||||
className="w-7 h-7 rounded-[7px] flex items-center justify-center transition-all duration-150 cursor-pointer"
|
||||
style={{ background: 'rgba(200,255,0,0.12)', border: '1px solid rgba(200,255,0,0.2)' }}
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.12)', border: '1px solid rgba(var(--accent-rgb),0.2)' }}
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="#c8ff00"><path d="M8 5v14l11-7z" /></svg>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="var(--accent)"><path d="M8 5v14l11-7z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@ export default function SearchPage() {
|
||||
<button
|
||||
onClick={handlePlayAll}
|
||||
className="flex items-center gap-1.5 text-[11px] font-display font-bold px-3 py-1.5 rounded-[9px] cursor-pointer transition-all"
|
||||
style={{ background: 'rgba(200,255,0,0.12)', color: '#c8ff00' }}
|
||||
style={{ background: 'rgba(var(--accent-rgb),0.12)', color: 'var(--accent)' }}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>
|
||||
Все в очередь
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
'use client'
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useFavoritesStore } from '@/store/favoritesStore'
|
||||
@@ -59,16 +59,16 @@ export default function AddToPlaylist({ trackTitle, onClose, anchorRef }: Props)
|
||||
onClick={() => toggleFavorite(trackTitle)}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2.5 hover:bg-surface2 transition-colors cursor-pointer border-b border-white/[0.05]"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-md flex items-center justify-center shrink-0" style={{ background: favorited ? 'rgba(200,255,0,0.15)' : 'rgba(255,255,255,0.06)' }}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={favorited ? '#c8ff00' : 'none'} stroke={favorited ? '#c8ff00' : '#666'} strokeWidth="2">
|
||||
<div className="w-6 h-6 rounded-md flex items-center justify-center shrink-0" style={{ background: favorited ? 'rgba(var(--accent-rgb),0.15)' : 'rgba(255,255,255,0.06)' }}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill={favorited ? 'var(--accent)' : 'none'} stroke={favorited ? 'var(--accent)' : '#666'} strokeWidth="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[12px] font-medium" style={{ color: favorited ? '#c8ff00' : 'var(--color-app-text)' }}>
|
||||
<span className="text-[12px] font-medium" style={{ color: favorited ? 'var(--accent)' : 'var(--color-app-text)' }}>
|
||||
{favorited ? 'В избранном' : 'В избранное'}
|
||||
</span>
|
||||
{favorited && (
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#c8ff00" strokeWidth="2.5">
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5">
|
||||
<polyline points="20 6 9 17 4 12" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
@@ -84,7 +84,7 @@ export default function AddToPlaylist({ trackTitle, onClose, anchorRef }: Props)
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2.5 hover:bg-surface2 transition-colors cursor-pointer border-b border-white/[0.04] last:border-b-0"
|
||||
>
|
||||
<div className="w-6 h-6 rounded-md bg-surface2 flex items-center justify-center shrink-0">
|
||||
<svg className="shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#c8ff00" strokeWidth="2.5">
|
||||
<svg className="shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5">
|
||||
<path d="M9 18V5l12-2v13" strokeLinecap="round" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<circle cx="18" cy="16" r="3" />
|
||||
@@ -92,11 +92,11 @@ export default function AddToPlaylist({ trackTitle, onClose, anchorRef }: Props)
|
||||
</div>
|
||||
<span className="text-[12px] font-medium text-app-text truncate flex-1 text-left">{pl.name}</span>
|
||||
{added[pl.id] ? (
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#c8ff00" strokeWidth="2.5">
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5">
|
||||
<polyline points="20 6 9 17 4 12" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#c8ff00" strokeWidth="2.5">
|
||||
<svg className="ml-auto shrink-0" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5">
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
@@ -55,16 +55,17 @@ export default function AudioBackground() {
|
||||
ctx.fillStyle = '#0a0a0f'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
const accentRgb = document.documentElement.style.getPropertyValue('--accent-rgb') || '200,255,0'
|
||||
const diag = Math.hypot(W, H)
|
||||
const base = diag * 0.55
|
||||
|
||||
// Lime orb — bass driven, top-left
|
||||
// Accent orb — bass driven, top-left
|
||||
const r1 = base * (0.75 + smoothBass * 0.55 + breathe)
|
||||
const a1 = 0.055 + smoothBass * 0.07
|
||||
const g1 = ctx.createRadialGradient(W * 0.18, H * 0.08, 0, W * 0.18, H * 0.08, r1)
|
||||
g1.addColorStop(0, `rgba(200,255,0,${a1})`)
|
||||
g1.addColorStop(0.45, `rgba(200,255,0,${a1 * 0.28})`)
|
||||
g1.addColorStop(1, 'rgba(200,255,0,0)')
|
||||
g1.addColorStop(0, `rgba(${accentRgb},${a1})`)
|
||||
g1.addColorStop(0.45, `rgba(${accentRgb},${a1 * 0.28})`)
|
||||
g1.addColorStop(1, `rgba(${accentRgb},0)`)
|
||||
ctx.fillStyle = g1
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
|
||||
@@ -430,9 +430,9 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
onClick={(e) => { e.stopPropagation(); saved ? removeVersion(trackTitle) : saveVersion(trackTitle, r) }}
|
||||
title={saved ? 'Забыть версию' : 'Запомнить'}
|
||||
className="w-[26px] h-[26px] rounded-full border flex items-center justify-center shrink-0 transition-all cursor-pointer hover:border-accent/40"
|
||||
style={{ borderColor: saved ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(200,255,0,0.08)' : 'transparent' }}
|
||||
style={{ borderColor: saved ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)', background: saved ? 'rgba(var(--accent-rgb),0.08)' : 'transparent' }}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? '#c8ff00' : 'none'} stroke={saved ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? 'var(--accent)' : 'none'} stroke={saved ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -470,7 +470,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
key={i}
|
||||
draggable
|
||||
className="q-item flex items-center gap-2 px-4 py-2 border-b border-white/[0.07] last:border-b-0 cursor-pointer hover:bg-surface2 transition-colors select-none"
|
||||
style={{ background: active ? 'rgba(200,255,0,0.04)' : undefined }}
|
||||
style={{ background: active ? 'rgba(var(--accent-rgb),0.04)' : undefined }}
|
||||
onClick={(e) => { if (!(e.target as HTMLElement).closest('.drag-handle')) setCurIdx(i) }}
|
||||
onDragStart={(e) => onDragStart(i, e.currentTarget)}
|
||||
onDragEnd={(e) => onDragEnd(e.currentTarget)}
|
||||
@@ -583,7 +583,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
<button
|
||||
onClick={() => setPanel(p => p === 'versions' ? null : 'versions')}
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-muted hover:bg-surface2 transition-all cursor-pointer"
|
||||
style={{ color: panel === 'versions' ? '#c8ff00' : undefined, background: panel === 'versions' ? 'rgba(200,255,0,0.06)' : undefined }}
|
||||
style={{ color: panel === 'versions' ? 'var(--accent)' : undefined, background: panel === 'versions' ? 'rgba(var(--accent-rgb),0.06)' : undefined }}
|
||||
title="Версии трека"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -598,7 +598,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
onClick={() => setPlaylistOpen(v => !v)}
|
||||
title="Добавить в плейлист"
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-muted hover:bg-surface2 transition-all cursor-pointer"
|
||||
style={{ color: playlistOpen ? '#c8ff00' : undefined, background: playlistOpen ? 'rgba(200,255,0,0.06)' : undefined }}
|
||||
style={{ color: playlistOpen ? 'var(--accent)' : undefined, background: playlistOpen ? 'rgba(var(--accent-rgb),0.06)' : undefined }}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
@@ -616,9 +616,9 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
<button
|
||||
onClick={() => toggleFavorite(trackTitle)}
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer hover:bg-surface2"
|
||||
style={{ color: favorited ? '#c8ff00' : undefined }}
|
||||
style={{ color: favorited ? 'var(--accent)' : undefined }}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill={favorited ? '#c8ff00' : 'none'} stroke={favorited ? '#c8ff00' : 'currentColor'} strokeWidth="2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill={favorited ? 'var(--accent)' : 'none'} stroke={favorited ? 'var(--accent)' : 'currentColor'} strokeWidth="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -626,7 +626,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
<button
|
||||
onClick={() => setPanel(p => p === 'queue' ? null : 'queue')}
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-muted hover:bg-surface2 transition-all cursor-pointer"
|
||||
style={{ color: panel === 'queue' ? '#c8ff00' : undefined, background: panel === 'queue' ? 'rgba(200,255,0,0.06)' : undefined }}
|
||||
style={{ color: panel === 'queue' ? 'var(--accent)' : undefined, background: panel === 'queue' ? 'rgba(var(--accent-rgb),0.06)' : undefined }}
|
||||
title="Очередь"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -652,7 +652,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
}}
|
||||
title="Пульт управления"
|
||||
className="w-8 h-8 rounded-[8px] flex items-center justify-center text-muted hover:bg-surface2 transition-all cursor-pointer"
|
||||
style={{ color: shareOpen || roomId ? '#c8ff00' : undefined, background: shareOpen ? 'rgba(200,255,0,0.06)' : undefined }}
|
||||
style={{ color: shareOpen || roomId ? 'var(--accent)' : undefined, background: shareOpen ? 'rgba(var(--accent-rgb),0.06)' : undefined }}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0" />
|
||||
@@ -679,7 +679,7 @@ export default function BottomPlayer({ onTrackEnd }: BottomPlayerProps) {
|
||||
}
|
||||
}}
|
||||
className="shrink-0 px-2 py-1.5 text-[11px] rounded-[7px] border border-white/[0.07] hover:bg-surface2 transition-all cursor-pointer font-display"
|
||||
style={{ color: shareCopied ? '#c8ff00' : undefined }}
|
||||
style={{ color: shareCopied ? 'var(--accent)' : undefined }}
|
||||
>
|
||||
{shareCopied ? '✓' : 'Копировать'}
|
||||
</button>
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { usePartyStore } from '@/store/partyStore'
|
||||
import { useThemeStore, ACCENT_PRESETS } from '@/store/themeStore'
|
||||
import { COLORS, initials } from '@/lib/colors'
|
||||
import type { Color } from '@/types'
|
||||
|
||||
export default function ExtraTab() {
|
||||
const { queue, curIdx, addFairToQueue } = usePartyStore()
|
||||
const { accentIdx, setAccent } = useThemeStore()
|
||||
const [name, setName] = useState('')
|
||||
const [tracks, setTracks] = useState('')
|
||||
const [selectedColor, setSelectedColor] = useState<Color>(COLORS[1])
|
||||
@@ -173,11 +175,38 @@ export default function ExtraTab() {
|
||||
type="submit"
|
||||
disabled={!name.trim() || parseTracks(tracks).length === 0}
|
||||
className="w-full py-2.5 font-display text-[13px] font-bold border-none rounded-[9px] transition-all duration-200 cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{ background: done ? 'rgba(200,255,0,0.2)' : '#c8ff00', color: done ? '#c8ff00' : '#0a0a0f' }}
|
||||
style={{ background: done ? 'rgba(var(--accent-rgb),0.2)' : 'var(--accent)', color: done ? 'var(--accent)' : '#0a0a0f' }}
|
||||
>
|
||||
{done ? '✓ Добавлено в очередь' : '↗ Добавить в очередь'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Theme color picker */}
|
||||
<div className="bg-surface border border-white/[0.07] rounded-app p-4 mt-4">
|
||||
<p className="text-[11px] font-display font-semibold tracking-[1.2px] uppercase text-muted mb-3">
|
||||
Цветовая тема
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{ACCENT_PRESETS.map((preset, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setAccent(i)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-[9px] text-[12px] font-display font-semibold transition-all duration-150 cursor-pointer border"
|
||||
style={accentIdx === i
|
||||
? { background: `rgba(${preset.rgb},0.15)`, color: preset.accent, borderColor: `${preset.accent}60` }
|
||||
: { background: 'transparent', color: '#555', borderColor: 'rgba(255,255,255,0.07)' }
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="w-3 h-3 rounded-full shrink-0"
|
||||
style={{ background: preset.accent, boxShadow: accentIdx === i ? `0 0 6px ${preset.accent}80` : 'none' }}
|
||||
/>
|
||||
{preset.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ export default function FavoritesTab() {
|
||||
title="Запустить"
|
||||
className="w-7 h-7 rounded-full border flex items-center justify-center shrink-0 transition-all cursor-pointer"
|
||||
style={{
|
||||
background: 'rgba(200,255,0,0.12)',
|
||||
borderColor: 'rgba(200,255,0,0.2)',
|
||||
background: 'rgba(var(--accent-rgb),0.12)',
|
||||
borderColor: 'rgba(var(--accent-rgb),0.2)',
|
||||
}}
|
||||
>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="#c8ff00"><path d="M8 5v14l11-7z" /></svg>
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="var(--accent)"><path d="M8 5v14l11-7z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Header() {
|
||||
href={href}
|
||||
className="flex items-center gap-1.5 text-[12px] font-display font-semibold px-3 py-1.5 rounded-xl border transition-all duration-150 hidden sm:flex"
|
||||
style={active
|
||||
? { background: 'rgba(200,255,0,0.12)', borderColor: 'rgba(200,255,0,0.3)', color: '#c8ff00' }
|
||||
? { background: 'rgba(var(--accent-rgb),0.12)', borderColor: 'rgba(var(--accent-rgb),0.3)', color: 'var(--accent)' }
|
||||
: { background: 'rgba(255,255,255,0.04)', borderColor: 'rgba(255,255,255,0.07)', color: 'var(--color-muted)' }
|
||||
}
|
||||
>
|
||||
|
||||
@@ -273,11 +273,11 @@ export default function PlayerCard({ onTrackEnd }: PlayerCardProps) {
|
||||
title={saved ? 'Забыть версию' : 'Запомнить эту версию'}
|
||||
className="w-[26px] h-[26px] rounded-full border flex items-center justify-center shrink-0 transition-all duration-150 cursor-pointer hover:border-accent/40"
|
||||
style={{
|
||||
borderColor: saved ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: saved ? 'rgba(200,255,0,0.08)' : 'transparent',
|
||||
borderColor: saved ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: saved ? 'rgba(var(--accent-rgb),0.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? '#c8ff00' : 'none'} stroke={saved ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill={saved ? 'var(--accent)' : 'none'} stroke={saved ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -316,11 +316,11 @@ export default function PlayerCard({ onTrackEnd }: PlayerCardProps) {
|
||||
title="Добавить в плейлист"
|
||||
className="w-9 h-9 rounded-[9px] border flex items-center justify-center shrink-0 transition-all duration-150 cursor-pointer sm:w-8 sm:h-8"
|
||||
style={{
|
||||
borderColor: playlistOpen ? 'rgba(200,255,0,0.35)' : 'rgba(255,255,255,0.07)',
|
||||
background: playlistOpen ? 'rgba(200,255,0,0.06)' : 'transparent',
|
||||
borderColor: playlistOpen ? 'rgba(var(--accent-rgb),0.35)' : 'rgba(255,255,255,0.07)',
|
||||
background: playlistOpen ? 'rgba(var(--accent-rgb),0.06)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={playlistOpen ? '#c8ff00' : '#666'} strokeWidth="2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={playlistOpen ? 'var(--accent)' : '#666'} strokeWidth="2">
|
||||
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -338,11 +338,11 @@ export default function PlayerCard({ onTrackEnd }: PlayerCardProps) {
|
||||
title={favorited ? 'Убрать из избранного' : 'В избранное'}
|
||||
className="w-9 h-9 rounded-[9px] border flex items-center justify-center shrink-0 transition-all duration-150 cursor-pointer sm:w-8 sm:h-8"
|
||||
style={{
|
||||
borderColor: favorited ? 'rgba(200,255,0,0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: favorited ? 'rgba(200,255,0,0.08)' : 'transparent',
|
||||
borderColor: favorited ? 'rgba(var(--accent-rgb),0.4)' : 'rgba(255,255,255,0.07)',
|
||||
background: favorited ? 'rgba(var(--accent-rgb),0.08)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill={favorited ? '#c8ff00' : 'none'} stroke={favorited ? '#c8ff00' : '#555'} strokeWidth="2">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill={favorited ? 'var(--accent)' : 'none'} stroke={favorited ? 'var(--accent)' : '#555'} strokeWidth="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function QueueCard() {
|
||||
data-idx={i}
|
||||
draggable
|
||||
className="q-item flex items-center gap-2 px-4 py-2 border-b border-white/[0.07] last:border-b-0 cursor-pointer hover:bg-surface2 transition-colors duration-100 select-none relative sm:px-3"
|
||||
style={{ background: active ? 'rgba(200,255,0,0.04)' : undefined }}
|
||||
style={{ background: active ? 'rgba(var(--accent-rgb),0.04)' : undefined }}
|
||||
onClick={(e) => handleQueueClick(e, i)}
|
||||
onDragStart={(e) => onDragStart(i, e.currentTarget)}
|
||||
onDragEnd={(e) => onDragEnd(e.currentTarget)}
|
||||
|
||||
@@ -34,8 +34,8 @@ function TopChartsCard({ tracks }: { tracks: string[] }) {
|
||||
onClick={handlePlay}
|
||||
className="text-[12px] font-display font-bold px-3 py-1.5 rounded-[9px] transition-all duration-200 cursor-pointer whitespace-nowrap shrink-0 mt-1"
|
||||
style={{
|
||||
background: launched ? '#c8ff00' : 'rgba(200,255,0,0.15)',
|
||||
color: launched ? '#0a0a0f' : '#c8ff00',
|
||||
background: launched ? 'var(--accent)' : 'rgba(var(--accent-rgb),0.15)',
|
||||
color: launched ? '#0a0a0f' : 'var(--accent)',
|
||||
}}
|
||||
>
|
||||
{launched ? '▶ Играет' : '▶ Слушать'}
|
||||
|
||||
16
apps/web/src/components/ThemeApplier.tsx
Normal file
16
apps/web/src/components/ThemeApplier.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useThemeStore, ACCENT_PRESETS } from '@/store/themeStore'
|
||||
|
||||
export default function ThemeApplier() {
|
||||
const { accentIdx } = useThemeStore()
|
||||
|
||||
useEffect(() => {
|
||||
const preset = ACCENT_PRESETS[accentIdx] ?? ACCENT_PRESETS[0]
|
||||
document.documentElement.style.setProperty('--accent', preset.accent)
|
||||
document.documentElement.style.setProperty('--accent-rgb', preset.rgb)
|
||||
}, [accentIdx])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Color } from '@/types'
|
||||
|
||||
export const COLORS: Color[] = [
|
||||
{ bg: 'rgba(200,255,0,0.1)', text: '#c8ff00' },
|
||||
{ bg: 'rgba(var(--accent-rgb),0.1)', text: 'var(--accent)' },
|
||||
{ bg: 'rgba(255,60,172,0.1)', text: '#ff3cac' },
|
||||
{ bg: 'rgba(0,212,255,0.1)', text: '#00d4ff' },
|
||||
{ bg: 'rgba(255,165,0,0.1)', text: '#ffa500' },
|
||||
@@ -29,5 +29,5 @@ export function hexToRgba(color: string, alpha: number): string {
|
||||
const b = parseInt(color.slice(5, 7), 16)
|
||||
return `rgba(${r},${g},${b},${alpha})`
|
||||
}
|
||||
return `rgba(200,255,0,${alpha})`
|
||||
return `rgba(var(--accent-rgb),${alpha})`
|
||||
}
|
||||
|
||||
38
apps/web/src/store/themeStore.ts
Normal file
38
apps/web/src/store/themeStore.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface AccentPreset {
|
||||
name: string
|
||||
accent: string
|
||||
rgb: string
|
||||
}
|
||||
|
||||
export const ACCENT_PRESETS: AccentPreset[] = [
|
||||
{ name: 'Лайм', accent: '#c8ff00', rgb: '200,255,0' },
|
||||
{ name: 'Синий', accent: '#00D4FF', rgb: '0,212,255' },
|
||||
{ name: 'Розовый', accent: '#FF2D78', rgb: '255,45,120' },
|
||||
{ name: 'Фиолет', accent: '#A855F7', rgb: '168,85,247' },
|
||||
{ name: 'Оранж', accent: '#FF6B35', rgb: '255,107,53' },
|
||||
{ name: 'Минт', accent: '#00FFB2', rgb: '0,255,178' },
|
||||
]
|
||||
|
||||
const STORAGE_KEY = 'pm_accent'
|
||||
|
||||
interface ThemeStore {
|
||||
accentIdx: number
|
||||
setAccent: (idx: number) => void
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeStore>((set) => ({
|
||||
accentIdx: (() => {
|
||||
if (typeof window === 'undefined') return 0
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
const idx = saved !== null ? parseInt(saved, 10) : 0
|
||||
return idx >= 0 && idx < ACCENT_PRESETS.length ? idx : 0
|
||||
})(),
|
||||
setAccent: (idx) => {
|
||||
if (typeof window !== 'undefined') localStorage.setItem(STORAGE_KEY, String(idx))
|
||||
set({ accentIdx: idx })
|
||||
},
|
||||
}))
|
||||
Reference in New Issue
Block a user