release: 0.60.10 (r10-selected-paint)

This commit is contained in:
2026-08-11 00:50:24 +02:00
15 changed files with 128 additions and 91 deletions
+1 -1
View File
@@ -1 +1 @@
0.60.9
0.60.10
@@ -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"
>
<span className="max-w-56 truncate">{f.label}</span>
<X className="w-3 h-3 shrink-0 opacity-60 group-hover:opacity-100" />
+1 -1
View File
@@ -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"
>
<span className="truncate">{nameOf(id)}</span>
<X className="w-3 h-3 shrink-0" />
+1 -2
View File
@@ -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 max-w-36 truncate";
return onClick ? (
<button
type="button"
+5 -15
View File
@@ -483,11 +483,8 @@ export default function Channels() {
<button
key={f.id}
onClick={() => setStatusFilter(f.id)}
className={`text-xs px-2.5 py-1 rounded-full border transition ${
statusFilter === f.id
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
aria-pressed={statusFilter === f.id}
className="pill text-xs px-2.5 py-1 rounded-full border"
>
{t(f.labelKey)}
</button>
@@ -622,11 +619,8 @@ export default function Channels() {
<button
key={v}
onClick={() => setView(v)}
className={`text-sm px-3 py-1.5 rounded-full border transition ${
view === v
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
aria-pressed={view === v}
className="pill text-sm px-3 py-1.5 rounded-full border"
>
{t(`channels.tabs.${v}`)}
</button>
@@ -667,11 +661,7 @@ export default function Channels() {
setTagFilter((f) => (active ? f.filter((x) => x !== tg.id) : [...f, tg.id]))
}
aria-pressed={active}
className={`text-xs px-2 py-1 rounded-full border transition ${
active
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:text-fg hover:border-accent"
}`}
className="pill text-xs px-2 py-1 rounded-full border"
>
{tg.name}
</button>
+2 -10
View File
@@ -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)}
</button>
@@ -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)}
</button>
+2 -8
View File
@@ -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`}
>
<span className="relative shrink-0">
<Icon className="w-4.5 h-4.5" />
@@ -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`}
>
<Settings className="w-4.5 h-4.5 shrink-0" />
{!slim && <span className="truncate">{t("header.account.settings")}</span>}
+2 -3
View File
@@ -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}
</button>
+7 -9
View File
@@ -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}`)}
</button>
@@ -150,8 +148,9 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
<button
key={s}
onClick={() => setScope(s)}
className={`min-w-0 px-1.5 py-1.5 rounded-lg text-xs font-medium truncate transition ${
s === scope ? "bg-accent text-accent-fg" : "glass-card glass-hover"
aria-pressed={s === scope}
className={`pill min-w-0 px-1.5 py-1.5 rounded-lg text-xs font-medium truncate ${
s === scope ? "" : "glass-card glass-hover"
}`}
>
{t(`plex.filter.scopeOpt.${s}`)}
@@ -507,9 +506,8 @@ function Chip({
return (
<button
onClick={onClick}
className={`px-2.5 py-1 rounded-full text-xs transition ${
active ? "bg-accent text-accent-fg" : "glass-card glass-hover"
}`}
aria-pressed={active}
className={`pill px-2.5 py-1 rounded-full text-xs ${active ? "" : "glass-card glass-hover"}`}
>
{children}
</button>
@@ -518,7 +516,7 @@ function Chip({
function RemovableChip({ label, onRemove }: { label: string; onRemove: () => void }) {
return (
<span className="inline-flex items-center gap-1 rounded-full bg-accent/15 text-accent px-2 py-1 text-xs">
<span className="chip px-2 py-1 text-xs">
<span className="truncate max-w-36">{label}</span>
<button onClick={onRemove} className="hover:opacity-70" aria-label="remove">
<X className="w-3 h-3" />
+3 -4
View File
@@ -69,10 +69,9 @@ export default function SettingsPanel() {
key={tabItem.id}
data-testid={`settings-tab-${tabItem.id}`}
onClick={() => setTab(tabItem.id)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left transition ${
active
? "bg-accent text-accent-fg font-medium shadow-md shadow-accent/20"
: "text-muted hover:text-fg hover:bg-card/60"
aria-pressed={active}
className={`pill flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left ${
active ? "font-medium shadow-md shadow-accent/20" : "hover:bg-card/60"
}`}
>
<tabItem.icon className="w-4 h-4 shrink-0" />
+8 -23
View File
@@ -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"
>
<span>{tag.name}</span>
<span
@@ -169,11 +166,8 @@ export default function Sidebar({
dateTo: undefined,
})
}
className={`text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px transition ${
activePreset
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border hover:border-accent"
}`}
aria-pressed={activePreset}
className="pill text-xs px-2.5 py-1 rounded-full border shadow-xs active:translate-y-px"
>
{t("sidebar.datePreset." + p.key)}
</button>
@@ -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")}
</button>
@@ -281,11 +272,7 @@ export default function Sidebar({
key={m}
onClick={() => setFilters({ ...filters, tagMode: m })}
aria-pressed={filters.tagMode === m}
className={`px-2 py-0.5 rounded-full transition ${
filters.tagMode === m
? "bg-accent text-accent-fg"
: "text-muted hover:text-fg"
}`}
className="pill px-2 py-0.5 rounded-full"
>
{m === "or" ? t("sidebar.any") : t("sidebar.all")}
</button>
@@ -392,9 +379,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" ? <User className="w-3.5 h-3.5" /> : <Library className="w-3.5 h-3.5" />}
<span>{t("header.scope." + s)}</span>
+4 -10
View File
@@ -35,11 +35,8 @@ export default function Stats() {
<button
key={id}
onClick={() => setTab(id)}
className={`px-3 py-1.5 rounded-full text-sm border transition ${
tab === id
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
aria-pressed={tab === id}
className="pill px-3 py-1.5 rounded-full text-sm border"
>
{t(`stats.tabs.${id}`)}
</button>
@@ -236,11 +233,8 @@ function SystemStats() {
<button
key={d}
onClick={() => setDays(d)}
className={`px-3 py-1.5 rounded-full text-sm border transition ${
days === d
? "bg-accent text-accent-fg border-accent"
: "bg-card border-border text-muted hover:border-accent"
}`}
aria-pressed={days === d}
className="pill px-3 py-1.5 rounded-full text-sm border"
>
{t("stats.rangeDays", { count: d })}
</button>
+3 -4
View File
@@ -31,10 +31,9 @@ export default function Tabs({
<button
key={tb.id}
onClick={() => onChange(tb.id)}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition ${
on
? "bg-accent text-accent-fg font-medium shadow-md shadow-accent/20"
: "text-muted hover:text-fg hover:bg-card/60"
aria-pressed={on}
className={`pill inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm ${
on ? "font-medium shadow-md shadow-accent/20" : "hover:bg-card/60"
}`}
>
{tb.label}
+78
View File
@@ -418,6 +418,84 @@ body {
outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
outline-offset: -2px;
}
/* ===== R10 S4 · Selection-paint recipes =====
The ~14 hand-rolled "this is the selected / current / active one" idioms all painted the SAME
bg-accent fill via an inline ternary (`sel ? "bg-accent text-accent-fg border-accent" : "…"`),
which drifted over time. These recipes OWN that paint, keyed on the semantic ARIA the control
already carries — the JSX just toggles `aria-pressed` (a segment/toggle) or `aria-current` (the
single current item) and the CSS paints, so one control's selected look can never diverge from
the next's. Geometry (padding / radius / border-width / font) stays a per-site utility since it
legitimately varies. NO focus-visible here on purpose: the `@layer base` rule above already rings
every button/a/[role]; re-adding it would duplicate that (an R9 review finding). Because these sit
in @layer components, a site's own utility still wins — so a MIGRATED site must DROP its bg/text/
border-color utilities (else a utility-layer `bg-*` beats the recipe's pressed fill). Add
data-soft="true" for the subtle tint some surfaces use instead of the solid fill. */
.pill,
.is-current {
color: var(--muted);
transition:
color 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease,
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 {
color: var(--fg);
}
.pill:hover {
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}
.is-current:hover {
background-color: var(--card);
}
/* selected / current — SOLID accent fill (the dominant idiom). The `:not([aria-current="false"])`
guard matters: `.is-current` is a shared primitive and a consumer using the common
`aria-current={x ? "true" : "false"}` idiom would otherwise paint a NON-current row as current. */
.pill[aria-pressed="true"],
.is-current[aria-current]:not([aria-current="false"]) {
background-color: var(--accent);
color: var(--accent-fg);
border-color: var(--accent);
}
/* selected — SOFT tint variant (the plex layout toggle keeps a glass-card resting look, so its
"on" state is a subtle accent tint rather than the solid fill). */
.pill[aria-pressed="true"][data-soft="true"] {
background-color: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
}
/* .chip — an applied tag / filter token (rounded, tinted, self-contained: display + geometry +
paint). The tint is the resting look (a filter is "on" whenever the chip exists); removable
variants add their own × button as a child.
.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 own geometry (`rounded-full`, optional `border`) or on top of `.chip`, never bare. */
.chip {
display: inline-flex;
align-items: center;
gap: 0.25rem;
border-radius: 9999px;
border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
background-color: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--accent);
}
.chip-solid {
background-color: var(--accent);
color: var(--accent-fg);
border-color: var(--accent);
}
}
.glass-hover:hover {
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
+10
View File
@@ -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",