diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e189ca6..887daf9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -60,7 +60,9 @@ "Bash(git -C \"/d/!toyffee/party-mix-app/party-mix-app\" log --oneline -3)", "Bash(docker run *)", "Bash(docker exec *)", - "PowerShell(docker exec party-mix-app-web-1 *)" + "PowerShell(docker exec party-mix-app-web-1 *)", + "Bash(git add *)", + "Bash(git commit -m ' *)" ] } } diff --git a/apps/web/src/app/community/page.tsx b/apps/web/src/app/community/page.tsx index 2f53081..89f55f9 100644 --- a/apps/web/src/app/community/page.tsx +++ b/apps/web/src/app/community/page.tsx @@ -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({
{pl.username[0].toUpperCase()}
@@ -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'} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index a8a7f9b..6d48b10 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -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 { diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ea6a524..cdf1cf5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -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 ( +
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index d469699..33a2e45 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -120,7 +120,7 @@ export default function LandingPage() {
Начать вечеринку → diff --git a/apps/web/src/app/playlists/page.tsx b/apps/web/src/app/playlists/page.tsx index f6049f9..e149524 100644 --- a/apps/web/src/app/playlists/page.tsx +++ b/apps/web/src/app/playlists/page.tsx @@ -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' }} > - +
) @@ -87,13 +87,13 @@ function Toggle({ value, onChange, label }: { value: boolean; onChange: (v: bool
{label} @@ -196,7 +196,7 @@ function PlaylistCard({
{pl.name} {pl.is_public && ( - + публичный )} @@ -225,7 +225,7 @@ function PlaylistCard({ {trackCount > 0 && ( )} @@ -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' }}> - + 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' }}> + @@ -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)' }}> - + style={{ background: 'rgba(var(--accent-rgb),0.12)', border: '1px solid rgba(var(--accent-rgb),0.2)' }}> +
{versionsFor === track.title && ( @@ -371,17 +371,17 @@ function FavoritesCard() { } return ( -
+
-
- +
+
Избранное - + локальный
@@ -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'} @@ -420,16 +420,16 @@ function FavoritesCard() { @@ -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 ? (
diff --git a/apps/web/src/app/search/page.tsx b/apps/web/src/app/search/page.tsx index c427251..9a1bcf2 100644 --- a/apps/web/src/app/search/page.tsx +++ b/apps/web/src/app/search/page.tsx @@ -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', }} > - + @@ -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', }} > - + @@ -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', }} > - + @@ -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)' }} > - +
@@ -221,7 +221,7 @@ export default function SearchPage() { @@ -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) { + + {/* Theme color picker */} +
+

+ Цветовая тема +

+
+ {ACCENT_PRESETS.map((preset, i) => ( + + ))} +
+
) } diff --git a/apps/web/src/components/FavoritesTab/FavoritesTab.tsx b/apps/web/src/components/FavoritesTab/FavoritesTab.tsx index c21b3c8..86a1168 100644 --- a/apps/web/src/components/FavoritesTab/FavoritesTab.tsx +++ b/apps/web/src/components/FavoritesTab/FavoritesTab.tsx @@ -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)', }} > - +
))} diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx index f75e718..e2ee817 100644 --- a/apps/web/src/components/Header.tsx +++ b/apps/web/src/components/Header.tsx @@ -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)' } } > diff --git a/apps/web/src/components/Player/PlayerCard.tsx b/apps/web/src/components/Player/PlayerCard.tsx index 17f328f..80b3a07 100644 --- a/apps/web/src/components/Player/PlayerCard.tsx +++ b/apps/web/src/components/Player/PlayerCard.tsx @@ -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', }} > - + @@ -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', }} > - + @@ -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', }} > - + diff --git a/apps/web/src/components/Queue/QueueCard.tsx b/apps/web/src/components/Queue/QueueCard.tsx index 8e178c6..790605b 100644 --- a/apps/web/src/components/Queue/QueueCard.tsx +++ b/apps/web/src/components/Queue/QueueCard.tsx @@ -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)} diff --git a/apps/web/src/components/SoloTab/SoloTab.tsx b/apps/web/src/components/SoloTab/SoloTab.tsx index 7b53b73..570ff4d 100644 --- a/apps/web/src/components/SoloTab/SoloTab.tsx +++ b/apps/web/src/components/SoloTab/SoloTab.tsx @@ -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 ? '▶ Играет' : '▶ Слушать'} diff --git a/apps/web/src/components/ThemeApplier.tsx b/apps/web/src/components/ThemeApplier.tsx new file mode 100644 index 0000000..be58d1d --- /dev/null +++ b/apps/web/src/components/ThemeApplier.tsx @@ -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 +} diff --git a/apps/web/src/lib/colors.ts b/apps/web/src/lib/colors.ts index e8d8179..54d9708 100644 --- a/apps/web/src/lib/colors.ts +++ b/apps/web/src/lib/colors.ts @@ -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})` } diff --git a/apps/web/src/store/themeStore.ts b/apps/web/src/store/themeStore.ts new file mode 100644 index 0000000..82bd776 --- /dev/null +++ b/apps/web/src/store/themeStore.ts @@ -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((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 }) + }, +})) diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 4c1124e..35bed50 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -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', },