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,34 @@
import React, { useLayoutEffect } from 'react';
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '@/store/queueUndo';
import type { QueueItemRef, Track } from '@/store/playerStoreTypes';
interface Args {
queue: QueueItemRef[];
queueIndex: number;
currentTrack: Track | null;
queueListRef: React.RefObject<HTMLDivElement | null>;
suppressNextAutoScrollRef: React.MutableRefObject<boolean>;
}
/** Queue scroll-position bridge for undo: publishes the list's scrollTop to the
* undo store and restores any pending scrollTop snapshot (set when an undo
* restores a prior queue state). The "scroll the next track into view" path
* lives in `QueueList` now that the list is virtualized (scrollToIndex). */
export function useQueueAutoScroll({
queue, queueIndex, currentTrack, queueListRef, suppressNextAutoScrollRef,
}: Args) {
useLayoutEffect(() => {
registerQueueListScrollTopReader(() => queueListRef.current?.scrollTop);
return () => registerQueueListScrollTopReader(null);
}, [queueListRef]);
useLayoutEffect(() => {
const top = consumePendingQueueListScrollTop();
if (top === undefined) return;
const el = queueListRef.current;
if (!el) return;
suppressNextAutoScrollRef.current = true;
el.scrollTop = top;
el.dispatchEvent(new Event('scroll', { bubbles: false }));
}, [queue, queueIndex, currentTrack?.id, queueListRef, suppressNextAutoScrollRef]);
}