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:
@@ -10,10 +10,11 @@ import { notifyActionError } from "../lib/actionErrors";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import { formatDate } from "../lib/format";
|
||||
import { api, type DownloadJob, type ShareLink, type ShareRecipient } from "../lib/api";
|
||||
import { inputCls } from "./ui/form";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Input, InnerInput } from "./ui/Input";
|
||||
import { CheckRow } from "./ui/NativeControls";
|
||||
const EXPIRY_DAYS = [null, 1, 7, 30] as const;
|
||||
|
||||
// --- share with a registered user (ACL) -----------------------------------------------------
|
||||
@@ -56,7 +57,8 @@ function UserShare({ job }: { job: DownloadJob }) {
|
||||
</div>
|
||||
<p className="text-xs text-muted mb-2">{t("downloads.share.userHint")}</p>
|
||||
<div className="relative">
|
||||
<input
|
||||
<Input
|
||||
className="w-full"
|
||||
value={q}
|
||||
onChange={(e) => {
|
||||
setQ(e.target.value);
|
||||
@@ -66,7 +68,6 @@ function UserShare({ job }: { job: DownloadJob }) {
|
||||
// Close on blur, but after a tick so a click on a dropdown row still registers.
|
||||
onBlur={() => setTimeout(() => setOpen(false), 150)}
|
||||
placeholder={t("downloads.share.pickPlaceholder")}
|
||||
className={inputCls}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu overflow-hidden">
|
||||
@@ -144,11 +145,7 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
||||
<div className="rounded-lg glass-card p-2.5 space-y-2">
|
||||
<div className="flex items-center gap-2 rounded-md focus-within:outline-2 focus-within:outline-accent/60 focus-within:outline-offset-2">
|
||||
<Link2 className="w-4 h-4 text-muted shrink-0" />
|
||||
<input
|
||||
readOnly
|
||||
value={fullUrl}
|
||||
className="flex-1 min-w-0 bg-transparent text-xs text-muted truncate outline-hidden"
|
||||
/>
|
||||
<InnerInput className="text-xs text-muted truncate" readOnly value={fullUrl} />
|
||||
<IconButton label={t("downloads.share.copy")} size="sm" variant="ghost" onClick={copy}>
|
||||
{copied ? <Check className="w-4 h-4 text-success" /> : <Copy className="w-4 h-4" />}
|
||||
</IconButton>
|
||||
@@ -207,14 +204,15 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
||||
</span>
|
||||
))}
|
||||
<div className="flex-1" />
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" checked={link.allow_download} onChange={() => toggle.mutate()} />
|
||||
<CheckRow checked={link.allow_download} onChange={() => toggle.mutate()}>
|
||||
{t("downloads.share.allowDownload")}
|
||||
</label>
|
||||
</CheckRow>
|
||||
</div>
|
||||
{editPass && (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
className="flex-1"
|
||||
size="sm"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={newPass}
|
||||
@@ -225,7 +223,6 @@ 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-surface border border-border rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-2 focus:ring-accent/40"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -327,14 +324,9 @@ function LinkShare({ job }: { job: DownloadJob }) {
|
||||
|
||||
{creating ? (
|
||||
<div className="mt-2 rounded-lg border border-border p-3 space-y-3">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allowDownload}
|
||||
onChange={(e) => setAllowDownload(e.target.checked)}
|
||||
/>
|
||||
<CheckRow checked={allowDownload} onChange={setAllowDownload} className="text-sm">
|
||||
{t("downloads.share.allowDownload")}
|
||||
</label>
|
||||
</CheckRow>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted">{t("downloads.share.expiry")}:</span>
|
||||
{EXPIRY_DAYS.map((d) => (
|
||||
@@ -351,12 +343,12 @@ function LinkShare({ job }: { job: DownloadJob }) {
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
<Input
|
||||
className="w-full"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("downloads.share.password")}
|
||||
className={inputCls}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
|
||||
Reference in New Issue
Block a user