From 53c75f69e27c0e28677c94e581c703aea6052978 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 29 Jun 2026 00:04:45 +0200 Subject: [PATCH] refactor(ui): shared form primitives (Switch, Section, SettingRow, HintLabel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit components/ui/form.tsx replaces the per-panel copies: - Switch — was duplicated in SettingsPanel (Switch) + ConfigPanel (Toggle) + Sidebar (inline labeled toggle). - Section (with a card variant) — was in SettingsPanel + Stats (plain) and AdminUsers (glass card). - SettingRow + HintLabel — the label+hint+control row was identical in SettingsPanel + Stats. No visual change intended (Sidebar's toggle gains the standard knob shadow). --- frontend/src/components/AdminUsers.tsx | 16 ++--- frontend/src/components/ConfigPanel.tsx | 21 +----- frontend/src/components/SettingsPanel.tsx | 75 ++++---------------- frontend/src/components/Sidebar.tsx | 15 +--- frontend/src/components/Stats.tsx | 60 +++++----------- frontend/src/components/ui/form.tsx | 85 +++++++++++++++++++++++ 6 files changed, 125 insertions(+), 147 deletions(-) create mode 100644 frontend/src/components/ui/form.tsx diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index 078ae9c..356b99d 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -6,6 +6,7 @@ import { api, type AdminUserRow, type Invite, type Me } from "../lib/api"; import { notify } from "../lib/notifications"; import { LS } from "../lib/storage"; import Tooltip from "./Tooltip"; +import { Section } from "./ui/form"; import { useConfirm } from "./ConfirmProvider"; import Tabs, { usePersistedTab } from "./Tabs"; @@ -41,15 +42,6 @@ export default function AdminUsers({ me }: { me: Me }) { ); } -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-
{title}
- {children} -
- ); -} - function Badge({ tone, children }: { tone: "accent" | "muted" | "warning"; children: React.ReactNode }) { const cls = tone === "accent" @@ -138,7 +130,7 @@ function UsersRoles({ me }: { me: Me }) { const rows = q.data ?? []; return ( -
+

{t("users.roles.intro")}

{rows.length === 0 ? (

{t("users.roles.empty")}

@@ -266,7 +258,7 @@ function AdminInvites() { const decided = list.filter((i) => i.status !== "pending"); return ( -
+
{pending.length === 0 ? (

{t("settings.invites.noPending")}

) : ( @@ -360,7 +352,7 @@ function AdminDemo() { const rows = list.data ?? []; return ( -
+

{t("settings.demo.intro")}

{rows.length > 0 && ( diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index 1da0e7a..8fa14b2 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -6,6 +6,7 @@ import { api, type ConfigItem } from "../lib/api"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; import Tabs, { usePersistedTab } from "./Tabs"; +import { Switch } from "./ui/form"; // Admin Configuration page: edit DB-overridable operational settings (registry-driven from the // backend — adding a key there makes it appear here automatically). Edits are drafted and @@ -241,7 +242,7 @@ function Field({
{isBool ? ( - onChange(v ? "true" : "false")} /> + onChange(v ? "true" : "false")} /> ) : (
); -} - -function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} +} \ No newline at end of file diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index cd72d7b..8227a37 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -8,6 +8,7 @@ import { LS, usePersistedState } 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 { useConfirm } from "./ConfirmProvider"; // The Settings page edits server-persisted preferences as a draft: changes apply locally @@ -137,56 +138,6 @@ function PrefsSaveBar({ prefs }: { prefs: PrefsController }) { ); } -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-
{title}
- {children} -
- ); -} - -function Row({ - label, - hint, - children, -}: { - label: string; - hint?: string; - children: React.ReactNode; -}) { - return ( -
- - - {label} - - - {children} -
- ); -} - -function Switch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ); -} - function Appearance({ prefs }: { prefs: PrefsController }) { const { t } = useTranslation(); // Controlled by App's prefs draft: each change applies locally for preview but is only @@ -212,27 +163,27 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
- + setTheme({ ...theme, mode: v ? "dark" : "light" })} /> - - + + setView(v ? "list" : "grid")} /> - - + - - + - +
@@ -261,18 +212,18 @@ function Notifications({ prefs }: { prefs: PrefsController }) { return (
- update({ sound: v })} /> - - + update({ durationMs: v ? 6000 : 0 })} /> - + {autoOn && (
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index df82d27..ecf64f0 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -36,6 +36,7 @@ import { } from "../lib/sidebarLayout"; import { shareUrl } from "../lib/urlState"; import { notify } from "../lib/notifications"; +import { Switch } from "./ui/form"; import TagManager from "./TagManager"; // Filter ids; display labels resolved at render time via t("sidebar.sort.") etc. @@ -622,19 +623,7 @@ function Toggle({ return ( ); } diff --git a/frontend/src/components/Stats.tsx b/frontend/src/components/Stats.tsx index ec14479..f1b6db6 100644 --- a/frontend/src/components/Stats.tsx +++ b/frontend/src/components/Stats.tsx @@ -7,6 +7,7 @@ import { formatEta, quotaActionLabel } from "../lib/format"; import { notify } from "../lib/notifications"; import { LS, usePersistedState } from "../lib/storage"; import Tooltip from "./Tooltip"; +import { Section, SettingRow } from "./ui/form"; const RANGES = [7, 30, 90] as const; type StatsTab = "overview" | "system"; @@ -47,31 +48,6 @@ export default function Stats({ me }: { me: Me }) { ); } -function Section({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-
{title}
- {children} -
- ); -} - -function Row({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { - return ( -
- - - {label} - - - {children} -
- ); -} // Per-user view: sync status + your own API usage + (for real accounts) the manual sync actions. function Overview({ me }: { me: Me }) { @@ -107,29 +83,29 @@ function Overview({ me }: { me: Me }) {
{s ? (
- + {s.channels_total} - - + + {`${s.channels_recent_synced}/${s.channels_total}`} - - + + {`${s.channels_deep_done}/${s.channels_deep_requested}`} - + {s.deep_pending_count > 0 && ( - {formatEta(s.deep_eta_seconds)} - + )} - + {s.my_videos.toLocaleString()} - - + + {s.quota_remaining_today.toLocaleString()} - +
) : (
{t("stats.sync.loading")}
@@ -140,12 +116,12 @@ function Overview({ me }: { me: Me }) { {usage.data ? ( <>
- + {usage.data.today.toLocaleString()} - - {usage.data.last_7d.toLocaleString()} - {usage.data.last_30d.toLocaleString()} - {usage.data.all_time.toLocaleString()} + + {usage.data.last_7d.toLocaleString()} + {usage.data.last_30d.toLocaleString()} + {usage.data.all_time.toLocaleString()}
{Object.keys(usage.data.by_action).length > 0 && (
diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..c99fcd3 --- /dev/null +++ b/frontend/src/components/ui/form.tsx @@ -0,0 +1,85 @@ +import type { ReactNode } from "react"; +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. + +/** Pill on/off switch — the bare control; compose it with your own label/row. */ +export function Switch({ + checked, + onChange, +}: { + checked: boolean; + onChange: (v: boolean) => void; +}) { + return ( + + ); +} + +/** A titled settings block. Default = a plain block with an uppercase caption; `card` wraps it + * in a glass card (the admin pages' style). */ +export function Section({ + title, + card, + children, +}: { + title: string; + card?: boolean; + children: ReactNode; +}) { + return ( +
+
+ {title} +
+ {children} +
+ ); +} + +/** A label that reveals an explanatory caption on hover (dotted underline) when `hint` is set. + * No-op styling when there's no hint, so it's safe to use unconditionally. */ +export function HintLabel({ hint, children }: { hint?: string; children: ReactNode }) { + return ( + + + {children} + + + ); +} + +/** A settings row: a (hint-able) label on the left, a control on the right. */ +export function SettingRow({ + label, + hint, + children, +}: { + label: string; + hint?: string; + children: ReactNode; +}) { + return ( +
+ {label} + {children} +
+ ); +}