Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acec77a20c | ||
|
|
59413a8b64 | ||
|
|
cf69cac410 | ||
|
|
b0998fe524 | ||
|
|
8dfab618f7 |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 335 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
@@ -245,6 +245,38 @@ test('hver fane under indstillinger renderer', async ({ page }) => {
|
|||||||
expect(trouble).toEqual([]);
|
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
|
* 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
|
* kind — a manifest promising icons that were never generated — and they are
|
||||||
@@ -453,10 +485,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
.evaluate((el) => el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft));
|
.evaluate((el) => el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft));
|
||||||
expect(headLeft, 'gruppehovedet er rykket ind').toBeLessThanOrEqual(looseLeft);
|
expect(headLeft, 'gruppehovedet er rykket ind').toBeLessThanOrEqual(looseLeft);
|
||||||
|
|
||||||
// A project inside a group starts further in than a row outside one. Measured
|
// A project sits at the same left margin whether or not it is in a group — one
|
||||||
// rather than asserted on a class, because indentation is a fact about where
|
// straight edge down the sidebar, with the bold heading doing the grouping the
|
||||||
// the row's contents land — and measured at the *content* edge, not the box:
|
// indent used to. Measured at the *content* edge, not the box, because what the
|
||||||
// the indent is padding, so the element's own left edge does not move.
|
// eye follows is where the text lands.
|
||||||
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
|
await sidebar.getByRole('link', { name: 'Regnskab' }).dragTo(heading);
|
||||||
const contentLeft = (el) =>
|
const contentLeft = (el) =>
|
||||||
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
|
el.getBoundingClientRect().left + parseFloat(getComputedStyle(el).paddingLeft);
|
||||||
@@ -464,7 +496,10 @@ test('en projektgruppe kan foldes, fyldes og slettes uden at tage projekterne me
|
|||||||
sidebar.locator('.folder > a').first().evaluate(contentLeft),
|
sidebar.locator('.folder > a').first().evaluate(contentLeft),
|
||||||
sidebar.locator('.views a').first().evaluate(contentLeft)
|
sidebar.locator('.views a').first().evaluate(contentLeft)
|
||||||
]);
|
]);
|
||||||
expect(inside, 'et projekt i en gruppe er ikke rykket ind').toBeGreaterThan(outside);
|
expect(
|
||||||
|
Math.abs(inside - outside),
|
||||||
|
'et projekt i en gruppe står ikke i flugt med de faste visninger'
|
||||||
|
).toBeLessThan(2);
|
||||||
|
|
||||||
await sidebar
|
await sidebar
|
||||||
.getByRole('link', { name: 'Regnskab' })
|
.getByRole('link', { name: 'Regnskab' })
|
||||||
|
|||||||
@@ -86,6 +86,12 @@
|
|||||||
--text-xl: 1.375rem;
|
--text-xl: 1.375rem;
|
||||||
--text-2xl: 1.75rem;
|
--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 <html> overrides it. */
|
||||||
|
--menu-size: 0.9375rem;
|
||||||
|
|
||||||
/* Spacing on a 4px grid. */
|
/* Spacing on a 4px grid. */
|
||||||
--s1: 0.25rem;
|
--s1: 0.25rem;
|
||||||
--s2: 0.5rem;
|
--s2: 0.5rem;
|
||||||
@@ -516,3 +522,41 @@ button {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
letter-spacing: -0.01em;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,14 @@
|
|||||||
// face and again in another, which is a visible jump.
|
// face and again in another, which is a visible jump.
|
||||||
const look = localStorage.getItem('verdande:look');
|
const look = localStorage.getItem('verdande:look');
|
||||||
if (look && look !== 'verdande') document.documentElement.dataset.look = 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) {
|
} catch (e) {
|
||||||
// Private browsing can refuse localStorage; the dark default stands.
|
// Private browsing can refuse localStorage; the dark default stands.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,6 +529,52 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
{:else if item.key === 'today'}
|
||||||
|
<!-- A star, the way Things marks Today: the one view you open first. -->
|
||||||
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M8 2l1.7 3.6 3.9.5-2.9 2.7.7 3.9L8 10.9 4.6 13.4l.7-3.9L2.4 6.1l3.9-.5z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.3"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else if item.key === 'upcoming'}
|
||||||
|
<!-- A calendar with a day marked — the Calendar view carries the plain
|
||||||
|
grid, so the marked day keeps the two apart. -->
|
||||||
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<rect
|
||||||
|
x="2.2"
|
||||||
|
y="3.2"
|
||||||
|
width="11.6"
|
||||||
|
height="10.6"
|
||||||
|
rx="1.4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.2 6.4h11.6M5.6 2.2v2M10.4 2.2v2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<rect x="8.8" y="8.6" width="2.6" height="2.6" rx="0.5" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
{:else if item.key === 'delegated'}
|
||||||
|
<!-- A person: the tasks you are waiting on someone else for. -->
|
||||||
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<circle cx="8" cy="5.4" r="2.5" fill="none" stroke="currentColor" stroke-width="1.4" />
|
||||||
|
<path
|
||||||
|
d="M3.4 13.4c0-2.6 2.1-4.2 4.6-4.2s4.6 1.6 4.6 4.2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="dot" class:today={item.key === 'today'} aria-hidden="true"></span>
|
<span class="dot" class:today={item.key === 'today'} aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -593,7 +639,29 @@
|
|||||||
? onTaskDrop(e, project)
|
? onTaskDrop(e, project)
|
||||||
: sortable && onDrop(e, project)}
|
: sortable && onDrop(e, project)}
|
||||||
>
|
>
|
||||||
|
{#if project.is_inbox}
|
||||||
|
<!-- A tray, not a coloured dot: the Inbox is a fixed view that happens to
|
||||||
|
be a project, and it reads as one of the views above, not as the first
|
||||||
|
of the projects below. -->
|
||||||
|
<svg class="mark" viewBox="0 0 16 16" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M2.3 9.2L4 3.4a1.1 1.1 0 011-.8h6a1.1 1.1 0 011 .8l1.7 5.8"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.3"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.3 9.2v2.9A1.3 1.3 0 003.6 13.4h8.8a1.3 1.3 0 001.3-1.3V9.2h-3.3l-1 1.7H6.6l-1-1.7z"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.3"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
<span class="dot" style="background: {colorVar(project.color)}" aria-hidden="true"></span>
|
<span class="dot" style="background: {colorVar(project.color)}" aria-hidden="true"></span>
|
||||||
|
{/if}
|
||||||
{projectName(project)}
|
{projectName(project)}
|
||||||
<!-- Open tasks, not people. The same grey number meant three things in this
|
<!-- Open tasks, not people. The same grey number meant three things in this
|
||||||
one column — people at a project, projects at a group, tasks at a label
|
one column — people at a project, projects at a group, tasks at a label
|
||||||
@@ -1370,21 +1438,31 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Projects inside a group are indented to the group's name, not to its dot:
|
/* Projects sit at the same left margin whether or not they are in an area —
|
||||||
the eye follows the text, and lining up under the dot leaves the names
|
one straight edge down the whole sidebar reads calmer than a staircase, and
|
||||||
looking a step short of where the heading starts. */
|
the bold area heading above already does the grouping the indent used to. */
|
||||||
.folder > a {
|
.folder > a {
|
||||||
padding-left: calc(var(--s2) + var(--s4));
|
padding-left: var(--s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The line is indented with them, so a drop between two projects in a group
|
|
||||||
reads as landing in the group rather than across the whole sidebar. */
|
|
||||||
.folder > a.sortable::before {
|
.folder > a.sortable::before {
|
||||||
left: calc(var(--s2) + var(--s4));
|
left: var(--s2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder > .empty {
|
.folder > .empty {
|
||||||
padding-left: calc(var(--s2) + var(--s4));
|
padding-left: var(--s2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An area is the sidebar's own heading — Things treats it as bold and plainly
|
||||||
|
cased, a thing the eye lands on, not another faint uppercase label like the
|
||||||
|
system sections. So an area's name is set apart while "Projekter", "Filtre"
|
||||||
|
and "Etiketter" stay quiet. */
|
||||||
|
.folder-head h2 {
|
||||||
|
font-size: var(--menu-size);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inside the rename form now, so it needs no padding of its own — the form
|
/* Inside the rename form now, so it needs no padding of its own — the form
|
||||||
@@ -1441,7 +1519,10 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
text-decoration: none;
|
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:
|
transition:
|
||||||
background var(--fast) var(--ease),
|
background var(--fast) var(--ease),
|
||||||
color var(--fast) var(--ease);
|
color var(--fast) var(--ease);
|
||||||
@@ -1455,7 +1536,7 @@
|
|||||||
a.active {
|
a.active {
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Only your own projects reorder. A shared one sits where its owner put it —
|
/* Only your own projects reorder. A shared one sits where its owner put it —
|
||||||
|
|||||||
@@ -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.',
|
'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.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.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.verdande': 'Verdande',
|
||||||
'look.verdandeNote': 'Som du kender den.',
|
'look.verdandeNote': 'Som du kender den.',
|
||||||
'look.rolig': 'Rolig',
|
'look.rolig': 'Rolig',
|
||||||
|
|||||||
@@ -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.',
|
'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.look': 'Appearance',
|
||||||
'account.lookHint': 'The theme says how bright. This says how it reads — typeface, air and corners. The two combine freely.',
|
'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.verdande': 'Verdande',
|
||||||
'look.verdandeNote': 'As you know it.',
|
'look.verdandeNote': 'As you know it.',
|
||||||
'look.rolig': 'Calm',
|
'look.rolig': 'Calm',
|
||||||
|
|||||||
@@ -1051,6 +1051,52 @@ class Look {
|
|||||||
|
|
||||||
export const look = new 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 <html>, 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. */
|
/** Theme, kept in localStorage and applied to the document element. */
|
||||||
class Theme {
|
class Theme {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
/** Konto: who you are, your password, and the second factor. */
|
/** Konto: who you are, your password, and the second factor. */
|
||||||
import { api, humanMessage } from '$lib/api.js';
|
import { api, humanMessage } from '$lib/api.js';
|
||||||
import { app, theme, THEMES, look, LOOKS } from '$lib/stores.svelte.js';
|
import { app, theme, THEMES, look, LOOKS, menuSize, textSize, SIZES } from '$lib/stores.svelte.js';
|
||||||
import { t, plural, i18n } from '$lib/i18n.svelte.js';
|
import { t, plural, i18n } from '$lib/i18n.svelte.js';
|
||||||
import { supported, register } from '$lib/passkey.js';
|
import { supported, register } from '$lib/passkey.js';
|
||||||
import { ago } from '$lib/when.js';
|
import { ago } from '$lib/when.js';
|
||||||
@@ -362,6 +362,40 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Two more axes: how large the menu reads and how large the body text reads.
|
||||||
|
Separate on purpose — one is a glance down a list of names, the other is a
|
||||||
|
paragraph you sit and read. -->
|
||||||
|
<div class="sizes">
|
||||||
|
<h3>{t('account.sizes')}</h3>
|
||||||
|
<p class="hint">{t('account.sizesHint')}</p>
|
||||||
|
<div class="size-row">
|
||||||
|
<div class="size-field">
|
||||||
|
<label for="menu-size">{t('account.menuSize')}</label>
|
||||||
|
<select
|
||||||
|
id="menu-size"
|
||||||
|
value={menuSize.current}
|
||||||
|
onchange={(e) => menuSize.set(e.currentTarget.value)}
|
||||||
|
>
|
||||||
|
{#each SIZES as option (option.id)}
|
||||||
|
<option value={option.id}>{t(option.name)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="size-field">
|
||||||
|
<label for="text-size">{t('account.textSize')}</label>
|
||||||
|
<select
|
||||||
|
id="text-size"
|
||||||
|
value={textSize.current}
|
||||||
|
onchange={(e) => textSize.set(e.currentTarget.value)}
|
||||||
|
>
|
||||||
|
{#each SIZES as option (option.id)}
|
||||||
|
<option value={option.id}>{t(option.name)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
@@ -619,6 +653,37 @@
|
|||||||
margin-bottom: var(--s1);
|
margin-bottom: var(--s1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sizes {
|
||||||
|
margin-top: var(--s4);
|
||||||
|
padding-top: var(--s3);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sizes h3 {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
margin-bottom: var(--s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: var(--s3);
|
||||||
|
margin-top: var(--s3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.size-field label {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
.look-row {
|
.look-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
|||||||