fix(archive): keep the Moved to Archive toast off the next events

Centred at the bottom for eight seconds, the Undo toast sat over the
events moving up into the gap an ignored or completed row left. It now
sits in the bottom-right corner, lasts four seconds, and has an OK that
closes it without undoing. On a phone there is no side to clear and the
bottom is where an event's page ends, so it goes to the top, over the
header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-11-10 03:50:44 +01:00
1 parent 8c9812648c
commit 56e034c34e
6 files changed
+79 -24

No files matched your search

+6 -1
View File
@@ -496,7 +496,12 @@ text. On focus the border moves to faint. The field gets no glow, because a form
- **Detail sheet:** `surface` fill with a hairline border and 16px corners. It rises from the bottom - **Detail sheet:** `surface` fill with a hairline border and 16px corners. It rises from the bottom
on a phone, is centred at `max-w-lg` from `sm` up, and sits over the scrim. on a phone, is centred at `max-w-lg` from `sm` up, and sits over the scrim.
- **Toast / update notice:** `raised` fill, hairline border, 12px corners and the only shadow in the - **Toast / update notice:** `raised` fill, hairline border, 12px corners and the only shadow in the
system. Each offers one action. system. Each offers one action, and the "Moved to Archive" toast also a plain OK that only closes
it. That toast sits in the bottom-right corner, 24px in, at most 420px wide, so it stays off the
reading column and the events moving up into the gap; up to 700px wide there is no side to clear,
so it spans the width 16px in from each edge at the top, over the header, because at the bottom it
covered the end of an event's page opened while it was up. It was centred, for eight seconds,
until 2026-11-10, when the owner found it covering the next events.
### Daily burst ### Daily burst
A small spray of 3px sparks that fires once, in the row, when a reader finishes one thing: a daily A small spray of 3px sparks that fires once, in the row, when a reader finishes one thing: a daily
+2 -1
View File
@@ -193,7 +193,8 @@ the cut are still counted in the header, still on the timeline, and one tap away
**F3 — Mark completed.** **F3 — Mark completed.**
Done writes durable progress and a metadata snapshot immediately. A confirmation remains for about Done writes durable progress and a metadata snapshot immediately. A confirmation remains for about
0.9 seconds (1.5 until 2026-10-20, which read as unresponsive) before the row fades/collapses into 0.9 seconds (1.5 until 2026-10-20, which read as unresponsive) before the row fades/collapses into
Archive. Undo is available for eight seconds; Archive Restore remains available afterwards. A failed Archive. Undo is offered in a toast, out of the list's way, for four seconds (eight until 2026-11-10, when it sat
over the next events), and its OK closes it sooner; Archive Restore remains available afterwards. A failed
write is disclosed and never presented as saved. Reduced motion removes the fade. write is disclosed and never presented as saved. Reduced motion removes the fade.
**F4 — Settings and visibility.** **F4 — Settings and visibility.**
+25 -16
View File
@@ -69,6 +69,7 @@ import { ENTRIES, NOTES, newestId, unseen } from "../shared/changelog.ts";
import { import {
ARCHIVE_COLLAPSE_MS, ARCHIVE_COLLAPSE_MS,
ARCHIVE_HOLD_MS, ARCHIVE_HOLD_MS,
ARCHIVE_UNDO_MS,
ChecklistEvent, ChecklistEvent,
leavingMarks, leavingMarks,
LedgerColumns, LedgerColumns,
@@ -498,7 +499,7 @@ function ReaderContent({
() => setTransition((t) => (t ? { ...t, phase: "hidden" } : null)), () => setTransition((t) => (t ? { ...t, phase: "hidden" } : null)),
ARCHIVE_COLLAPSE_MS, ARCHIVE_COLLAPSE_MS,
); );
const undo = setTimeout(() => setTransition(null), 8000); const undo = setTimeout(() => setTransition(null), ARCHIVE_UNDO_MS);
return () => { return () => {
clearTimeout(t); clearTimeout(t);
clearTimeout(collapse); clearTimeout(collapse);
@@ -1137,21 +1138,29 @@ function ReaderContent({
transition.phase === "hold" && ( transition.phase === "hold" && (
<Fireworks key={transition.key} /> <Fireworks key={transition.key} />
)} )}
{transition.label} “{transition.title}” · Moved to Archive{" "} <span className="archive-toast-text">
<button {transition.label} “{transition.title}” · Moved to Archive
type="button" </span>
onClick={() => { <span className="archive-toast-actions">
void dispatch({ <button
type: "restore", type="button"
accountId: transition.accountId, onClick={() => {
eventId: transition.eventId, void dispatch({
...actionMeta(), type: "restore",
}); accountId: transition.accountId,
setTransition(null); eventId: transition.eventId,
}} ...actionMeta(),
> });
Undo setTransition(null);
</button> }}
>
Undo
</button>
{/* Closes the notice only: the event stays archived. */}
<button type="button" onClick={() => setTransition(null)}>
OK
</button>
</span>
</aside> </aside>
)} )}
{archivedOpen && ( {archivedOpen && (
@@ -56,7 +56,13 @@ export function UrgencyLegend() {
* when it is gone once the 250 ms collapse has run. * when it is gone once the 250 ms collapse has run.
*/ */
export const ARCHIVE_HOLD_MS = 900, export const ARCHIVE_HOLD_MS = 900,
ARCHIVE_COLLAPSE_MS = ARCHIVE_HOLD_MS + 250; ARCHIVE_COLLAPSE_MS = ARCHIVE_HOLD_MS + 250,
/**
* How long the "Moved to Archive" toast offers Undo. Eight seconds until
* 2026-11-10, when the owner found it sitting over the next events; OK closes
* it sooner, and Archive Restore stays available after it is gone.
*/
ARCHIVE_UNDO_MS = 4000;
/** /**
* The marks on a checklist row on its way to the Archive. While it holds, its * The marks on a checklist row on its way to the Archive. While it holds, its
* text greys; a completed one also gets the codes' shine, an ignored one does * text greys; a completed one also gets the codes' shine, an ignored one does
+33 -5
View File
@@ -1003,25 +1003,53 @@ textarea {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 3px; text-underline-offset: 3px;
} }
/* Bottom right, out of the reading column, so it does not sit over the next
events (centred until 2026-11-10). On a phone there is no side to move to,
and the bottom is where the list continues and an event's page ends, so it
spans the width at the top, over the header, instead. */
.archive-toast { .archive-toast {
position: fixed; position: fixed;
bottom: 24px; bottom: 24px;
left: 50%; right: 24px;
transform: translateX(-50%);
width: max-content; width: max-content;
max-width: calc(100vw - 32px); max-width: min(420px, calc(100vw - 32px));
z-index: 70; z-index: 70;
padding: 14px 20px; display: flex;
align-items: center;
gap: 6px 16px;
padding: 12px 16px;
border: 1px solid var(--color-hairline);
border-radius: 12px; border-radius: 12px;
background: var(--color-raised); background: var(--color-raised);
color: var(--color-ink); color: var(--color-ink);
font-size: 13px; font-size: 13px;
box-shadow: 0 8px 30px #0003; box-shadow: 0 8px 30px #0003;
} }
.archive-toast-text {
min-width: 0;
}
.archive-toast-actions {
display: flex;
flex: none;
gap: 4px;
}
.archive-toast button { .archive-toast button {
min-height: 32px;
padding: 0 8px;
border-radius: 8px;
color: var(--color-near); color: var(--color-near);
margin-left: 14px;
text-decoration: underline; text-decoration: underline;
text-underline-offset: 3px;
}
@media (max-width: 700px) {
.archive-toast {
top: calc(16px + env(safe-area-inset-top, 0px));
bottom: auto;
left: 16px;
right: 16px;
width: auto;
max-width: none;
}
} }
.archive-confirm { .archive-confirm {
padding: 8px 20px; padding: 8px 20px;
+6
View File
@@ -86,6 +86,7 @@ test("a completed event shines and greys while it holds, then fades; an ignored
import { import {
ARCHIVE_COLLAPSE_MS, ARCHIVE_COLLAPSE_MS,
ARCHIVE_HOLD_MS, ARCHIVE_HOLD_MS,
ARCHIVE_UNDO_MS,
} from "../src/client/components/redesign/ChecklistEvent.tsx"; } from "../src/client/components/redesign/ChecklistEvent.tsx";
test("a completed event holds under a second, long enough for its shine, then collapses", () => { test("a completed event holds under a second, long enough for its shine, then collapses", () => {
expect(ARCHIVE_HOLD_MS).toBe(900); expect(ARCHIVE_HOLD_MS).toBe(900);
@@ -94,3 +95,8 @@ test("a completed event holds under a second, long enough for its shine, then co
// The collapse waits out the 250 ms max-height transition. // The collapse waits out the 250 ms max-height transition.
expect(ARCHIVE_COLLAPSE_MS - ARCHIVE_HOLD_MS).toBe(250); expect(ARCHIVE_COLLAPSE_MS - ARCHIVE_HOLD_MS).toBe(250);
}); });
test("the Moved to Archive notice offers Undo for four seconds, outlasting the row's collapse", () => {
// Halved from eight on 2026-11-10: the notice sat over the next events.
expect(ARCHIVE_UNDO_MS).toBe(4000);
expect(ARCHIVE_UNDO_MS).toBeGreaterThan(ARCHIVE_COLLAPSE_MS);
});