diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 6a7abbb..850ad2e 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -245,6 +245,38 @@ test('hver fane under indstillinger renderer', async ({ page }) => { expect(trouble).toEqual([]); }); +test('menu- og brødtekststørrelse sættes hver for sig og overlever en genindlæsning', async ({ + page +}) => { + const trouble = watchForTrouble(page); + await page.goto('/indstillinger'); + + // Two independent knobs under Udseende — the sidebar's size, and the body text's. + await page.getByLabel('Menu', { exact: true }).selectOption('large'); + await page.getByLabel('Brødtekst', { exact: true }).selectOption('xl'); + await expect(page.getByLabel('Menu', { exact: true })).toHaveValue('large'); + + const attrs = () => + page.evaluate(() => [ + document.documentElement.dataset.menuSize, + document.documentElement.dataset.textSize + ]); + expect(await attrs()).toEqual(['large', 'xl']); + + // They live in localStorage like the theme, so they survive a reload — and are + // applied before first paint, so the picker comes back showing the saved choice. + await page.reload(); + expect(await attrs()).toEqual(['large', 'xl']); + await expect(page.getByLabel('Menu', { exact: true })).toHaveValue('large'); + + // Put them back, so the shared account does not leak a size into other tests. + await page.getByLabel('Menu', { exact: true }).selectOption('default'); + await page.getByLabel('Brødtekst', { exact: true }).selectOption('default'); + expect(await attrs()).toEqual([undefined, undefined]); + + expect(trouble).toEqual([]); +}); + /** * The PWA's own files. Both of the bugs found last in this project were of this * kind — a manifest promising icons that were never generated — and they are diff --git a/web/src/app.css b/web/src/app.css index 403566f..40d29a4 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -86,6 +86,12 @@ --text-xl: 1.375rem; --text-2xl: 1.75rem; + /* The sidebar's own text size, its own knob (Settings → Appearance) so the menu + can be made easier to read without dragging the whole list with it. Default is + a step up from the old --text-sm the nav used, at medium weight, so the menu + items read as menu items. [data-menu-size] on overrides it. */ + --menu-size: 0.9375rem; + /* Spacing on a 4px grid. */ --s1: 0.25rem; --s2: 0.5rem; @@ -516,3 +522,41 @@ button { line-height: 1.5; letter-spacing: -0.01em; } + +/* Body text size — its own control in Settings → Appearance, for reading the list + and the notes larger (or smaller) without touching the menu. It re-scales the + shared type tokens the same way a look does; placed after the looks so that an + explicit size choice wins the tokens they both set. The menu has its own knob + below and does not move with this one. */ +[data-text-size='small'] { + --text-base: 0.875rem; + --text-sm: 0.75rem; + --text-lg: 1rem; +} + +[data-text-size='large'] { + --text-base: 1.0625rem; + --text-sm: 0.9375rem; + --text-lg: 1.1875rem; +} + +[data-text-size='xl'] { + --text-base: 1.1875rem; + --text-sm: 1.0625rem; + --text-lg: 1.3125rem; +} + +/* Menu (sidebar) font size — independent of the body size above, because the two + are separate questions: a menu you can read at a glance, and a list you can read + a paragraph of. */ +[data-menu-size='small'] { + --menu-size: 0.8125rem; +} + +[data-menu-size='large'] { + --menu-size: 1.0625rem; +} + +[data-menu-size='xl'] { + --menu-size: 1.1875rem; +} diff --git a/web/src/app.html b/web/src/app.html index ddd5df8..eaf5d37 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -28,6 +28,14 @@ // face and again in another, which is a visible jump. const look = localStorage.getItem('verdande:look'); if (look && look !== 'verdande') document.documentElement.dataset.look = look; + + // The font-size choices change layout too, so they belong before first + // paint for the same reason: otherwise the sidebar and the list are drawn + // once at one size and again at another. + const menuSize = localStorage.getItem('verdande:menu-size'); + if (menuSize && menuSize !== 'default') document.documentElement.dataset.menuSize = menuSize; + const textSize = localStorage.getItem('verdande:text-size'); + if (textSize && textSize !== 'default') document.documentElement.dataset.textSize = textSize; } catch (e) { // Private browsing can refuse localStorage; the dark default stands. } diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte index 9ffe437..b7950a9 100644 --- a/web/src/lib/components/Sidebar.svelte +++ b/web/src/lib/components/Sidebar.svelte @@ -1441,7 +1441,10 @@ border-radius: var(--radius); color: var(--ink-muted); text-decoration: none; - font-size: var(--text-sm); + /* Its own size (Settings → Appearance) and medium weight, so the menu items + carry more than the muted small text they used to and read at a glance. */ + font-size: var(--menu-size); + font-weight: 500; transition: background var(--fast) var(--ease), color var(--fast) var(--ease); @@ -1455,7 +1458,7 @@ a.active { background: var(--surface-raised); color: var(--ink); - font-weight: 500; + font-weight: 600; } /* Only your own projects reorder. A shared one sits where its owner put it — diff --git a/web/src/lib/locales/da.js b/web/src/lib/locales/da.js index 9e7a17d..6cb8867 100644 --- a/web/src/lib/locales/da.js +++ b/web/src/lib/locales/da.js @@ -230,6 +230,14 @@ export const da = { 'Gemmes i denne browser. Det er en egenskab ved den skærm, du sidder ved — en bærbar i solen og en skærm i et mørkt rum vil have hvert sit svar.', 'account.look': 'Udseende', 'account.lookHint': 'Temaet siger hvor lyst. Det her siger hvordan det læses — skrift, luft og hjørner. De to kan kombineres frit.', + 'account.sizes': 'Skriftstørrelse', + 'account.sizesHint': 'Menuen og brødteksten kan sættes hver for sig — den ene er et blik ned over en liste, den anden en tekst, man læser.', + 'account.menuSize': 'Menu', + 'account.textSize': 'Brødtekst', + 'size.small': 'Lille', + 'size.default': 'Standard', + 'size.large': 'Stor', + 'size.xl': 'Størst', 'look.verdande': 'Verdande', 'look.verdandeNote': 'Som du kender den.', 'look.rolig': 'Rolig', diff --git a/web/src/lib/locales/en.js b/web/src/lib/locales/en.js index f6435f4..df4eed9 100644 --- a/web/src/lib/locales/en.js +++ b/web/src/lib/locales/en.js @@ -226,6 +226,14 @@ export const en = { 'Kept in this browser. It is a property of the screen you are sitting at — a laptop in the sun and a monitor in a dark room want different answers.', 'account.look': 'Appearance', 'account.lookHint': 'The theme says how bright. This says how it reads — typeface, air and corners. The two combine freely.', + 'account.sizes': 'Text size', + 'account.sizesHint': 'The menu and the body text set separately — one is a glance down a list, the other a paragraph you read.', + 'account.menuSize': 'Menu', + 'account.textSize': 'Body text', + 'size.small': 'Small', + 'size.default': 'Default', + 'size.large': 'Large', + 'size.xl': 'Largest', 'look.verdande': 'Verdande', 'look.verdandeNote': 'As you know it.', 'look.rolig': 'Calm', diff --git a/web/src/lib/stores.svelte.js b/web/src/lib/stores.svelte.js index 83d4dab..9c26539 100644 --- a/web/src/lib/stores.svelte.js +++ b/web/src/lib/stores.svelte.js @@ -1051,6 +1051,52 @@ class Look { export const look = new Look(); +/** + * The font-size steps, shared by the menu and the body-text controls. `default` + * carries no attribute, so the base tokens in app.css stand unchanged. + */ +export const SIZES = [ + { id: 'small', name: 'size.small' }, + { id: 'default', name: 'size.default' }, + { id: 'large', name: 'size.large' }, + { id: 'xl', name: 'size.xl' } +]; + +/** + * A font-size preference — the same shape as Look: kept in localStorage and + * mirrored onto , where app.html sets it before first paint so a chosen size + * does not flash in at the default and resize. Two instances below, because the + * menu and the body text are separate questions. + */ +class Sizing { + current = $state('default'); + #key; + #attr; + + constructor(key, attr) { + this.#key = key; + this.#attr = attr; + if (typeof document !== 'undefined') { + this.current = document.documentElement.dataset[attr] ?? 'default'; + } + } + + set(id) { + if (!SIZES.some((s) => s.id === id)) return; + this.current = id; + if (id === 'default') delete document.documentElement.dataset[this.#attr]; + else document.documentElement.dataset[this.#attr] = id; + try { + localStorage.setItem(this.#key, id); + } catch { + // Private browsing; the choice simply will not persist. + } + } +} + +export const menuSize = new Sizing('verdande:menu-size', 'menuSize'); +export const textSize = new Sizing('verdande:text-size', 'textSize'); + /** Theme, kept in localStorage and applied to the document element. */ class Theme { /** diff --git a/web/src/routes/indstillinger/+page.svelte b/web/src/routes/indstillinger/+page.svelte index 2537b97..4a80ef9 100644 --- a/web/src/routes/indstillinger/+page.svelte +++ b/web/src/routes/indstillinger/+page.svelte @@ -1,7 +1,7 @@