Files
siftlode/frontend/src/design-lab/batchCActions.tsx
T
peter 4aaaaa7860 chore(comments): drop epic, sprint and review-round ids from the code
The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").

Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.

Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).

One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
2026-10-05 00:19:39 +02:00

405 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import clsx from "clsx";
import { Bookmark, Check, ListPlus, MoreHorizontal, Play } from "lucide-react";
import { Reference, Sample } from "./labKit";
import { LAYER } from "./labTokens";
import { iconButtonClass } from "../components/ui/buttonStyles";
import { SURFACE } from "../components/ui/surfaces";
// Batch C, part 1 — action placement: the classification table (object × action → where it
// lives), DECIDED (owner, 2026-09-24: accepted as proposed). It is the source each object's
// ONE action list is built from. It applies
// the decided guard rails (DESIGN.md "Action placement"): ONE action list per object feeds every
// trigger; the primary action and visible state stay on the object; rare, secondary and destructive
// actions go to the menu; nothing is gesture-only, because the "…" button opens the same menu.
type Where = "rest" | "hover" | "menu";
type Row = {
action: string;
/** mouse: visible at rest / on hover / only in the menu (the menu always lists everything) */
mouse: Where;
/** touch card / row: visible, or reached through "…" */
touch: "visible" | "…";
/** what changes against today's actions, empty when nothing does */
change?: string;
danger?: boolean;
};
type Group = { object: string; rows: Row[] };
const TABLE: Group[] = [
{
object: "Video card (feed grid and list rows, Home shelves)",
rows: [
{
action: "Play / continue",
mouse: "rest",
touch: "visible",
change:
"the thumbnail plays; the title is a link-styled element that ignores a click ending a text selection",
},
{ action: "Open channel", mouse: "rest", touch: "visible" },
{
action: "Watched (toggle)",
mouse: "hover",
touch: "…",
change: "the watched / progress STATE stays visible at rest",
},
{ action: "Save (watch later)", mouse: "hover", touch: "…" },
{
action: "Add to playlist",
mouse: "hover",
touch: "…",
change: "today hover only, not in the menu",
},
{
action: "Download",
mouse: "menu",
touch: "…",
change: "today hover only, not in the menu",
},
{
action: "Details (ⓘ)",
mouse: "menu",
touch: "…",
change: "today hover only, gone on touch",
},
{
action: "Hide",
mouse: "menu",
touch: "…",
change: "leaves the hover bar (undo via the toast)",
},
{ action: "Reset state", mouse: "menu", touch: "…", change: "leaves the hover bar" },
{ action: "Pin to shelf", mouse: "menu", touch: "…" },
{
action: "Copy title / Copy link",
mouse: "menu",
touch: "…",
change: "new (text stays selectable)",
},
],
},
{
object: "Playlist item row",
rows: [
{ action: "Play", mouse: "rest", touch: "visible" },
{
action: "Reorder (drag grip)",
mouse: "rest",
touch: "visible",
change: "the grip gets touch-action: none",
},
{ action: "Move top / up / down / bottom", mouse: "menu", touch: "…" },
{ action: "Move to… / to a recent playlist", mouse: "menu", touch: "…" },
{ action: "Pin", mouse: "menu", touch: "…" },
{ action: "Copy title / Copy link", mouse: "menu", touch: "…", change: "new" },
{
action: "Remove from playlist",
mouse: "menu",
touch: "…",
danger: true,
change: "the X column goes; Delete key + undo toast",
},
],
},
{
object: "Playlist (the rail row and the page header)",
rows: [
{ action: "Open", mouse: "rest", touch: "visible" },
{ action: "Play all", mouse: "rest", touch: "visible", change: "header" },
{
action: "Rename",
mouse: "menu",
touch: "…",
change: "the header pencil becomes a menu item",
},
{ action: "Pin", mouse: "menu", touch: "…" },
{
action: "Open on YouTube / Push to YouTube",
mouse: "menu",
touch: "…",
change: "from the header into the menu",
},
{ action: "Revert to YouTube", mouse: "menu", touch: "…", danger: true },
{
action: "Delete",
mouse: "menu",
touch: "…",
danger: true,
change: "the header trash goes into the menu",
},
],
},
{
object: "Channel row (Channels manager)",
rows: [
{ action: "Open the channel", mouse: "rest", touch: "visible" },
{
action: "Priority (stepper)",
mouse: "rest",
touch: "visible",
change: "it is a value, so it stays",
},
{ action: "Tags", mouse: "rest", touch: "visible", change: "a value, stays" },
{
action: "Hidden from feed (state)",
mouse: "rest",
touch: "visible",
change: "a badge; the toggle moves to the menu",
},
{
action: "Hide / unhide, Backfill",
mouse: "menu",
touch: "…",
change: "from row buttons into the menu",
},
{ action: "Pin, Copy name / link", mouse: "menu", touch: "…" },
{
action: "Reset backfill (admin), Unsubscribe",
mouse: "menu",
touch: "…",
danger: true,
change: "from row buttons into the menu",
},
],
},
{
object: "Plex card (movie, show, season, episode)",
rows: [
{ action: "Open / play", mouse: "rest", touch: "visible" },
{
action: "Watched (state)",
mouse: "rest",
touch: "visible",
change: "the badge stays; the toggle is hover + menu",
},
{
action: "Watched (toggle)",
mouse: "hover",
touch: "…",
change: "today hover only, not in the menu",
},
{
action: "Add to playlist",
mouse: "hover",
touch: "…",
change: "today hover only on seasons",
},
{ action: "Details", mouse: "menu", touch: "…", change: "today hover only" },
{ action: "Pin", mouse: "menu", touch: "…" },
],
},
{
object: "Download row",
rows: [
{ action: "Play (library, shared)", mouse: "rest", touch: "visible" },
{
action: "Pause / resume / retry (queue)",
mouse: "rest",
touch: "visible",
change: "the queue's primary action",
},
{ action: "Save file", mouse: "rest", touch: "visible" },
{ action: "Error details (state)", mouse: "rest", touch: "visible" },
{
action: "Edit clip, Edit details, Share, Manage shares, Redownload, Copy source URL",
mouse: "menu",
touch: "…",
change: "six row buttons become one '…'; a menu is new here",
},
{
action: "Cancel, Delete, Remove shared",
mouse: "menu",
touch: "…",
danger: true,
change: "from row buttons into the menu",
},
],
},
{
object: "Notification row",
rows: [
{
action: "Its link (open Downloads / Scheduler, review access…)",
mouse: "rest",
touch: "visible",
},
{ action: "Unread (state), Mark read", mouse: "rest", touch: "visible" },
{
action: "Revert / find (activity rows)",
mouse: "menu",
touch: "…",
change: "a menu is new here",
},
{ action: "Dismiss", mouse: "hover", touch: "…" },
{ action: "Header: Mark all read", mouse: "rest", touch: "visible" },
{
action: "Header: Clear everything",
mouse: "menu",
touch: "…",
danger: true,
change: "into the header's '…'",
},
],
},
{
object: "Scheduler job row and page",
rows: [
{ action: "Run now", mouse: "rest", touch: "visible" },
{ action: "Interval (value, click to edit)", mouse: "rest", touch: "visible" },
{ action: "Page: Run all, Pause / resume", mouse: "rest", touch: "visible" },
{
action: "Page: Purge discovery",
mouse: "menu",
touch: "…",
danger: true,
change: "into the page's '…'",
},
],
},
];
const BACKGROUND: [string, string][] = [
["Feed", "Sort · View (grid / list) · Watch state · Shorts on / off"],
["Plex browse", "Sort · View"],
["Channels", "Sort · Sync subscriptions · Backfill everything"],
["Playlists", "New playlist · Sync with YouTube"],
["Downloads", "Queue / Library / Shared (the tabs)"],
["Notifications", "Mark all read"],
];
const MOUSE: Record<Where, string> = {
rest: "● at rest",
hover: "◐ on hover",
menu: "☰ menu",
};
function Table() {
return (
<div className="overflow-x-auto rounded-xl border border-border/60">
<table className="w-full text-left text-xs">
<thead className="text-muted">
<tr className="[&>th]:px-2 [&>th]:py-1.5 [&>th]:font-medium">
<th>Action</th>
<th>Mouse</th>
<th>Touch</th>
<th>Change against today</th>
</tr>
</thead>
{TABLE.map((g) => (
<tbody key={g.object} className="border-t border-border/60">
<tr>
<th colSpan={4} className="bg-card/60 px-2 py-1.5 font-semibold">
{g.object}
</th>
</tr>
{g.rows.map((r) => (
<tr key={r.action} className="[&>td]:px-2 [&>td]:py-1 align-top">
<td className={clsx(r.danger && "text-danger")}>
{r.action}
{r.danger && " ⚠"}
</td>
<td className="whitespace-nowrap">{MOUSE[r.mouse]}</td>
<td className="whitespace-nowrap">
{r.touch === "visible" ? "● visible" : "… menu"}
</td>
<td className="text-muted">{r.change}</td>
</tr>
))}
</tbody>
))}
</table>
</div>
);
}
function Card({ touch }: { touch: boolean }) {
// The IconButton `media` face (a dark button on a picture); the touch rendering shows the 44 box
// that a coarse pointer gives it (A3).
const act = clsx(iconButtonClass({ variant: "media", size: "sm" }), touch && "size-11");
return (
<div className={`${SURFACE.card} group flex w-60 flex-col gap-1.5 p-1.5`}>
<div className="relative aspect-video overflow-hidden rounded-lg bg-linear-to-br from-accent/50 to-card">
<span className="absolute inset-x-0 bottom-0 h-1 bg-white/25">
<span className="block h-1 w-1/3 bg-accent" />
</span>
<span className="absolute left-1.5 top-1.5 rounded-full bg-black/60 px-1.5 py-0.5 text-3xs text-white">
12:04 left
</span>
{!touch && (
<span className="absolute bottom-2 right-1.5 flex gap-1">
<span className={act}>
<Check className="h-3.5 w-3.5" />
</span>
<span className={act}>
<Bookmark className="h-3.5 w-3.5" />
</span>
<span className={act}>
<ListPlus className="h-3.5 w-3.5" />
</span>
<span className={act}>
<MoreHorizontal className="h-3.5 w-3.5" />
</span>
</span>
)}
<span className="absolute inset-0 grid place-items-center">
<span className="grid h-10 w-10 place-items-center rounded-full bg-black/55 text-white">
<Play className="h-5 w-5" />
</span>
</span>
</div>
<div className="flex items-start gap-1">
<div className="min-w-0 flex-1 px-0.5">
<div className="line-clamp-2 select-text text-xs font-medium">
Building a home lab on a budget: Proxmox, ZFS and a cheap NAS
</div>
<div className="text-2xs text-muted">Techno Tim · 2 days ago</div>
</div>
{touch && (
<span
className={clsx(
"grid shrink-0 place-items-center rounded-lg text-muted",
"h-11 w-11",
LAYER,
)}
>
<MoreHorizontal className="h-4 w-4" />
</span>
)}
</div>
</div>
);
}
export function C1() {
return (
<Reference
id="c1"
code="C1"
title="Action placement: the classification"
rule="The menu (right-click, long-press, Shift+F10 or the '…' button) lists ALL of an object's actions from one list; the table says what is ALSO visible. Mouse: the primary action and the state at rest, the everyday toggles on hover. Touch: the state and a 44px '…'. Destructive row buttons move into the menu."
>
<div className="flex flex-wrap gap-4">
<Sample label="Card · mouse, hovered">
<Card touch={false} />
</Sample>
<Sample label="Card · touch">
<Card touch />
</Sample>
<Sample label="The background menu (desktop only; it mirrors the toolbar, Shift+right-click = the browser's)">
<table className="text-xs">
<tbody>
{BACKGROUND.map(([page, items]) => (
<tr key={page} className="align-top [&>td]:py-1 [&>td]:pr-3">
<td className="whitespace-nowrap font-medium">{page}</td>
<td className="text-muted">{items}</td>
</tr>
))}
</tbody>
</table>
</Sample>
</div>
<Table />
</Reference>
);
}