From 489e47b9a24b9a3f0da98efa2c38ef1c75bddba3 Mon Sep 17 00:00:00 2001 From: danielededo <37807398+danielededo@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:52:48 +0000 Subject: [PATCH 1/2] feat(web): the note on payslips added goes after five seconds It folds away, its height and opacity easing to nothing, so that the report rises smoothly instead of jumping. It stays while the pointer or the focus is on it, and with reduced motion it simply goes. --- CHANGELOG.md | 2 ++ web/app.js | 40 +++++++++++++++++++++++++++++++++++++--- web/style.css | 2 ++ web/test/e2e.test.mjs | 2 ++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a8e9f..6cd14a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ How to use this file: ### Changed +- The note confirming the payslips added goes after five seconds, folding + away smoothly; not while the pointer or the focus is on it. - In the web app the page no longer lists the files: it says only how many need a look, if any. The files are in a sheet of their own, opened from that line or from "Files read" under the report: what needs a look diff --git a/web/app.js b/web/app.js index 0b87a95..c8852a5 100644 --- a/web/app.js +++ b/web/app.js @@ -321,9 +321,42 @@ function added(files) { let text = t('web.added.none'); if (months.length === 1) text = t('web.added.one', months[0]); if (months.length > 1) text = t('web.added.many', months.length, months.join(', ')); - $('added').textContent = text; - $('added').classList.toggle('none', months.length === 0); - $('added').hidden = false; + const note = $('added'); + note.textContent = text; + note.classList.toggle('none', months.length === 0); + note.classList.remove('going'); + note.hidden = false; + fadeAdded(); +} + +// The confirmation goes after a few seconds, folding away so that the +// report rises smoothly under it; not while the pointer or the focus is +// on it, so that it can be read. +const ADDED_FOR = 5000; +let addedTimer = 0; +function fadeAdded() { + clearTimeout(addedTimer); + addedTimer = setTimeout(() => { + const note = $('added'); + if (note.hidden) return; + if (note.matches(':hover, :focus-within')) { + fadeAdded(); + return; + } + if (calm()) { + note.hidden = true; + return; + } + note.style.height = `${note.offsetHeight}px`; + note.getBoundingClientRect(); + note.classList.add('going'); + note.style.height = '0px'; + note.addEventListener('transitionend', () => { + note.hidden = true; + note.classList.remove('going'); + note.style.height = ''; + }, { once: true }); + }, ADDED_FOR); } // progress shows how many files have been read, or hides the bar. @@ -347,6 +380,7 @@ async function handle(fileList) { // the progress and any password. const adding = !$('result').hidden; const before = state.files.length; + clearTimeout(addedTimer); $('added').hidden = true; if (adding) { $('pick').hidden = false; diff --git a/web/style.css b/web/style.css index 50d1062..aac6fda 100644 --- a/web/style.css +++ b/web/style.css @@ -147,6 +147,8 @@ h1 { font-size: clamp(30px, 6vw, 40px); line-height: 1.1; margin: 0 0 12px; lett .added::before { content: "✓ "; color: var(--good); font-weight: 700; } .added.none { background: var(--raised); color: var(--ink-2); } .added.none::before { content: ""; } +.added.going { overflow: hidden; opacity: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; + transition: height .3s ease, opacity .3s ease, margin .3s ease, padding .3s ease; } .misread { margin: 16px 0 0; text-align: center; } .report-tabs { display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; border-top: 1px solid var(--line); } .report-tabs[hidden] { display: none; } diff --git a/web/test/e2e.test.mjs b/web/test/e2e.test.mjs index d80541a..b42c508 100644 --- a/web/test/e2e.test.mjs +++ b/web/test/e2e.test.mjs @@ -390,6 +390,8 @@ test('adds payslips to a report', async () => { await frame.waitForFunction(() => document.querySelectorAll('.picker button').length === 3); // The report opens on the latest month again. assert.equal(await frame.$eval('.picker button[aria-pressed="true"]', (b) => b.textContent), 'Mar 26'); + // The confirmation goes by itself after a few seconds. + await page.waitForSelector('#added', { state: 'hidden', timeout: 8000 }); assert.deepEqual(seen.errors, []); onlyThisSite(seen); await page.close(); From 7f7a6d415e9edd84629dcd633879c5d98e9b61e5 Mon Sep 17 00:00:00 2001 From: danielededo <37807398+danielededo@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:56:08 +0000 Subject: [PATCH 2/2] feat(web): the note dissolves before its room closes It fades out over 0.6 s, then its height eases to nothing, instead of both at once. A note shown again ends the fading of the one before. --- CHANGELOG.md | 5 +++-- web/app.js | 34 ++++++++++++++++++++++------------ web/style.css | 5 +++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd14a5..4b78cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,8 +46,9 @@ How to use this file: ### Changed -- The note confirming the payslips added goes after five seconds, folding - away smoothly; not while the pointer or the focus is on it. +- The note confirming the payslips added goes after five seconds: it + dissolves, then the room it took closes smoothly; not while the pointer + or the focus is on it. - In the web app the page no longer lists the files: it says only how many need a look, if any. The files are in a sheet of their own, opened from that line or from "Files read" under the report: what needs a look diff --git a/web/app.js b/web/app.js index c8852a5..1b6e59d 100644 --- a/web/app.js +++ b/web/app.js @@ -324,18 +324,21 @@ function added(files) { const note = $('added'); note.textContent = text; note.classList.toggle('none', months.length === 0); - note.classList.remove('going'); + note.classList.remove('fading', 'going'); + note.style.height = ''; note.hidden = false; fadeAdded(); } -// The confirmation goes after a few seconds, folding away so that the -// report rises smoothly under it; not while the pointer or the focus is -// on it, so that it can be read. +// The confirmation goes after a few seconds, dissolving and then folding +// away so that the report rises smoothly under it; not while the pointer +// or the focus is on it, so that it can be read. const ADDED_FOR = 5000; -let addedTimer = 0; +let addedTimer = 0, addedTurn = 0; function fadeAdded() { clearTimeout(addedTimer); + // A note shown again ends the fading of the one before. + const turn = ++addedTurn; addedTimer = setTimeout(() => { const note = $('added'); if (note.hidden) return; @@ -347,14 +350,20 @@ function fadeAdded() { note.hidden = true; return; } - note.style.height = `${note.offsetHeight}px`; - note.getBoundingClientRect(); - note.classList.add('going'); - note.style.height = '0px'; + // First it dissolves, then the room it took closes. + note.classList.add('fading'); note.addEventListener('transitionend', () => { - note.hidden = true; - note.classList.remove('going'); - note.style.height = ''; + if (turn !== addedTurn) return; + note.style.height = `${note.offsetHeight}px`; + note.getBoundingClientRect(); + note.classList.add('going'); + note.style.height = '0px'; + note.addEventListener('transitionend', () => { + if (turn !== addedTurn) return; + note.hidden = true; + note.classList.remove('fading', 'going'); + note.style.height = ''; + }, { once: true }); }, { once: true }); }, ADDED_FOR); } @@ -381,6 +390,7 @@ async function handle(fileList) { const adding = !$('result').hidden; const before = state.files.length; clearTimeout(addedTimer); + addedTurn++; $('added').hidden = true; if (adding) { $('pick').hidden = false; diff --git a/web/style.css b/web/style.css index aac6fda..a40ad57 100644 --- a/web/style.css +++ b/web/style.css @@ -147,8 +147,9 @@ h1 { font-size: clamp(30px, 6vw, 40px); line-height: 1.1; margin: 0 0 12px; lett .added::before { content: "✓ "; color: var(--good); font-weight: 700; } .added.none { background: var(--raised); color: var(--ink-2); } .added.none::before { content: ""; } -.added.going { overflow: hidden; opacity: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; - transition: height .3s ease, opacity .3s ease, margin .3s ease, padding .3s ease; } +.added.fading { opacity: 0; transition: opacity .6s ease; } +.added.going { overflow: hidden; margin-bottom: 0; padding-top: 0; padding-bottom: 0; + transition: height .25s ease, margin .25s ease, padding .25s ease; } .misread { margin: 16px 0 0; text-align: center; } .report-tabs { display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px; overflow-x: auto; scrollbar-width: none; border-top: 1px solid var(--line); } .report-tabs[hidden] { display: none; }