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 { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { qk } from "../lib/queryKeys";
|
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 { api, type Playlist } from "../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../lib/actionErrors";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
@@ -177,13 +178,7 @@ function MembershipPanel({
|
|||||||
label={playlistName(pl, t)}
|
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"
|
className="w-full flex items-center gap-2 text-sm px-1.5 py-1.5 rounded-lg text-fg outline-hidden transition"
|
||||||
>
|
>
|
||||||
<span
|
<MenuCheck kind="many" on={!!pl.has_video} />
|
||||||
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>
|
|
||||||
<span className="truncate flex-1 text-left">{playlistName(pl, t)}</span>
|
<span className="truncate flex-1 text-left">{playlistName(pl, t)}</span>
|
||||||
{(pl.source === "youtube" || pl.yt_playlist_id) && (
|
{(pl.source === "youtube" || pl.yt_playlist_id) && (
|
||||||
<Youtube className="w-3 h-3 shrink-0 text-muted" />
|
<Youtube className="w-3 h-3 shrink-0 text-muted" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRef, useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { type Tag } from "../lib/api";
|
import { type Tag } from "../lib/api";
|
||||||
import { PopoverSurface, PopoverItem } from "./ui/AnchoredPopover";
|
import { PopoverSurface, PopoverItem } from "./ui/AnchoredPopover";
|
||||||
@@ -232,13 +233,7 @@ function TagEditorMenu({
|
|||||||
on ? "text-fg" : "text-muted"
|
on ? "text-fg" : "text-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<MenuCheck kind="many" on={on} />
|
||||||
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>
|
|
||||||
<span className="truncate">{tg.name}</span>
|
<span className="truncate">{tg.name}</span>
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useLayoutEffect, useState } from "react";
|
import { useLayoutEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||||
import { type FeedFilters } from "../lib/api";
|
import { type FeedFilters } from "../lib/api";
|
||||||
@@ -132,7 +133,7 @@ export default function DateFilterSelect({
|
|||||||
on ? "text-fg" : "text-muted"
|
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>
|
<span className="flex-1 truncate">{o.label}</span>
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
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 { LANGUAGES, type LangCode } from "../i18n";
|
||||||
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "./ui/AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./ui/anchoredPopoverHooks";
|
||||||
@@ -83,10 +84,10 @@ export default function LanguageSwitcher({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
label={l.label}
|
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>
|
<span>{l.label}</span>
|
||||||
{l.code === value && <Check className="w-4 h-4 text-accent" />}
|
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
))}
|
))}
|
||||||
</PopoverSurface>
|
</PopoverSurface>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useMemo, useState } from "react";
|
|||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { api } from "../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../lib/actionErrors";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
@@ -202,13 +203,7 @@ export default function PlexCollectionEditor({
|
|||||||
inIt ? "text-accent" : ""
|
inIt ? "text-accent" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<MenuCheck kind="many" on={inIt} />
|
||||||
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>
|
|
||||||
<span className="truncate">{c.title}</span>
|
<span className="truncate">{c.title}</span>
|
||||||
<span className="shrink-0 text-xs text-muted">
|
<span className="shrink-0 text-xs text-muted">
|
||||||
{t("plex.collEditor.count", { count: c.child_count ?? 0 })}
|
{t("plex.collEditor.count", { count: c.child_count ?? 0 })}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useState } from "react";
|
|||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
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 { api, type PlexPlaylist } from "../lib/api";
|
||||||
import { notifyActionError } from "../lib/actionErrors";
|
import { notifyActionError } from "../lib/actionErrors";
|
||||||
import { useScrollFade } from "../lib/useScrollFade";
|
import { useScrollFade } from "../lib/useScrollFade";
|
||||||
@@ -158,17 +159,7 @@ export default function PlexPlaylistAdd({
|
|||||||
all ? "text-accent" : ""
|
all ? "text-accent" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<MenuCheck kind="many" on={all ? true : partial ? "mixed" : false} />
|
||||||
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>
|
|
||||||
<span className="min-w-0 flex-1 truncate">{p.title}</span>
|
<span className="min-w-0 flex-1 truncate">{p.title}</span>
|
||||||
{isGroup ? (
|
{isGroup ? (
|
||||||
<span className="shrink-0 text-xs text-muted tabular-nums">
|
<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 |
|
| 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` |
|
| 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
|
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` |
|
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 { 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 { SelectTrigger } from "./SelectTrigger";
|
||||||
import { pickSort, type SortOption, type SortState } from "../../lib/columnSort";
|
import { pickSort, type SortOption, type SortState } from "../../lib/columnSort";
|
||||||
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||||
@@ -112,9 +113,7 @@ export default function SortControl({
|
|||||||
active === null ? "text-fg" : "text-muted"
|
active === null ? "text-fg" : "text-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Check
|
<MenuCheck kind="one" on={active === null} />
|
||||||
className={`h-3.5 w-3.5 shrink-0 ${active === null ? "text-accent" : "opacity-0"}`}
|
|
||||||
/>
|
|
||||||
<span className="flex-1 truncate">{defaultLabel}</span>
|
<span className="flex-1 truncate">{defaultLabel}</span>
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
)}
|
)}
|
||||||
@@ -131,7 +130,7 @@ export default function SortControl({
|
|||||||
on ? "text-fg" : "text-muted"
|
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>
|
<span className="flex-1 truncate">{o.label}</span>
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRef, useState, type ReactNode } from "react";
|
import { useRef, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { PopoverItem, PopoverSurface } from "./AnchoredPopover";
|
||||||
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
import { useAnchoredPopover } from "./anchoredPopoverHooks";
|
||||||
import { useScrollFade } from "../../lib/useScrollFade";
|
import { useScrollFade } from "../../lib/useScrollFade";
|
||||||
@@ -158,13 +159,7 @@ function FilterFunnel<T>({
|
|||||||
on ? "text-fg" : "text-muted"
|
on ? "text-fg" : "text-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<MenuCheck kind="many" on={on} />
|
||||||
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>
|
|
||||||
{o.label}
|
{o.label}
|
||||||
</PopoverItem>
|
</PopoverItem>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user