feat: enhance discovery, vibe sessions, and library enrichment
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled

This commit is contained in:
kami
2026-08-01 14:40:48 +04:00
parent a0c9f42a89
commit 4c48d11e9d
54 changed files with 4136 additions and 521 deletions
+34 -8
View File
@@ -1,4 +1,5 @@
import { X, Play, Pause, SkipBack, SkipForward, Disc3 } from 'lucide-react';
import { useEffect, useRef } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { usePlaybackStore } from '../store/usePlaybackStore';
@@ -8,6 +9,31 @@ import { TrackRow, formatDuration } from './TrackRow';
import { albumService } from '../services/albumService';
export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
const panelRef = useRef<HTMLElement>(null);
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => { closeRef.current?.focus(); }, []);
useEffect(() => {
const previous = document.activeElement as HTMLElement | null;
const trap = (event: KeyboardEvent) => {
if (event.key !== 'Tab' || !panelRef.current) return;
const focusable = [...panelRef.current.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
)].filter((element) => !element.hasAttribute('hidden'));
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault(); last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault(); first.focus();
}
};
document.addEventListener('keydown', trap);
return () => {
document.removeEventListener('keydown', trap);
previous?.focus();
};
}, []);
const { currentTrack, queue, isPlaying, position, duration, play, pause, next, prev, setPosition } = usePlaybackStore();
const currentIdx = currentTrack ? queue.findIndex((t) => t.id === currentTrack.id) : -1;
@@ -22,25 +48,25 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
const artwork = albumQ.data?.artwork_id ?? currentTrack?.artwork_id ?? null;
return (
<aside className="w-96 flex flex-col border-l border-border/70 bg-bg1/80 backdrop-blur-sm overflow-hidden shrink-0">
<aside ref={panelRef} role="dialog" aria-modal="true" aria-label="Now playing queue" className="absolute inset-0 z-30 flex w-full flex-col overflow-hidden border-l border-border/70 bg-bg1 backdrop-blur-sm animate-slide-in sm:left-auto sm:w-96 lg:relative lg:z-auto lg:bg-bg1/80">
<div className="flex items-center justify-between px-4 py-3 border-b border-border/70">
<span className="text-sm font-semibold text-text">Now Playing</span>
<button onClick={onClose} className="text-muted hover:text-text p-1 rounded">
<button ref={closeRef} onClick={onClose} aria-label="Close now playing" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface1">
<X size={16} />
</button>
</div>
<div className="p-4 space-y-4">
<div className="p-3 space-y-3 sm:p-4 sm:space-y-4">
{currentTrack?.album_id ? (
<Link to="/albums/$albumId" params={{ albumId: currentTrack.album_id }}
className="group block aspect-square rounded-xl overflow-hidden relative shadow-lg shadow-black/40" title="Go to album">
className="group mx-auto block aspect-square w-full max-w-sm rounded-xl overflow-hidden relative shadow-lg shadow-black/40" title="Go to album">
<Artwork seed={`${currentTrack.title} ${currentTrack.artist}`} src={artwork} className="w-full h-full transition-transform group-hover:scale-105" rounded="xl" />
<div className="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/40 transition-colors">
<Disc3 size={28} className="text-on-accent opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</Link>
) : (
<div className="aspect-square rounded-xl overflow-hidden shadow-lg shadow-black/40">
<div className="mx-auto aspect-square w-full max-w-sm rounded-xl overflow-hidden shadow-lg shadow-black/40">
<Artwork seed={currentTrack ? `${currentTrack.title} ${currentTrack.artist}` : 'empty'} src={artwork} className="w-full h-full" rounded="xl" />
</div>
)}
@@ -74,8 +100,8 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
</div>
</div>
<div className="flex items-center justify-center gap-6">
<button onClick={prev} className="text-muted hover:text-text"><SkipBack size={20} /></button>
<div className="flex items-center justify-center gap-4 sm:gap-6">
<button onClick={prev} aria-label="Previous" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipBack size={20} /></button>
<button
onClick={() => isPlaying ? pause() : play()}
disabled={!currentTrack}
@@ -83,7 +109,7 @@ export function NowPlayingPanel({ onClose }: { onClose: () => void }) {
>
{isPlaying ? <Pause size={18} fill="currentColor" /> : <Play size={18} fill="currentColor" />}
</button>
<button onClick={next} className="text-muted hover:text-text"><SkipForward size={20} /></button>
<button onClick={next} aria-label="Next" className="rounded-md p-2 text-muted hover:text-text hover:bg-surface0"><SkipForward size={20} /></button>
</div>
</div>