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:
2026-04-25 12:59:58 +03:00
parent 0097fb5183
commit 24856a644f
21 changed files with 187 additions and 96 deletions

View File

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

View File

@@ -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 {

View File

@@ -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 />

View File

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

View File

@@ -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 ? (

View File

@@ -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">

View File

@@ -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>
Все в очередь

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

View File

@@ -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)' }
}
>

View File

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

View File

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

View File

@@ -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 ? '▶ Играет' : '▶ Слушать'}

View 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
}

View File

@@ -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})`
}

View 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 })
},
}))

View File

@@ -8,7 +8,9 @@ const config: Config = {
bg: '#0a0a0f',
surface: '#12121a',
surface2: '#1a1a26',
accent: '#c8ff00',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
accent: (({ opacityValue }: { opacityValue?: string }) =>
opacityValue !== undefined ? `rgba(var(--accent-rgb),${opacityValue})` : 'var(--accent)') as any,
muted: '#555555',
'app-text': '#f0f0f0',
},