refactor(design): one drawn check for every checkable row that cannot be an input (E28 S3e3b)
ui/MenuCheck: a box filled with the accent for a set (a minus for a group only partly in), a bare tick for one choice, in the row's leading slot. Replaces seven hand-drawn marks: three box sizes, a tinted box, a cross for "on" in the table filter, a trailing tick in the language menu.
This commit is contained in:
@@ -2,7 +2,8 @@ import { useLayoutEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { Check, ListPlus, Plus, Youtube } from "lucide-react";
|
||||
import { ListPlus, Plus, Youtube } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { api, type Playlist } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
@@ -177,13 +178,7 @@ function MembershipPanel({
|
||||
label={playlistName(pl, t)}
|
||||
className="w-full flex items-center gap-2 text-sm px-1.5 py-1.5 rounded-lg text-fg outline-hidden transition"
|
||||
>
|
||||
<span
|
||||
className={`grid place-items-center w-4 h-4 rounded border ${
|
||||
pl.has_video ? "bg-accent border-accent text-accent-fg" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{pl.has_video && <Check className="w-3 h-3" />}
|
||||
</span>
|
||||
<MenuCheck kind="many" on={!!pl.has_video} />
|
||||
<span className="truncate flex-1 text-left">{playlistName(pl, t)}</span>
|
||||
{(pl.source === "youtube" || pl.yt_playlist_id) && (
|
||||
<Youtube className="w-3 h-3 shrink-0 text-muted" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Check, Plus, Sparkles } from "lucide-react";
|
||||
import { Plus, Sparkles } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type Tag } from "../lib/api";
|
||||
import { PopoverSurface, PopoverItem } from "./ui/AnchoredPopover";
|
||||
@@ -232,13 +233,7 @@ function TagEditorMenu({
|
||||
on ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-3.5 h-3.5 rounded border flex items-center justify-center shrink-0 ${
|
||||
on ? "bg-accent border-accent text-accent-fg" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{on && <Check className="w-2.5 h-2.5" />}
|
||||
</span>
|
||||
<MenuCheck kind="many" on={on} />
|
||||
<span className="truncate">{tg.name}</span>
|
||||
</PopoverItem>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CalendarDays, Check } from "lucide-react";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
import { type FeedFilters } from "../lib/api";
|
||||
@@ -132,7 +133,7 @@ export default function DateFilterSelect({
|
||||
on ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
<Check className={`h-3.5 w-3.5 shrink-0 ${on ? "text-accent" : "opacity-0"}`} />
|
||||
<MenuCheck kind="one" on={on} />
|
||||
<span className="flex-1 truncate">{o.label}</span>
|
||||
</PopoverItem>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Check, Globe } from "lucide-react";
|
||||
import { Globe } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { LANGUAGES, type LangCode } from "../i18n";
|
||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||
@@ -83,10 +84,10 @@ export default function LanguageSwitcher({
|
||||
setOpen(false);
|
||||
}}
|
||||
label={l.label}
|
||||
className="w-full flex items-center justify-between gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg"
|
||||
className="w-full flex items-center gap-2 text-sm px-2 py-2 rounded-lg text-muted outline-hidden transition hover:text-fg"
|
||||
>
|
||||
<MenuCheck kind="one" on={l.code === value} />
|
||||
<span>{l.label}</span>
|
||||
{l.code === value && <Check className="w-4 h-4 text-accent" />}
|
||||
</PopoverItem>
|
||||
))}
|
||||
</PopoverSurface>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMemo, useState } from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, Plus } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { api } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
@@ -202,13 +203,7 @@ export default function PlexCollectionEditor({
|
||||
inIt ? "text-accent" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`grid h-5 w-5 shrink-0 place-items-center rounded border ${
|
||||
inIt ? "border-accent bg-accent/20" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{inIt && <Check className="h-3.5 w-3.5" />}
|
||||
</span>
|
||||
<MenuCheck kind="many" on={inIt} />
|
||||
<span className="truncate">{c.title}</span>
|
||||
<span className="shrink-0 text-xs text-muted">
|
||||
{t("plex.collEditor.count", { count: c.child_count ?? 0 })}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useState } from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, Minus, Plus } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { MenuCheck } from "./ui/MenuCheck";
|
||||
import { api, type PlexPlaylist } from "../lib/api";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
@@ -158,17 +159,7 @@ export default function PlexPlaylistAdd({
|
||||
all ? "text-accent" : ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`grid h-5 w-5 shrink-0 place-items-center rounded border ${
|
||||
all || partial ? "border-accent bg-accent/20" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{all ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : partial ? (
|
||||
<Minus className="h-3.5 w-3.5" />
|
||||
) : null}
|
||||
</span>
|
||||
<MenuCheck kind="many" on={all ? true : partial ? "mixed" : false} />
|
||||
<span className="min-w-0 flex-1 truncate">{p.title}</span>
|
||||
{isGroup ? (
|
||||
<span className="shrink-0 text-xs text-muted tabular-nums">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Check, Minus } from "lucide-react";
|
||||
|
||||
// The ONE drawn check of a checkable row that cannot hold the browser's own checkbox (A6) — a menu
|
||||
// row, or a whole-row button whose `role` says checkbox: an `<input>` inside a `<button>` is invalid
|
||||
// nesting. It sits in the row's leading icon slot, so every label in a list starts at the same x.
|
||||
// - `many`: one of a set that can all be on (tags, playlists, a filter's values) — a box, filled
|
||||
// with the accent when on; `mixed` for a group only partly in (a Plex show's episodes).
|
||||
// - `one`: one choice of several (a language, a sort, a priority step) — a bare tick; an unticked
|
||||
// choice keeps the slot empty.
|
||||
// Decorative: the row carries the state (`aria-checked` on its menuitemcheckbox / radio / checkbox).
|
||||
export function MenuCheck({ kind, on }: { kind: "many" | "one"; on: boolean | "mixed" }) {
|
||||
if (kind === "one")
|
||||
return (
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className={`size-4 shrink-0 ${on === true ? "text-accent" : "opacity-0"}`}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`grid size-4 shrink-0 place-items-center rounded border ${
|
||||
on ? "border-accent bg-accent text-accent-fg" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{on === "mixed" ? <Minus className="size-3" /> : on && <Check className="size-3" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ domain-free.
|
||||
| area | components |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| overlays & anchoring | `AnchoredPopover` + `anchoredPopoverHooks` (the popover engine: arrows, typeahead, Escape, anchor-flip), `ActionMenu`, `ContextMenu` + `useContextMenu` (the SAME menu, opened by right-click / long-press / Shift+F10 / a row's `MoreButton` "…" via `buttonProps`; `anchor` for a panel a row opens), `Modal`, `Overlay`, `SidePanel`, `Tooltip`, `Toaster` (+ `toastLevels`, `overlayOrder`; `lib/toastDock` docks one control under the stack, in every host), `CommandPalette`, `KeyboardHelp` |
|
||||
| actions & inputs | `Button`, `IconButton`, `Pressable`, `Chip` + `chipStyles`, `DragHandle` (one per button family, docs/DESIGN.md; a raw `<button>` outside this folder is a lint error), `Input` + `fieldStyles`, `NativeControls`, `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the |
|
||||
| actions & inputs | `Button`, `IconButton`, `Pressable`, `Chip` + `chipStyles`, `DragHandle` (one per button family, docs/DESIGN.md; a raw `<button>` outside this folder is a lint error), `Input` + `fieldStyles`, `NativeControls`, `MenuCheck` (the drawn check of a menu row or a whole-row checkbox button), `SelectTrigger` (the field family; a raw `<input>` is a lint error), `useReturnFocus`, `surfaces` (the |
|
||||
|
||||
B5 glass roles; a bare glass class is a lint error), `Badge` (a static label), `LinkButton` / `IconLink` (a
|
||||
link wearing the button face) over `buttonStyles`, `Skeleton` (B8 first loads, through `LoadingState shape`), `NavRail` (B7), `form`, `DraftSaveBar`, `Combobox`, `FilterSelect`, `SegmentedToggle`, `SortControl`, `ColumnSortControl`, `PriorityStepper`, `UndoToolbar` |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { ArrowDown, ArrowUp, Check } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { MenuCheck } from "./MenuCheck";
|
||||
import { SelectTrigger } from "./SelectTrigger";
|
||||
import { pickSort, type SortOption, type SortState } from "../../lib/columnSort";
|
||||
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||
@@ -112,9 +113,7 @@ export default function SortControl({
|
||||
active === null ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
<Check
|
||||
className={`h-3.5 w-3.5 shrink-0 ${active === null ? "text-accent" : "opacity-0"}`}
|
||||
/>
|
||||
<MenuCheck kind="one" on={active === null} />
|
||||
<span className="flex-1 truncate">{defaultLabel}</span>
|
||||
</PopoverItem>
|
||||
)}
|
||||
@@ -131,7 +130,7 @@ export default function SortControl({
|
||||
on ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
<Check className={`h-3.5 w-3.5 shrink-0 ${on ? "text-accent" : "opacity-0"}`} />
|
||||
<MenuCheck kind="one" on={on} />
|
||||
<span className="flex-1 truncate">{o.label}</span>
|
||||
</PopoverItem>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDown, ArrowUp, ListFilter, X } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, ListFilter } from "lucide-react";
|
||||
import { MenuCheck } from "./MenuCheck";
|
||||
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||
import { useScrollFade } from "../../lib/useScrollFade";
|
||||
@@ -158,13 +159,7 @@ function FilterFunnel<T>({
|
||||
on ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${
|
||||
on ? "bg-accent border-accent text-accent-fg" : "border-border"
|
||||
}`}
|
||||
>
|
||||
{on && <X className="w-2.5 h-2.5" />}
|
||||
</span>
|
||||
<MenuCheck kind="many" on={on} />
|
||||
{o.label}
|
||||
</PopoverItem>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user