feat(design): the field family, the dropdown trigger, native controls, the no-raw-input lint rule (E28 S3c)
- ui/Input, Textarea, InnerInput + fieldClass (A4): the >= 3:1 edge, a 2px accent focus edge, the danger edge for invalid, md/sm/xs/heading rungs, 1rem and 44 on touch - ui/SelectTrigger (A5): SortControl, FilterSelect, the view switcher and the date filter on one trigger, a pill in a toolbar, r8 in a form - ui/NativeControls (A6): Checkbox, CheckRow, Slider, ColorInput - every raw input swept (~100 sites); inputCls retired; local/input-primitive - useReturnFocus: an in-place editor hands the focus back to its trigger on Enter/Escape (the playlist and Plex title renames, the priority number); the Plex playlist rename gains Escape and an accessible name - back to top: a round 48 FAB (owner decision); DESIGN.md A2 and the log
This commit is contained in:
@@ -14,7 +14,7 @@ import { LS, useAccountPersistedState } from "../lib/storage";
|
||||
import Avatar from "./ui/Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./ui/Tooltip";
|
||||
import { inputCls, Section, SettingRow, Switch } from "./ui/form";
|
||||
import { Section, SettingRow, Switch } from "./ui/form";
|
||||
import { Button } from "./ui/Button";
|
||||
import { type SaveState } from "./ui/DraftSaveBar";
|
||||
import { useConfirm } from "./confirmContext";
|
||||
@@ -25,6 +25,8 @@ import { usePrefs } from "./prefsContext";
|
||||
import Tabs from "./ui/Tabs";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Slider } from "./ui/NativeControls";
|
||||
import { Input, Textarea } from "./ui/Input";
|
||||
|
||||
// The Settings page edits server-persisted preferences. Each change applies locally for instant
|
||||
// preview AND auto-saves (debounced) — no draft, no Save button; a small indicator flashes the
|
||||
@@ -186,15 +188,13 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
|
||||
<span>{t("settings.appearance.backgroundFade")}</span>
|
||||
<span className="tabular-nums">{theme.bgFade}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={theme.bgFade}
|
||||
onChange={(e) => setTheme({ ...theme, bgFade: Number(e.target.value) })}
|
||||
onChange={(v) => setTheme({ ...theme, bgFade: v })}
|
||||
aria-label={t("settings.appearance.backgroundFade")}
|
||||
className="w-full accent-accent"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -230,15 +230,13 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
|
||||
</Section>
|
||||
|
||||
<Section title={t("settings.appearance.textSize")}>
|
||||
<input
|
||||
type="range"
|
||||
<Slider
|
||||
min={0.9}
|
||||
max={1.3}
|
||||
step={0.02}
|
||||
value={theme.fontScale}
|
||||
onChange={(e) => setTheme({ ...theme, fontScale: Number(e.target.value) })}
|
||||
onChange={(v) => setTheme({ ...theme, fontScale: v })}
|
||||
aria-label={t("settings.appearance.textSize")}
|
||||
className="w-full accent-accent"
|
||||
/>
|
||||
<div className="text-right text-xs text-muted">{Math.round(theme.fontScale * 100)}%</div>
|
||||
</Section>
|
||||
@@ -282,15 +280,14 @@ function Notifications({ prefs }: { prefs: PrefsController }) {
|
||||
<span className="text-muted text-xs">{t("settings.notifications.dismissAfter")}</span>
|
||||
<span className="text-muted text-xs">{(cfg.durationMs / 1000).toFixed(0)}s</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
<Slider
|
||||
className="mt-1"
|
||||
min={2000}
|
||||
max={15000}
|
||||
step={1000}
|
||||
value={cfg.durationMs}
|
||||
onChange={(e) => update({ durationMs: Number(e.target.value) })}
|
||||
onChange={(v) => update({ durationMs: v })}
|
||||
aria-label={t("settings.notifications.dismissAfter")}
|
||||
className="w-full accent-accent mt-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -481,22 +478,22 @@ function SignInMethods({ me }: { me: Me }) {
|
||||
{open && (
|
||||
<form onSubmit={submit} className="space-y-2 pt-1 pb-1">
|
||||
{me.has_password && (
|
||||
<input
|
||||
<Input
|
||||
className="w-full"
|
||||
type="password"
|
||||
value={current}
|
||||
onChange={(e) => setCurrent(e.target.value)}
|
||||
placeholder={t("settings.account.password.current")}
|
||||
autoComplete="current-password"
|
||||
className={inputCls}
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
<Input
|
||||
className="w-full"
|
||||
type="password"
|
||||
value={next}
|
||||
onChange={(e) => setNext(e.target.value)}
|
||||
placeholder={t("settings.account.password.new")}
|
||||
autoComplete="new-password"
|
||||
className={inputCls}
|
||||
/>
|
||||
{err && (
|
||||
<p role="alert" className="text-xs text-danger">
|
||||
@@ -731,12 +728,12 @@ export function DownloadAuth() {
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* Write-only: the field is ALWAYS empty on open — the stored blob is never
|
||||
fetched or prefilled. */}
|
||||
<textarea
|
||||
<Textarea
|
||||
className="w-full font-mono resize-y"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
rows={5}
|
||||
placeholder={t("settings.downloadAuth.placeholder")}
|
||||
className={`${inputCls} font-mono text-xs resize-y`}
|
||||
data-testid={`cookie-input-${h.host_key}`}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
|
||||
Reference in New Issue
Block a user