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
@@ -84,3 +84,27 @@ describe("useCardPager — an out-of-range page", () => {
expect(result.current.paged).toEqual(rows(30)); // and the rows are never sliced in table mode
});
});
describe("useCardPager — following a moved row", () => {
it("turns to the page that holds the row on each new seq, and not on mount", () => {
const { result, rerender } = renderHook(
({ list, seq }) =>
useCardPager(list, KEY, true, seq ? { seq, match: (r: number) => r === 3 } : null),
{ initialProps: { list: rows(30), seq: 0 } },
);
act(() => result.current.pagerProps.onSizeChange(10));
act(() => result.current.pagerProps.onPageChange(2));
// The row 3 sank to place 25: seq 1 follows it to the third page…
const sunk = [...rows(30).filter((r) => r !== 3)];
sunk.splice(25, 0, 3);
rerender({ list: sunk, seq: 1 });
expect(result.current.pagerProps.page).toBe(2);
// …and bubbled up to place 4: seq 2 follows it back to the first.
rerender({ list: rows(30), seq: 2 });
expect(result.current.pagerProps.page).toBe(0);
// A re-render with the same seq leaves the reader's own paging alone.
act(() => result.current.pagerProps.onPageChange(1));
rerender({ list: rows(30), seq: 2 });
expect(result.current.pagerProps.page).toBe(1);
});
});