M12 — the Android client, end to end #2
@@ -32,6 +32,10 @@ const emit = defineEmits<{
|
|||||||
/** The card this one was dropped ON, by id — the dragged card hit-tests the DOM
|
/** 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. */
|
* for it, so an id is all it can know without looking the note up again. */
|
||||||
(e: "drop", targetId: string): void;
|
(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 notes = useNotesStore();
|
||||||
const config = useConfigStore();
|
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 {
|
function cardClass(color: NoteColor): string {
|
||||||
return NOTE_CARD_CLASSES[color] ?? NOTE_CARD_CLASSES.default;
|
return NOTE_CARD_CLASSES[color] ?? NOTE_CARD_CLASSES.default;
|
||||||
}
|
}
|
||||||
@@ -261,7 +275,7 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
>
|
>
|
||||||
</div>
|
</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
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
||||||
:class="
|
:class="
|
||||||
@@ -283,7 +297,23 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
|||||||
<polyline points="12 6 12 12 16 14" />
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</svg>
|
||||||
{{ formatReminder(note.remind_at) }}
|
{{ 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>
|
</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>
|
||||||
|
|
||||||
<div v-if="trashCountdown" class="mt-2">
|
<div v-if="trashCountdown" class="mt-2">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ defineEmits<{
|
|||||||
(e: "dragstart", note: Note): void;
|
(e: "dragstart", note: Note): void;
|
||||||
(e: "dragend", note: Note): void;
|
(e: "dragend", note: Note): void;
|
||||||
(e: "drop", targetId: string): void;
|
(e: "drop", targetId: string): void;
|
||||||
|
(e: "reminder-changed"): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ defineEmits<{
|
|||||||
@dragstart="$emit('dragstart', $event)"
|
@dragstart="$emit('dragstart', $event)"
|
||||||
@dragend="$emit('dragend', $event)"
|
@dragend="$emit('dragend', $event)"
|
||||||
@drop="$emit('drop', $event)"
|
@drop="$emit('drop', $event)"
|
||||||
|
@reminder-changed="$emit('reminder-changed')"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -156,6 +156,21 @@ body {
|
|||||||
@apply inline-flex min-h-[2.75rem] min-w-[2.75rem] items-center justify-center;
|
@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 {
|
.nav-link {
|
||||||
@apply flex items-center gap-2 rounded-lg px-3 py-2 font-medium text-neutral-600 transition
|
@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
|
hover:bg-neutral-200/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { useNotesStore, type Note } from "../stores/notes";
|
|
||||||
import { useReminderStore } from "../stores/reminders";
|
import { useReminderStore } from "../stores/reminders";
|
||||||
import { formatReminder, isOverdue } from "../notes/datetime";
|
|
||||||
import { useNoteList } from "../composables/useNoteList";
|
import { useNoteList } from "../composables/useNoteList";
|
||||||
import { useNoteEditor } from "../composables/useNoteEditor";
|
import { useNoteEditor } from "../composables/useNoteEditor";
|
||||||
import AsyncState from "../components/AsyncState.vue";
|
import AsyncState from "../components/AsyncState.vue";
|
||||||
import EmptyState from "../components/EmptyState.vue";
|
import EmptyState from "../components/EmptyState.vue";
|
||||||
import NoteEditor from "../components/NoteEditor.vue";
|
import NoteEditor from "../components/NoteEditor.vue";
|
||||||
|
import NoteGrid from "../components/NoteGrid.vue";
|
||||||
|
|
||||||
const notes = useNotesStore();
|
|
||||||
const reminders = useReminderStore();
|
const reminders = useReminderStore();
|
||||||
|
|
||||||
const { items, loading, error, load } = useNoteList(() => reminders.fetchReminders(), "Couldn't load reminders.");
|
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,
|
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);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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). -->
|
<!-- Titled by the shell's persistent lens name (task 1913). -->
|
||||||
<div class="mb-2 flex items-center justify-end gap-3">
|
<div class="mb-2 flex items-center justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -55,45 +47,12 @@ onMounted(load);
|
|||||||
title="No reminders"
|
title="No reminders"
|
||||||
subtitle="Set a reminder on a note (in its editor) to see it here."
|
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">
|
<!-- The same cards, same grid, same motion as the board — reminders is a lens
|
||||||
<li v-for="n in items" :key="n.id" class="flex items-center gap-3 py-3">
|
on those notes, not its own kind of thing (task 1913). Done/snooze moved
|
||||||
<button
|
onto the card itself, so one-tap triage survived the change; `load` on
|
||||||
type="button"
|
reminder-changed because this list is fetched separately from the store
|
||||||
class="min-w-0 flex-1 rounded text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
and a cleared note has to leave it. -->
|
||||||
@click="openEditor(n)"
|
<NoteGrid v-else :notes="items" @open="openEditor" @reminder-changed="load" />
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</AsyncState>
|
</AsyncState>
|
||||||
|
|
||||||
<template v-if="editing">
|
<template v-if="editing">
|
||||||
|
|||||||
Reference in New Issue
Block a user