feat(web): group top nav, add mobile drawer and app-wide text size control
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m37s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m18s

The appbar had grown to 11 flat links with no responsive behaviour, and
overflowed below ~1100px.

Nav is now 7 top-level entries: Inicio, Clientes, Pólizas, Propiedades,
Reportes stay one click away, while the movement screens (Captura, Estado de
cuenta, Chequera) and the admin screens (Catálogos, Usuarios, Operaciones)
collapse into "Cobranza" and "Admin" dropdowns. Groups are ability-filtered
and disappear entirely when the user can see none of their items, so VIEWER
never renders an empty Admin menu. activeHref now scans the flattened link
list, and a group trigger highlights while one of its children is current.

Below 980px the nav collapses to a burger drawer that lists every group
expanded, closing on navigation and on Escape.

Text size is user-adjustable app-wide. Every font-size in globals.css is
converted from px to rem (mechanically, 133 declarations) and the root size
becomes calc(100% * var(--ui-scale)), so one variable on <html> rescales the
whole UI. The preference persists in localStorage and is applied by a
pre-hydration script in layout.tsx to avoid a flash at the default size; the
Aa control lives in the appbar and, as a segmented row, in the drawer.
Spacing stays in px by design, which is why 1.3 is the largest preset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 22:16:30 -07:00
co-authored by Claude Opus 5
parent 7df928c3ab
commit 0bf97e6d2c
5 changed files with 787 additions and 161 deletions
+51
View File
@@ -0,0 +1,51 @@
// App-wide text size. Every font-size in globals.css is in rem and the root
// size is `calc(100% * var(--ui-scale))`, so writing one variable on <html>
// rescales the entire UI — no per-component work, and the browser's own base
// font size still applies underneath.
//
// The value lives in localStorage (per browser, per machine) and is applied by
// a pre-hydration script in app/layout.tsx so the page never paints at the
// wrong size first. Keep UI_SCALE_KEY and the bounds in sync with that script.
export const UI_SCALE_KEY = "jc.ui-scale";
export const DEFAULT_UI_SCALE = 1;
export const MIN_UI_SCALE = 0.9;
export const MAX_UI_SCALE = 1.4;
export const UI_SCALES: { value: number; label: string; short: string }[] = [
{ value: 0.9, label: "Compacto", short: "A" },
{ value: 1, label: "Normal", short: "A" },
{ value: 1.15, label: "Grande", short: "A" },
{ value: 1.3, label: "Muy grande", short: "A" },
];
/** Clamp to the supported range; anything unparseable falls back to default. */
export function normalizeUiScale(value: unknown): number {
const n = typeof value === "number" ? value : Number.parseFloat(String(value));
if (!Number.isFinite(n)) return DEFAULT_UI_SCALE;
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, n));
}
export function readUiScale(): number {
if (typeof window === "undefined") return DEFAULT_UI_SCALE;
try {
const raw = window.localStorage.getItem(UI_SCALE_KEY);
return raw === null ? DEFAULT_UI_SCALE : normalizeUiScale(raw);
} catch {
// Private mode / storage disabled — the default is still usable.
return DEFAULT_UI_SCALE;
}
}
export function applyUiScale(scale: number): void {
if (typeof document === "undefined") return;
document.documentElement.style.setProperty("--ui-scale", String(scale));
}
export function saveUiScale(scale: number): void {
try {
window.localStorage.setItem(UI_SCALE_KEY, String(scale));
} catch {
/* ignore — the setting just won't survive a reload */
}
}