feat(theme): R10 S2 — re-close the rem system (px-frozen labels/chrome -> rem)
The ~124 px-frozen text-[Npx] labels didn't track the Settings font-scale slider while their rem-sized neighbours did. Add text-2xs (0.6875rem) / text-3xs (0.625rem) font-size-ONLY tokens (no paired line-height -> matches the arbitrary they replace, zero vertical-rhythm shift) and codemod every text-[Npx] to a rem class (rare 8/9/12/13px -> rem arbitraries). Also: --hdr-h 68px -> 4.25rem so header content clears the rem-sized pills at scale 1.3 instead of sliding under; NavSidebar/SidePanel slot widths + their icon/badge px sizes go rem so the rails keep pace with their (now-scaling) labels; a w-24/h-[54px] thumb -> h-13.5 to preserve 16:9 at scale. Media thumbnails, page-width caps and fixed container widths deliberately stay px. Verified via getComputedStyle at scale 1.06/1.3: text-2xs 11.66->14.3px, header clearance 72->88px, nav slot 246->302px — all scale proportionally.
This commit is contained in:
@@ -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) => (
|
||||
|
||||
@@ -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(),
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 <html>). 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"
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}`}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
-4
@@ -43,6 +43,15 @@
|
||||
--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. */
|
||||
@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 +88,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. ===== */
|
||||
|
||||
Reference in New Issue
Block a user