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.
105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
import { forwardRef } from "react";
|
|
import type { ButtonHTMLAttributes, ReactNode, RefAttributes } from "react";
|
|
import clsx from "clsx";
|
|
import { Loader2 } from "lucide-react";
|
|
import { FocusSafeButton } from "./FocusSafeButton";
|
|
import { buttonClass, isLinkVariant, type ButtonSize, type ButtonVariant } from "./buttonStyles";
|
|
|
|
// The ONE action-button primitive: a labelled action (Save, Cancel, Subscribe, Delete, "Show all").
|
|
// docs/DESIGN.md decides its look (A1 shape, B1 ladder, B3 state layers); this file is where that
|
|
// holds. The app's other button families have their own primitives beside it: `IconButton` (a glyph
|
|
// with no text), `Pressable` (a card, row or tile whose content is the control), `Chip` and
|
|
// `FilterChipGroup` (filters), `SegmentedToggle`, `DragHandle`. A hand-rolled `<button>` is none of
|
|
// them.
|
|
//
|
|
// No focus-visible here on purpose: @layer base in index.css already rings every <button> for keyboard
|
|
// users. Adding one would double the ring.
|
|
|
|
// A toggle says so through `pressed`, never a bare `aria-pressed`: the prop also paints the pressed
|
|
// state (B3), and a raw attribute left the Server log's Live toggle looking like any outline button.
|
|
interface ButtonOwnProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "aria-pressed"> {
|
|
variant?: ButtonVariant;
|
|
/** Defaults to "md", except for the link variants, whose default is "none" (a link owns no box). */
|
|
size?: ButtonSize;
|
|
/** Leading icon (a lucide element; the primitive sizes it). Replaced by a spinner while `loading`. */
|
|
icon?: ReactNode;
|
|
/** Accessible name + tooltip, when the visible text alone is not enough. */
|
|
label?: string;
|
|
/** Busy: swap the icon for a spinner, mark aria-busy + aria-disabled, and swallow activation — but
|
|
* NOT native `disabled`, which would throw the focus out of the page (see the render below). */
|
|
loading?: boolean;
|
|
/** A toggle: sets aria-pressed and paints the pressed state (the solid accent). */
|
|
pressed?: boolean;
|
|
/** Not accepted: a toggle is `pressed` (JSX lets any undeclared `aria-*` through, so it is `never`). */
|
|
"aria-pressed"?: never;
|
|
/** Sits on a picture (a thumbnail, a poster) or inside a pill field: the pill shape. Implied by the
|
|
* `media` variant. */
|
|
round?: boolean;
|
|
fullWidth?: boolean;
|
|
/** Its text is a title or a name (a link back to a show): it stays selectable (FocusSafeButton). */
|
|
selectable?: boolean;
|
|
}
|
|
|
|
// A button of this primitive always renders text, or it is an `IconButton`. `<Button icon={x} />`
|
|
// with no children would typecheck into a nameless button, so a text-less one must be named — and
|
|
// the named, text-less shape is what `IconButton` exists for.
|
|
type NamedSomehow = { label: string } | { "aria-label": string } | { title: string };
|
|
export type ButtonProps = ButtonOwnProps & ({ children: ReactNode } | NamedSomehow);
|
|
|
|
/** The shared action button. `type` defaults to "button" so it never accidentally submits a form. */
|
|
const ButtonImpl = forwardRef<HTMLButtonElement, ButtonOwnProps>(function Button(
|
|
{
|
|
variant = "primary",
|
|
size,
|
|
icon,
|
|
label,
|
|
loading = false,
|
|
pressed,
|
|
round = false,
|
|
fullWidth = false,
|
|
className,
|
|
children,
|
|
disabled,
|
|
type,
|
|
onClick,
|
|
onFocus,
|
|
onBlur,
|
|
...rest
|
|
},
|
|
ref,
|
|
) {
|
|
// A link must not inherit the action-button box, so its size default differs. Resolved here rather
|
|
// than by asking every call site to pass `size="none"` alongside a link variant — a pairing everyone
|
|
// would eventually forget, which is how a "link" grows a button's box.
|
|
const resolvedSize: ButtonSize = size ?? (isLinkVariant(variant) ? "none" : "md");
|
|
const leading = loading ? <Loader2 className="animate-spin" /> : icon;
|
|
return (
|
|
<FocusSafeButton
|
|
ref={ref}
|
|
type={type ?? "button"}
|
|
disabled={disabled}
|
|
busy={loading}
|
|
onClick={onClick}
|
|
onFocus={onFocus}
|
|
onBlur={onBlur}
|
|
aria-label={label}
|
|
aria-pressed={pressed}
|
|
title={label}
|
|
className={clsx(
|
|
buttonClass({ variant, size: resolvedSize, round, fullWidth, pressed }),
|
|
className,
|
|
)}
|
|
{...rest}
|
|
>
|
|
{leading}
|
|
{children}
|
|
</FocusSafeButton>
|
|
);
|
|
});
|
|
|
|
// The implementation takes the permissive shape (it has to destructure every prop); the EXPORT is
|
|
// what call sites see, and that is where the "a text-less button must be named" union binds.
|
|
export const Button = ButtonImpl as (
|
|
props: ButtonProps & RefAttributes<HTMLButtonElement>,
|
|
) => ReturnType<typeof ButtonImpl>;
|