feat(ui): the six native selects become the house controls

Owner's pick from the S7 mockups (2026-09-19): a short fixed set is a SegmentedToggle — the live
search's result count, the table's rows per page, a download profile's mode (on its own row, so the
three worded choices fit a phone) and audio format — and a longer set or the user's own list is a
FilterSelect: the profile's quality (seven heights) and the download dialog's profile.

A lint rule refuses a native <select> from now on (the canonical "FE control" row). The px-frozen
size rules also caught up with a fraction: `text-[9.5px]` had slipped past the integer-only pattern
in the Glass tuner, which is rem now.
This commit is contained in:
2026-09-19 17:53:22 +02:00
parent 17a26894b0
commit 0ef25a8037
9 changed files with 171 additions and 84 deletions
+18 -15
View File
@@ -51,6 +51,7 @@ import ItemCount from "./ui/ItemCount";
import ViewSwitcher from "./ViewSwitcher";
import VirtualFeed from "./VirtualFeed";
import { FocusSafeButton } from "./ui/FocusSafeButton";
import SegmentedToggle from "./ui/SegmentedToggle";
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
const PlayerModal = lazy(() => import("./PlayerModal"));
@@ -95,6 +96,9 @@ function matchesView(status: VideoStatus, show: string): boolean {
}
}
// How many live-search results one fetch asks for; `Load more` pulls another batch of the same size.
const YT_COUNTS = [20, 40, 60, 100] as const;
export default function Feed({
filters,
setFilters,
@@ -441,21 +445,20 @@ export default function Feed({
<span className="text-xs text-muted">
{zeroQuota ? t("feed.yt.freeNote") : t("feed.yt.quotaNote")}
</span>
<label className="ml-auto flex items-center gap-1.5 text-xs text-muted shrink-0">
{t("feed.yt.show")}
<select
value={ytCount}
onChange={(e) => setYtCount(Number(e.target.value))}
title={t("feed.yt.showHint")}
className="bg-surface border border-border rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
{[20, 40, 60, 100].map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</label>
{/* Four fixed counts: the house's segmented control, not a native <select> (owner's
pick, 2026-09-19). The caption is the group's name; the hint rides on the row. */}
<div
className="ml-auto flex items-center gap-1.5 text-xs text-muted shrink-0"
title={t("feed.yt.showHint")}
>
<span aria-hidden="true">{t("feed.yt.show")}</span>
<SegmentedToggle
ariaLabel={t("feed.yt.show")}
value={String(ytCount)}
onChange={(v) => setYtCount(Number(v))}
options={YT_COUNTS.map((n) => ({ value: String(n), label: String(n) }))}
/>
</div>
{/* Sorts the results already on screen (see sortYtResults) — it never re-queries, so it
costs nothing and works the same on both search sources. */}
<SortControl