release: 0.60.6 (r10-rem-system)

This commit is contained in:
2026-08-09 04:11:31 +02:00
40 changed files with 174 additions and 143 deletions
+1 -1
View File
@@ -1 +1 @@
0.60.5
0.60.6
+17
View File
@@ -48,6 +48,23 @@ export default tseslint.config(
"error",
{ allowShortCircuit: true, allowTernary: true },
],
// R10 S2: keep the rem system from rotting. A px-frozen `text-[Npx]` font size does NOT scale
// with the Settings --font-scale slider while its rem neighbours do; use `text-2xs`/`text-3xs`
// or a `text-[Nrem]` arbitrary instead. Matches both plain strings and template-literal chunks;
// rem arbitraries (text-[0.75rem]) don't match (no "px"). Font-size `text-` only — not w-/h-/p-.
"no-restricted-syntax": [
"error",
{
selector: "Literal[value=/text-\\[\\d+px\\]/]",
message:
"px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx] (R10 S2).",
},
{
selector: "TemplateElement[value.raw=/text-\\[\\d+px\\]/]",
message:
"px-frozen font size doesn't scale with --font-scale: use text-2xs/text-3xs or text-[Nrem], not text-[Npx] (R10 S2).",
},
],
},
},
prettier,
@@ -21,7 +21,7 @@ export default function ActiveFilterChips({
className="flex flex-wrap items-center gap-1.5 pb-3"
aria-label={t("feed.chips.label")}
>
<span className="text-[11px] uppercase tracking-wider text-muted mr-0.5">
<span className="text-2xs uppercase tracking-wider text-muted mr-0.5">
{t("feed.chips.label")}
</span>
{filters.map((f) => (
+4 -6
View File
@@ -61,9 +61,7 @@ function Badge({
? "border-warn/40 text-warn"
: "border-border text-muted";
return (
<span className={`shrink-0 text-[11px] px-2 py-0.5 rounded-full border ${cls}`}>
{children}
</span>
<span className={`shrink-0 text-2xs px-2 py-0.5 rounded-full border ${cls}`}>{children}</span>
);
}
@@ -180,7 +178,7 @@ function UsersRoles({ me }: { me: Me }) {
{u.display_name ?? u.email.split("@")[0]}
{self && <span className="text-muted"> · {t("users.roles.you")}</span>}
</div>
<div className="text-[11px] text-muted truncate">{u.email}</div>
<div className="text-2xs text-muted truncate">{u.email}</div>
</div>
{u.role === "admin" && <Badge tone="accent">{t("users.roles.admin")}</Badge>}
{u.is_demo && <Badge tone="muted">{t("users.roles.demo")}</Badge>}
@@ -436,7 +434,7 @@ function AdminDemo() {
<div className="min-w-0 flex-1">
<div className="text-sm truncate">{r.email}</div>
{r.added_by && (
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{t("settings.demo.addedBy", { who: r.added_by })}
</div>
)}
@@ -521,7 +519,7 @@ function InviteRow({
<div className="min-w-0 flex-1">
<div className="text-sm truncate">{inv.email}</div>
{inv.requested_at && (
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("settings.invites.requested", {
time: new Date(inv.requested_at).toLocaleString(),
})}
+1 -1
View File
@@ -84,7 +84,7 @@ export default function AuditLog() {
emptyText={t("audit.empty")}
/>
{truncated && (
<p className="text-[11px] text-muted mt-2">
<p className="text-2xs text-muted mt-2">
{t("audit.truncated", { returned: data!.returned, total: data!.total })}
</p>
)}
+1 -1
View File
@@ -71,7 +71,7 @@ export default function ChannelPicker({
random to anyone who hasn't set a priority. Only while unfiltered: a search reorders nothing
but does drop the rows that explain it. */}
{!q && all.length > 0 && (
<div className="px-2 pt-1.5 text-[11px] text-muted">{t("sidebar.channelOrder")}</div>
<div className="px-2 pt-1.5 text-2xs text-muted">{t("sidebar.channelOrder")}</div>
)}
<div
ref={fade.ref}
+3 -3
View File
@@ -23,7 +23,7 @@ interface TagChipProps {
function TagChip({ tag, onClick, title }: TagChipProps) {
const cls =
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent max-w-36 truncate";
"shrink-0 text-3xs px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent max-w-36 truncate";
return onClick ? (
<button
type="button"
@@ -91,7 +91,7 @@ function TagEditorMenu({
);
})}
{userTags.length === 0 && (
<p className="text-[11px] text-muted px-2 py-1.5">{t("channels.tags.noneYet")}</p>
<p className="text-2xs text-muted px-2 py-1.5">{t("channels.tags.noneYet")}</p>
)}
</div>
<div className="mt-1 pt-1.5 border-t border-border flex items-center gap-1">
@@ -238,7 +238,7 @@ export function ChannelTagsControl({
}, [hidden, overflowOpen]);
const pillClass =
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";
"shrink-0 text-3xs px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";
return (
// When `bounded`, a FIXED width keeps the cell's footprint constant under `table-layout:auto`: a
+2 -2
View File
@@ -748,7 +748,7 @@ export default function Channels() {
</span>
))}
</div>
<p className="text-[11px] text-muted mt-2">{t("channels.blocked.hint")}</p>
<p className="text-2xs text-muted mt-2">{t("channels.blocked.hint")}</p>
</>
)}
</div>
@@ -814,7 +814,7 @@ function SyncBadge({ ok, label, hint }: { ok: boolean; label: string; hint?: str
return (
<Tooltip hint={hint ?? ""}>
<span
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full border ${
className={`inline-flex items-center gap-1 text-3xs px-1.5 py-0.5 rounded-full border ${
ok ? "border-accent/40 text-accent" : "border-border text-muted"
}`}
>
+1 -1
View File
@@ -183,7 +183,7 @@ export default function ChatThread({
}`}
>
{text}
<div className={`text-[10px] mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
<div className={`text-3xs mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
{relativeTime(m.created_at)}
</div>
</div>
+7 -9
View File
@@ -230,9 +230,7 @@ export default function ConfigPanel() {
{t("config.plexTest")}
</button>
</Tooltip>
{dirty && (
<p className="text-[11px] text-muted mt-2">{t("config.plexTestDirty")}</p>
)}
{dirty && <p className="text-2xs text-muted mt-2">{t("config.plexTestDirty")}</p>}
{plexResult && (
<div className="mt-3">
<p className="text-xs text-muted mb-2">
@@ -243,15 +241,15 @@ export default function ConfigPanel() {
</p>
{plexResult.media_check?.checked &&
(plexResult.media_check.ok ? (
<p className="text-[11px] text-success mb-2">{t("config.plexMediaOk")}</p>
<p className="text-2xs text-success mb-2">{t("config.plexMediaOk")}</p>
) : (
<p className="text-[11px] text-danger mb-2">
<p className="text-2xs text-danger mb-2">
{t("config.plexMediaMissing", {
path: plexResult.media_check.local_path ?? "",
})}
</p>
))}
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<p className="text-2xs text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<div className="flex flex-col gap-1.5">
{plexResult.sections.map((s) => (
<label
@@ -265,14 +263,14 @@ export default function ConfigPanel() {
className="accent-accent"
/>
<span>{s.title}</span>
<span className="text-[11px] text-muted">
<span className="text-2xs text-muted">
{s.type === "movie" ? t("config.plexMovies") : t("config.plexShows")}
{s.count != null ? ` · ${s.count}` : ""}
</span>
</label>
))}
</div>
<p className="text-[11px] text-muted mt-2">{t("config.plexPickHint")}</p>
<p className="text-2xs text-muted mt-2">{t("config.plexPickHint")}</p>
</div>
)}
</div>
@@ -348,7 +346,7 @@ function Field({
<p className="text-xs text-muted leading-relaxed mt-0.5">
{t(`config.fields.${item.key}.hint`, "")}
</p>
<p className="text-[11px] text-muted mt-1">{status}</p>
<p className="text-2xs text-muted mt-1">{status}</p>
</div>
<div className="flex flex-col items-end gap-1.5 shrink-0">
+2 -2
View File
@@ -227,7 +227,7 @@ function DownloadRow({
<div className="min-w-0 flex-1">
<div className="font-medium leading-snug line-clamp-1 flex items-center gap-1.5">
{job.job_kind === "edit" && (
<span className="inline-flex items-center gap-1 shrink-0 px-1.5 py-0.5 rounded-sm text-[10px] font-semibold uppercase tracking-wide bg-accent/15 text-accent">
<span className="inline-flex items-center gap-1 shrink-0 px-1.5 py-0.5 rounded-sm text-3xs font-semibold uppercase tracking-wide bg-accent/15 text-accent">
<Scissors className="w-3 h-3" /> {t("downloads.clipBadge")}
</span>
)}
@@ -280,7 +280,7 @@ function DownloadRow({
style={{ width: `${job.progress}%` }}
/>
</div>
<div className="text-[11px] text-muted mt-0.5 flex gap-2">
<div className="text-2xs text-muted mt-0.5 flex gap-2">
<span>{job.progress}%</span>
{job.speed_bps ? <span>{formatSpeed(job.speed_bps)}</span> : null}
{job.eta_s ? <span>{formatEta(job.eta_s)}</span> : null}
+7 -7
View File
@@ -288,7 +288,7 @@ export default function GlassTuner() {
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
<span className="text-[10px] uppercase tracking-wider text-muted">lab</span>
<span className="text-3xs uppercase tracking-wider text-muted">lab</span>
<button
onClick={() => persistOpen(false)}
className="ml-auto text-muted hover:text-fg text-lg leading-none px-1"
@@ -301,7 +301,7 @@ export default function GlassTuner() {
<div className="overflow-y-auto px-3 py-3 flex flex-col gap-4 text-sm">
{/* Treatment presets */}
<div className="flex flex-col gap-1.5">
<div className="text-[10px] uppercase tracking-wider text-muted font-semibold">
<div className="text-3xs uppercase tracking-wider text-muted font-semibold">
Dark treatment
</div>
<div className="grid grid-cols-2 gap-1.5">
@@ -313,7 +313,7 @@ export default function GlassTuner() {
className="glass-card glass-hover rounded-lg px-2 py-1.5 text-left text-xs text-fg"
>
<b className="block font-semibold">{p.name}</b>
<span className="text-muted text-[10px]">{p.hint}</span>
<span className="text-muted text-3xs">{p.hint}</span>
</button>
))}
</div>
@@ -351,7 +351,7 @@ export default function GlassTuner() {
{tab === "palette" && (
<div className="flex flex-col gap-2">
<p className="text-[10px] text-muted leading-relaxed">
<p className="text-3xs text-muted leading-relaxed">
Overrides the live scheme (inline on &lt;html&gt;). Reset clears these back to
the scheme.
</p>
@@ -364,7 +364,7 @@ export default function GlassTuner() {
className="w-8 h-6 rounded-sm border border-border bg-transparent cursor-pointer"
/>
<span className="flex-1">{p.label}</span>
<code className="text-[10px] text-muted">{swatches[p.k]}</code>
<code className="text-3xs text-muted">{swatches[p.k]}</code>
</label>
))}
</div>
@@ -372,7 +372,7 @@ export default function GlassTuner() {
{/* Export */}
<div className="flex flex-col gap-1.5 pt-1 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider text-muted font-semibold">
<div className="text-3xs uppercase tracking-wider text-muted font-semibold">
Export → bake in
</div>
<button
@@ -419,7 +419,7 @@ function Ctl({ s, value, onChange }: { s: Slider; value: number; onChange: (v: n
step={s.step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-16 text-right text-[11px] tabular-nums rounded-sm bg-card border border-border px-1.5 py-0.5 text-accent"
className="w-16 text-right text-2xs tabular-nums rounded-sm bg-card border border-border px-1.5 py-0.5 text-accent"
/>
<input
type="range"
+1 -1
View File
@@ -56,7 +56,7 @@ export default function LanguageSwitcher({
{rail ? (
/* Current-language badge (HU/EN) so the active language reads at a glance without
opening the menu. The ring matches the page background to detach it from the icon. */
<span className="absolute -bottom-1 -right-1 text-[8px] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
<span className="absolute -bottom-1 -right-1 text-[0.5rem] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
{current.code.toUpperCase()}
</span>
) : (
+2 -2
View File
@@ -165,7 +165,7 @@ function ConversationList({
>
{c.partner.name}
</span>
<span className="ml-auto text-[11px] text-muted shrink-0">
<span className="ml-auto text-2xs text-muted shrink-0">
{relativeTime(c.last_message.created_at)}
</span>
</div>
@@ -176,7 +176,7 @@ function ConversationList({
{previewText(c)}
</span>
{c.unread > 0 && (
<span className="ml-auto shrink-0 min-w-5 h-5 px-1.5 grid place-items-center rounded-full bg-accent text-accent-fg text-[11px] font-semibold">
<span className="ml-auto shrink-0 min-w-5 h-5 px-1.5 grid place-items-center rounded-full bg-accent text-accent-fg text-2xs font-semibold">
{c.unread}
</span>
)}
+12 -12
View File
@@ -198,12 +198,12 @@ export default function NavSidebar({
}`}
>
<span className="relative shrink-0">
<Icon className="w-[18px] h-[18px]" />
<Icon className="w-4.5 h-4.5" />
{/* Collapsed rail: a numeric badge centred on a circle at the icon corner; the ring
matches the row background so it reads cleanly whether the row is active or not. */}
{show && slim && (
<span
className={`absolute -top-2 -right-2 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
className={`absolute -top-2 -right-2 min-w-4 h-4 px-1 rounded-full text-3xs font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
active ? "ring-accent" : "ring-bg"
}`}
>
@@ -214,7 +214,7 @@ export default function NavSidebar({
{!slim && <span className="truncate">{label}</span>}
{show && !slim && (
<span
className={`ml-auto min-w-[18px] h-[18px] px-1.5 rounded-full text-[11px] font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
className={`ml-auto min-w-4.5 h-4.5 px-1.5 rounded-full text-2xs font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
>
{badge > 99 ? "99+" : badge}
</span>
@@ -226,14 +226,14 @@ export default function NavSidebar({
return (
// Layout slot: reserves only the slim footprint when unpinned, so the expanded rail (below)
// overlays content instead of pushing it. Pinned reserves the full width (in-flow).
<div className={`relative shrink-0 transition-[width] ${pinned ? "w-[232px]" : "w-20"}`}>
<div className={`relative shrink-0 transition-[width] ${pinned ? "w-58" : "w-20"}`}>
<nav
// Peek handlers sit on the CARD, not the slot: the slot is wider than the card by its
// margin, and a hover zone out there would fire from the dead space next to the rail —
// leaving the side panel to the left would pop the rail open with nothing under the cursor.
{...peek.handlers}
className={`glass absolute top-0 bottom-0 left-0 z-rail rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
slim ? "w-[56px] px-2" : "w-52 px-3"
slim ? "w-14 px-2" : "w-52 px-3"
}`}
aria-label={t("nav.primary")}
>
@@ -270,7 +270,7 @@ export default function NavSidebar({
<div className="my-2 mx-auto w-7 border-t-2 border-border" />
) : (
<div className="mt-4 mb-1 px-2.5 flex items-center gap-2">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted/70">
<span className="text-3xs font-semibold uppercase tracking-wider text-muted/70">
{t("nav.adminSection")}
</span>
<span className="flex-1 border-t border-border" />
@@ -314,7 +314,7 @@ export default function NavSidebar({
: "text-muted hover:text-fg hover:bg-card"
}`}
>
<Settings className="w-[18px] h-[18px] shrink-0" />
<Settings className="w-4.5 h-4.5 shrink-0" />
{!slim && <span className="truncate">{t("header.account.settings")}</span>}
</button>
@@ -329,7 +329,7 @@ export default function NavSidebar({
<AvatarImg
src={me.avatar_url}
fallback={name}
className="w-[22px] h-[22px] rounded-full text-[10px]"
className="w-5.5 h-5.5 rounded-full text-3xs"
/>
</span>
{!slim && (
@@ -356,7 +356,7 @@ export default function NavSidebar({
</div>
</div>
{me.role === "admin" && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-medium text-accent">
<div className="flex items-center gap-1.5 mt-2 text-2xs font-medium text-accent">
<Shield className="w-3.5 h-3.5" />
{t("header.account.admin")}
</div>
@@ -373,13 +373,13 @@ export default function NavSidebar({
<AvatarImg
src={a.avatar_url}
fallback={a.display_name ?? a.email}
className="w-6 h-6 rounded-full text-[10px] shrink-0"
className="w-6 h-6 rounded-full text-3xs shrink-0"
/>
<span className="min-w-0 text-left">
<span className="block text-[13px] text-fg truncate">
<span className="block text-[0.8125rem] text-fg truncate">
{a.display_name ?? a.email.split("@")[0]}
</span>
<span className="block text-[11px] text-muted truncate">{a.email}</span>
<span className="block text-2xs text-muted truncate">{a.email}</span>
</span>
</button>
))}
@@ -263,7 +263,7 @@ function NotificationRow({
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-medium text-sm">{title}</span>
<span className="text-[11px] text-muted shrink-0">{relativeTime(n.created_at)}</span>
<span className="text-2xs text-muted shrink-0">{relativeTime(n.created_at)}</span>
</div>
{body && <div className="text-sm text-muted mt-0.5">{body}</div>}
{videos.length > 0 && (
@@ -348,15 +348,15 @@ function ClientActivityRow({
<div className="text-sm wrap-break-word">
{n.message}
{n.repeat > 1 && (
<span className="ml-1.5 align-middle text-[11px] font-semibold text-muted tabular-nums">
<span className="ml-1.5 align-middle text-2xs font-semibold text-muted tabular-nums">
×{n.repeat}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] text-muted">{relativeFromMs(n.ts)}</span>
<span className="text-2xs text-muted">{relativeFromMs(n.ts)}</span>
{needsAction && (
<span className="text-[10px] uppercase tracking-wide font-semibold text-accent">
<span className="text-3xs uppercase tracking-wide font-semibold text-accent">
{t("notifications.actionNeeded")}
</span>
)}
+2 -2
View File
@@ -1056,7 +1056,7 @@ export default function PlayerModal({
)}
{/* Discreet badge while YouTube's own controls have taken over (overlay yielded). */}
{playerError == null && nativeControls && (
<div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-xs">
<div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-2xs text-white/90 shadow-lg backdrop-blur-xs">
<Settings className="h-3.5 w-3.5" />
{t("player.nativeControls")}
</div>
@@ -1250,7 +1250,7 @@ export default function PlayerModal({
<span>· {formatDuration(active.duration_seconds)}</span>
)}
{active.live_status === "was_live" && (
<span className="bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
<span className="bg-accent text-accent-fg text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
{t("player.stream")}
</span>
)}
+3 -3
View File
@@ -573,7 +573,7 @@ export default function Playlists() {
<PageToolbar>
<div className="px-4 pt-3 pb-2">
<div className="flex gap-3 items-start mb-4">
<div className="w-24 h-[54px] rounded-lg bg-card overflow-hidden shrink-0">
<div className="w-24 h-13.5 rounded-lg bg-card overflow-hidden shrink-0">
{items[0]?.thumbnail_url && (
<img src={items[0].thumbnail_url} alt="" className="w-full h-full object-cover" />
)}
@@ -681,7 +681,7 @@ export default function Playlists() {
{linked && detail.dirty && (
<span
title={t("playlists.unsyncedHint")}
className="inline-flex items-center gap-1.5 text-[11px] text-accent ml-1"
className="inline-flex items-center gap-1.5 text-2xs text-accent ml-1"
>
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
{t("playlists.unsynced")}
@@ -724,7 +724,7 @@ export default function Playlists() {
stored order ALREADY matches the sort produced a warning, no Save button and no
visible change, which reads as a malfunction. Now the line says which of the three
situations you are in; the disabled grip still explains itself on hover/focus. */}
{viewActive && <span className="text-[11px] text-muted/80">{viewStatus}</span>}
{viewActive && <span className="text-2xs text-muted/80">{viewStatus}</span>}
{canEdit && (
<div className="ml-auto">
<UndoToolbar
+3 -3
View File
@@ -154,7 +154,7 @@ export default function PlaylistsRail({
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-card border border-border rounded-md px-1.5 py-1 text-[11px] outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-card border border-border rounded-md px-1.5 py-1 text-2xs outline-hidden focus:border-accent"
>
<option value="custom">{t("playlists.railSortCustom")}</option>
<option value="name">{t("playlists.railSortName")}</option>
@@ -246,7 +246,7 @@ export default function PlaylistsRail({
)}
</div>
<div className="min-w-0">
<div className="flex items-center gap-1 text-[13px] text-fg">
<div className="flex items-center gap-1 text-[0.8125rem] text-fg">
<span className="truncate">{plName(pl)}</span>
{(pl.source === "youtube" || pl.yt_playlist_id) && (
<Youtube
@@ -254,7 +254,7 @@ export default function PlaylistsRail({
/>
)}
</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("playlists.itemCount", { count: pl.item_count })}
{pl.total_duration_seconds > 0 &&
` · ${formatDuration(pl.total_duration_seconds)}`}
+13 -13
View File
@@ -342,7 +342,7 @@ export default function PlexBrowse() {
)}
<div className="min-w-0">
<div className="text-sm font-medium truncate max-w-[150px]">{p.name}</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("plex.people.count", { count: p.count })}
</div>
</div>
@@ -525,18 +525,18 @@ function PlexPosterCard({
)}
{/* Watched badge when not hovering (the toggle replaces it on hover). */}
{card.status === "watched" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{card.type === "show" && card.season_count != null && (
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 left-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.seasons", { count: card.season_count })}
</span>
)}
{/* Aggregate in-progress badge for a partially-watched show. */}
{card.type === "show" && card.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
@@ -679,7 +679,7 @@ function ActionBtn({
<Icon className="w-4 h-4 shrink-0" />
<span className="flex flex-col items-start leading-tight">
<span>{label}</span>
{sub && <span className="text-[11px] font-normal opacity-70">{sub}</span>}
{sub && <span className="text-2xs font-normal opacity-70">{sub}</span>}
</span>
</button>
);
@@ -753,16 +753,16 @@ function SeasonCard({
</button>
)}
{watched && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{se.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 left-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.series.episodeCount", { count: se.episode_count })}
</span>
</div>
@@ -811,7 +811,7 @@ function CastStrip({
>
{c.name}
</div>
{c.role && <div className="text-[11px] text-muted line-clamp-1">{c.role}</div>}
{c.role && <div className="text-2xs text-muted line-clamp-1">{c.role}</div>}
</>
);
return onFilter ? (
@@ -857,7 +857,7 @@ function RelatedStrip({ related, onOpen }: { related: PlexCard[]; onOpen: (id: s
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/>
{r.status === "watched" && (
<span className="absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.watched")}
</span>
)}
@@ -948,7 +948,7 @@ function EpisodeCard({
>
<div className="min-w-0 flex-1">
{withShowTitle && ep.show_title && (
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{ep.show_title}
{ep.season_number != null && ep.episode_number != null
? ` · S${ep.season_number}·E${ep.episode_number}`
@@ -959,7 +959,7 @@ function EpisodeCard({
{!withShowTitle && <span className="text-muted mr-1">{ep.episode_number}.</span>}
{ep.title}
</div>
<div className="text-[11px] text-muted mt-0.5">{formatRuntime(ep.duration_seconds)}</div>
<div className="text-2xs text-muted mt-0.5">{formatRuntime(ep.duration_seconds)}</div>
</div>
{onAdd && (
<button
@@ -1113,7 +1113,7 @@ function PlexShowView({
{show.genres.map((g) => (
<Filterable
key={g}
className="text-[11px] rounded-full bg-surface px-2 py-0.5 text-muted"
className="text-2xs rounded-full bg-surface px-2 py-0.5 text-muted"
onClick={onFilter ? () => onFilter({ genres: [g] }) : undefined}
>
{g}
+3 -3
View File
@@ -360,7 +360,7 @@ export default function PlexInfo({
</div>
{c.role && (
<div
className={`truncate text-[11px] ${overlay ? "text-white/55" : "text-muted"}`}
className={`truncate text-2xs ${overlay ? "text-white/55" : "text-muted"}`}
>
{c.role}
</div>
@@ -420,7 +420,7 @@ export default function PlexInfo({
className="h-full w-full object-cover transition group-hover/col:scale-105"
/>
{m.status === "watched" && (
<span className="absolute right-1 top-1 rounded-sm bg-black/70 px-1 text-[9px] text-white">
<span className="absolute right-1 top-1 rounded-sm bg-black/70 px-1 text-[0.5625rem] text-white">
✓
</span>
)}
@@ -447,7 +447,7 @@ export default function PlexInfo({
>
{m.title}
</div>
{m.year ? <div className="text-[11px] text-muted">{m.year}</div> : null}
{m.year ? <div className="text-2xs text-muted">{m.year}</div> : null}
</button>
))}
</div>
+13 -15
View File
@@ -1221,7 +1221,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{/* Hover timestamp tooltip — follows the cursor along the bar. */}
{hover && duration > 0 && (
<div
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white"
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-2xs tabular-nums text-white"
style={{ left: `${hover.frac * 100}%` }}
>
{fmt(hover.t)}
@@ -1317,7 +1317,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{/* hover value tooltip (0–100), like the seek bar */}
{volHover != null && (
<div
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white"
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-2xs tabular-nums text-white"
style={{ left: `${volHover * 100}%` }}
>
{Math.round(volHover * 100)}
@@ -1431,7 +1431,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
<button
key={tab.key}
onClick={() => setSettingsTab(tab.key)}
className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-[11px] ${
className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-2xs ${
activeTab === tab.key
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
@@ -1537,7 +1537,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ subPos: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subColor")}
</span>
<input
@@ -1548,11 +1548,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
</div>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subBg")}
</span>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 text-[11px] text-white/70">
<label className="flex items-center gap-1 text-2xs text-white/70">
<input
type="checkbox"
checked={prefs.subBg === "transparent"}
@@ -1586,7 +1586,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
{prefs.subShadow > 0 && (
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subShadowColor")}
</span>
<input
@@ -1622,7 +1622,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ clockSize: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.clockColor")}
</span>
<input
@@ -1643,7 +1643,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
<button
key={st}
onClick={() => patchPrefs({ clockDateStyle: st })}
className={`flex-1 rounded px-2 py-1 text-[11px] ${
className={`flex-1 rounded px-2 py-1 text-2xs ${
prefs.clockDateStyle === st
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
@@ -1776,9 +1776,7 @@ function BackClose({ onBack }: { onBack: () => void }) {
// --- settings-panel building blocks ------------------------------------------------------------
function PanelHead({ children }: { children: ReactNode }) {
return (
<div className="px-1 pt-2 pb-1 text-[11px] uppercase tracking-wide text-white/50">
{children}
</div>
<div className="px-1 pt-2 pb-1 text-2xs uppercase tracking-wide text-white/50">{children}</div>
);
}
function Slider({
@@ -1802,7 +1800,7 @@ function Slider({
}) {
return (
<div className="px-1 py-1">
<div className="mb-1 flex items-center justify-between text-[12px] text-white/80">
<div className="mb-1 flex items-center justify-between text-[0.75rem] text-white/80">
<span>{label}</span>
<span className="flex items-center gap-1.5">
<span className="tabular-nums text-white/60">
@@ -1839,7 +1837,7 @@ function Toggle({
}) {
return (
<label className="flex cursor-pointer items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{label}</span>
<span className="text-[0.75rem] text-white/80">{label}</span>
<input
type="checkbox"
checked={checked}
@@ -1879,7 +1877,7 @@ function Ctrl({
{children}
</button>
<span
className={`pointer-events-none absolute bottom-full ${pos} mb-2 whitespace-nowrap rounded-sm bg-black/90 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover/ctrl:opacity-100`}
className={`pointer-events-none absolute bottom-full ${pos} mb-2 whitespace-nowrap rounded-sm bg-black/90 px-2 py-1 text-2xs text-white opacity-0 transition group-hover/ctrl:opacity-100`}
>
{label}
</span>
+5 -5
View File
@@ -153,7 +153,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
className={`flex items-center gap-2.5 rounded-lg ${dense ? "px-1.5 py-1" : "glass-card p-1.5"}`}
>
<DragHandle {...attributes} {...listeners} />
<span className="w-5 shrink-0 text-center text-[11px] text-muted tabular-nums">
<span className="w-5 shrink-0 text-center text-2xs text-muted tabular-nums">
{ep.episode_number}
</span>
<button
@@ -165,7 +165,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
>
<img src={ep.thumb} alt="" loading="lazy" className="h-full w-full object-cover" />
{ep.status === "watched" && (
<span className="absolute right-0.5 top-0.5 rounded-sm bg-black/70 px-1 text-[9px] text-white">
<span className="absolute right-0.5 top-0.5 rounded-sm bg-black/70 px-1 text-[0.5625rem] text-white">
✓
</span>
)}
@@ -174,7 +174,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
<Play className="h-4 w-4 text-white" fill="currentColor" />
</div>
</div>
<div className="min-w-0 truncate text-[13px]">{ep.title}</div>
<div className="min-w-0 truncate text-[0.8125rem]">{ep.title}</div>
</button>
<button
onClick={() => ctx.onRemove([ep.id])}
@@ -277,7 +277,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
)}
<button onClick={() => ctx.toggleShow(block.showKey)} className="min-w-0 flex-1 text-left">
<div className="truncate text-sm font-semibold">{block.title}</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("plex.playlist.episodes", { count: block.items.length })}
</div>
</button>
@@ -316,7 +316,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
)}
</button>
)}
<span className="text-[11px] font-medium uppercase tracking-wide text-muted">
<span className="text-2xs font-medium uppercase tracking-wide text-muted">
{seasonLabel(s)} · {s.items.length}
</span>
<button
+2 -2
View File
@@ -46,11 +46,11 @@ function MatchToggle({
const { t } = useTranslation();
return (
<div className="mb-1.5 flex items-center justify-between">
<span className="text-[10px] uppercase tracking-wide text-muted">
<span className="text-3xs uppercase tracking-wide text-muted">
{t("plex.filter.matchLabel")}
</span>
<div
className="flex items-center rounded-full border border-border bg-card p-0.5 text-[10px]"
className="flex items-center rounded-full border border-border bg-card p-0.5 text-3xs"
role="group"
aria-label={t("plex.filter.matchLabel")}
>
+1 -1
View File
@@ -38,7 +38,7 @@ export default function ReleaseNotes({
<h3 className="text-base font-semibold">v{r.version}</h3>
<span className="text-xs text-muted">{fmtDate(r.date)}</span>
{r.sha && (
<span className="text-[11px] font-mono text-muted bg-surface px-1.5 py-0.5 rounded-sm">
<span className="text-2xs font-mono text-muted bg-surface px-1.5 py-0.5 rounded-sm">
{r.sha}
</span>
)}
+1 -1
View File
@@ -135,7 +135,7 @@ export default function SavedViewsWidget({
return (
<div className="space-y-2">
{views.length === 0 && !naming && (
<p className="text-[11px] text-muted leading-snug">{t("views.empty")}</p>
<p className="text-2xs text-muted leading-snug">{t("views.empty")}</p>
)}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
+8 -8
View File
@@ -75,7 +75,7 @@ function StatusDot({
function StatusLegend() {
const { t } = useTranslation();
return (
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap text-[11px] text-muted mb-2">
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap text-2xs text-muted mb-2">
{(["running", "ok", "idle", "error", "skipped"] as const).map((k) => (
<span key={k} className="inline-flex items-center gap-1.5">
<StatusDot k={k} withTooltip={false} />
@@ -97,7 +97,7 @@ function JobProgress({ p }: { p: SchedulerJob["progress"] }) {
p?.total && p.total > 0 ? Math.min(100, Math.round((p.current / p.total) * 100)) : null;
return (
<div className="mt-1.5">
<div className="flex items-center justify-between text-[11px] text-muted mb-0.5">
<div className="flex items-center justify-between text-2xs text-muted mb-0.5">
<span className="truncate">{phase}</span>
<span className="tabular-nums shrink-0">
{p?.total != null
@@ -203,7 +203,7 @@ function JobRow({
</Tooltip>
)}
</div>
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{job.running
? t("scheduler.runningNow")
: job.last_finished
@@ -217,12 +217,12 @@ function JobRow({
</div>
{job.running && <JobProgress p={job.progress} />}
</div>
<div className="shrink-0 text-right text-[11px] text-muted tabular-nums">
<div className="shrink-0 text-right text-2xs text-muted tabular-nums">
{job.running ? (
<span className="text-accent">{t("scheduler.active")}</span>
) : next != null ? (
<>
<div className="uppercase tracking-wide text-[10px]">{t("scheduler.nextRun")}</div>
<div className="uppercase tracking-wide text-3xs">{t("scheduler.nextRun")}</div>
<div>{fmtCountdown(next)}</div>
</>
) : (
@@ -291,7 +291,7 @@ function MaintenanceSettings({
{t("common.save")}
</button>
</div>
<div className="text-[11px] text-muted mt-1.5">
<div className="text-2xs text-muted mt-1.5">
{t("scheduler.maintenance.batchNote", {
default: m.revalidate_batch_default.toLocaleString(),
})}
@@ -330,7 +330,7 @@ function Stat({
</Tooltip>
</div>
<div className="text-xl font-semibold mt-1 tabular-nums">{value}</div>
{sub && <div className="text-[11px] text-muted">{sub}</div>}
{sub && <div className="text-2xs text-muted">{sub}</div>}
</div>
);
}
@@ -595,7 +595,7 @@ export default function Scheduler() {
style={{ width: `${quotaPct}%` }}
/>
</div>
<div className="text-[11px] text-muted mt-1.5">{t("scheduler.quotaNote")}</div>
<div className="text-2xs text-muted mt-1.5">{t("scheduler.quotaNote")}</div>
</div>
</div>
+2 -2
View File
@@ -323,7 +323,7 @@ function AccessRow({
</p>
</div>
{granted ? (
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
<span className="shrink-0 text-2xs px-2 py-1 rounded-full border border-accent/40 text-accent">
{t("settings.account.granted")}
</span>
) : (
@@ -422,7 +422,7 @@ function SignInMethods({ me }: { me: Me }) {
</p>
</div>
{me.has_google ? (
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
<span className="shrink-0 text-2xs px-2 py-1 rounded-full border border-accent/40 text-accent">
{t("settings.account.googleLink.connected")}
</span>
) : (
+5 -7
View File
@@ -47,9 +47,7 @@ export default function SidePanel({
// content instead of pushing it. Pinned reserves the full width (in-flow), as before.
<div
{...peek.handlers}
className={`hidden md:block relative shrink-0 transition-[width] ${
pinned ? "w-[268px]" : "w-9"
}`}
className={`hidden md:block relative shrink-0 transition-[width] ${pinned ? "w-67" : "w-9"}`}
>
{/* The tab: a small handle flush against the nav rail, flat on the docked edge. It stays
mounted while the card is open — swapping it out from under the pointer would drop the very
@@ -66,12 +64,12 @@ export default function SidePanel({
>
<span className="text-accent">{icon}</span>
{count > 0 && (
<span className="bg-accent text-accent-fg text-[10px] font-bold px-1.5 py-0.5 rounded-full tabular-nums">
<span className="bg-accent text-accent-fg text-3xs font-bold px-1.5 py-0.5 rounded-full tabular-nums">
{count > 9 ? "9+" : count}
</span>
)}
<span
className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted"
className="text-2xs font-semibold uppercase tracking-[0.18em] text-muted"
style={{ writingMode: "vertical-rl", transform: "rotate(180deg)" }}
>
{title}
@@ -98,7 +96,7 @@ export default function SidePanel({
{count > 0 && (
<span
title={t("sidebar.activeCount", { count })}
className="shrink-0 min-w-[18px] h-[18px] px-1.5 rounded-full bg-accent/15 text-accent text-[11px] font-semibold inline-flex items-center justify-center tabular-nums"
className="shrink-0 min-w-4.5 h-4.5 px-1.5 rounded-full bg-accent/15 text-accent text-2xs font-semibold inline-flex items-center justify-center tabular-nums"
>
{count}
</span>
@@ -138,7 +136,7 @@ export default function SidePanel({
className="flex-1 min-h-0 overflow-y-auto no-scrollbar"
>
<div className="p-3 space-y-3">
{editing && editHint && <div className="text-[11px] text-muted">{editHint}</div>}
{editing && editHint && <div className="text-2xs text-muted">{editHint}</div>}
{children}
</div>
</div>
+7 -7
View File
@@ -54,7 +54,7 @@ function TagChip({
>
<span>{tag.name}</span>
<span
className={`tabular-nums text-[10px] leading-none px-1 py-0.5 rounded-full ${
className={`tabular-nums text-3xs leading-none px-1 py-0.5 rounded-full ${
active ? "bg-accent-fg/20 text-accent-fg" : "bg-muted/15 text-muted"
}`}
>
@@ -232,7 +232,7 @@ export default function Sidebar({
onClick={() =>
setFilters({ ...filters, dateFrom: undefined, dateTo: undefined })
}
className="text-[11px] text-muted hover:text-accent self-end"
className="text-2xs text-muted hover:text-accent self-end"
>
{t("sidebar.clearDates")}
</button>
@@ -255,7 +255,7 @@ export default function Sidebar({
/>
))}
{facetsReady && chips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
</div>
);
@@ -268,11 +268,11 @@ export default function Sidebar({
className="flex items-center justify-between mb-1.5"
title={t("sidebar.tagModeTooltip")}
>
<span className="text-[10px] uppercase tracking-wide text-muted">
<span className="text-3xs uppercase tracking-wide text-muted">
{t("sidebar.match")}
</span>
<div
className="flex items-center rounded-full border border-border bg-card p-0.5 text-[10px]"
className="flex items-center rounded-full border border-border bg-card p-0.5 text-3xs"
role="group"
aria-label={t("sidebar.match")}
>
@@ -303,7 +303,7 @@ export default function Sidebar({
/>
))}
{facetsReady && chips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
</div>
</>
@@ -323,7 +323,7 @@ export default function Sidebar({
/>
))}
{facetsReady && tagChips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
<button
onClick={() => setTagManagerOpen(true)}
+1 -1
View File
@@ -40,7 +40,7 @@ export default function Tabs({
{tb.label}
{tb.badge != null && tb.badge > 0 && (
<span
className={`text-[11px] leading-none px-1.5 py-0.5 rounded-full ${
className={`text-2xs leading-none px-1.5 py-0.5 rounded-full ${
on ? "bg-accent-fg/20" : "bg-accent/20 text-accent"
}`}
>
+1 -1
View File
@@ -76,7 +76,7 @@ function TagRow({
popover={pop}
className="glass-menu w-56 rounded-xl p-1.5 animate-[popIn_0.16s_ease]"
>
<div className="text-[11px] uppercase tracking-wide text-muted px-2 py-1">
<div className="text-2xs uppercase tracking-wide text-muted px-2 py-1">
{t("tagManager.onChannels")}
</div>
{/* Inner scroller so the scroll-fade mask rides the scrolling box, not the glass panel. */}
+1 -1
View File
@@ -41,7 +41,7 @@ export default function Toaster() {
<div className="text-sm wrap-break-word">
{toast.message}
{toast.repeat > 1 && (
<span className="ml-1.5 align-middle text-[11px] font-semibold text-muted tabular-nums">
<span className="ml-1.5 align-middle text-2xs font-semibold text-muted tabular-nums">
×{toast.repeat}
</span>
)}
+3 -3
View File
@@ -245,12 +245,12 @@ function Thumb({
<div className="w-full h-full" />
)}
{video.duration_seconds != null ? (
<span className="absolute bottom-1.5 right-1.5 bg-black/80 text-white text-[11px] font-medium px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 right-1.5 bg-black/80 text-white text-2xs font-medium px-1.5 py-0.5 rounded-sm">
{formatDuration(video.duration_seconds)}
</span>
) : video.live_status === "live" || video.live_status === "upcoming" ? (
<span
className={`absolute bottom-1.5 right-1.5 text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
className={`absolute bottom-1.5 right-1.5 text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
video.live_status === "live" ? "bg-red-500/90 text-white" : "bg-black/80 text-white"
}`}
>
@@ -258,7 +258,7 @@ function Thumb({
</span>
) : null}
{video.live_status === "was_live" && (
<span className="absolute top-1.5 left-1.5 bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
<span className="absolute top-1.5 left-1.5 bg-accent text-accent-fg text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
{t("card.stream")}
</span>
)}
+1 -1
View File
@@ -487,7 +487,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))),
}}
/>
<div className="text-[10px] text-center text-muted py-0.5 tabular-nums">
<div className="text-3xs text-center text-muted py-0.5 tabular-nums">
{tc(hoverTime)}
</div>
</div>
+2 -2
View File
@@ -554,7 +554,7 @@ function AuthCard() {
>
{t("welcome.auth.resendLink")}
</button>
<div className="flex items-center gap-3 my-4 text-[11px] uppercase tracking-wide text-muted">
<div className="flex items-center gap-3 my-4 text-2xs uppercase tracking-wide text-muted">
<span className="h-px flex-1 bg-border" />
{t("welcome.auth.or")}
<span className="h-px flex-1 bg-border" />
@@ -576,7 +576,7 @@ function AuthCard() {
</button>
) : (
<form onSubmit={onDemo} className="mt-2 flex flex-col gap-2">
<p className="text-[11px] text-muted">{t("welcome.auth.demoHint")}</p>
<p className="text-2xs text-muted">{t("welcome.auth.demoHint")}</p>
<div className="flex gap-2">
<input
type="email"
+1 -1
View File
@@ -86,7 +86,7 @@ export function auditColumns(
// card (the row rules bled into the page). Wrapping keeps every row inside the container.
<div className="min-w-0">
{r.summary && <div className="wrap-break-word">{r.summary}</div>}
{change && <div className="text-[11px] text-muted wrap-break-word">{change}</div>}
{change && <div className="text-2xs text-muted wrap-break-word">{change}</div>}
</div>
);
},
+4 -4
View File
@@ -69,7 +69,7 @@ export function playlistColumns({
</span>
</button>
<button onClick={() => onPlay(v)} className="min-w-0 flex-1 text-left">
<span className="block text-[13px] text-fg truncate">{v.title}</span>
<span className="block text-[0.8125rem] text-fg truncate">{v.title}</span>
</button>
</div>
),
@@ -83,7 +83,7 @@ export function playlistColumns({
sortValue: (v) => v.channel_title ?? "",
filter: { kind: "text", get: (v) => v.channel_title ?? "" },
render: (v) => (
<span className="text-[13px] text-muted truncate block">{v.channel_title}</span>
<span className="text-[0.8125rem] text-muted truncate block">{v.channel_title}</span>
),
},
{
@@ -99,12 +99,12 @@ export function playlistColumns({
sortValue: (v) => v.duration_seconds ?? Number.MAX_SAFE_INTEGER,
render: (v) =>
v.duration_seconds != null ? (
<span className="text-[11px] text-muted tabular-nums">
<span className="text-2xs text-muted tabular-nums">
{formatDuration(v.duration_seconds)}
</span>
) : v.live_status === "live" || v.live_status === "upcoming" ? (
<span
className={`text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
className={`text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
v.live_status === "live"
? "bg-red-500/90 text-white" /* fixed live-red, not the status token */
: "bg-card border border-border text-muted"
+18 -4
View File
@@ -43,6 +43,19 @@
--z-index-tuner: 9999;
}
/* Extra-small text steps in rem, so these labels scale with --font-scale like every other
text (the ~124 px-frozen text-[10/11px] labels did NOT). Font-size ONLY — no paired
--text-*--line-height — to match the `text-[Npx]` arbitraries they replace and avoid any
vertical-rhythm shift.
Convention: the two COMMON small steps get names (below); the rare one-offs (12/13/8/9px)
stay as `text-[Nrem]` arbitraries rather than growing the token set — DON'T use `text-xs`
for 12px, it carries a line-height these labels never had. New small labels: reach for a
token first, an arbitrary only for a genuinely one-off size. */
@theme {
--text-2xs: 0.6875rem; /* 11px @ base — the most common frozen label */
--text-3xs: 0.625rem; /* 10px @ base */
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
@@ -79,10 +92,11 @@
:root {
--font-scale: 1.06;
/* Vertical space the floating top header needs to clear (top offset 12px + pill height 44px +
bottom breathing room ~12px). Module content pads its top by this so nothing hides under the
header; kept in one place since App and Playlists both reference it. */
--hdr-h: 68px;
/* Vertical space the floating top header needs to clear (top offset ~0.75rem + pill height ~2.75rem
+ bottom breathing room ~0.75rem). In rem so it grows with --font-scale in lock-step with the
rem-sized header pills — a px value let content slide under the header at scale 1.3. Kept in one
place since App and Playlists both reference it. */
--hdr-h: 4.25rem;
/* ===== Glass tunables — every value that materially drives the look lives here so it can be
tuned in ONE place. ===== */
+8
View File
@@ -14,6 +14,14 @@ export interface ReleaseEntry {
}
export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.60.6",
date: "2026-08-09",
fixes: [
"The text-size setting now scales every label — small counts, badges and captions that used to stay a fixed size now grow and shrink with the slider like the rest of the text.",
"At larger text sizes the floating top bar no longer overlaps the content beneath it, and the side rails widen to keep their labels from being clipped.",
],
},
{
version: "0.60.5",
date: "2026-08-09",