refactor(ui): close the sort/layout reuse-gaps (R13 S2c step 5)

Two surfaces reinvented controls the app already consolidated:
- PlaylistsRail sort (a bespoke <select> + hand-rolled asc/desc flip) now uses
  the shared SortControl; "custom" is a directionless option (no arrow), the
  "unsynced first" pin rides the trailing slot as a pill toggle
- PlexPlaylistView Accordion/Tree layout (two hand-rolled toggle buttons) now
  uses the shared ViewSwitcher with a Rows3/ListTree option set

Adds aria-label i18n (plex.playlist.layoutLabel; HU+EN), drops the now-unused
ArrowUp/ArrowDown + ReactNode imports. Self-E2E'd the rail sort: key switch
reveals the arrow, asc<->desc flips, pin toggles, custom hides the arrow.
gate green (tsc/eslint-0/vitest 136/knip/prettier). Branch not merged.
This commit is contained in:
2026-08-15 01:01:03 +02:00
parent 42d0173095
commit b57aec113e
4 changed files with 44 additions and 64 deletions
+29 -38
View File
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import { ArrowDown, ArrowUp, ListVideo, Pin, Plus, RefreshCw, Youtube } from "lucide-react";
import { ListVideo, Pin, Plus, RefreshCw, Youtube } from "lucide-react";
import { api, type Playlist } from "../lib/api";
import { formatDuration } from "../lib/format";
import { notify } from "../lib/notifications";
@@ -11,6 +11,8 @@ import { LS, readAccountMerged, writeAccount } from "../lib/storage";
import { defaultLayout, type PanelLayout } from "../lib/panelLayout";
import SidePanel from "./SidePanel";
import PanelGroups, { type PanelItem } from "./PanelGroups";
import SortControl from "./SortControl";
import { type SortOption } from "../lib/columnSort";
import { usePlaylists, usePlaylistsActions } from "./PlaylistsProvider";
import { useMe } from "../lib/useMe";
import { playlistName } from "../lib/playlistName";
@@ -45,6 +47,12 @@ export default function PlaylistsRail({
const { t } = useTranslation();
const qc = useQueryClient();
const [plSort, setPlSort] = useState<PlSort>(() => readAccountMerged(LS.plSort, PL_SORT_DEFAULT));
const plSortOptions: SortOption[] = [
{ key: "custom", label: t("playlists.railSortCustom"), directionless: true },
{ key: "name", label: t("playlists.railSortName"), defaultDir: "asc" },
{ key: "count", label: t("playlists.railSortCount"), defaultDir: "desc" },
{ key: "duration", label: t("playlists.railSortDuration"), defaultDir: "desc" },
];
useEffect(() => {
writeAccount(LS.plSort, plSort);
}, [plSort]);
@@ -149,43 +157,26 @@ export default function PlaylistsRail({
{t("playlists.syncYoutube")}
</button>
{playlists.length > 1 && (
<div className="flex items-center gap-1">
<select
value={plSort.key}
onChange={(e) => setPlSort({ ...plSort, key: e.target.value as PlSort["key"] })}
aria-label={t("playlists.sortLabel")}
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-1.5 py-1 text-2xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
<option value="custom">{t("playlists.railSortCustom")}</option>
<option value="name">{t("playlists.railSortName")}</option>
<option value="count">{t("playlists.railSortCount")}</option>
<option value="duration">{t("playlists.railSortDuration")}</option>
</select>
<button
onClick={() => setPlSort({ ...plSort, dir: plSort.dir === "asc" ? "desc" : "asc" })}
disabled={plSort.key === "custom"}
title={plSort.dir === "asc" ? t("playlists.dirAsc") : t("playlists.dirDesc")}
className="p-1 rounded-md border border-border text-muted enabled:hover:text-fg enabled:hover:border-accent disabled:opacity-30 transition"
>
{plSort.dir === "asc" ? (
<ArrowUp className="w-3.5 h-3.5" />
) : (
<ArrowDown className="w-3.5 h-3.5" />
)}
</button>
<button
onClick={() => setPlSort({ ...plSort, dirtyFirst: !plSort.dirtyFirst })}
title={t("playlists.dirtyFirst")}
aria-pressed={plSort.dirtyFirst}
className={`p-1 rounded-md border transition ${
plSort.dirtyFirst
? "border-accent text-accent"
: "border-border text-muted hover:text-fg hover:border-accent"
}`}
>
<Pin className="w-3.5 h-3.5" />
</button>
</div>
<SortControl
label={t("playlists.sortLabel")}
ascLabel={t("playlists.dirAsc")}
descLabel={t("playlists.dirDesc")}
options={plSortOptions}
sort={{ key: plSort.key, dir: plSort.dir }}
onChange={(next) =>
next && setPlSort({ ...plSort, key: next.key as PlSort["key"], dir: next.dir })
}
trailing={
<button
onClick={() => setPlSort({ ...plSort, dirtyFirst: !plSort.dirtyFirst })}
title={t("playlists.dirtyFirst")}
aria-pressed={plSort.dirtyFirst}
className="pill shrink-0 rounded-lg border p-1.5"
>
<Pin className="h-4 w-4" />
</button>
}
/>
)}
<form
onSubmit={(e) => {