Review: 5/5 lanes pass, 238/238, tsc clean. Verified live: scram duty-cycling with tanks covering (zero brownout), shipment stamps, strain fever 0.824. Found 1 real bug in review: UI BROWNOUT banner fires on covered deficit (topstrip.ts:66) - ordered. Contracts v4: research (lab kind, setResearch, ResearchState, researched event), SelectionState protocol (kills __remove sentinel, main.ts owns remove dispatch), accent?, coolRadius? (spatial cooling), bandwidth.capacity?, save/load hardened, heat ruled GROSS (net = heat - cool). tsconfig now typechecks data/ (DATA's proof). Orchestrator patched lab entries into exhaustive kind maps (my breakage, my fix). Rulings: pathspec-commit rule after 3 shared-index collisions (worktrees = named escalation); bloom escalator reading accepted; UI ?uidemo + SCREEN boredom approved. Codex: docs/FKTRY_LORE.md grew §10 THE COMPOSITE SEAM - keying/transparency artifact family from the Corridor Crew compositing history + our corridorkey-mrp-mlx (which doubles as a PYXLFK ground-truth artifact factory). DATA transcribes it round 4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
/**
|
|
* LANE-UI — build bar pagination (pure; see pages.test.ts).
|
|
*
|
|
* Round 2 orders: kind tabs, `[`/`]` to page, 1-9 within the active page, "every machine
|
|
* hotkey-reachable at any catalog size".
|
|
*
|
|
* Kind tabs alone don't deliver that last clause: LANE-DATA's 21 machines include 14
|
|
* crafters, and 1-9 cannot reach 14. So a page is at most PER_PAGE machines, and a kind
|
|
* group larger than that spills into numbered pages (REFINE 1/2, REFINE 2/2). That keeps
|
|
* the invariant true no matter how big the catalog gets.
|
|
*/
|
|
import type { MachineDef, MachineKind } from '../contracts';
|
|
|
|
/** Hotkeys are 1-9, so a page can never hold more than nine. */
|
|
export const PER_PAGE = 9;
|
|
|
|
/** Kinds collapse into these tabs, in house order: dig, move, refine, power, ship. */
|
|
const GROUP_OF: Record<MachineKind, string> = {
|
|
extractor: 'EXTRACT',
|
|
belt: 'LOGISTICS',
|
|
splitter: 'LOGISTICS',
|
|
buffer: 'LOGISTICS',
|
|
crafter: 'REFINE',
|
|
power: 'POWER',
|
|
shipper: 'SHIP',
|
|
lab: 'RESEARCH', // v4 kind
|
|
};
|
|
|
|
const GROUP_ORDER = ['EXTRACT', 'LOGISTICS', 'REFINE', 'POWER', 'SHIP'];
|
|
|
|
export interface BuildPage {
|
|
/** Tab label, e.g. "REFINE" or "REFINE 2/2". */
|
|
label: string;
|
|
machines: MachineDef[];
|
|
}
|
|
|
|
export function paginate(machines: MachineDef[], perPage: number = PER_PAGE): BuildPage[] {
|
|
const groups = new Map<string, MachineDef[]>();
|
|
for (const m of machines) {
|
|
// An unknown kind still gets a home rather than vanishing from the bar.
|
|
const g = GROUP_OF[m.kind] ?? 'OTHER';
|
|
const list = groups.get(g);
|
|
if (list) list.push(m);
|
|
else groups.set(g, [m]);
|
|
}
|
|
|
|
const names = [...groups.keys()].sort((a, b) => {
|
|
const ia = GROUP_ORDER.indexOf(a);
|
|
const ib = GROUP_ORDER.indexOf(b);
|
|
// Unknown groups sort last, alphabetically, rather than jumping the queue.
|
|
if (ia === -1 && ib === -1) return a < b ? -1 : a > b ? 1 : 0;
|
|
if (ia === -1) return 1;
|
|
if (ib === -1) return -1;
|
|
return ia - ib;
|
|
});
|
|
|
|
const pages: BuildPage[] = [];
|
|
for (const name of names) {
|
|
const list = groups.get(name)!;
|
|
const chunks = Math.max(1, Math.ceil(list.length / perPage));
|
|
for (let i = 0; i < chunks; i++) {
|
|
pages.push({
|
|
label: chunks > 1 ? `${name} ${i + 1}/${chunks}` : name,
|
|
machines: list.slice(i * perPage, (i + 1) * perPage),
|
|
});
|
|
}
|
|
}
|
|
return pages;
|
|
}
|
|
|
|
/** Page index holding `defId`, or -1. Used to follow a click-selection to its tab. */
|
|
export function pageOf(pages: BuildPage[], defId: string): number {
|
|
return pages.findIndex((p) => p.machines.some((m) => m.id === defId));
|
|
}
|