release: 0.60.8 (r10-recipe-consolidation)

This commit is contained in:
2026-08-09 17:45:02 +02:00
34 changed files with 76 additions and 59 deletions
+1 -1
View File
@@ -1 +1 @@
0.60.7
0.60.8
+1 -1
View File
@@ -156,7 +156,7 @@ export default function AddToPlaylist({
}
}}
placeholder={t("playlists.newPlaylist")}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
</div>
</div>
+2 -2
View File
@@ -347,7 +347,7 @@ function AdminInvites() {
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder={t("settings.invites.addPlaceholder")}
className="flex-1 min-w-0 bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-xl px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
type="submit"
@@ -466,7 +466,7 @@ function AdminDemo() {
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder={t("settings.demo.addPlaceholder")}
className="flex-1 min-w-0 bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-xl px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
type="submit"
+2 -2
View File
@@ -8,12 +8,12 @@ const TONES: Record<BannerTone, { bar: string; icon: string; action: string }> =
accent: {
bar: "bg-accent/15 border-accent/30",
icon: "text-accent",
action: "bg-accent text-accent-fg",
action: "bg-accent text-accent-fg hover:opacity-90",
},
warning: {
bar: "bg-warn/15 border-warn/30",
icon: "text-warn",
action: "bg-warn text-warn-fg",
action: "bg-warn text-warn-fg hover:opacity-90",
},
};
+1 -1
View File
@@ -111,7 +111,7 @@ function TagEditorMenu({
}}
maxLength={64}
placeholder={t("channels.tags.createPlaceholder")}
className="flex-1 min-w-0 bg-transparent text-xs px-2 py-1 rounded-md border border-border focus:border-accent outline-hidden"
className="flex-1 min-w-0 bg-transparent text-xs px-2 py-1 rounded-md border border-border focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
type="button"
+1 -1
View File
@@ -211,7 +211,7 @@ export default function ChatThread({
}}
rows={1}
placeholder={t("messages.writePlaceholder")}
className="flex-1 resize-none max-h-28 bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent"
className="flex-1 resize-none max-h-28 bg-surface border border-border rounded-xl px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={submit}
@@ -40,7 +40,7 @@ export default function ColumnSortControl<T>({
onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null);
}}
aria-label={label}
className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-lg px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
<option value="">{defaultLabel}</option>
{sortable.map((c) => (
+1 -1
View File
@@ -366,7 +366,7 @@ function Field({
max={isNum && item.max != null ? item.max : undefined}
placeholder={item.secret ? "••••••••" : String(item.default ?? "")}
aria-label={t(`config.fields.${item.key}.label`, item.key)}
className="w-52 bg-card border border-border rounded-xl px-3 py-1.5 text-sm outline-hidden focus:border-accent disabled:opacity-50"
className="w-52 bg-surface border border-border rounded-xl px-3 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40 disabled:opacity-50"
/>
)}
{showReset && !secretDisabled && (
+3 -3
View File
@@ -410,7 +410,7 @@ export default function Feed({
value={ytCount}
onChange={(e) => setYtCount(Number(e.target.value))}
title={t("feed.yt.showHint")}
className="bg-card border border-border rounded-lg px-2 py-1 text-xs outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
{[20, 40, 60, 100].map((n) => (
<option key={n} value={n}>
@@ -603,7 +603,7 @@ export default function Feed({
})
}
title={t("feed.source.tip")}
className="bg-card border border-border rounded-lg px-2 py-1 text-xs outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
<option value="organic">{t("feed.source.organic")}</option>
<option value="all">{t("feed.source.all")}</option>
@@ -637,7 +637,7 @@ export default function Feed({
});
}}
aria-label={t("feed.sortLabel")}
className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-lg px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
{/* Relevance only ranks meaningfully when there's a search term — offer it then. */}
{(filters.q.trim() ? (["relevance", ...sortKeys] as SortKey[]) : sortKeys).map((k) => (
+1 -3
View File
@@ -5,9 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, Lock, ShieldCheck } from "lucide-react";
import { api, HttpError } from "../lib/api";
import * as e2ee from "../lib/e2ee";
const inputCls =
"w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent";
import { inputCls } from "./ui/form";
// Secure-messaging key gate: first-run setup (choose a passphrase) or unlock on this device.
// Self-contained — on success it updates the shared e2ee unlock state, so any observing view
+2 -1
View File
@@ -13,6 +13,7 @@ import { openChat } from "../lib/chatDock";
import * as e2ee from "../lib/e2ee";
import Avatar from "./Avatar";
import KeyGate from "./KeyGate";
import { inputCls } from "./ui/form";
import ChatThread from "./ChatThread";
type View =
@@ -224,7 +225,7 @@ function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBac
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder={t("messages.searchPeople")}
className="w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent"
className={inputCls}
/>
{q.isError && !q.data ? (
<StateMessage tone="error" onRetry={() => q.refetch()}>
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Modal({
return createPortal(
<div
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/70 backdrop-blur-xs"
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/70 overlay-backdrop"
onMouseDown={(e) => {
pressedOnBackdrop.current = e.target === e.currentTarget;
}}
@@ -255,7 +255,7 @@ function NotificationRow({
}
return (
<div
className={`glass rounded-xl p-3.5 flex items-start gap-3 transition ${
className={`glass-card rounded-xl p-3.5 flex items-start gap-3 transition ${
n.read ? "opacity-70" : ""
}`}
>
@@ -340,7 +340,7 @@ function ClientActivityRow({
const access = n.meta?.kind === "access-requests" ? n.meta : null;
return (
<div
className={`glass rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
className={`glass-card rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
>
<Icon className={`w-4 h-4 shrink-0 mt-0.5 ${color}`} />
<div className="min-w-0 flex-1">
+2 -2
View File
@@ -87,7 +87,7 @@ export default function Pager({
onBlur={commitPageInput}
onKeyDown={(e) => e.key === "Enter" && commitPageInput()}
aria-label={t("datatable.pager.pageLabel")}
className="w-14 bg-card border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums outline-hidden focus:border-accent"
className="w-14 bg-surface border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
{t("datatable.pager.ofTotal", { total: totalPages })}
</span>
@@ -110,7 +110,7 @@ export default function Pager({
onSizeChange(Number(e.target.value));
scrollListToTop();
}}
className="bg-card border border-border rounded-md px-1.5 py-1 text-xs outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-md px-1.5 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
{pageSizeOptions.map((n) => (
<option key={n} value={n}>
+1 -1
View File
@@ -889,7 +889,7 @@ export default function PlayerModal({
<div
ref={dialogRef}
data-testid="player-modal"
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-xs"
className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/80 overlay-backdrop"
onClick={onClose}
role="dialog"
aria-modal="true"
+1 -1
View File
@@ -590,7 +590,7 @@ export default function Playlists() {
if (e.key === "Escape") setRenaming(false);
}}
aria-label={t("playlists.rename")}
className="text-lg font-semibold bg-card border border-border rounded-md px-2 py-0.5 outline-hidden focus:border-accent"
className="text-lg font-semibold bg-surface border border-border rounded-md px-2 py-0.5 focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
) : (
<div className="flex items-center gap-2 min-w-0">
+2 -2
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-2xs outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-1.5 py-1 text-2xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
>
<option value="custom">{t("playlists.railSortCustom")}</option>
<option value="name">{t("playlists.railSortName")}</option>
@@ -200,7 +200,7 @@ export default function PlaylistsRail({
onChange={(e) => setNewName(e.target.value)}
placeholder={t("playlists.newPlaylist")}
disabled={!canWrite}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent disabled:opacity-50"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40 disabled:opacity-50"
/>
</form>
</div>
@@ -131,7 +131,7 @@ export default function PlexCollectionEditor({
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createNew()}
placeholder={t("plex.collEditor.newPlaceholder")}
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-2 text-sm outline-hidden focus:border-accent"
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={createNew}
@@ -149,7 +149,7 @@ export default function PlexCollectionEditor({
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder={t("plex.collEditor.search")}
className="glass-card mb-2 w-full rounded-lg px-3 py-1.5 text-sm outline-hidden focus:border-accent"
className="glass-card mb-2 w-full rounded-lg px-3 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
)}
+1 -1
View File
@@ -100,7 +100,7 @@ export default function PlexPlaylistAdd({
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createNew()}
placeholder={t("plex.playlistAdd.newPlaceholder")}
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-2 text-sm outline-hidden focus:border-accent"
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={createNew}
+1 -1
View File
@@ -532,7 +532,7 @@ export default function PlexPlaylistView({
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && rename()}
onBlur={rename}
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-1.5 text-lg font-bold outline-hidden focus:border-accent"
className="glass-card min-w-0 flex-1 rounded-lg px-3 py-1.5 text-lg font-bold focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
) : (
<h1 className="min-w-0 flex-1 truncate text-2xl font-bold">{q.data?.title ?? " "}</h1>
+3 -3
View File
@@ -171,7 +171,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
onChange={(e) => setNewPlaylist(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createPlaylist()}
placeholder={t("plex.playlist.newPlaceholder")}
className="glass-card min-w-0 flex-1 rounded-lg px-2 py-1 text-xs outline-hidden focus:border-accent"
className="glass-card min-w-0 flex-1 rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={createPlaylist}
@@ -298,7 +298,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
value={collSearch}
onChange={(e) => setCollSearch(e.target.value)}
placeholder={t("plex.filter.collectionSearch")}
className="mb-1.5 w-full rounded-lg border border-border bg-card px-2 py-1 text-sm focus:border-accent focus:outline-hidden"
className="mb-1.5 w-full rounded-lg border border-border bg-surface px-2 py-1 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<div
ref={collFade.ref}
@@ -546,7 +546,7 @@ function NumberInput({
const n = e.target.value === "" ? null : Number(e.target.value);
onChange(n != null && Number.isFinite(n) ? n : null);
}}
className="w-20 rounded-lg border border-border bg-card px-2 py-1 text-sm tabular-nums focus:border-accent focus:outline-hidden"
className="w-20 rounded-lg border border-border bg-surface px-2 py-1 text-sm tabular-nums focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
);
}
+2 -2
View File
@@ -172,7 +172,7 @@ export default function SavedViewsWidget({
}}
placeholder={t("views.namePlaceholder")}
maxLength={80}
className="flex-1 min-w-0 bg-card border border-border rounded-lg px-2 py-1 text-xs outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={() => void save()}
@@ -256,7 +256,7 @@ function ViewRow({
if (e.key === "Escape") onCancelRename();
}}
maxLength={80}
className="flex-1 min-w-0 bg-card border border-border rounded-lg px-2 py-1 text-xs outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-lg px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={() => onRename(draft)}
+2 -2
View File
@@ -172,7 +172,7 @@ function JobRow({
if (e.key === "Enter") save();
if (e.key === "Escape") setEditing(false);
}}
className="w-16 bg-card border border-border rounded-sm px-1.5 py-0.5 text-xs outline-hidden focus:border-accent"
className="w-16 bg-surface border border-border rounded-sm px-1.5 py-0.5 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<span className="text-muted text-xs">{t("scheduler.minutes")}</span>
<button
@@ -281,7 +281,7 @@ function MaintenanceSettings({
onChange={(e) => setVal(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && save()}
aria-label={t("scheduler.maintenance.batchLabel")}
className="w-28 bg-card border border-border rounded-sm px-2 py-1 text-sm tabular-nums outline-hidden focus:border-accent"
className="w-28 bg-surface border border-border rounded-sm px-2 py-1 text-sm tabular-nums focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={save}
+1 -4
View File
@@ -9,7 +9,7 @@ import { LS, useAccountPersistedState } from "../lib/storage";
import Avatar from "./Avatar";
import { notify, type NotifSettings } from "../lib/notifications";
import Tooltip from "./Tooltip";
import { Section, SettingRow, Switch } from "./ui/form";
import { inputCls, Section, SettingRow, Switch } from "./ui/form";
import { type SaveState } from "./ui/DraftSaveBar";
import { useConfirm } from "./ConfirmProvider";
import { useWizard } from "./WizardProvider";
@@ -402,9 +402,6 @@ function SignInMethods({ me }: { me: Me }) {
}
};
const inputCls =
"w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent";
// Offer Google only when this instance has Google OAuth configured (or the account is already
// linked — then we still show its "Connected" status even if the instance creds were removed).
const showGoogle = me.google_enabled || me.has_google;
+1 -3
View File
@@ -4,6 +4,7 @@ import { Check, ChevronLeft, ChevronRight, Loader2, Send } from "lucide-react";
import { api, HttpError } from "../lib/api";
import { setLanguage, type LangCode } from "../i18n";
import LanguageSwitcher from "./LanguageSwitcher";
import { inputCls } from "./ui/form";
// First-run install wizard. Shown by App.tsx while the instance is unconfigured. The one-time
// setup token comes from the URL the app printed to the container logs (?token=…); every step
@@ -129,9 +130,6 @@ export default function SetupWizard() {
}
}
const inputCls =
"w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent";
return (
<Centered>
<div className="w-full max-w-lg">
+1 -1
View File
@@ -211,7 +211,7 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
placeholder={t(
link.has_password ? "downloads.share.newPassword" : "downloads.share.setPassword",
)}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
onClick={() => savePass.mutate(newPass.trim())}
+2 -2
View File
@@ -209,7 +209,7 @@ export default function Sidebar({
maxAgeDays: undefined,
})
}
className="bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
</label>
<label className="flex items-center justify-between gap-2 text-xs">
@@ -224,7 +224,7 @@ export default function Sidebar({
maxAgeDays: undefined,
})
}
className="bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent"
className="bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
</label>
{(filters.dateFrom || filters.dateTo) && (
+1 -1
View File
@@ -115,7 +115,7 @@ export default function SyncStatus({
// Short label only: the popover already explains the numbers, and the long one overflowed
// the viewport edge from here.
title={countsText}
className="glass glass-hover relative flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl text-xs text-muted"
className="glass-menu glass-hover relative flex items-center gap-1.5 px-2.5 h-11 rounded-2xl text-xs text-muted"
>
{active ? (
<Loader2 className="w-3.5 h-3.5 shrink-0 animate-spin text-accent" />
+2 -2
View File
@@ -51,7 +51,7 @@ function TagRow({
onChange={(e) => setName(e.target.value)}
onBlur={commit}
onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1.5 text-sm outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<span
ref={pop.refs.setReference}
@@ -201,7 +201,7 @@ export default function TagManager({
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder={t("tagManager.newPlaceholder")}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1.5 text-sm outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-surface border border-border rounded-md px-2 py-1.5 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
<button
type="submit"
+2 -3
View File
@@ -22,6 +22,7 @@ import { api } from "../lib/api";
import { HttpError } from "../lib/api";
import { setLanguage, type LangCode } from "../i18n";
import LanguageSwitcher from "./LanguageSwitcher";
import { inputCls } from "./ui/form";
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
const PASSWORD_MIN = 10;
@@ -258,7 +259,7 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
}, [onClose]);
return (
<div
className="fixed inset-0 z-popover grid place-items-center bg-black/80 backdrop-blur-xs p-4 sm:p-8"
className="fixed inset-0 z-popover grid place-items-center bg-black/80 overlay-backdrop p-4 sm:p-8"
onClick={onClose}
role="dialog"
aria-modal="true"
@@ -430,8 +431,6 @@ function AuthCard() {
}
}
const inputCls =
"w-full bg-card border border-border rounded-xl px-3 py-2.5 text-sm outline-hidden focus:border-accent";
const titleKey = TITLE_KEY[mode];
return (
+1 -1
View File
@@ -148,7 +148,7 @@ function FilterFunnel<T>({
value={(value as string) ?? ""}
onChange={(e) => onApply(col.key, e.target.value)}
placeholder={col.header}
className="w-full bg-card border border-border rounded-md px-2 py-1 text-xs outline-hidden focus:border-accent"
className="w-full bg-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
/>
)}
{f.kind === "select" && (
+6 -5
View File
@@ -4,11 +4,12 @@ import Tooltip from "../Tooltip";
// Shared form/settings primitives, factored out of the many panels that each re-declared
// them (Settings, Config, Stats, admin pages, Setup wizard). Visual contract unchanged.
/** The standard text/select/number input style (full-width, surface bg, accent focus ring) — the
* literal string was copy-pasted into DownloadCenter/DownloadDialog/ProfileEditor/ShareDialog/
* VideoEditor. NOTE: the settings-family panels (Settings/Config/Setup/Welcome) use a DIFFERENT
* `bg-card rounded-xl focus:border-accent` style; unifying the two is a design call left to the
* glass-consistency epic, not this DRY extract. */
/** The ONE text/select/number input recipe (full-width, surface bg, rounded-lg, 2px accent focus
* RING). R10 S3 unified every input onto this: the old settings-family border-focus style
* (`bg-card rounded-xl focus:border-accent`) and its three copy-pasted local `inputCls` constants
* (KeyGate/SettingsPanel/SetupWizard) are gone. Size variants (a small pager/table-filter/chat
* input) keep their own width/radius/padding but adopt the same bg-surface + ring-focus look
* inline — reach for this const for a default full-size field, match its look for a smaller one. */
export const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-hidden focus:ring-2 focus:ring-accent/40";
+11
View File
@@ -479,6 +479,17 @@ html[data-perf="1"] body {
background: var(--bg);
}
/* Full-viewport modal scrim blur, as a named recipe so perf mode can DROP it. This is the most
expensive blur in the app (viewport-sized); as a raw `backdrop-blur-xs` utility on each overlay
it survived data-perf while every .glass surface had its blur dropped. Overlays keep their own
bg-black/NN; this adds only the (perf-aware) 4px blur, matching backdrop-blur-xs. */
.overlay-backdrop {
backdrop-filter: blur(4px);
}
html[data-perf="1"] .overlay-backdrop {
backdrop-filter: none;
}
/* Skip layout/paint for off-screen feed cards so long lists stay smooth while
scrolling. contain-intrinsic-size reserves an approximate box so the scrollbar
stays stable; `auto` lets the browser remember each card's real size after it
+12
View File
@@ -14,6 +14,18 @@ export interface ReleaseEntry {
}
export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.60.8",
date: "2026-08-09",
fixes: [
"Text fields across the app now share one consistent style with a clearer accent focus ring when you tab to them.",
"Warning and info banners' action buttons now respond to hover.",
],
chores: [
"Performance mode now also drops the heavy background blur behind dialogs and the player.",
"Consolidated the input, header-pill and modal-backdrop styles onto shared recipes (no behaviour change).",
],
},
{
version: "0.60.7",
date: "2026-08-09",