Files
siftlode/frontend/src/components/ui/pageMenuItems.ts
T
peter 97381e65d0 feat(design): a page's background menu on the five list pages (E28 S3e4c)
A right-click on the EMPTY area of a page opens a menu of that page's own
toolbar: the feed (sort, reshuffle, view, watch state, content, source, clear
filters), the Plex library grid (sort, watch state, clear), the channel manager
(layout, the cards' sort, sync status, edit tags, clear the tag filter, read
subscriptions), a playlist (play all, group by, reset view, save order,
undo/redo, the list's own menu) and the notification center (mark all read).
No destructive row and no backfill.

PageShell listens on the page area; lib/backgroundTarget decides empty (not
text, a control, a link, media, or a row/card with its own menu). Shift+right-
click, a touch long-press and a right-click on selected words keep the
browser's menu. ui/PageMenu registers while mounted, so a loading or error
state keeps the browser's menu too. The rows mirror each control through
ui/pageMenuItems (sortSubmenu via pickSort, choiceSubmenu, multiSubmenu).
pageMenuScope.contract.test.ts pins the five pages.
2026-09-26 20:48:53 +02:00

115 lines
3.3 KiB
TypeScript

import { type ActionMenuItem } from "./ActionMenu";
import { menuGroups } from "./menuGroups";
import { pickSort, type SortOption, type SortState } from "../../lib/columnSort";
// The rows a page's background menu (./pageMenu) mirrors its toolbar with. Each takes the SAME
// vocabulary and setter as the control it mirrors (a SortControl's options, a FilterSelect's options),
// so the menu can never offer a choice the toolbar does not, or set it another way.
/** "Sort ▸": the keys as a pick-one set, then the direction when the active key has one. Picking a
* key keeps its direction, a new one starts at its default — `pickSort`, as the SortControl does.
* `defaultLabel` leads with the "no sort" choice, for a control that offers one (a table's). */
export function sortSubmenu({
id,
label,
options,
sort,
onChange,
ascLabel,
descLabel,
defaultLabel,
}: {
id: string;
label: string;
options: readonly SortOption[];
sort: SortState;
onChange: (next: SortState) => void;
ascLabel: string;
descLabel: string;
defaultLabel?: string;
}): ActionMenuItem {
const active = sort ? options.find((o) => o.key === sort.key) : undefined;
const none: ActionMenuItem[] = defaultLabel
? [
{
id: `${id}.none`,
label: defaultLabel,
check: { kind: "one", checked: !active },
onSelect: () => onChange(null),
},
]
: [];
const keys: ActionMenuItem[] = options.map((o) => ({
id: `${id}.${o.key}`,
label: o.label,
check: { kind: "one", checked: active?.key === o.key },
onSelect: () => onChange(pickSort([...options], sort, o.key)),
}));
const dirs: ActionMenuItem[] =
active && sort && !active.directionless
? (["asc", "desc"] as const).map((dir) => ({
id: `${id}.dir.${dir}`,
label: dir === "asc" ? ascLabel : descLabel,
check: { kind: "one", checked: sort.dir === dir },
onSelect: () => onChange({ key: sort.key, dir }),
}))
: [];
return { id, label, items: menuGroups([...none, ...keys], dirs) };
}
/** "Label ▸" over one choice of several (a FilterSelect without `multiple`, a view switcher). */
export function choiceSubmenu<V extends string>({
id,
label,
options,
selected,
onPick,
}: {
id: string;
label: string;
options: readonly { value: V; label: string }[];
selected: V;
onPick: (value: V) => void;
}): ActionMenuItem {
return {
id,
label,
items: options.map((o) => ({
id: `${id}.${o.value}`,
label: o.label,
check: { kind: "one", checked: o.value === selected },
onSelect: () => onPick(o.value),
})),
};
}
/** "Label ▸" over a set that can all be on (a `multiple` FilterSelect): each toggles in place. */
export function multiSubmenu<V extends string>({
id,
label,
options,
selected,
onChange,
}: {
id: string;
label: string;
options: readonly { value: V; label: string }[];
selected: readonly V[];
onChange: (next: V[]) => void;
}): ActionMenuItem {
return {
id,
label,
items: options.map((o) => {
const on = selected.includes(o.value);
return {
id: `${id}.${o.value}`,
label: o.label,
check: { kind: "many", checked: on },
onSelect: () =>
onChange(on ? selected.filter((v) => v !== o.value) : [...selected, o.value]),
};
}),
};
}