master @ 63 LINES
[ HISTORY ] [ UP ]
┌─ TS ───────────────────────────────────────────────────────────────────────┐
│ // Nav row data and width math. Items render as [ LABEL ] buttons, so │
│ // each is buttonWidth(label) cells regardless of active state. │
│ import { buttonWidth } from "../Button/Button"; │
│ │
│ export interface NavItem { │
│ label: string; │
│ href: string; │
│ } │
│ │
│ export interface Props { │
│ width?: number; │
│ items?: NavItem[]; │
│ /** Spaces between buttons. Defaults to NAV_GAP (4); narrower frames │
│ * pass 1 to fit four bracketed labels in 48 cells. */ │
│ gap?: number; │
│ /** Whether to render the EN label and reserve space for it. */ │
│ lang?: boolean; │
│ } │
│ │
│ export const NAV_ITEMS: NavItem[] = [ │
│ { label: "KB", href: "/kb/" }, │
│ { label: "GIT", href: "/git/" }, │
│ { label: "PROJECTS", href: "/projects/" }, │
│ { label: "ABOUT", href: "/about/" }, │
│ ]; │
│ │
│ export const NAV_GAP = 4; // spaces between buttons │
│ export const NAV_LANG_W = 2; // visible width of "EN" │
│ │
│ /** Total width of all buttons + inter-button gaps. */ │
│ export function itemsWidth(items: NavItem[], gap = NAV_GAP): number { │
│ return items.reduce( │
│ (sum, it, i) => sum + buttonWidth(it.label) + (i > 0 ? gap : 0), │
│ 0, │
│ ); │
│ } │
│ │
│ /** Greedy row wrap. The first row reserves `reserved` cells (the EN │
│ * label and a space) so nav grows downward instead of overflowing the │
│ * narrow frame. */ │
│ export function navRows(items: NavItem[], width: number, gap = NAV_GAP, reserved = │
│ 0): NavItem[][] { │
│ const rows: NavItem[][] = [[]]; │
│ let used = reserved; │
│ for (const it of items) { │
│ const row = rows[rows.length - 1]; │
│ const w = buttonWidth(it.label) + (row.length ? gap : 0); │
│ if (row.length && used + w > width) { │
│ rows.push([it]); │
│ used = buttonWidth(it.label); │
│ } else { │
│ row.push(it); │
│ used += w; │
│ } │
│ } │
│ return rows; │
│ } │
│ │
│ /** Active match: exact for the root, prefix elsewhere. */ │
│ export function isActive(href: string, path: string): boolean { │
│ if (href === "/") return path === "/"; │
│ return path === href || path.startsWith(href); │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ TS ─────────────────────────────────┐
│ // Nav row data and width math. Items render │
│ as [ LABEL ] buttons, so │
│ // each is buttonWidth(label) cells regardle │
│ ss of active state. │
│ import { buttonWidth } from "../Button/Butto │
│ n"; │
│ │
│ export interface NavItem { │
│ label: string; │
│ href: string; │
│ } │
│ │
│ export interface Props { │
│ width?: number; │
│ items?: NavItem[]; │
│ /** Spaces between buttons. Defaults to NA │
│ V_GAP (4); narrower frames │
│ * pass 1 to fit four bracketed labels in │
│ 48 cells. */ │
│ gap?: number; │
│ /** Whether to render the EN label and res │
│ erve space for it. */ │
│ lang?: boolean; │
│ } │
│ │
│ export const NAV_ITEMS: NavItem[] = [ │
│ { label: "KB", href: "/kb/" }, │
│ { label: "GIT", href: "/git/" }, │
│ { label: "PROJECTS", href: "/projects/" }, │
│ { label: "ABOUT", href: "/about/" }, │
│ ]; │
│ │
│ export const NAV_GAP = 4; // spaces be │
│ tween buttons │
│ export const NAV_LANG_W = 2; // visible w │
│ idth of "EN" │
│ │
│ /** Total width of all buttons + inter-butto │
│ n gaps. */ │
│ export function itemsWidth(items: NavItem[], │
│ gap = NAV_GAP): number { │
│ return items.reduce( │
│ (sum, it, i) => sum + buttonWidth(it.lab │
│ el) + (i > 0 ? gap : 0), │
│ 0, │
│ ); │
│ } │
│ │
│ /** Greedy row wrap. The first row reserves │
│ `reserved` cells (the EN │
│ * label and a space) so nav grows downward │
│ instead of overflowing the │
│ * narrow frame. */ │
│ export function navRows(items: NavItem[], wi │
│ dth: number, gap = NAV_GAP, reserved = 0): N │
│ avItem[][] { │
│ const rows: NavItem[][] = [[]]; │
│ let used = reserved; │
│ for (const it of items) { │
│ const row = rows[rows.length - 1]; │
│ const w = buttonWidth(it.label) + (row.l │
│ ength ? gap : 0); │
│ if (row.length && used + w > width) { │
│ rows.push([it]); │
│ used = buttonWidth(it.label); │
│ } else { │
│ row.push(it); │
│ used += w; │
│ } │
│ } │
│ return rows; │
│ } │
│ │
│ /** Active match: exact for the root, prefix │
│ elsewhere. */ │
│ export function isActive(href: string, path: │
│ string): boolean { │
│ if (href === "/") return path === "/"; │
│ return path === href || path.startsWith(hr │
│ ef); │
│ } │
└──────────────────────────────────────────────┘
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET