fix(ui): give release source tabs their own row on phones (#1663)

Fixes #1662, thanks @mietu111 for the report and the screenshot.

The source tabs share a row with up to five icons, which take 240px on a
phone. On a 393px screen that leaves 113px for the tabs, so Prowlarr
fits and Direct Download is down to a sliver. On a 360px screen not even
Prowlarr fits. The scrollbar is hidden, so there is also nothing to tell
you there is more to scroll to.

With more than one source, the tabs now get their own row below the sm
breakpoint and the icons sit on a row beneath them. Picking a tab also
scrolls it into view, for when there are too many sources to fit. This
does make the sticky header 36px taller on phones. One source and
anything sm or wider stay on a single row, with the same tab positions
and row height as before.

I checked this with screenshots of the dialog using mocked search
results, at 360 and 393px with two sources, 360px with five, 393px with
one, and at 640 and 1280px with two. The filter menu and manual query
panel still open correctly from the new row at 393px. There are no
layout tests for this dialog, so I did not add one.

Before, with Direct Download selected. The tab has 113px of space and
its label is cut off.
<img width="786" height="1704" alt="download"
src="https://github.com/user-attachments/assets/7fc46034-4faf-438b-aed3-7d9adbd41ac9"
/>
After, with Direct Download selected. Both tabs fit and the icons sit on
the row beneath.
<img width="786" height="1704" alt="download"
src="https://github.com/user-attachments/assets/e9e961e8-5f04-4b24-9483-89482891252a"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Improvements**
  * The active source tab is centered in the tab strip when selected.
* On narrow screens, multiple source tabs wrap onto their own row, and
the result count is hidden.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
splitsec2 authored and GitHub committed 2026-11-04 16:07:35 -06:00
1 parent 7c90afca9e
commit 4c3ce3355a
1 file changed
+23 -4
+23 -4
View File
@@ -914,6 +914,18 @@ const ReleaseModalSession = ({
const tabRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const tabIndicatorStyle = useTabIndicator(tabRefs, activeTab, allTabs);
const tabStripRef = useRef<HTMLDivElement>(null);
// The strip's scrollbar is hidden, so bring the selected tab fully into view. This sets
// the strip's own scrollLeft: scrollIntoView would also scroll the modal vertically.
const selectTab = (name: string, tab: HTMLButtonElement) => {
setActiveTab(name);
const strip = tabStripRef.current;
strip?.scrollTo({
left: tab.offsetLeft - (strip.clientWidth - tab.offsetWidth) / 2,
behavior: 'smooth',
});
};
// Get unique formats from current releases for filter dropdown
// Only show formats that are in the supported list
@@ -1814,9 +1826,16 @@ const ReleaseModalSession = ({
</div>
)}
{!sourcesLoading && allTabs.length > 0 && (
<div className="flex items-center px-5">
{/* Tabs - scrollable on narrow screens */}
<div className="scrollbar-hide min-w-0 flex-1 overflow-x-auto">
<div
className={`flex items-center px-5 ${allTabs.length > 1 ? 'flex-wrap sm:flex-nowrap' : ''}`}
>
{/* Tabs - scrollable on narrow screens. With more than one source they get
their own row on phones, because the controls would otherwise leave
them about 110px. */}
<div
ref={tabStripRef}
className={`scrollbar-hide min-w-0 flex-1 overflow-x-auto ${allTabs.length > 1 ? 'basis-full sm:basis-0' : ''}`}
>
<div className="relative flex gap-1">
{/* Sliding indicator */}
<div
@@ -1833,7 +1852,7 @@ const ReleaseModalSession = ({
ref={(el) => {
tabRefs.current[tab.name] = el;
}}
onClick={() => setActiveTab(tab.name)}
onClick={(event) => selectTab(tab.name, event.currentTarget)}
className={`border-b-2 border-transparent px-4 py-2.5 text-sm font-medium whitespace-nowrap transition-colors ${
activeTab === tab.name
? 'text-emerald-600 dark:text-emerald-400'