feat(design): the channel menu's Tags and Priority submenus (C1, E28 S3e3b)

Tags A-Z as in-place checks (the menu stays open for the next); Priority
as the fixed steps 10 5 3 2 1 0, an off-scale current value ticked in its
place. A pick re-sorts the list at once, moving only that row
(placeByPriority, the server's order), and the table / card pager and
the focus follow it (DataTable and useCardPager take a reveal seq,
listNav.focusNavItem lands on the row's own "…", a virtualized card
revealed first). ActionMenu rows take check: many | one. The menu reads
the row fresh from the cache and keys its target by id.
This commit is contained in:
2026-09-26 17:34:47 +02:00
parent 2b3094ffc5
commit 5e5c8a42a9
15 changed files with 467 additions and 28 deletions
+41
View File
@@ -0,0 +1,41 @@
// The channel manager's list order, and the priority steps its menu offers (E28 S3e3b).
/** The steps a channel's menu offers (owner, 2026-09-26): fixed levels, so bubbling a channel up
* the list is one pick, not a run of +1 presses, and not straight to the top either. The row's
* stepper still sets any value, negatives included. */
const PRIORITY_STEPS: readonly number[] = [10, 5, 3, 2, 1, 0];
/** The steps to offer for a channel at `current`: the fixed ones, plus `current` itself in its
* place when it is off the scale (a 7, a -2) — the menu always shows, ticked, where it is now. */
export function priorityChoices(current: number): number[] {
return PRIORITY_STEPS.includes(current)
? [...PRIORITY_STEPS]
: [...PRIORITY_STEPS, current].sort((a, b) => b - a);
}
interface Ordered {
id: string;
priority: number;
title?: string | null;
}
/** The server's order (`routes/channels.py`: priority desc, then the title case-folded, a missing
* title last). */
function before(a: Ordered, b: Ordered): boolean {
if (a.priority !== b.priority) return a.priority > b.priority;
if (a.title == null) return false;
if (b.title == null) return true;
return a.title.toLowerCase() < b.title.toLowerCase();
}
/** `rows` with the channel `id` taken out and put back where its priority now places it, every
* other row left exactly where it was — the server's order, without a refetch that would reorder
* anything the reader did not touch. */
export function placeByPriority<T extends Ordered>(rows: readonly T[], id: string): T[] {
const moved = rows.find((r) => r.id === id);
if (!moved) return [...rows];
const rest = rows.filter((r) => r !== moved);
const at = rest.findIndex((r) => before(moved, r));
rest.splice(at < 0 ? rest.length : at, 0, moved);
return rest;
}