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
+10 -9
View File
@@ -25,7 +25,8 @@ import { IconButton } from "./ui/IconButton";
import Modal from "./ui/Modal";
import { LoadingState, StateMessage } from "./ui/QueryState";
import SegmentedToggle from "./ui/SegmentedToggle";
import { inputCls } from "./ui/form";
import { Textarea, Input } from "./ui/Input";
import { Checkbox } from "./ui/NativeControls";
// The issue composer (E16 S3, D4 A): one dialog — the draft on the left (title, description, the
// steps drafted from the session, the environment), what goes with it on the right (the two logs,
@@ -472,28 +473,28 @@ function DraftFields({
<label htmlFor="issue-title" className="text-sm text-muted">
{t("recorder.composer.fieldTitle")}
</label>
<input
<Input
className="w-full"
id="issue-title"
value={form.title}
readOnly={readOnly}
maxLength={200}
placeholder={t("recorder.composer.titlePlaceholder")}
onChange={(e) => edit((f) => ({ ...f, title: e.target.value }))}
className={inputCls}
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="issue-description" className="text-sm text-muted">
{t("recorder.composer.description")}
</label>
<textarea
<Textarea
className="w-full resize-y"
id="issue-description"
value={form.description}
readOnly={readOnly}
rows={4}
placeholder={t("recorder.composer.descriptionPlaceholder")}
onChange={(e) => edit((f) => ({ ...f, description: e.target.value }))}
className={`${inputCls} resize-y`}
/>
</div>
<fieldset className="flex flex-col gap-1.5 min-w-0">
@@ -507,13 +508,14 @@ function DraftFields({
<span className="w-6 shrink-0 text-sm font-semibold text-accent tabular-nums">
{i + 1}.
</span>
<input
<Input
className="flex-1"
size="sm"
aria-label={t("recorder.composer.stepLabel", { n: i + 1 })}
value={step.text}
readOnly={readOnly}
placeholder={t("recorder.composer.stepPlaceholder")}
onChange={(e) => setStep(i, { text: e.target.value })}
className="min-w-0 flex-1 bg-transparent text-sm outline-none focus-visible:ring-2 focus-visible:ring-accent/60 rounded px-1 py-1"
/>
<span className="chip shrink-0 px-2 py-0.5 text-2xs">
{t(`recorder.composer.stepKind.${step.kind}`)}
@@ -764,8 +766,7 @@ function ReviewList({
off ? "opacity-50 line-through" : ""
}`}
>
<input
type="checkbox"
<Checkbox
checked={!off}
disabled={readOnly}
aria-label={t("recorder.composer.keepRow", {