feat(web,api): scale spacing with text size, persist preference per account
Two follow-ups to the text-size control. Spacing now scales with the text. All padding, margin, gap and min-height declarations in globals.css move from px to rem (263 declarations, converted mechanically), so --ui-scale drives the whole layout rather than just the glyphs. Deliberately left in px: border widths, which must stay hairlines; box-shadow offsets; border-radius, which reads as bloated when scaled on large cards; --shell-max, a container cap that must not outgrow the viewport; and media-query breakpoints, which are conditions rather than declarations. With spacing following along, the presets gain a 1.5 "Máximo" step and MAX_UI_SCALE rises from 1.4. The preference now lives on the account instead of only in one browser. User.uiScale (Float, default 1) is added to the schema and to the safe select, so it rides along on /auth/login and /auth/me. PATCH /auth/preferences writes it, guarded by AuthenticatedGuard only — every role including VIEWER may set their own, and the target is always the session's user id, never a body parameter, so this cannot be used to touch another account. The global ValidationPipe's whitelist rejects any extra field, so role cannot ride in alongside uiScale. localStorage stays, demoted to a pre-paint cache for the layout.tsx script; AppShell reconciles it against the account once /auth/me answers, with the account winning. FontScaleControl becomes a controlled component since the same value is now edited from the appbar and the drawer. Verified against the dev API: PATCH persists and is reflected by a subsequent /auth/me, out-of-range values are rejected 400, and an extra "role" field in the body is rejected 400. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,14 @@ export function me(): Promise<AuthUser> {
|
||||
return apiFetch<AuthUser>("/auth/me");
|
||||
}
|
||||
|
||||
/** Persist the caller's own text-size preference on their account. */
|
||||
export function updateUiScale(uiScale: number): Promise<AuthUser> {
|
||||
return apiFetch<AuthUser>("/auth/preferences", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ uiScale }),
|
||||
});
|
||||
}
|
||||
|
||||
export function logout(): Promise<{ success: boolean }> {
|
||||
return apiFetch<{ success: boolean }>("/auth/logout", { method: "POST" });
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ export interface AuthUser {
|
||||
email: string;
|
||||
role: Role;
|
||||
active: boolean;
|
||||
// Text-size preference, stored per account so it follows the person across
|
||||
// machines. localStorage still holds a copy, but only as a pre-paint cache —
|
||||
// this value is the source of truth. See lib/ui-scale.ts.
|
||||
uiScale: number;
|
||||
// Resolved server-side from role (abilitiesFor in the API); the UI only ever
|
||||
// reads this map, never re-derives the rules. Server still enforces.
|
||||
abilities: Record<Ability, boolean>;
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
// 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.
|
||||
// App-wide text size. Every font-size *and* every spacing value 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.
|
||||
// The account is the source of truth (User.uiScale, served on /auth/me).
|
||||
// localStorage holds a copy purely so the pre-hydration script in
|
||||
// app/layout.tsx can paint at the right size before the session is known;
|
||||
// AppShell reconciles the two once /auth/me answers. Keep UI_SCALE_KEY and the
|
||||
// bounds in sync with that script and with the API's UpdatePreferencesDto.
|
||||
|
||||
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 MAX_UI_SCALE = 1.5;
|
||||
|
||||
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" },
|
||||
{ value: 1.5, label: "Máximo", short: "A" },
|
||||
];
|
||||
|
||||
/** Clamp to the supported range; anything unparseable falls back to default. */
|
||||
|
||||
Reference in New Issue
Block a user