diff --git a/src/components/common/Marquee.tsx b/src/components/common/Marquee.tsx new file mode 100644 index 0000000..9a78026 --- /dev/null +++ b/src/components/common/Marquee.tsx @@ -0,0 +1,39 @@ +import { useLayoutEffect, useRef, useState, type CSSProperties } from 'react'; + +/** Single-line text that ping-pong scrolls (like a news ticker) only when it + * overflows its container, otherwise renders as static clipped text. Keeps the + * queue panel from ever growing a horizontal scrollbar on long titles. */ +export function Marquee({ + text, + className, +}: { + text: string; + className?: string; +}) { + const ref = useRef(null); + const [shift, setShift] = useState(0); + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + const measure = () => { + const inner = el.firstElementChild as HTMLElement | null; + const overflow = (inner?.scrollWidth ?? 0) - el.clientWidth; + setShift(overflow > 1 ? overflow : 0); + }; + measure(); + const ro = new ResizeObserver(measure); + ro.observe(el); + return () => ro.disconnect(); + }, [text]); + + return ( + + {text} + + ); +} diff --git a/src/components/player/QueuePanel.tsx b/src/components/player/QueuePanel.tsx index 3c6073d..55cc555 100644 --- a/src/components/player/QueuePanel.tsx +++ b/src/components/player/QueuePanel.tsx @@ -26,6 +26,7 @@ import { import { CSS } from '@dnd-kit/utilities'; import { Icon } from '../common/Icon'; import { ArtTile } from '../common/ArtTile'; +import { Marquee } from '../common/Marquee'; import { PlayingIndicator } from '../common/PlayingIndicator'; import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch'; import { @@ -259,13 +260,9 @@ function QueueRow({ onDoubleClick={onPlay} title={t('queue.doubleClickPlay')} > - {isCurrent ? ( - - ) : ( - - - - )} + + +
{isCurrent && ( @@ -275,8 +272,8 @@ function QueueRow({ )}
-
{resolved?.title ?? entry.title}
-
{resolved?.artistName ?? entry.artistName}
+ + diff --git a/src/styles/shell.css b/src/styles/shell.css index cc182f1..04b4a73 100644 --- a/src/styles/shell.css +++ b/src/styles/shell.css @@ -651,6 +651,7 @@ flex: 1; min-height: 0; overflow-y: auto; + overflow-x: hidden; padding: 12px 12px 18px; } .qrow { @@ -700,23 +701,46 @@ font-size: 13px; font-weight: 500; color: var(--fg-1); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .qrow .qt .r { font-size: 11px; color: var(--fg-3); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: flex; - align-items: center; - gap: 4px; } -.qrow .qt .r .ph { - color: var(--lime); - font-size: 11px; + +/* News-ticker text: clips by default, ping-pong scrolls only when it overflows + (the .on class is set by the Marquee component after measuring). */ +.marquee { + display: block; + max-width: 100%; + overflow: hidden; + white-space: nowrap; +} +.marquee-inner { + display: inline-block; + white-space: nowrap; + text-overflow: ellipsis; + max-width: 100%; + overflow: hidden; + vertical-align: bottom; +} +.marquee.on .marquee-inner { + max-width: none; + animation: marquee-pingpong 9s ease-in-out infinite alternate; +} +@keyframes marquee-pingpong { + 0%, + 12% { + transform: translateX(0); + } + 88%, + 100% { + transform: translateX(var(--mq-shift, 0)); + } +} +@media (prefers-reduced-motion: reduce) { + .marquee.on .marquee-inner { + animation: none; + } } .qd-radio { margin-bottom: 14px;