M12 — the Android client, end to end #2

Merged
bvandeusen merged 86 commits from dev into main 2026-08-21 08:53:58 -04:00
4 changed files with 59 additions and 53 deletions
Showing only changes of commit 641999de58 - Show all commits
+31 -1
View File
@@ -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">
+2
View File
@@ -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>
+15
View File
@@ -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
+11 -52
View File
@@ -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">