Merge chore/code-hygiene: Phase 3 — extract shared inputCls + btnCls to ui/form.tsx

Byte-identical className strings de-duplicated from 5 (inputCls) + 2 (btnCls)
components into ui/form.tsx. Behavior-neutral. Settings-family input style + A/B
unification deferred to the glass-consistency epic.
This commit is contained in:
npeter83 2026-07-12 00:35:48 +02:00
commit eba1c8f01a
6 changed files with 22 additions and 20 deletions

View file

@ -30,9 +30,8 @@ import { api, type DownloadJob, type Me } from "../lib/api";
import { invalidateDownloads } from "../lib/downloads"; import { invalidateDownloads } from "../lib/downloads";
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { formatBytes, formatDuration, formatEta, formatSpeed } from "../lib/format"; import { formatBytes, formatDuration, formatEta, formatSpeed } from "../lib/format";
import { inputCls } from "./ui/form";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
const GiB = 1024 ** 3; const GiB = 1024 ** 3;
const STATUS_CLS: Record<string, string> = { const STATUS_CLS: Record<string, string> = {

View file

@ -9,9 +9,7 @@ import { invalidateDownloads } from "../lib/downloads";
const ProfileEditor = lazy(() => import("./ProfileEditor")); const ProfileEditor = lazy(() => import("./ProfileEditor"));
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { navigateTo } from "../lib/nav"; import { navigateTo } from "../lib/nav";
import { inputCls } from "./ui/form";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
// Preset picker shown when the user hits Download on a card / in the player, or adds a URL from // Preset picker shown when the user hits Download on a card / in the player, or adds a URL from
// the Download Center. Enqueues via the API and points the user at the Download Center. // the Download Center. Enqueues via the API and points the user at the Download Center.

View file

@ -5,9 +5,8 @@ import { Lock, Pencil, Plus, Trash2 } from "lucide-react";
import Modal from "./Modal"; import Modal from "./Modal";
import { useConfirm } from "./ConfirmProvider"; import { useConfirm } from "./ConfirmProvider";
import { api, type DownloadProfile, type DownloadSpec } from "../lib/api"; import { api, type DownloadProfile, type DownloadSpec } from "../lib/api";
import { inputCls } from "./ui/form";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
const HEIGHTS = [null, 2160, 1440, 1080, 720, 480, 360]; const HEIGHTS = [null, 2160, 1440, 1080, 720, 480, 360];
const BLANK: DownloadSpec = { const BLANK: DownloadSpec = {

View file

@ -7,10 +7,7 @@ import Modal from "./Modal";
import { useConfirm } from "./ConfirmProvider"; import { useConfirm } from "./ConfirmProvider";
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { api, type DownloadJob, type ShareLink } from "../lib/api"; import { api, type DownloadJob, type ShareLink } from "../lib/api";
import { inputCls, btnCls } from "./ui/form";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
const btn = "px-3 py-1.5 rounded-lg text-sm font-medium transition disabled:opacity-50";
const EXPIRY_DAYS = [null, 1, 7, 30] as const; const EXPIRY_DAYS = [null, 1, 7, 30] as const;
// --- share with a registered user (ACL) ----------------------------------------------------- // --- share with a registered user (ACL) -----------------------------------------------------
@ -221,10 +218,10 @@ function LinkShare({ job }: { job: DownloadJob }) {
autoComplete="new-password" autoComplete="new-password"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<button onClick={() => setCreating(false)} className={clsx(btn, "text-muted hover:text-fg hover:bg-surface")}> <button onClick={() => setCreating(false)} className={clsx(btnCls, "text-muted hover:text-fg hover:bg-surface")}>
{t("common.cancel")} {t("common.cancel")}
</button> </button>
<button onClick={() => create.mutate()} disabled={create.isPending} className={clsx(btn, "bg-accent text-accent-fg hover:opacity-90")}> <button onClick={() => create.mutate()} disabled={create.isPending} className={clsx(btnCls, "bg-accent text-accent-fg hover:opacity-90")}>
{t("downloads.share.createLink")} {t("downloads.share.createLink")}
</button> </button>
</div> </div>
@ -232,7 +229,7 @@ function LinkShare({ job }: { job: DownloadJob }) {
) : ( ) : (
<button <button
onClick={() => setCreating(true)} onClick={() => setCreating(true)}
className={clsx(btn, "mt-2 border border-border text-muted hover:text-fg hover:border-muted inline-flex items-center gap-1.5")} className={clsx(btnCls, "mt-2 border border-border text-muted hover:text-fg hover:border-muted inline-flex items-center gap-1.5")}
> >
<Link2 className="w-4 h-4" /> {t("downloads.share.newLink")} <Link2 className="w-4 h-4" /> {t("downloads.share.newLink")}
</button> </button>

View file

@ -6,6 +6,7 @@ import clsx from "clsx";
import Modal from "./Modal"; import Modal from "./Modal";
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { api, type DownloadJob, type EditSpec } from "../lib/api"; import { api, type DownloadJob, type EditSpec } from "../lib/api";
import { inputCls, btnCls } from "./ui/form";
// mm:ss.d timecode <-> seconds. // mm:ss.d timecode <-> seconds.
function tc(s: number): string { function tc(s: number): string {
@ -30,9 +31,6 @@ type Seg = { id: number; start: number; end: number; keep: boolean };
type Frac = { x: number; y: number; w: number; h: number }; type Frac = { x: number; y: number; w: number; h: number };
type Drag = { type: "seek" } | { type: "boundary"; bi: number }; type Drag = { type: "seek" } | { type: "boundary"; bi: number };
const btn = "px-3 py-1.5 rounded-lg text-sm font-medium transition disabled:opacity-50";
const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
const numCls = const numCls =
"w-24 rounded-md bg-surface border border-border px-2 py-1 text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-accent/40"; "w-24 rounded-md bg-surface border border-border px-2 py-1 text-sm tabular-nums focus:outline-none focus:ring-2 focus:ring-accent/40";
const MIN_SEG = 0.1; const MIN_SEG = 0.1;
@ -338,7 +336,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
</button> </button>
<div className="text-xs text-muted tabular-nums">{tc(current)} / {tc(duration)}</div> <div className="text-xs text-muted tabular-nums">{tc(current)} / {tc(duration)}</div>
<div className="flex-1" /> <div className="flex-1" />
<button onClick={splitAtPlayhead} className={clsx(btn, "bg-surface hover:bg-card inline-flex items-center gap-1.5")}> <button onClick={splitAtPlayhead} className={clsx(btnCls, "bg-surface hover:bg-card inline-flex items-center gap-1.5")}>
<SplitSquareHorizontal className="w-4 h-4" /> {t("editor.splitHere")} <SplitSquareHorizontal className="w-4 h-4" /> {t("editor.splitHere")}
</button> </button>
</div> </div>
@ -553,13 +551,13 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
<div className="flex items-center justify-between pt-1"> <div className="flex items-center justify-between pt-1">
<span className="text-xs text-muted">{reencode ? t("editor.willReencode") : t("editor.willCopy")}</span> <span className="text-xs text-muted">{reencode ? t("editor.willReencode") : t("editor.willCopy")}</span>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={onClose} className={clsx(btn, "text-muted hover:text-fg hover:bg-surface")}> <button onClick={onClose} className={clsx(btnCls, "text-muted hover:text-fg hover:bg-surface")}>
{t("common.cancel")} {t("common.cancel")}
</button> </button>
<button <button
onClick={() => create.mutate()} onClick={() => create.mutate()}
disabled={!valid || create.isPending} disabled={!valid || create.isPending}
className={clsx(btn, "bg-accent text-accent-fg hover:opacity-90 inline-flex items-center gap-1.5")} className={clsx(btnCls, "bg-accent text-accent-fg hover:opacity-90 inline-flex items-center gap-1.5")}
> >
{create.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Scissors className="w-4 h-4" />} {create.isPending ? <Loader2 className="w-4 h-4 animate-spin" /> : <Scissors className="w-4 h-4" />}
{willJoin {willJoin

View file

@ -4,6 +4,17 @@ 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
* 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. */
export const inputCls =
"w-full rounded-lg bg-surface border border-border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/40";
/** The standard text button style (used verbatim by ShareDialog + VideoEditor). */
export const btnCls = "px-3 py-1.5 rounded-lg text-sm font-medium transition disabled:opacity-50";
/** Pill on/off switch the bare control; compose it with your own label/row. Pass `label` for /** Pill on/off switch the bare control; compose it with your own label/row. Pass `label` for
* the accessible name (the visible row text) since the control itself has no inner text. */ * the accessible name (the visible row text) since the control itself has no inner text. */
export function Switch({ export function Switch({