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:
2026-09-24 23:48:22 +02:00
parent 81936e9f56
commit dac9d89f91
63 changed files with 923 additions and 416 deletions
+4 -4
View File
@@ -4,6 +4,7 @@ import { BatchA } from "./batchA";
import { BatchB } from "./batchB";
import { BatchC } from "./batchC";
import { Chip } from "../components/ui/Chip";
import { Slider } from "../components/ui/NativeControls";
// The design lab (E28): the living style guide. Dev-only — see main.tsx and devOnlyGuard.ts. It sits
// OUTSIDE <App>, so it owns the theme attributes on <html> itself, through the same data-theme /
@@ -59,14 +60,13 @@ export default function DesignLab() {
</Chip>
<label className="flex items-center gap-2 text-xs text-muted">
text {look.fontScale.toFixed(2)}
<input
type="range"
<Slider
className="w-24"
min={0.9}
max={1.2}
step={0.02}
value={look.fontScale}
onChange={(e) => set({ fontScale: Number(e.target.value) })}
className="w-24 accent-accent"
onChange={(v) => set({ fontScale: v })}
/>
</label>
<nav className="flex flex-wrap gap-1 text-xs">
+42 -47
View File
@@ -12,9 +12,14 @@ import {
Trash2,
} from "lucide-react";
import { Contrast, Reference, Sample } from "./labKit";
import { H, LAYER, STRONG } from "./labTokens";
import { LAYER, STRONG } from "./labTokens";
import { Button } from "../components/ui/Button";
import { IconButton } from "../components/ui/IconButton";
import { Checkbox, Slider } from "../components/ui/NativeControls";
import { Input, InnerInput } from "../components/ui/Input";
import { fieldClass } from "../components/ui/fieldStyles";
import { SelectTrigger } from "../components/ui/SelectTrigger";
import { Switch } from "../components/ui/form";
// E28 S2 · Batch A — controls, DECIDED (owner, 2026-09-24: all six as recommended), rendered with the
// batch B decisions that refine them: B1's compact ladder and B3's state layers. Only the chosen
@@ -84,15 +89,9 @@ function TouchRow() {
function Trigger({ label, value }: { label: string; value: string }) {
return (
<span
className={clsx(
"glass-card glass-hover inline-flex items-center gap-1.5 rounded-full border px-3 text-sm",
H.bar,
)}
>
<span className="text-muted">{label}:</span> {value}
<ChevronDown className="h-3.5 w-3.5 text-muted" />
</span>
<SelectTrigger context="toolbar" open={false} label={label}>
{value}
</SelectTrigger>
);
}
@@ -101,24 +100,16 @@ function NativeSet() {
return (
<>
<label className={row}>
<input type="checkbox" defaultChecked className="h-4.5 w-4.5 accent-accent" />
<Checkbox defaultChecked />
Only unwatched
</label>
<label className={clsx(row, "justify-between")}>
<div className={clsx(row, "justify-between")}>
Hide Shorts
<span className="relative h-5 w-9 rounded-full bg-accent">
<span className="absolute right-0.5 top-0.5 h-4 w-4 rounded-full bg-accent-fg" />
</span>
</label>
<Switch checked onChange={() => {}} label="Hide Shorts" />
</div>
<label className="flex flex-col gap-1 px-2 text-sm">
Text size
<input
type="range"
min={90}
max={120}
defaultValue={102}
className="w-full accent-accent"
/>
<Slider min={90} max={120} defaultValue={102} onChange={() => {}} />
</label>
</>
);
@@ -136,14 +127,13 @@ export function BatchA() {
<Sample label="Toolbar · sm, with a search, a trigger and an icon button, all 28">
<div className="flex flex-wrap items-center gap-2">
<label
className={clsx("flex w-40 items-center gap-2 rounded-full border px-3", H.bar)}
style={{ borderColor: STRONG }}
className={clsx(
fieldClass({ size: "sm", pill: true }),
"flex w-40 items-center gap-2",
)}
>
<Search className="h-4 w-4 text-muted" />
<input
className="min-w-0 flex-1 bg-transparent text-sm outline-none"
placeholder="Search"
/>
<Search className="h-4 w-4 shrink-0 text-muted" />
<InnerInput placeholder="Search" aria-label="Search" />
</label>
<Trigger label="Sort" value="Newest" />
<Btn tone="glass" size="sm" icon={<RefreshCw className={ico} />}>
@@ -228,16 +218,27 @@ export function BatchA() {
>
<label className="flex flex-col gap-1 text-sm">
<span className="font-medium">Playlist name</span>
<input
className={clsx(
"rounded-lg border bg-transparent px-3 text-sm outline-none focus:ring-2 focus:ring-accent",
H.field,
)}
style={{ borderColor: STRONG }}
defaultValue="Evening mix"
/>
<span className="text-xs text-danger">A playlist with this name already exists.</span>
<Input defaultValue="Evening mix" className="w-full" />
</label>
<label className="flex flex-col gap-1 text-sm">
<span className="font-medium">Playlist name (invalid)</span>
<Input
invalid
defaultValue="Favourites"
className="w-full"
aria-describedby="lab-a4-err"
/>
<span id="lab-a4-err" className="text-xs text-danger">
A playlist with this name already exists.
</span>
</label>
<Sample label="Rungs · md 32, sm 28 (pill: a toolbar search), xs 24">
<div className="flex flex-wrap items-center gap-2">
<Input placeholder="md" className="w-24" />
<Input size="sm" pill placeholder="sm pill" className="w-24" />
<Input size="xs" placeholder="xs" className="w-16" />
</div>
</Sample>
<Contrast fg={STRONG} min={3} label="Edge vs page" />
</Reference>
<Reference
@@ -252,15 +253,9 @@ export function BatchA() {
</div>
<label className="flex flex-col gap-1 text-sm">
<span className="font-medium">Download format</span>
<span
className={clsx(
"glass-card flex items-center justify-between rounded-lg border px-3 text-sm",
H.field,
)}
>
<SelectTrigger context="form" open={false}>
MP4 · 1080p
<ChevronDown className="h-3.5 w-3.5 text-muted" />
</span>
</SelectTrigger>
</label>
</Reference>
<Reference