frontend: reminders becomes a lens, and cards can clear a reminder (task 1913)
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / Build & push image (push) Successful in 46s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
Android (Tauri) / Android APK (debug) (push) Successful in 3m41s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m2s
Desktop (Tauri) / Update manifest (push) Successful in 6s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 11s
CI & Build / Build & push image (push) Successful in 46s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m50s
Android (Tauri) / Android APK (debug) (push) Successful in 3m41s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 5m2s
Desktop (Tauri) / Update manifest (push) Successful in 6s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> {
|
||||
await notes.completeReminder(props.note.id);
|
||||
emit("reminder-changed");
|
||||
}
|
||||
|
||||
async function snoozeReminder(minutes: number): Promise<void> {
|
||||
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))
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="note.remind_at" class="mt-2">
|
||||
<div v-if="note.remind_at" class="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
@@ -283,7 +297,23 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
{{ formatReminder(note.remind_at) }}
|
||||
<!-- Recurrence was only ever shown in the reminders list; carried onto the
|
||||
card so unifying them didn't quietly drop it, and so a repeating note
|
||||
reads as repeating on the board too. -->
|
||||
<span v-if="note.recurrence" :title="`Repeats ${note.recurrence}`">· ↻ {{ note.recurrence }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Clearing a reminder is UPKEEP, and upkeep that costs three clicks is
|
||||
upkeep people stop doing — so it happens right here rather than by
|
||||
opening the note. Shown wherever a note carries a reminder, the board
|
||||
included, not only in the reminders lens.
|
||||
Always visible rather than hover-revealed: a finger cannot hover, and
|
||||
these are the primary action for a due note (task 2697). -->
|
||||
<button type="button" class="chip-btn" title="Mark this reminder done" @click.stop="completeReminder">
|
||||
Done
|
||||
</button>
|
||||
<button type="button" class="chip-btn" title="Snooze 1 hour" @click.stop="snoozeReminder(60)">1h</button>
|
||||
<button type="button" class="chip-btn" title="Snooze 1 day" @click.stop="snoozeReminder(1440)">1d</button>
|
||||
</div>
|
||||
|
||||
<div v-if="trashCountdown" class="mt-2">
|
||||
|
||||
@@ -29,6 +29,7 @@ defineEmits<{
|
||||
(e: "dragstart", note: Note): void;
|
||||
(e: "dragend", note: Note): void;
|
||||
(e: "drop", targetId: string): void;
|
||||
(e: "reminder-changed"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -51,6 +52,7 @@ defineEmits<{
|
||||
@dragstart="$emit('dragstart', $event)"
|
||||
@dragend="$emit('dragend', $event)"
|
||||
@drop="$emit('drop', $event)"
|
||||
@reminder-changed="$emit('reminder-changed')"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { useNotesStore, type Note } from "../stores/notes";
|
||||
import { useReminderStore } from "../stores/reminders";
|
||||
import { formatReminder, isOverdue } from "../notes/datetime";
|
||||
import { useNoteList } from "../composables/useNoteList";
|
||||
import { useNoteEditor } from "../composables/useNoteEditor";
|
||||
import AsyncState from "../components/AsyncState.vue";
|
||||
import EmptyState from "../components/EmptyState.vue";
|
||||
import NoteEditor from "../components/NoteEditor.vue";
|
||||
import NoteGrid from "../components/NoteGrid.vue";
|
||||
|
||||
const notes = useNotesStore();
|
||||
const reminders = useReminderStore();
|
||||
|
||||
const { items, loading, error, load } = useNoteList(() => reminders.fetchReminders(), "Couldn't load reminders.");
|
||||
@@ -19,20 +17,14 @@ const { editing, open: openEditor, close: closeEditor, navigate: onNavigate } =
|
||||
onClose: load,
|
||||
});
|
||||
|
||||
async function done(n: Note) {
|
||||
await notes.completeReminder(n.id);
|
||||
await load();
|
||||
}
|
||||
async function snooze(n: Note, minutes: number) {
|
||||
await notes.snoozeReminder(n.id, minutes);
|
||||
await load();
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto w-full max-w-2xl px-4 py-6">
|
||||
<!-- max-w-6xl, matching the board and every other lens: this is the same masonry
|
||||
now, and a narrower column would reintroduce the "different page" feeling the
|
||||
card unification just removed. -->
|
||||
<div class="mx-auto w-full max-w-6xl px-4 py-6">
|
||||
<!-- Titled by the shell's persistent lens name (task 1913). -->
|
||||
<div class="mb-2 flex items-center justify-end gap-3">
|
||||
<button
|
||||
@@ -55,45 +47,12 @@ onMounted(load);
|
||||
title="No reminders"
|
||||
subtitle="Set a reminder on a note (in its editor) to see it here."
|
||||
/>
|
||||
<ul v-else class="divide-y divide-neutral-100 dark:divide-neutral-800">
|
||||
<li v-for="n in items" :key="n.id" class="flex items-center gap-3 py-3">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 rounded text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
||||
@click="openEditor(n)"
|
||||
>
|
||||
<p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100">
|
||||
{{ n.display_title || "Untitled" }}
|
||||
</p>
|
||||
<p class="text-xs" :class="isOverdue(n.remind_at) ? 'text-red-500 dark:text-red-400' : 'text-neutral-400'">
|
||||
{{ formatReminder(n.remind_at) }}<span v-if="n.recurrence"> · ↻ {{ n.recurrence }}</span>
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md border border-neutral-300 px-2 py-1 text-xs text-neutral-600 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
@click="done(n)"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 text-xs text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
|
||||
title="Snooze 1 hour"
|
||||
@click="snooze(n, 60)"
|
||||
>
|
||||
1h
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 text-xs text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
|
||||
title="Snooze 1 day"
|
||||
@click="snooze(n, 1440)"
|
||||
>
|
||||
1d
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- The same cards, same grid, same motion as the board — reminders is a lens
|
||||
on those notes, not its own kind of thing (task 1913). Done/snooze moved
|
||||
onto the card itself, so one-tap triage survived the change; `load` on
|
||||
reminder-changed because this list is fetched separately from the store
|
||||
and a cleared note has to leave it. -->
|
||||
<NoteGrid v-else :notes="items" @open="openEditor" @reminder-changed="load" />
|
||||
</AsyncState>
|
||||
|
||||
<template v-if="editing">
|
||||
|
||||
Reference in New Issue
Block a user