refactor(queue): co-locate QueuePanel UI into features/queue

Extract the queue UI (QueuePanel + queuePanel/* components + useQueue* hooks)
into features/queue/ with a barrel. This is the chosen playback/queue boundary:
queue = the QueuePanel UI that CONSUMES the playback store; queue STATE and the
audio engine stay in store/ (playback-core, moved separately). Verified
one-way: no playback-core file imports the queue UI back (only doc comments
mention it).

Excluded as NOT queue-UI: useIdlePlayQueuePull (AppShell) and
usePlayQueueSyncLedState (ConnectionIndicator) are queue-SYNC orchestration,
not panel UI — left in hooks/ to move with playback.

Pure move. One test fix: QueuePanel.test.tsx reads its subject via a hardcoded
readFileSync('src/components/QueuePanel.tsx') path (architecture-pin grep for
forbidden HTML5 DnD) — repointed to the new location (the move tool can't
rewrite non-import string paths). tsc 0, lint 0/0, suite 319/2353 green.
This commit is contained in:
Psychotoxical
2026-06-30 08:40:51 +02:00
parent 7ad196711e
commit d5bbabac1d
20 changed files with 89 additions and 84 deletions
@@ -0,0 +1,38 @@
import { useMemo, useSyncExternalStore } from 'react';
import { usePlayerStore } from '@/store/playerStore';
import type { QueueItemRef, Track } from '@/store/playerStoreTypes';
import { resolveQueueTrack } from '@/utils/library/queueTrackView';
import {
getQueueResolverVersion,
subscribeQueueResolver,
} from '@/utils/library/queueTrackResolver';
/**
* Stable queue selectors (queue thin-state). The store is refs-canonical now:
* full `Track`s come from the resolver cache (placeholder until a fetch lands),
* with session star/rating overrides (F4) merged on read via resolveQueueTrack.
*/
/** The track at a queue index, or null. */
export function useQueueTrackAt(idx: number): Track | null {
const ref = usePlayerStore(s => s.queueItems[idx] ?? null);
const starredOverrides = usePlayerStore(s => s.starredOverrides);
const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides);
const version = useSyncExternalStore(subscribeQueueResolver, getQueueResolverVersion);
return useMemo(() => {
if (!ref) return null;
return resolveQueueTrack(ref);
// version drives re-resolution as the cache fills; overrides drive the merge.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, starredOverrides, userRatingOverrides, version]);
}
/** The currently playing track, or null. */
export function useCurrentTrack(): Track | null {
return usePlayerStore(s => s.currentTrack);
}
/** The whole queue as thin refs (the canonical list). */
export function useQueueItems(): QueueItemRef[] {
return usePlayerStore(s => s.queueItems);
}