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
+27 -3
View File
@@ -213,20 +213,44 @@ function focusItem(el: HTMLElement, reveal = true): void {
if (reveal) el.scrollIntoView?.({ block: "nearest" });
}
function focusIndex(list: NavList, index: number): void {
function focusIndex(
list: NavList,
index: number,
land: (el: HTMLElement) => void = (el) => focusItem(el),
): void {
const el = itemAt(list, index);
if (el) return focusItem(el);
if (el) return land(el);
// Virtualized and not mounted: have the list scroll it in, then take it as soon as it renders.
list.reveal(index);
let frames = 0;
const wait = () => {
const got = itemAt(list, index);
if (got) focusItem(got);
if (got) land(got);
else if (++frames < REVEAL_FRAMES) requestAnimationFrame(wait);
};
requestAnimationFrame(wait);
}
/** Hand the focus to item `index` of the list registered inside `within` — or, with `pick`, to the
* control `pick` finds in it (the row's own "…") — bringing a virtualized item into the DOM first
* and scrolling it just far enough to be seen. For a caller that MOVED an item and follows it: a
* channel re-sorted by the priority its menu set, whose node may have been re-inserted (the focus
* then fell to <body>) or scrolled off. */
export function focusNavItem(
within: HTMLElement,
index: number,
pick?: (item: HTMLElement) => HTMLElement | null,
): void {
const list = [...lists.values()].find((l) => within.contains(l.el));
if (!list) return;
focusIndex(list, index, (item) => {
const to = pick?.(item);
if (!to) return focusItem(item);
to.focus({ preventScroll: true });
item.scrollIntoView?.({ block: "nearest" });
});
}
function step(delta: 1 | -1): void {
const cur = current();
if (!cur) {