Compare commits

...
Author SHA1 Message Date
kristianwindandClaude Opus 4.8 8a5dfef41a fix(theme): lighter day separators in Charcoal
In Charcoal the note-list day separators used --ground, the darkest colour
in the theme, so they read as dark bars on the lighter list. They take the
menu's own resting grey (#2d3133) now — the colour "Projekter" sits on —
via a --note-group-bg fallback token, so every other theme keeps --ground.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 20:46:51 +02:00
Kristian Wind 1a610eaac4 Merge pull request #52 from kristianwind/notes-single-tap
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 10m25s
CI / Docker image builds (push) Successful in 1m58s
Release / Build and publish image (push) Failing after 6s
CI / Go (push) Failing after 15m11s
Notes: open on the first tap, not the second
2026-09-03 20:35:30 +02:00
kristianwindandClaude Opus 4.8 c2d4ce0179 fix(notes): open a note on the first tap, not the second
On a phone the list/editor switch keyed on the loaded note, which arrives a
moment after the tap — so the first tap looked like nothing happened and
people tapped again. It keys on the selected id now, set the instant a row
is tapped, so the pane switches at once; the back button is there
immediately and the body fills in when it arrives, with a quiet pane rather
than the "pick a note" prompt in the half-second between.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 20:27:28 +02:00
Kristian Wind a1b8cc6ddc Merge pull request #51 from kristianwind/notes-mobile-detail
CI / OpenAPI is valid (push) Successful in 10s
CI / End-to-end (push) Successful in 8m51s
CI / Docker image builds (push) Successful in 1m58s
Release / Build and publish image (push) Failing after 5s
CI / Go (push) Failing after 14m24s
Notes on a phone: show the note (master-detail layout)
2026-09-03 19:15:05 +02:00
kristianwindandClaude Opus 4.8 8a2c46f5ea fix(notes): show the note on a phone — one pane at a time
On a phone the note list and the editor stacked in one column, so a tapped
note opened below a full-height list, out of sight — "kan vælge en note men
kan ikke se indholdet". Now it is master-detail: the list fills the screen
until a note is opened, the editor takes over once one is, and an iOS-style
"‹ Noter" at the top of the editor goes back. The cards drop their border
and corners on one column, where there is nothing beside them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 19:07:02 +02:00
Kristian Wind 8d42f34065 Merge pull request #50 from kristianwind/charcoal-apple-colors
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Successful in 8m47s
CI / Docker image builds (push) Successful in 1m48s
Release / Build and publish image (push) Failing after 6s
CI / Go (push) Failing after 14m30s
Match Charcoal theme to Apple Notes' exact colours
2026-09-03 17:51:16 +02:00
kristianwindandClaude Opus 4.8 905ceebc06 fix(theme): match Charcoal to Apple Notes' sampled colours
Kristian sent the exact values; the first Charcoal was a neutral grey guess
and missed them. Now: three panel greys, lightest sidebar (#2d3133) to
darkest editor (#1e1e1e) with the list (#242b2e) between — the reverse of
the usual sunken-is-darkest order, so the editor takes its own --editor-bg
rather than sharing the list's surface. The accent is amber: the selected
menu item is solid #ffce35 with dark text, a selected note is gold #a78c26
with dark text, and the section headings drop to a quiet grey (#8a8e90).

Each per-element colour is a fallback token only Charcoal sets, so every
other theme reads through to the shared token unchanged. Verified the
computed values land on Kristian's samples exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 17:42:55 +02:00
Kristian Wind 87c557941a Merge pull request #49 from kristianwind/theme-charcoal-polish
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Successful in 8m47s
CI / Docker image builds (push) Successful in 1m47s
Release / Build and publish image (push) Failing after 5s
CI / Go (push) Failing after 14m25s
Charcoal theme, elegant selection, rounded note list, confirm sign-out
2026-09-03 16:37:44 +02:00
Kristian Wind 0e1e803486 Merge pull request #48 from kristianwind/totp-qr
2FA: show a scannable QR code when enabling two-factor auth
2026-09-03 16:37:36 +02:00
kristianwindandClaude Opus 4.8 1dc9751a33 test(e2e): sign out with the two-step confirm
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 16:29:18 +02:00
kristianwindandClaude Opus 4.8 b81f8f67cc feat(ui): Charcoal theme, elegant selection, rounded note list, confirm sign-out
- A new Charcoal theme — neutral dark greys on the menu, note list and
  editor instead of the default's faint green cast, with matching inks;
  the green accent stays. It joins the theme picker on its own (the picker
  is data-driven from THEMES).
- The text selection is a translucent wash of the accent, not the old
  near-invisible tint and not a solid slab — visible on white and on the
  dark surfaces, keeping the text's own colour.
- The note list is a rounded card now, the same corner and border as the
  editor beside it, with a few pixels more air around the menu and the list.
- Signing out asks first: a second click within a few seconds confirms, so
  a button the pointer passes over does not drop you at the login screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 16:27:55 +02:00
7 changed files with 192 additions and 25 deletions
+6 -1
View File
@@ -2030,7 +2030,12 @@ test('en passkey kan registreres og logges ind med', async ({ browser, browserNa
// Now sign out and back in with it. No email typed: the device knows which
// account its key belongs to, which is also why this page cannot be used to
// find out who has an account here.
await page.getByRole('navigation', { name: 'Hovedmenu' }).getByText('Log ud').click();
// Signing out asks first now: one click arms it, a second within a few seconds
// does it. The whole account row is the button; its label changes to the prompt.
const signOut = page.getByRole('navigation', { name: 'Hovedmenu' }).locator('.user');
await signOut.click();
await expect(signOut).toContainText('Tryk igen for at logge ud');
await signOut.click();
// `exact`, because "Log ind" is a substring of "Log ind med en passkey" and
// Playwright matches an accessible name loosely unless told otherwise.
await expect(page.getByRole('button', { name: 'Log ind', exact: true })).toBeVisible();
+52 -2
View File
@@ -210,6 +210,52 @@
--shadow-lg: 0 8px 32px rgb(0 0 0 / 0.4);
}
/* Kul — the greys of Apple Notes' dark mode, to Kristian's own sampled values.
Three panel greys, lightest sidebar to darkest editor — the reverse of the usual
sunken-is-darkest order, which is why the editor gets its own --editor-bg rather
than sharing --surface-raised with the list. The accent turns amber, and the
selected menu item, selected note and section headings each get a fallback token
so this theme can hit its colours without touching any other. */
[data-theme='charcoal'] {
--ground: #1e1e1e;
--surface: #2a3033;
--surface-raised: #242b2e; /* the note list */
--surface-sunken: #2d3133; /* the menu / sidebar — lightest here */
--line: #34393b;
--line-strong: #414749;
--ink: #ffffff;
--ink-muted: #dcdcdc;
--ink-faint: #9ba0a2;
/* Amber, Apple's Notes accent. The bright value is the selected menu item; the
dark text rides on top of it. */
--accent: #ffce35;
--accent-hover: #ffd75a;
--accent-sunken: #ffce35;
--accent-ink: #1e1e1e;
/* Per-element, and only here — every other theme falls back to the shared token
in the var() calls that read these. */
--editor-bg: #1e1e1e; /* the text field, a shade below the list */
--note-on-bg: #a78c26; /* a selected note in the list — gold */
--note-on-ink: #1e1e1e; /* dark text on the gold */
--nav-active-ink: #1e1e1e; /* dark text on the amber menu item */
--heading-ink: #8a8e90; /* PROJEKTER and the group names — quiet grey */
--note-group-bg: #2d3133; /* day separators take the menu's resting grey, not the darker ground */
--p1: #cf6b6b;
--p2: #d69f5c;
--p3: #6b9ecf;
--p4: var(--ink-faint);
--danger: #cf6b6b;
--danger-sunken: #33201f;
--shadow: 0 1px 2px rgb(0 0 0 / 0.25);
--shadow-lg: 0 8px 32px rgb(0 0 0 / 0.4);
}
/* Papir — warm light, closer to paper than to a screen. */
[data-theme='paper'] {
--ground: #f6f1e7;
@@ -375,9 +421,13 @@ button {
søgefelt. */
}
/* A translucent wash of the accent, not a solid block and not the old near-invisible
tint. --accent-sunken sat a hair off the editor's own surface in dark, so a
selection could not be seen; a solid accent could, but read as a heavy slab. Mixing
the accent part-way into transparent lays a clear green film over the text while it
keeps its own colour — visible on white and on the dark surfaces alike. */
::selection {
background: var(--accent-sunken);
color: var(--ink);
background: color-mix(in oklab, var(--accent) 42%, transparent);
}
/* Scrollbars, made quiet. The default on Windows is a wide grey slab that
+41 -7
View File
@@ -34,6 +34,23 @@
location.href = '/';
}
// Signing out is one click from a button the pointer passes over on its way to
// everything below it, and it drops you at the login screen — so it asks first.
// A second click within a few seconds confirms; anything else lets it lapse. Not
// a dialog: a two-step button is lighter and stays where the eye already is.
let confirmSignOut = $state(false);
let signOutTimer;
function onSignOutClick() {
if (confirmSignOut) {
clearTimeout(signOutTimer);
signOut();
return;
}
confirmSignOut = true;
clearTimeout(signOutTimer);
signOutTimer = setTimeout(() => (confirmSignOut = false), 3500);
}
let filters = $state([]);
let labels = $state([]);
@@ -1050,12 +1067,12 @@
{#if !app.connected}
<span class="offline" title={t('nav.offlineHint')}>{t('nav.offline')}</span>
{/if}
<button class="user" onclick={signOut}>
<button class="user" class:confirming={confirmSignOut} onclick={onSignOutClick}>
<span class="avatar" style="background: {app.user?.avatar_color}">
{app.user?.name?.[0]?.toUpperCase() ?? '?'}
</span>
<span class="user-name">{app.user?.name}</span>
<span class="signout">{t('nav.signOut')}</span>
<span class="user-name">{confirmSignOut ? t('nav.signOutConfirm') : app.user?.name}</span>
<span class="signout">{confirmSignOut ? '' : t('nav.signOut')}</span>
</button>
</div>
</div>
@@ -1091,14 +1108,16 @@
display: flex;
flex-direction: column;
gap: var(--s5);
padding: var(--s4) var(--s3);
/* A few pixels more air down each side than the rows strictly need, so the
menu breathes rather than sitting flush against its own edges. */
padding: var(--s4) calc(var(--s3) + 3px);
background: var(--surface-sunken);
border-right: 1px solid var(--line);
/* Selve bjælken ruller ikke længere — .scroller gør. `hidden` frem for
ingenting, fordi den stadig er den yderste kant: løber en række over,
skal den klippes her og ikke skubbe kolonnen bredere. */
overflow: hidden;
padding-left: max(var(--s3), env(safe-area-inset-left));
padding-left: max(calc(var(--s3) + 3px), env(safe-area-inset-left));
position: relative;
}
@@ -1317,7 +1336,9 @@
h2 {
font-size: var(--menu-size);
font-weight: 600;
color: var(--ink);
/* Kul sets these headings to a quiet grey (--heading-ink); every other theme
falls back to the reading ink and keeps them bold and bright. */
color: var(--heading-ink, var(--ink));
}
/* The heading's mark, in the muted ink so the bold name still leads. Sized and
@@ -1572,7 +1593,10 @@
in both themes. */
a.active {
background: var(--accent-sunken);
color: var(--ink);
/* On a solid accent (Kul's amber menu item) the label goes dark via
--nav-active-ink; on the tinted accent-sunken every other theme uses, it
falls back to the reading ink. */
color: var(--nav-active-ink, var(--ink));
font-weight: 600;
}
@@ -1695,6 +1719,16 @@
background: var(--surface);
}
/* Asking to confirm: the row goes to the danger tint and the name is replaced by
"click again", so the second click is plainly the one that signs out. */
.user.confirming {
background: var(--danger-sunken);
}
.user.confirming .user-name {
color: var(--danger);
}
.avatar {
width: 22px;
height: 22px;
+3
View File
@@ -119,6 +119,7 @@ export const da = {
'nav.calendar': 'Kalender',
'nav.settings': 'Indstillinger',
'nav.signOut': 'Log ud',
'nav.signOutConfirm': 'Tryk igen for at logge ud',
'nav.main': 'Hovedmenu',
'nav.search': 'Søg',
'nav.searchLong': 'Søg i opgaver og projekter',
@@ -843,6 +844,8 @@ export const da = {
// --- the themes -------------------------------------------------------------------
'theme.dark': 'Nordlys',
'theme.darkNote': 'Mørk med grøn accent.',
'theme.charcoal': 'Kul',
'theme.charcoalNote': 'Neutral mørkegrå.',
'theme.dusk': 'Skumring',
'theme.duskNote': 'Mørk og varm, med rav.',
'theme.light': 'Dagslys',
+3
View File
@@ -115,6 +115,7 @@ export const en = {
'nav.calendar': 'Calendar',
'nav.settings': 'Settings',
'nav.signOut': 'Sign out',
'nav.signOutConfirm': 'Click again to sign out',
'nav.main': 'Main menu',
'nav.search': 'Search',
'nav.searchLong': 'Search tasks and projects',
@@ -836,6 +837,8 @@ export const en = {
// --- the themes -------------------------------------------------------------------
'theme.dark': 'Northern lights',
'theme.darkNote': 'Dark, with a green accent.',
'theme.charcoal': 'Charcoal',
'theme.charcoalNote': 'Neutral dark grey.',
'theme.dusk': 'Dusk',
'theme.duskNote': 'Dark and warm, with amber.',
'theme.light': 'Daylight',
+1
View File
@@ -1006,6 +1006,7 @@ export const completedView = new CompletedView();
*/
export const THEMES = [
{ id: 'dark', name: 'theme.dark', note: 'theme.darkNote', dark: true },
{ id: 'charcoal', name: 'theme.charcoal', note: 'theme.charcoalNote', dark: true },
{ id: 'dusk', name: 'theme.dusk', note: 'theme.duskNote', dark: true },
{ id: 'light', name: 'theme.light', note: 'theme.lightNote', dark: false },
{ id: 'paper', name: 'theme.paper', note: 'theme.paperNote', dark: false },
+86 -15
View File
@@ -707,7 +707,10 @@
}}
/>
<div class="notes">
<!-- `has-note` drives the phone layout: one column, showing the list until a note
is opened and the editor once one is. On a wide screen both are always up and
the class does nothing. -->
<div class="notes" class:has-note={selectedId}>
<aside>
<div class="head">
<h1>{t('notes.title')}</h1>
@@ -916,7 +919,15 @@
</aside>
<section class="editor">
{#if selected}
{#if selectedId}
<!-- Keyed on the id, which is set the instant a row is tapped — not on the
loaded note, which arrives a moment later. On a phone that moment was the
whole bug: the pane did not switch until the body had loaded, so the first
tap looked like nothing and people tapped again. -->
<!-- Phone only (hidden on a wide screen by CSS): the way back to the list,
since there the editor fills the screen and the list is not beside it. -->
<button class="back" onclick={() => open(null)}>‹ {t('notes.title')}</button>
{#if selected}
<NoteEditor
note={selected}
notes={notes}
@@ -1050,6 +1061,12 @@
{/if}
</div>
</footer>
{:else}
<!-- Chosen, but its full text is still loading. Keep the pane (so the tap
felt like it landed) and stay quiet — the "pick a note" line is for
when nothing is chosen at all, not for this half-second. -->
<p class="empty" aria-busy="true"></p>
{/if}
{:else}
<p class="empty">{t('notes.pickOne')}</p>
{/if}
@@ -1068,8 +1085,9 @@
against the sidebar's rule, which reads as a rendering fault rather than
as a layout. */
/* Luft hele vejen rundt om arket. Højre og bund var nul, dengang editoren
ingen kant havde og ingenting kunne støde imod. */
padding: var(--s3) var(--s4) var(--s3) var(--s4);
ingen kant havde og ingenting kunne støde imod. Et par pixels mere om
notelisten, så kortet ikke ligger klods op ad menuen. */
padding: var(--s3) var(--s4) var(--s3) calc(var(--s4) + 3px);
display: grid;
grid-template-columns: minmax(220px, 300px) 1fr;
gap: var(--s4);
@@ -1083,13 +1101,15 @@
gap: var(--s2);
min-height: 0;
min-width: 0;
border-right: 1px solid var(--line);
padding-right: var(--s3);
/* A rounded card, the same corner and border as the editor beside it — the
list is its own sheet, not a column ruled off the page. */
border: 1px solid var(--line);
border-radius: var(--radius);
/* White in light, a nuance lighter than the ground in dark — the note list
is a surface you read down, so it lifts off the page like the editor does.
--surface-raised is #fff in the light themes and one step up in the dark. */
background: var(--surface-raised);
padding-left: var(--s3);
padding: var(--s3);
}
.head {
@@ -1386,7 +1406,10 @@
top: 0;
z-index: 1;
margin: 0 0 var(--s1);
background: var(--ground);
/* --ground everywhere but Kul, where the ground is the darkest thing on screen
and a day separator on it reads as a dark bar. There it takes the menu's own
resting colour instead, via --note-group-bg. */
background: var(--note-group-bg, var(--ground));
border-radius: var(--radius);
}
@@ -1477,8 +1500,20 @@
}
.row.on {
background: var(--surface-raised);
color: var(--ink);
background: var(--note-on-bg, var(--surface-raised));
color: var(--note-on-ink, var(--ink));
}
/* On a themed selection colour (Kul's gold), the row's own quieter tiers go dark
too, or the date and the filed line vanish into it. Other themes have no
--note-on-ink, so these fall back to their normal muted inks. */
.row.on .when,
.row.on .filed {
color: var(--note-on-ink, var(--ink-muted));
}
.row.on .filed-icon {
color: var(--note-on-ink, var(--ink-faint));
}
/* Samme størrelse som en opgavetitel. En note og en opgave er to ting af samme
@@ -1574,8 +1609,10 @@
min-height: 0;
min-width: 0;
/* A nuance lighter than the ground in dark, still white in light — matches
the note list beside it so the two read as one writing surface. */
background: var(--surface-raised);
the note list beside it so the two read as one writing surface. Kul gives
the text field its own darker grey via --editor-bg; other themes fall back
to the shared surface so the list and the editor stay the same colour. */
background: var(--editor-bg, var(--surface-raised));
border: 1px solid var(--line);
border-radius: var(--radius);
/* Luft nok til at teksten ikke rører kanten.
@@ -1894,14 +1931,48 @@
font-size: var(--text-sm);
}
/* The way back to the list on a phone — an iOS-style "‹ Noter" at the top of the
editor. Hidden on a wide screen, where the list is always beside the editor. */
.back {
display: none;
align-self: flex-start;
align-items: center;
gap: var(--s1);
margin-bottom: var(--s2);
padding: var(--s1) 0;
font-size: var(--text-base);
color: var(--accent);
}
@media (max-width: 700px) {
.notes {
grid-template-columns: 1fr;
padding: 0;
gap: 0;
}
aside {
border-right: 0;
padding-right: 0;
/* One pane at a time. The two-column card layout has no room on a phone, so
the list shows until a note is opened and the editor takes over once one is
— the back button is the way between them. Without this the editor stacked
below a full-height list, so a tapped note opened out of sight. */
.notes.has-note aside {
display: none;
}
.notes:not(.has-note) .editor {
display: none;
}
/* Full-bleed: the card border and rounded corners are for sitting beside
another card, and on one column there is nothing beside it. */
aside,
.editor {
border: 0;
border-radius: 0;
}
.back {
display: inline-flex;
}
}
</style>