From 641999de5875dfb6fbae10ce4ee9c67d6eebaec2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 17 Aug 2026 14:48:28 -0400 Subject: [PATCH] frontend: reminders becomes a lens, and cards can clear a reminder (task 1913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reminders was the last surface still reading as its own page — a bespoke row list rather than the board's cards. It is now the same NoteGrid as every other lens. The reason it wasn't already is that the list was a TRIAGE surface: one tap for Done, 1h, 1d. Cards had none of that, so converting naively would have turned each of those into open-act-close. Reminder upkeep is exactly the "maintenance must stay dead simple or people stop coming back" case from the north star, so making it three times more work to look tidier would have been a bad trade. So the actions moved onto the card instead, shown wherever a note carries a reminder — the board included. That turns out to be the better place for them anyway: seeing something due while browsing and clearing it there is useful outside the reminders lens. Always visible rather than hover-revealed, because a finger cannot hover and these are the primary action on a due note; .chip-btn takes the same coarse-pointer sizing rule as .icon-btn. The card acts on the store directly, which the board picks up through reconcile. The reminders lens fetches its own list, so it needs telling — hence the reminder-changed event, which exists only for hosts that hold a list of their own. Also carried recurrence (↻) onto the card. It was shown only in the reminders list, so unifying would have silently dropped it; a repeating note now reads as repeating on the board too. And the container went max-w-2xl → max-w-6xl, since a narrower column would have reintroduced the different-page feeling the cards just removed. RemindersView is ~40 lines lighter for it. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/components/NoteCard.vue | 32 +++++++++++++- frontend/src/components/NoteGrid.vue | 2 + frontend/src/style.css | 15 +++++++ frontend/src/views/RemindersView.vue | 63 +++++----------------------- 4 files changed, 59 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/NoteCard.vue b/frontend/src/components/NoteCard.vue index 9a46202..4370a1b 100644 --- a/frontend/src/components/NoteCard.vue +++ b/frontend/src/components/NoteCard.vue @@ -32,6 +32,10 @@ const emit = defineEmits<{ /** The card this one was dropped ON, by id — the dragged card hit-tests the DOM * for it, so an id is all it can know without looking the note up again. */ (e: "drop", targetId: string): void; + /** A reminder was completed or snoozed. The store reconciles itself, so the board + * needs nothing — this is for hosts holding their OWN list (the reminders lens + * fetches separately) that must re-query to drop the note they just cleared. */ + (e: "reminder-changed"): void; }>(); const notes = useNotesStore(); const config = useConfigStore(); @@ -127,6 +131,16 @@ watch( }, ); +async function completeReminder(): Promise { + await notes.completeReminder(props.note.id); + emit("reminder-changed"); +} + +async function snoozeReminder(minutes: number): Promise { + await notes.snoozeReminder(props.note.id, minutes); + emit("reminder-changed"); +} + function cardClass(color: NoteColor): string { return NOTE_CARD_CLASSES[color] ?? NOTE_CARD_CLASSES.default; } @@ -261,7 +275,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown)) > -
+
{{ formatReminder(note.remind_at) }} + + · ↻ {{ note.recurrence }} + + + + +
diff --git a/frontend/src/components/NoteGrid.vue b/frontend/src/components/NoteGrid.vue index 5d3d402..25c71d5 100644 --- a/frontend/src/components/NoteGrid.vue +++ b/frontend/src/components/NoteGrid.vue @@ -29,6 +29,7 @@ defineEmits<{ (e: "dragstart", note: Note): void; (e: "dragend", note: Note): void; (e: "drop", targetId: string): void; + (e: "reminder-changed"): void; }>(); @@ -51,6 +52,7 @@ defineEmits<{ @dragstart="$emit('dragstart', $event)" @dragend="$emit('dragend', $event)" @drop="$emit('drop', $event)" + @reminder-changed="$emit('reminder-changed')" /> diff --git a/frontend/src/style.css b/frontend/src/style.css index 8330f34..576a0d5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -156,6 +156,21 @@ body { @apply inline-flex min-h-[2.75rem] min-w-[2.75rem] items-center justify-center; } } + /* A small action sitting beside a chip on a card — reminder Done/snooze today. + * Quiet at rest so a board full of reminders doesn't read as a wall of buttons, + * and it earns contrast on hover/focus. */ + .chip-btn { + @apply rounded-full px-2 py-0.5 text-xs text-neutral-500 transition hover:bg-black/5 + hover:text-neutral-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand + dark:text-neutral-400 dark:hover:bg-white/10 dark:hover:text-neutral-100; + } + /* Same finger rule as .icon-btn: comfortable for a cursor is too small for a + * thumb, and these are the primary action on a due note. */ + @media (pointer: coarse) { + .chip-btn { + @apply inline-flex min-h-[2.25rem] items-center justify-center px-3; + } + } .nav-link { @apply flex items-center gap-2 rounded-lg px-3 py-2 font-medium text-neutral-600 transition hover:bg-neutral-200/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand diff --git a/frontend/src/views/RemindersView.vue b/frontend/src/views/RemindersView.vue index 2af13b8..01aab40 100644 --- a/frontend/src/views/RemindersView.vue +++ b/frontend/src/views/RemindersView.vue @@ -1,15 +1,13 @@