From dcb91066f53c7a63313646d2f20ba7f561b874b2 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 9 Aug 2026 21:06:58 +0200 Subject: [PATCH 1/5] =?UTF-8?q?refactor(ui):=20R10=20S4=20=E2=80=94=20unif?= =?UTF-8?q?y=20selected-paint=20into=20.pill/.is-current/.chip=20recipes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ~14 hand-rolled "this is the selected/current/active one" idioms each repeated the same `sel ? "bg-accent text-accent-fg border-accent" : "…"` inline ternary, which had drifted. Three attribute-driven recipes in @layer components now own that paint, keyed on the ARIA the control already carries: .pill (aria-pressed segments/toggles), .is-current (aria-current nav/list items), and .chip / .chip-solid (tag & filter tokens). The JSX just toggles the ARIA; the CSS paints, so one control's selected look can't diverge from the next's. No focus-visible in the recipes — the @layer base rule already rings every button/a. Migrated 15 sites (Feed show/content, Channels tag filter, Sidebar or-and + scope, PlexSidebar match, Tabs, SettingsPanel tab-rail, PlexPlaylistView layout [soft], NavSidebar nav + settings, ActiveFilterChips, PlexSidebar RemovableChip, ChannelTags, ChannelPicker). Deliberately left: PlexSidebar facet Chip and PlexPlaylistView already covered; SavedViews row (paint split across two elements), the scheme swatch (border-only on a coloured swatch), and ChannelPicker's option rows (text-only selection) — noted for a follow-up. --- frontend/src/components/ActiveFilterChips.tsx | 2 +- frontend/src/components/ChannelPicker.tsx | 2 +- frontend/src/components/ChannelTags.tsx | 3 +- frontend/src/components/Channels.tsx | 6 +- frontend/src/components/Feed.tsx | 12 +--- frontend/src/components/NavSidebar.tsx | 10 +-- frontend/src/components/PlexPlaylistView.tsx | 5 +- frontend/src/components/PlexSidebar.tsx | 6 +- frontend/src/components/SettingsPanel.tsx | 7 +- frontend/src/components/Sidebar.tsx | 10 +-- frontend/src/components/Tabs.tsx | 7 +- frontend/src/index.css | 64 +++++++++++++++++++ 12 files changed, 84 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/ActiveFilterChips.tsx b/frontend/src/components/ActiveFilterChips.tsx index 625f4235..a6429473 100644 --- a/frontend/src/components/ActiveFilterChips.tsx +++ b/frontend/src/components/ActiveFilterChips.tsx @@ -32,7 +32,7 @@ export default function ActiveFilterChips({ onClick={f.remove} title={t("feed.chips.remove", { name: f.label })} aria-label={t("feed.chips.remove", { name: f.label })} - className="group inline-flex items-center gap-1 pl-2.5 pr-1.5 py-1 rounded-full border border-accent/40 bg-accent/10 text-accent text-xs hover:border-accent hover:bg-accent/20 transition" + className="chip group pl-2.5 pr-1.5 py-1 text-xs hover:border-accent hover:bg-accent/20 transition" > {f.label} diff --git a/frontend/src/components/ChannelPicker.tsx b/frontend/src/components/ChannelPicker.tsx index e2fd5225..e09f037b 100644 --- a/frontend/src/components/ChannelPicker.tsx +++ b/frontend/src/components/ChannelPicker.tsx @@ -52,7 +52,7 @@ export default function ChannelPicker({ key={id} onClick={() => toggle(id)} title={t("sidebar.channelRemove", { name: nameOf(id) })} - className="inline-flex items-center gap-1 max-w-full pl-2.5 pr-1.5 py-1 rounded-full bg-accent text-accent-fg text-xs shadow-xs hover:opacity-90 transition" + className="chip-solid inline-flex items-center gap-1 max-w-full pl-2.5 pr-1.5 py-1 rounded-full text-xs shadow-xs hover:opacity-90 transition" > {nameOf(id)} diff --git a/frontend/src/components/ChannelTags.tsx b/frontend/src/components/ChannelTags.tsx index ea6fa675..703b1990 100644 --- a/frontend/src/components/ChannelTags.tsx +++ b/frontend/src/components/ChannelTags.tsx @@ -22,8 +22,7 @@ interface TagChipProps { } function TagChip({ tag, onClick, title }: TagChipProps) { - const cls = - "shrink-0 text-3xs px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent max-w-36 truncate"; + const cls = "chip-solid shrink-0 text-3xs px-1.5 py-0.5 rounded-full border max-w-36 truncate"; return onClick ? ( diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index c5dd0c26..5d49bcf6 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -557,11 +557,7 @@ export default function Feed({ data-testid={`feed-show-${id}`} onClick={() => setFilters({ ...filters, show: id })} aria-pressed={filters.show === id} - className={`text-xs px-3 py-1.5 rounded-full border transition ${ - filters.show === id - ? "bg-accent text-accent-fg border-accent" - : "border-border text-muted hover:text-fg hover:border-accent" - }`} + className="pill text-xs px-3 py-1.5 rounded-full border" > {t("sidebar.show." + id)} @@ -575,11 +571,7 @@ export default function Feed({ data-testid={`feed-content-${c.key}`} onClick={() => setFilters({ ...filters, [c.key]: !on })} aria-pressed={on} - className={`text-xs px-3 py-1.5 rounded-full border transition ${ - on - ? "bg-accent text-accent-fg border-accent" - : "border-border text-muted hover:text-fg hover:border-accent" - }`} + className="pill text-xs px-3 py-1.5 rounded-full border" > {t(c.label)} diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 92b54547..0f9642b7 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -193,9 +193,7 @@ export default function NavSidebar({ onClick={() => setPage(p)} title={slim ? label : undefined} aria-current={active ? "page" : undefined} - className={`${rowBase} ${slim ? "justify-center px-0" : ""} relative ${ - active ? "bg-accent text-accent-fg" : "text-muted hover:text-fg hover:bg-card" - }`} + className={`${rowBase} ${slim ? "justify-center px-0" : ""} relative is-current`} > @@ -308,11 +306,7 @@ export default function NavSidebar({ onClick={() => setPage("settings")} title={slim ? t("header.account.settings") : undefined} aria-current={page === "settings" ? "page" : undefined} - className={`${rowBase} ${slim ? "justify-center px-0" : ""} ${ - page === "settings" - ? "bg-accent text-accent-fg" - : "text-muted hover:text-fg hover:bg-card" - }`} + className={`${rowBase} ${slim ? "justify-center px-0" : ""} is-current`} > {!slim && {t("header.account.settings")}} diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index d0daae57..60ca7dac 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -505,9 +505,8 @@ export default function PlexPlaylistView({ title={label} aria-label={label} aria-pressed={layout === v} - className={`rounded-lg p-2 text-sm transition ${ - layout === v ? "bg-accent/15 text-accent" : "glass-card glass-hover text-muted" - }`} + data-soft="true" + className={`pill rounded-lg p-2 text-sm ${layout === v ? "" : "glass-card glass-hover text-muted"}`} > {icon} diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index c332e729..4e882374 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -59,9 +59,7 @@ function MatchToggle({ key={m} onClick={() => onChange(m)} aria-pressed={mode === m} - className={`rounded-full px-2 py-0.5 transition ${ - mode === m ? "bg-accent text-accent-fg" : "text-muted hover:text-fg" - }`} + className="pill rounded-full px-2 py-0.5" > {t(`plex.filter.match.${m}`)} @@ -518,7 +516,7 @@ function Chip({ function RemovableChip({ label, onRemove }: { label: string; onRemove: () => void }) { return ( - + {label} @@ -392,9 +388,7 @@ export default function Sidebar({ onClick={() => setFilters({ ...filters, scope: s })} title={t("header.scope." + s + "Tip")} aria-pressed={filters.scope === s} - className={`flex-1 inline-flex items-center justify-center gap-1 px-2.5 py-1 rounded-full transition ${ - filters.scope === s ? "bg-accent text-accent-fg" : "text-muted hover:text-fg" - }`} + className="pill flex-1 inline-flex items-center justify-center gap-1 px-2.5 py-1 rounded-full" > {s === "my" ? : } {t("header.scope." + s)} diff --git a/frontend/src/components/Tabs.tsx b/frontend/src/components/Tabs.tsx index fbf25b84..3154827b 100644 --- a/frontend/src/components/Tabs.tsx +++ b/frontend/src/components/Tabs.tsx @@ -31,10 +31,9 @@ export default function Tabs({ @@ -622,11 +619,8 @@ export default function Channels() { diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index 4e882374..2b1ac4b9 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -148,8 +148,9 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index ae0a8f97..0eda45a9 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -46,11 +46,8 @@ function TagChip({ data-tag={tag.name} onClick={onClick} title={t("sidebar.channelCount", { count })} - className={`inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border shadow-xs hover:shadow-sm active:translate-y-px transition ${ - active - ? "bg-accent text-accent-fg border-accent" - : "bg-card border-border text-fg hover:border-accent" - }`} + aria-pressed={active} + className="pill inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border shadow-xs hover:shadow-sm active:translate-y-px" > {tag.name} {t("sidebar.datePreset." + p.key)} @@ -185,11 +179,8 @@ export default function Sidebar({ setCustomDates(next); if (next) setFilters({ ...filters, maxAgeDays: undefined }); }} - className={`text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px transition ${ - customDates || filters.dateFrom || filters.dateTo - ? "bg-accent text-accent-fg border-accent" - : "bg-card border-border hover:border-accent" - }`} + aria-pressed={!!(customDates || filters.dateFrom || filters.dateTo)} + className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px" > {t("sidebar.custom")} diff --git a/frontend/src/components/Stats.tsx b/frontend/src/components/Stats.tsx index a7d44392..59c4602e 100644 --- a/frontend/src/components/Stats.tsx +++ b/frontend/src/components/Stats.tsx @@ -35,11 +35,8 @@ export default function Stats() { @@ -236,11 +233,8 @@ function SystemStats() { diff --git a/frontend/src/index.css b/frontend/src/index.css index 560b6b4c..dba7d92f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -474,7 +474,7 @@ body { .chip-solid is intentionally a PAINT-ONLY modifier (bg/text/border colour, no display or geometry): it sits on a site's OWN chip geometry so a truncating single-element chip (ChannelTags) isn't forced into inline-flex, which would break its text-overflow. Use it WITH a - site's `rounded-full`/`border` utilities (or on top of `.chip`), never bare. */ + site's own geometry (`rounded-full`, optional `border`) or on top of `.chip`, never bare. */ .chip { display: inline-flex; align-items: center; From cb168c800a261f2ae1edd6257c85ddf7c271399a Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 11 Aug 2026 00:45:14 +0200 Subject: [PATCH 4/5] =?UTF-8?q?fix(ui):=20R10=20S4=20review=20r3=20?= =?UTF-8?q?=E2=80=94=20preserve=20glass-card=20edge=20on=20pills,=20ease?= =?UTF-8?q?=20transform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two low-severity follow-ups from the third /code-review round: - .pill's resting border-color is now scoped `.pill:not(.glass-card)`, so a glass-card-resting pill (PlexSidebar scope/facet, PlexPlaylistView layout) keeps glass-card's own border incl. its lighter top-edge specular highlight, instead of having it flattened to a uniform var(--border). - add `transform` to the recipe transition so the `active:translate-y-px` press nudge on the Sidebar tag/date pills eases again (it lost the utility transition). Verified plain pills keep their resting border (Feed + YOUR TAGS) and active pills still paint solid accent. --- frontend/src/index.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index dba7d92f..3ae68bfb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -434,12 +434,19 @@ body { .pill, .is-current { color: var(--muted); - border-color: var(--border); /* inert unless the site also adds a border-width utility */ transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease, - box-shadow 0.15s ease; /* so an on-state shadow (Tabs/SettingsPanel) fades in, not pops */ + box-shadow 0.15s ease, + transform 0.15s ease; /* box-shadow: an on-state shadow (Tabs) fades; transform: an active:translate press eases */ + } + /* Resting border colour for the plain pills. NOT applied to a glass-card-resting pill (`.pill.glass-card`), + whose own border — including its lighter top-edge specular highlight — must survive. Inert unless the + site also adds a border-width utility. */ + .pill:not(.glass-card), + .is-current { + border-color: var(--border); } .pill:hover, .is-current:hover { From a9b53bbc9d6aada0244e68960f070acf654a1beb Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 11 Aug 2026 00:48:26 +0200 Subject: [PATCH 5/5] =?UTF-8?q?chore(release):=200.60.10=20=E2=80=94=20VER?= =?UTF-8?q?SION=20bump=20+=20release=20note=20(R10=20S4=20selection-paint?= =?UTF-8?q?=20unification)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- frontend/src/lib/releaseNotes.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index fb7385c1..8576367b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.60.9 +0.60.10 diff --git a/frontend/src/lib/releaseNotes.ts b/frontend/src/lib/releaseNotes.ts index fa827086..e8fabff9 100644 --- a/frontend/src/lib/releaseNotes.ts +++ b/frontend/src/lib/releaseNotes.ts @@ -14,6 +14,16 @@ export interface ReleaseEntry { } export const RELEASE_NOTES: ReleaseEntry[] = [ + { + version: "0.60.10", + date: "2026-08-11", + fixes: [ + "Selected filters, tabs and chips now share one consistent highlight, so the same kind of control looks the same everywhere — e.g. the channel manager's filter rows no longer differ from each other.", + ], + chores: [ + "Unified the selected / current / active control styling onto shared recipes, which also gives filter pills proper keyboard-focus rings and screen-reader selected state (no behaviour change to what the controls do).", + ], + }, { version: "0.60.9", date: "2026-08-09",