feat(feed): toolbar pills named by a glyph, Sort and View on the filters' row

The watch-state, content-type and sort pills on the feed, a channel page and
Plex Browse show a glyph where their group's word was; the word moves to the
tooltip and stays in the accessible name. Sort + View end the toolbar's row
instead of a row of their own, the chip row shows only with a chip, and a
channel page drops the count pill its header already carries.

Also logs the docked bar's glyph-only decision in DESIGN.md.
This commit is contained in:
2026-11-01 04:18:01 +01:00
parent e9ce989998
commit a29afd916d
8 changed files with 282 additions and 54 deletions
+10 -3
View File
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useState, type ReactNode } from "react";
import { MenuCheck } from "./MenuCheck";
import { SelectTrigger } from "./SelectTrigger";
import { GroupGlyph, SelectTrigger } from "./SelectTrigger";
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
import { useAnchoredPopover } from "./anchoredPopoverHooks";
import { SURFACE } from "./surfaces";
@@ -27,6 +27,10 @@ export interface FilterSelectProps {
/** Show only the value on the trigger — for a form row whose own caption already names the field.
* The label still names the control and its menu for assistive tech. */
hideLabel?: boolean;
/** A glyph that names the group in place of its word, for a toolbar where the value says enough
* once the group is recognisable (the feed's watch state: an eye). The word moves to the tooltip
* and stays in the accessible name. */
icon?: ReactNode;
options: readonly FilterSelectOption[];
selected: string[];
onChange: (next: string[]) => void;
@@ -46,6 +50,7 @@ export default function FilterSelect({
multiple = false,
requireOne = true,
hideLabel = false,
icon,
testId,
}: FilterSelectProps) {
const [open, setOpen] = useState(false);
@@ -95,7 +100,9 @@ export default function FilterSelect({
<SelectTrigger
context="toolbar"
open={open}
label={hideLabel ? undefined : label}
label={hideLabel || icon ? undefined : label}
icon={icon && <GroupGlyph>{icon}</GroupGlyph>}
title={icon ? label : undefined}
ref={setReference}
{...pop.getReferenceProps()}
data-testid={testId}
@@ -12,6 +12,16 @@ import { FocusSafeButton } from "./FocusSafeButton";
// the pills' faint edge, its label naming it (owner decision F14). Both hover by the accent edge. Open is
// the accent edge and the chevron turned over. The popover wiring
// (ref, `getReferenceProps`, the accessible name carrying the value) is the caller's.
/** A toolbar trigger's group glyph in place of its "Label:" word (FilterSelect's and SortControl's
* `icon`): muted like the word, and silent — the trigger's accessible name and tooltip say it. */
export function GroupGlyph({ children }: { children: ReactNode }) {
return (
<span aria-hidden="true" className="flex shrink-0 text-muted [&>svg]:size-3.5">
{children}
</span>
);
}
export const SelectTrigger = forwardRef<
HTMLButtonElement,
ButtonHTMLAttributes<HTMLButtonElement> & {
+8 -2
View File
@@ -1,7 +1,7 @@
import { useState, type ReactNode } from "react";
import { ArrowDown, ArrowUp } from "lucide-react";
import { MenuCheck } from "./MenuCheck";
import { SelectTrigger } from "./SelectTrigger";
import { GroupGlyph, SelectTrigger } from "./SelectTrigger";
import { pickSort, type SortOption, type SortState } from "../../lib/columnSort";
import { PopoverItem, PopoverSurface } from "./AnchoredPopover";
import { useAnchoredPopover } from "./anchoredPopoverHooks";
@@ -27,6 +27,7 @@ export default function SortControl({
descLabel,
defaultLabel,
trailing,
icon,
}: {
options: SortOption[];
sort: SortState;
@@ -40,6 +41,9 @@ export default function SortControl({
defaultLabel?: string;
/** Rendered in the panel footer beside the direction segment (feed reshuffle, playlists dirty-first). */
trailing?: ReactNode;
/** A glyph in place of the visible "Sort:" prefix (FilterSelect's `icon`): the label moves to the
* tooltip and stays in the accessible name. */
icon?: ReactNode;
}) {
const [open, setOpen] = useState(false);
const pop = useAnchoredPopover({
@@ -71,7 +75,9 @@ export default function SortControl({
<SelectTrigger
context="toolbar"
open={open}
label={label}
label={icon ? undefined : label}
icon={icon && <GroupGlyph>{icon}</GroupGlyph>}
title={icon ? label : undefined}
ref={setReference}
{...pop.getReferenceProps()}
// Carry the active sort key AND direction in the accessible name (the visible arrow is