feat(web): group top nav, add mobile drawer and app-wide text size control
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:
@@ -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 */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user