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")} 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>
</div> </div>
+2 -2
View File
@@ -347,7 +347,7 @@ function AdminInvites() {
value={newEmail} value={newEmail}
onChange={(e) => setNewEmail(e.target.value)} onChange={(e) => setNewEmail(e.target.value)}
placeholder={t("settings.invites.addPlaceholder")} 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 <button
type="submit" type="submit"
@@ -466,7 +466,7 @@ function AdminDemo() {
value={newEmail} value={newEmail}
onChange={(e) => setNewEmail(e.target.value)} onChange={(e) => setNewEmail(e.target.value)}
placeholder={t("settings.demo.addPlaceholder")} 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 <button
type="submit" type="submit"
+2 -2
View File
@@ -8,12 +8,12 @@ const TONES: Record<BannerTone, { bar: string; icon: string; action: string }> =
accent: { accent: {
bar: "bg-accent/15 border-accent/30", bar: "bg-accent/15 border-accent/30",
icon: "text-accent", icon: "text-accent",
action: "bg-accent text-accent-fg", action: "bg-accent text-accent-fg hover:opacity-90",
}, },
warning: { warning: {
bar: "bg-warn/15 border-warn/30", bar: "bg-warn/15 border-warn/30",
icon: "text-warn", 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} maxLength={64}
placeholder={t("channels.tags.createPlaceholder")} 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 <button
type="button" type="button"
+1 -1
View File
@@ -211,7 +211,7 @@ export default function ChatThread({
}} }}
rows={1} rows={1}
placeholder={t("messages.writePlaceholder")} 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 <button
onClick={submit} onClick={submit}
@@ -40,7 +40,7 @@ export default function ColumnSortControl<T>({
onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null); onChange(key ? { key, dir: active?.key === key ? active.dir : "asc" } : null);
}} }}
aria-label={label} 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> <option value="">{defaultLabel}</option>
{sortable.map((c) => ( {sortable.map((c) => (
+1 -1
View File
@@ -366,7 +366,7 @@ function Field({
max={isNum && item.max != null ? item.max : undefined} max={isNum && item.max != null ? item.max : undefined}
placeholder={item.secret ? "••••••••" : String(item.default ?? "")} placeholder={item.secret ? "••••••••" : String(item.default ?? "")}
aria-label={t(`config.fields.${item.key}.label`, item.key)} 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 && ( {showReset && !secretDisabled && (
+3 -3
View File
@@ -410,7 +410,7 @@ export default function Feed({
value={ytCount} value={ytCount}
onChange={(e) => setYtCount(Number(e.target.value))} onChange={(e) => setYtCount(Number(e.target.value))}
title={t("feed.yt.showHint")} 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) => ( {[20, 40, 60, 100].map((n) => (
<option key={n} value={n}> <option key={n} value={n}>
@@ -603,7 +603,7 @@ export default function Feed({
}) })
} }
title={t("feed.source.tip")} 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="organic">{t("feed.source.organic")}</option>
<option value="all">{t("feed.source.all")}</option> <option value="all">{t("feed.source.all")}</option>
@@ -637,7 +637,7 @@ export default function Feed({
}); });
}} }}
aria-label={t("feed.sortLabel")} 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. */} {/* Relevance only ranks meaningfully when there's a search term — offer it then. */}
{(filters.q.trim() ? (["relevance", ...sortKeys] as SortKey[]) : sortKeys).map((k) => ( {(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 { AlertTriangle, Lock, ShieldCheck } from "lucide-react";
import { api, HttpError } from "../lib/api"; import { api, HttpError } from "../lib/api";
import * as e2ee from "../lib/e2ee"; import * as e2ee from "../lib/e2ee";
import { inputCls } from "./ui/form";
const inputCls =
"w-full bg-card border border-border rounded-xl px-3 py-2 text-sm outline-hidden focus:border-accent";
// Secure-messaging key gate: first-run setup (choose a passphrase) or unlock on this device. // 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 // 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 * as e2ee from "../lib/e2ee";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import KeyGate from "./KeyGate"; import KeyGate from "./KeyGate";
import { inputCls } from "./ui/form";
import ChatThread from "./ChatThread"; import ChatThread from "./ChatThread";
type View = type View =
@@ -224,7 +225,7 @@ function Directory({ onPick, onBack }: { onPick: (u: MessageUser) => void; onBac
value={filter} value={filter}
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
placeholder={t("messages.searchPeople")} 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 ? ( {q.isError && !q.data ? (
<StateMessage tone="error" onRetry={() => q.refetch()}> <StateMessage tone="error" onRetry={() => q.refetch()}>
+1 -1
View File
@@ -56,7 +56,7 @@ export default function Modal({
return createPortal( return createPortal(
<div <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) => { onMouseDown={(e) => {
pressedOnBackdrop.current = e.target === e.currentTarget; pressedOnBackdrop.current = e.target === e.currentTarget;
}} }}
@@ -255,7 +255,7 @@ function NotificationRow({
} }
return ( return (
<div <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" : "" n.read ? "opacity-70" : ""
}`} }`}
> >
@@ -340,7 +340,7 @@ function ClientActivityRow({
const access = n.meta?.kind === "access-requests" ? n.meta : null; const access = n.meta?.kind === "access-requests" ? n.meta : null;
return ( return (
<div <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}`} /> <Icon className={`w-4 h-4 shrink-0 mt-0.5 ${color}`} />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
+2 -2
View File
@@ -87,7 +87,7 @@ export default function Pager({
onBlur={commitPageInput} onBlur={commitPageInput}
onKeyDown={(e) => e.key === "Enter" && commitPageInput()} onKeyDown={(e) => e.key === "Enter" && commitPageInput()}
aria-label={t("datatable.pager.pageLabel")} 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 })} {t("datatable.pager.ofTotal", { total: totalPages })}
</span> </span>
@@ -110,7 +110,7 @@ export default function Pager({
onSizeChange(Number(e.target.value)); onSizeChange(Number(e.target.value));
scrollListToTop(); 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) => ( {pageSizeOptions.map((n) => (
<option key={n} value={n}> <option key={n} value={n}>
+1 -1
View File
@@ -889,7 +889,7 @@ export default function PlayerModal({
<div <div
ref={dialogRef} ref={dialogRef}
data-testid="player-modal" 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} onClick={onClose}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
+1 -1
View File
@@ -590,7 +590,7 @@ export default function Playlists() {
if (e.key === "Escape") setRenaming(false); if (e.key === "Escape") setRenaming(false);
}} }}
aria-label={t("playlists.rename")} 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"> <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} value={plSort.key}
onChange={(e) => setPlSort({ ...plSort, key: e.target.value as PlSort["key"] })} onChange={(e) => setPlSort({ ...plSort, key: e.target.value as PlSort["key"] })}
aria-label={t("playlists.sortLabel")} 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="custom">{t("playlists.railSortCustom")}</option>
<option value="name">{t("playlists.railSortName")}</option> <option value="name">{t("playlists.railSortName")}</option>
@@ -200,7 +200,7 @@ export default function PlaylistsRail({
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
placeholder={t("playlists.newPlaylist")} placeholder={t("playlists.newPlaylist")}
disabled={!canWrite} 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> </form>
</div> </div>
@@ -131,7 +131,7 @@ export default function PlexCollectionEditor({
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createNew()} onKeyDown={(e) => e.key === "Enter" && createNew()}
placeholder={t("plex.collEditor.newPlaceholder")} 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 <button
onClick={createNew} onClick={createNew}
@@ -149,7 +149,7 @@ export default function PlexCollectionEditor({
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
placeholder={t("plex.collEditor.search")} 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)} onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createNew()} onKeyDown={(e) => e.key === "Enter" && createNew()}
placeholder={t("plex.playlistAdd.newPlaceholder")} 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 <button
onClick={createNew} onClick={createNew}
+1 -1
View File
@@ -532,7 +532,7 @@ export default function PlexPlaylistView({
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && rename()} onKeyDown={(e) => e.key === "Enter" && rename()}
onBlur={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> <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)} onChange={(e) => setNewPlaylist(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && createPlaylist()} onKeyDown={(e) => e.key === "Enter" && createPlaylist()}
placeholder={t("plex.playlist.newPlaceholder")} 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 <button
onClick={createPlaylist} onClick={createPlaylist}
@@ -298,7 +298,7 @@ export default function PlexSidebar({ layout, setLayout, collapsed, onToggleColl
value={collSearch} value={collSearch}
onChange={(e) => setCollSearch(e.target.value)} onChange={(e) => setCollSearch(e.target.value)}
placeholder={t("plex.filter.collectionSearch")} 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 <div
ref={collFade.ref} ref={collFade.ref}
@@ -546,7 +546,7 @@ function NumberInput({
const n = e.target.value === "" ? null : Number(e.target.value); const n = e.target.value === "" ? null : Number(e.target.value);
onChange(n != null && Number.isFinite(n) ? n : null); 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")} placeholder={t("views.namePlaceholder")}
maxLength={80} 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 <button
onClick={() => void save()} onClick={() => void save()}
@@ -256,7 +256,7 @@ function ViewRow({
if (e.key === "Escape") onCancelRename(); if (e.key === "Escape") onCancelRename();
}} }}
maxLength={80} 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 <button
onClick={() => onRename(draft)} onClick={() => onRename(draft)}
+2 -2
View File
@@ -172,7 +172,7 @@ function JobRow({
if (e.key === "Enter") save(); if (e.key === "Enter") save();
if (e.key === "Escape") setEditing(false); 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> <span className="text-muted text-xs">{t("scheduler.minutes")}</span>
<button <button
@@ -281,7 +281,7 @@ function MaintenanceSettings({
onChange={(e) => setVal(e.target.value)} onChange={(e) => setVal(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && save()} onKeyDown={(e) => e.key === "Enter" && save()}
aria-label={t("scheduler.maintenance.batchLabel")} 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 <button
onClick={save} onClick={save}
+1 -4
View File
@@ -9,7 +9,7 @@ import { LS, useAccountPersistedState } from "../lib/storage";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import { notify, type NotifSettings } from "../lib/notifications"; import { notify, type NotifSettings } from "../lib/notifications";
import Tooltip from "./Tooltip"; 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 { type SaveState } from "./ui/DraftSaveBar";
import { useConfirm } from "./ConfirmProvider"; import { useConfirm } from "./ConfirmProvider";
import { useWizard } from "./WizardProvider"; 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 // 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). // linked — then we still show its "Connected" status even if the instance creds were removed).
const showGoogle = me.google_enabled || me.has_google; 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 { api, HttpError } from "../lib/api";
import { setLanguage, type LangCode } from "../i18n"; import { setLanguage, type LangCode } from "../i18n";
import LanguageSwitcher from "./LanguageSwitcher"; 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 // 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 // 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 ( return (
<Centered> <Centered>
<div className="w-full max-w-lg"> <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( placeholder={t(
link.has_password ? "downloads.share.newPassword" : "downloads.share.setPassword", 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 <button
onClick={() => savePass.mutate(newPass.trim())} onClick={() => savePass.mutate(newPass.trim())}
+2 -2
View File
@@ -209,7 +209,7 @@ export default function Sidebar({
maxAgeDays: undefined, 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>
<label className="flex items-center justify-between gap-2 text-xs"> <label className="flex items-center justify-between gap-2 text-xs">
@@ -224,7 +224,7 @@ export default function Sidebar({
maxAgeDays: undefined, 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>
{(filters.dateFrom || filters.dateTo) && ( {(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 // Short label only: the popover already explains the numbers, and the long one overflowed
// the viewport edge from here. // the viewport edge from here.
title={countsText} 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 ? ( {active ? (
<Loader2 className="w-3.5 h-3.5 shrink-0 animate-spin text-accent" /> <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)} onChange={(e) => setName(e.target.value)}
onBlur={commit} onBlur={commit}
onKeyDown={(e) => e.key === "Enter" && (e.target as HTMLInputElement).blur()} 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 <span
ref={pop.refs.setReference} ref={pop.refs.setReference}
@@ -201,7 +201,7 @@ export default function TagManager({
value={newName} value={newName}
onChange={(e) => setNewName(e.target.value)} onChange={(e) => setNewName(e.target.value)}
placeholder={t("tagManager.newPlaceholder")} 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 <button
type="submit" type="submit"
+2 -3
View File
@@ -22,6 +22,7 @@ import { api } from "../lib/api";
import { HttpError } from "../lib/api"; import { HttpError } from "../lib/api";
import { setLanguage, type LangCode } from "../i18n"; import { setLanguage, type LangCode } from "../i18n";
import LanguageSwitcher from "./LanguageSwitcher"; import LanguageSwitcher from "./LanguageSwitcher";
import { inputCls } from "./ui/form";
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/; const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
const PASSWORD_MIN = 10; const PASSWORD_MIN = 10;
@@ -258,7 +259,7 @@ function Lightbox({ src, label, onClose }: { src: string; label: string; onClose
}, [onClose]); }, [onClose]);
return ( return (
<div <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} onClick={onClose}
role="dialog" role="dialog"
aria-modal="true" 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]; const titleKey = TITLE_KEY[mode];
return ( return (
+1 -1
View File
@@ -148,7 +148,7 @@ function FilterFunnel<T>({
value={(value as string) ?? ""} value={(value as string) ?? ""}
onChange={(e) => onApply(col.key, e.target.value)} onChange={(e) => onApply(col.key, e.target.value)}
placeholder={col.header} 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" && ( {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 // 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. // 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 /** The ONE text/select/number input recipe (full-width, surface bg, rounded-lg, 2px accent focus
* literal string was copy-pasted into DownloadCenter/DownloadDialog/ProfileEditor/ShareDialog/ * RING). R10 S3 unified every input onto this: the old settings-family border-focus style
* VideoEditor. NOTE: the settings-family panels (Settings/Config/Setup/Welcome) use a DIFFERENT * (`bg-card rounded-xl focus:border-accent`) and its three copy-pasted local `inputCls` constants
* `bg-card rounded-xl focus:border-accent` style; unifying the two is a design call left to the * (KeyGate/SettingsPanel/SetupWizard) are gone. Size variants (a small pager/table-filter/chat
* glass-consistency epic, not this DRY extract. */ * 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 = 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"; "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); 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 /* 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 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 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[] = [ 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", version: "0.60.7",
date: "2026-08-09", date: "2026-08-09",