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:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user