From 8178502b73d7b88ed3816c4c04d1dd092bb429f6 Mon Sep 17 00:00:00 2001 From: mithral Date: Sat, 10 Oct 2026 17:07:43 -0400 Subject: [PATCH] Appearance settings: text size and card size, and a larger default - All 50 font-size declarations converted from px to rem, so a single root value scales headings, labels and body together. The root is now 15px, a deliberate ~7% increase over the previous 14px. - Card width and grid column width are now --card-w / --grid-min variables, driven by --card-scale independently of text. Defaults bumped too: shelf cards 150 -> 160px, grid columns 148 -> 158px. - Appearance panel (nav item, visible to everyone): Text size and Card size as segmented controls, a live preview, and a reset. Settings are stored in localStorage, per device rather than per account, because a TV across the room wants bigger text than a laptop and the same person uses both. - nav: 'settings' was missing from the View union after the home rewrite; added back along with the Appearance entry. Verified in the built bundle: root font 15px by default; "Larger" scales it to 19.2px; "Large" cards scale 160 to 192px; both persist across a reload and are independent of each other. --- web/src/app.css | 40 ++-- web/src/lib/components/AudiobookPlayer.svelte | 2 +- web/src/lib/components/Browse.svelte | 4 +- web/src/lib/components/Card.svelte | 10 +- web/src/lib/components/GamePlayer.svelte | 2 +- web/src/lib/components/Home.svelte | 12 +- web/src/lib/components/ItemDetail.svelte | 12 +- web/src/lib/components/Libraries.svelte | 4 +- web/src/lib/components/Login.svelte | 2 +- web/src/lib/components/Reader.svelte | 4 +- web/src/lib/components/SearchResults.svelte | 4 +- web/src/lib/components/Settings.svelte | 178 ++++++++++++++++++ web/src/lib/components/Shelf.svelte | 8 +- web/src/lib/components/Shell.svelte | 26 ++- web/src/lib/components/Users.svelte | 10 +- web/src/lib/nav.ts | 1 + web/src/lib/settings.ts | 79 ++++++++ 17 files changed, 340 insertions(+), 58 deletions(-) create mode 100644 web/src/lib/components/Settings.svelte create mode 100644 web/src/lib/settings.ts diff --git a/web/src/app.css b/web/src/app.css index 603ca35..b916c55 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -41,6 +41,15 @@ --nav-width: 232px; --shadow-cover: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); --shadow-lift: 0 12px 32px rgba(0, 0, 0, 0.5); + + /* User-controlled sizing. --ui-scale drives every rem in the app, so the + text-size setting scales headings, labels and body together. --card-scale + drives cover widths in shelves and grids, so card size is independent of + text size. Both are set from the Appearance panel and persisted locally. */ + --ui-scale: 1; + --card-scale: 1; + --card-w: calc(160px * var(--card-scale)); + --grid-min: calc(158px * var(--card-scale)); } @media (prefers-color-scheme: light) { @@ -73,11 +82,18 @@ body, margin: 0; } +html { + /* The single root size every rem is relative to. 15px is a deliberate ~7% + increase over the previous 14px baseline; the Appearance panel multiplies + it via --ui-scale. */ + font-size: calc(15px * var(--ui-scale)); +} + body { background: var(--bg); color: var(--text); font-family: var(--font); - font-size: 14px; + font-size: 1rem; line-height: 1.5; -webkit-font-smoothing: antialiased; } @@ -98,13 +114,13 @@ h3 { letter-spacing: -0.015em; } h1 { - font-size: 22px; + font-size: 1.571rem; } h2 { - font-size: 17px; + font-size: 1.214rem; } h3 { - font-size: 15px; + font-size: 1.071rem; } button, @@ -158,7 +174,7 @@ select { } .btn-lg { padding: 11px 22px; - font-size: 15px; + font-size: 1.071rem; border-radius: 12px; } @@ -183,7 +199,7 @@ select { gap: 6px; } .field label { - font-size: 12px; + font-size: .857rem; font-weight: 500; color: var(--muted); } @@ -201,7 +217,7 @@ select { padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border-strong); - font-size: 11px; + font-size: .786rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; @@ -214,7 +230,7 @@ select { background: transparent; color: var(--muted); cursor: pointer; - font-size: 13px; + font-size: .929rem; } .chip:hover { color: var(--text); @@ -231,14 +247,14 @@ select { border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--border)); border-radius: var(--radius); background: color-mix(in srgb, var(--danger) 10%, transparent); - font-size: 13px; + font-size: .929rem; } .ok { padding: 10px 14px; border: 1px solid color-mix(in srgb, var(--success) 40%, var(--border)); border-radius: var(--radius); background: color-mix(in srgb, var(--success) 9%, transparent); - font-size: 13px; + font-size: .929rem; } .muted { @@ -249,7 +265,7 @@ select { } .mono { font-family: var(--mono); - font-size: 12px; + font-size: .857rem; } /* ── Cover card ─────────────────────────────────────────────────────── */ @@ -277,7 +293,7 @@ select { inset: 0; display: grid; place-items: center; - font-size: 34px; + font-size: 2.429rem; font-weight: 700; color: var(--faint); background: linear-gradient(150deg, var(--surface-2), var(--surface-3)); diff --git a/web/src/lib/components/AudiobookPlayer.svelte b/web/src/lib/components/AudiobookPlayer.svelte index 752f1e9..45a5ef2 100644 --- a/web/src/lib/components/AudiobookPlayer.svelte +++ b/web/src/lib/components/AudiobookPlayer.svelte @@ -238,7 +238,7 @@ .times { display: flex; justify-content: space-between; - font-size: 12px; + font-size: .857rem; } .controls { display: flex; diff --git a/web/src/lib/components/Browse.svelte b/web/src/lib/components/Browse.svelte index 4f58b0c..7668f60 100644 --- a/web/src/lib/components/Browse.svelte +++ b/web/src/lib/components/Browse.svelte @@ -72,7 +72,7 @@ } .grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(var(--grid-min), 1fr)); gap: var(--space-5) var(--space-4); } .skeleton.tile { @@ -80,7 +80,7 @@ } @media (max-width: 700px) { .grid { - grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(calc(var(--grid-min) * 0.75), 1fr)); gap: var(--space-4) var(--space-3); } } diff --git a/web/src/lib/components/Card.svelte b/web/src/lib/components/Card.svelte index 4cd19f7..801a445 100644 --- a/web/src/lib/components/Card.svelte +++ b/web/src/lib/components/Card.svelte @@ -77,7 +77,7 @@ min-width: 0; } .title { - font-size: 13px; + font-size: .929rem; font-weight: 500; color: var(--text); overflow: hidden; @@ -88,7 +88,7 @@ line-height: 1.3; } .subtitle { - font-size: 11.5px; + font-size: .821rem; color: var(--faint); overflow: hidden; text-overflow: ellipsis; @@ -102,7 +102,7 @@ border-radius: 999px; background: rgba(0, 0, 0, 0.66); color: #fff; - font-size: 11px; + font-size: .786rem; font-weight: 600; backdrop-filter: blur(4px); } @@ -122,9 +122,9 @@ background: var(--accent); } .sm .title { - font-size: 12px; + font-size: .857rem; } .lg .title { - font-size: 14px; + font-size: 1.000rem; } diff --git a/web/src/lib/components/GamePlayer.svelte b/web/src/lib/components/GamePlayer.svelte index 01d0b31..f942cc8 100644 --- a/web/src/lib/components/GamePlayer.svelte +++ b/web/src/lib/components/GamePlayer.svelte @@ -173,7 +173,7 @@ } .status { margin: 0 0 var(--space-3); - font-size: 13px; + font-size: .929rem; } .stage { height: calc(100vh - 60px - 150px); diff --git a/web/src/lib/components/Home.svelte b/web/src/lib/components/Home.svelte index c68b23c..2134e21 100644 --- a/web/src/lib/components/Home.svelte +++ b/web/src/lib/components/Home.svelte @@ -107,7 +107,7 @@ diff --git a/web/src/lib/components/Shelf.svelte b/web/src/lib/components/Shelf.svelte index 4cdb1a8..f6b91bc 100644 --- a/web/src/lib/components/Shelf.svelte +++ b/web/src/lib/components/Shelf.svelte @@ -52,13 +52,13 @@ margin-bottom: var(--space-3); } h2 { - font-size: 17px; + font-size: 1.214rem; } .see-all { border: none; background: transparent; color: var(--muted); - font-size: 13px; + font-size: .929rem; cursor: pointer; padding: 0; } @@ -68,7 +68,7 @@ .row { display: grid; grid-auto-flow: column; - grid-auto-columns: 150px; + grid-auto-columns: var(--card-w); gap: var(--space-4); overflow-x: auto; padding-bottom: var(--space-2); @@ -79,7 +79,7 @@ } @media (max-width: 700px) { .row { - grid-auto-columns: 120px; + grid-auto-columns: calc(var(--card-w) * 0.8); } } diff --git a/web/src/lib/components/Shell.svelte b/web/src/lib/components/Shell.svelte index a2fd6ca..1b3ca19 100644 --- a/web/src/lib/components/Shell.svelte +++ b/web/src/lib/components/Shell.svelte @@ -8,6 +8,7 @@ import SearchResults from './SearchResults.svelte' import Libraries from './Libraries.svelte' import Users from './Users.svelte' + import Settings from './Settings.svelte' let searchBox = $state('') @@ -38,6 +39,8 @@ return 'Libraries' case 'users': return 'Users' + case 'settings': + return 'Appearance' } } @@ -66,6 +69,9 @@ + {#if $user?.is_admin} @@ -125,6 +131,8 @@ {:else if $view.name === 'users'} + {:else if $view.name === 'settings'} + {/if} @@ -154,7 +162,7 @@ background: transparent; color: var(--text); cursor: pointer; - font-size: 15px; + font-size: 1.071rem; } .mark { display: grid; @@ -164,7 +172,7 @@ border-radius: 7px; background: var(--accent); color: var(--accent-text); - font-size: 13px; + font-size: .929rem; font-weight: 700; } .links { @@ -184,7 +192,7 @@ color: var(--muted); text-align: left; cursor: pointer; - font-size: 13.5px; + font-size: .964rem; } .navlink:hover { background: var(--surface-2); @@ -196,13 +204,13 @@ font-weight: 500; } .count { - font-size: 12px; + font-size: .857rem; color: var(--faint); font-variant-numeric: tabular-nums; } .section { margin: 0 var(--space-2) -8px; - font-size: 11px; + font-size: .786rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--faint); @@ -228,7 +236,7 @@ height: 28px; border-radius: 50%; background: var(--surface-3); - font-size: 12px; + font-size: .857rem; font-weight: 600; flex: none; } @@ -239,13 +247,13 @@ min-width: 0; } .who strong { - font-size: 13px; + font-size: .929rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .who .faint { - font-size: 11px; + font-size: .786rem; } .main { display: flex; @@ -274,7 +282,7 @@ height: 34px; border-radius: 50%; cursor: pointer; - font-size: 16px; + font-size: 1.143rem; line-height: 1; } .back:hover { diff --git a/web/src/lib/components/Users.svelte b/web/src/lib/components/Users.svelte index 8cb2ed4..3ada004 100644 --- a/web/src/lib/components/Users.svelte +++ b/web/src/lib/components/Users.svelte @@ -204,7 +204,7 @@ flex: 1; } .id .muted { - font-size: 12px; + font-size: .857rem; } .grants { padding: 0 var(--space-4) var(--space-4); @@ -214,7 +214,7 @@ } .note { margin: 0; - font-size: 12px; + font-size: .857rem; } .grantrow { display: flex; @@ -223,10 +223,10 @@ } .libname { flex: 1; - font-size: 13px; + font-size: .929rem; } .libname .faint { - font-size: 11px; + font-size: .786rem; text-transform: uppercase; letter-spacing: 0.04em; } @@ -240,7 +240,7 @@ display: flex; align-items: center; gap: 6px; - font-size: 13px; + font-size: .929rem; color: var(--text-muted); white-space: nowrap; } diff --git a/web/src/lib/nav.ts b/web/src/lib/nav.ts index 08c496e..f0f1584 100644 --- a/web/src/lib/nav.ts +++ b/web/src/lib/nav.ts @@ -9,6 +9,7 @@ export type View = | { name: 'search'; query: string } | { name: 'libraries' } | { name: 'users' } + | { name: 'settings' } export const view = writable({ name: 'home' }) diff --git a/web/src/lib/settings.ts b/web/src/lib/settings.ts new file mode 100644 index 0000000..fa2ce92 --- /dev/null +++ b/web/src/lib/settings.ts @@ -0,0 +1,79 @@ +import { writable } from 'svelte/store' + +// Display preferences. These are per-device, not per-account: a TV across the +// room wants bigger text than a laptop, and the same person uses both. Stored +// in localStorage so a change is instant and needs no round trip. + +export interface UiSettings { + /** Multiplier on the 15px root size; scales all text. */ + textScale: number + /** Multiplier on cover width in shelves and grids. */ + cardScale: number +} + +export const DEFAULTS: UiSettings = { textScale: 1, cardScale: 1 } + +export const TEXT_STEPS = [ + { label: 'Small', value: 0.9 }, + { label: 'Default', value: 1 }, + { label: 'Large', value: 1.12 }, + { label: 'Larger', value: 1.28 }, +] + +export const CARD_STEPS = [ + { label: 'Compact', value: 0.85 }, + { label: 'Default', value: 1 }, + { label: 'Large', value: 1.2 }, + { label: 'Extra large', value: 1.45 }, +] + +const KEY = 'vellum.ui.settings' + +function read(): UiSettings { + try { + const raw = localStorage.getItem(KEY) + if (!raw) return DEFAULTS + const parsed = JSON.parse(raw) as Partial + return { + textScale: clamp(parsed.textScale ?? DEFAULTS.textScale, 0.8, 1.6), + cardScale: clamp(parsed.cardScale ?? DEFAULTS.cardScale, 0.7, 1.8), + } + } catch { + return DEFAULTS + } +} + +function clamp(n: number, lo: number, hi: number): number { + if (!Number.isFinite(n)) return 1 + return Math.min(Math.max(n, lo), hi) +} + +/** Apply the settings to the document as CSS variables. */ +export function applySettings(s: UiSettings) { + const root = document.documentElement + root.style.setProperty('--ui-scale', String(s.textScale)) + root.style.setProperty('--card-scale', String(s.cardScale)) +} + +export const settings = writable(read()) + +/** Update one or more settings, persist them, and apply them immediately. */ +export function setSettings(patch: Partial) { + settings.update((current) => { + const next = { ...current, ...patch } + applySettings(next) + try { + localStorage.setItem(KEY, JSON.stringify(next)) + } catch { + // Private browsing or a full quota: keep the change for this session. + } + return next + }) +} + +export function resetSettings() { + setSettings(DEFAULTS) +} + +// Apply whatever was stored before the first paint. +applySettings(read())