// --------------------------------------------------------------------------- // ui / blocks.ts — EXIT wave 2 (W2-6/W2-9). The block RULES, pure and // React-free so `verify_ui.py` can run them under bare node. // // The two rules in this file are the two owner constants the presentation layer // is most able to break quietly, which is exactly why they live here rather // than inline in a component: a rule inside JSX can only be checked by looking // at a screenshot, and both of these fail in ways a screenshot looks fine for. // // 1. **No silent caps.** `capDisclosure` renders from the SERVER's numbers. // 2. **One drill grammar.** `cellDescriptor` resolves a row to a descriptor // or to nothing — never to a half-populated one. // --------------------------------------------------------------------------- import { int } from "./fmt"; import { isRowDrillRule } from "./types"; import type { DecompDescriptor, DrillDescriptor, DrillRule, EntityKind, PageRow } from "./types"; export interface CapDisclosure { text: string; /** True when rows were actually withheld — the disclosure is louder then. */ capped: boolean; } /** * "Showing N of M", or nothing at all. * * ⛔ THE NUMBERS ARE THE SERVER'S AND ARE NEVER DERIVED FROM `rows.length`. * That is the whole point and it is subtle: a disclosure computed from the * array it is describing agrees with a truncated list BY CONSTRUCTION, so it * would read "Showing 25 of 25" over a list the server cut from 812. It cannot * be wrong, which means it is not a check — it is decoration that looks like * one ([[no-unverifiable-aggregates]]: a silent `[:N]` cap is a defect). * * Y1 rule 6 ships BOTH numbers even when they are equal, so `shown === total` * still renders — "Showing 12 of 12" is the sentence that tells a reader * nothing was withheld, and its absence is not the same statement. */ export function capDisclosure(shown?: number, total?: number): CapDisclosure | null { if (typeof shown !== "number" || typeof total !== "number") return null; if (!Number.isFinite(shown) || !Number.isFinite(total)) return null; return { text: `Showing ${int(shown)} of ${int(total)}`, capped: total > shown }; } /** * A row + a block-level drill rule → the descriptor to open, or `undefined`. * * Y1 rule 5's placements (b) and (c): * (b) an ENTITY rule names the columns holding the id and the label; * (c) a ROW rule names the column holding a complete `DecompDescriptor`. * * ⚠ A DESCRIPTOR IS COMPLETE OR IT DOES NOT EXIST. A half-populated one opens a * panel onto the wrong window — a period drill with no `date_from` would show * the reader a number attributed to a scope nobody computed. Returning * `undefined` costs a click; returning a partial costs the number's credibility. */ export function cellDescriptor( row: PageRow, rule: DrillRule | undefined ): DrillDescriptor | undefined { if (!rule) return undefined; if (isRowDrillRule(rule)) { const d = row[rule.row_key]; if (!d || typeof d !== "object") return undefined; const dd = d as DecompDescriptor; if (dd.kind !== "decomp") return undefined; // The window is what makes a decomp descriptor answerable at all. if (!dd.date_from || !dd.date_to) return undefined; return dd; } const id = row[rule.id_key]; if (id == null || id === "") return undefined; const label = rule.label_key ? row[rule.label_key] : undefined; return { kind: rule.kind as EntityKind, id: id as string | number, label: String(label ?? id), }; } /** The empty-state sentence. NEVER the bare words "no data": an empty table * that does not say what empty MEANS reads as a broken page, and sends people * looking in the wrong place. The server supplies the meaning (`empty`); this * is the fallback when it did not. */ export function emptyText(empty?: string): string { const s = (empty ?? "").trim(); return s || "Nothing matched this scope."; }