web: shared note actions, reminder chips, page header, formatters and settings seam
From the audit (#5179, web half). - NoteActions: share, pin, archive and trash (restore and delete forever when trashed), as both the card and the editor offer them. The editor's history toggle goes in its slot, and it closes on `acted`. - ReminderActions: the Done / 1h / 1d chips on the card and in the editor, which are now the same chips. - PageHeader: the back-to-board header that Settings, Sync and Linked devices each wrote out, now with a `back` icon from the shared set. - notes/datetime: formatShortDateTime (was formatReminder and the editor's revLabel) and formatDateTime (the two `fmt` copies). - notes/colors: labelDotClasses (the sidebar's and the tag manager's labelDot). - Drawer links use exact-active-class instead of route.name ternaries. - BoardView binds its three grids from one gridBinds object. - Settings goes through repo.settings (rest, plus a local adapter that answers "needs a server") and shows load failures through AsyncState. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import ShareDialog from "./ShareDialog.vue";
|
||||
import { isDesktop } from "../desktop/bridge";
|
||||
import { useLensName } from "../composables/useLensName";
|
||||
import { facetsToQuery } from "../notes/facets";
|
||||
import { NOTE_SWATCH_CLASSES, resolveLabelColor } from "../notes/colors";
|
||||
import { labelDotClasses } from "../notes/colors";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -191,14 +191,6 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", onKeydown);
|
||||
});
|
||||
|
||||
const currentLabelId = computed(() => (route.name === "label" ? String(route.params.id) : null));
|
||||
|
||||
// The drawer's tag list. Same resolution as every other chip and dot — a tag that is
|
||||
// green on a card must be green here, or the sidebar stops being a way to find it.
|
||||
function labelDot(label: { name: string; color: string }): string {
|
||||
return NOTE_SWATCH_CLASSES[resolveLabelColor(label)] ?? NOTE_SWATCH_CLASSES.default;
|
||||
}
|
||||
|
||||
// The board lenses — the routes a search can happen *within*. Searching while looking
|
||||
// at Trash should search Trash, not silently move you.
|
||||
const BOARD_ROUTES = new Set(["board", "archive", "trash", "label"]);
|
||||
@@ -412,7 +404,7 @@ async function signOut() {
|
||||
:class="drawer ? 'translate-x-0' : ''"
|
||||
>
|
||||
<nav class="flex flex-col gap-0.5 text-sm" @click="drawer = false">
|
||||
<RouterLink to="/" class="nav-link" :class="route.name === 'board' ? 'nav-link-active' : ''">
|
||||
<RouterLink to="/" class="nav-link" exact-active-class="nav-link-active">
|
||||
<Icon name="note" /> Notes
|
||||
</RouterLink>
|
||||
|
||||
@@ -434,11 +426,11 @@ async function signOut() {
|
||||
:key="lb.id"
|
||||
:to="`/label/${lb.id}`"
|
||||
class="nav-link"
|
||||
:class="currentLabelId === lb.id ? 'nav-link-active' : ''"
|
||||
exact-active-class="nav-link-active"
|
||||
>
|
||||
<span
|
||||
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
||||
:class="labelDot(lb)"
|
||||
:class="labelDotClasses(lb)"
|
||||
></span>
|
||||
<span class="truncate">{{ lb.name }}</span>
|
||||
</RouterLink>
|
||||
@@ -466,23 +458,23 @@ async function signOut() {
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<RouterLink to="/archive" class="nav-link mt-3" :class="route.name === 'archive' ? 'nav-link-active' : ''">
|
||||
<RouterLink to="/archive" class="nav-link mt-3" exact-active-class="nav-link-active">
|
||||
<Icon name="archive" /> Archive
|
||||
</RouterLink>
|
||||
<RouterLink to="/trash" class="nav-link" :class="route.name === 'trash' ? 'nav-link-active' : ''">
|
||||
<RouterLink to="/trash" class="nav-link" exact-active-class="nav-link-active">
|
||||
<Icon name="trash" /> Trash
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/reminders"
|
||||
class="nav-link"
|
||||
:class="route.name === 'reminders' ? 'nav-link-active' : ''"
|
||||
exact-active-class="nav-link-active"
|
||||
>
|
||||
<Icon name="bell" /> Reminders
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/timeline"
|
||||
class="nav-link"
|
||||
:class="route.name === 'timeline' ? 'nav-link-active' : ''"
|
||||
exact-active-class="nav-link-active"
|
||||
>
|
||||
<Icon name="calendar" /> Timeline
|
||||
</RouterLink>
|
||||
@@ -501,14 +493,14 @@ async function signOut() {
|
||||
class="mt-3 flex flex-col gap-0.5 border-t border-neutral-200 pt-3 sm:hidden dark:border-neutral-800"
|
||||
>
|
||||
<p class="truncate px-3 pb-1 text-xs text-neutral-400">{{ session.user?.display_name }}</p>
|
||||
<RouterLink to="/account" class="nav-link" :class="route.name === 'account' ? 'nav-link-active' : ''">
|
||||
<RouterLink to="/account" class="nav-link" exact-active-class="nav-link-active">
|
||||
<Icon name="device" /> Linked devices
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
v-if="session.user?.is_admin"
|
||||
to="/settings"
|
||||
class="nav-link"
|
||||
:class="route.name === 'settings' ? 'nav-link-active' : ''"
|
||||
exact-active-class="nav-link-active"
|
||||
>
|
||||
<Icon name="settings" /> Settings
|
||||
</RouterLink>
|
||||
|
||||
@@ -19,6 +19,7 @@ const paths: Record<string, string> = {
|
||||
grip: '<circle cx="9" cy="5" r="1" fill="currentColor"/><circle cx="9" cy="12" r="1" fill="currentColor"/><circle cx="9" cy="19" r="1" fill="currentColor"/><circle cx="15" cy="5" r="1" fill="currentColor"/><circle cx="15" cy="12" r="1" fill="currentColor"/><circle cx="15" cy="19" r="1" fill="currentColor"/>',
|
||||
calendar: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
|
||||
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
back: '<path d="m15 18-6-6 6-6"/>',
|
||||
merge: '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>',
|
||||
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||||
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/>',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
NOTE_COLOR_KEYS,
|
||||
NOTE_COLOR_LABELS,
|
||||
NOTE_SWATCH_CLASSES,
|
||||
labelDotClasses,
|
||||
resolveLabelColor,
|
||||
type NoteColor,
|
||||
} from "../notes/colors";
|
||||
@@ -31,14 +32,6 @@ async function rename(id: string, value: string) {
|
||||
if (name) await labels.rename(id, name);
|
||||
}
|
||||
|
||||
// Shows the colour the tag ACTUALLY wears, derived from its name when nobody has
|
||||
// picked one — so this screen agrees with the chips everywhere else. The ring in the
|
||||
// swatch grid below follows the same resolution, so opening the picker highlights
|
||||
// what you can already see rather than nothing at all.
|
||||
function labelDot(label: { name: string; color: string }): string {
|
||||
return NOTE_SWATCH_CLASSES[resolveLabelColor(label)] ?? NOTE_SWATCH_CLASSES.default;
|
||||
}
|
||||
|
||||
function openColor(id: string) {
|
||||
mergeFor.value = null;
|
||||
pickerFor.value = pickerFor.value === id ? null : id;
|
||||
@@ -85,7 +78,7 @@ async function doMerge(sourceId: string, targetId: string) {
|
||||
<button
|
||||
type="button"
|
||||
class="h-4 w-4 shrink-0 rounded-full border border-black/10 transition hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-white/15"
|
||||
:class="labelDot(lb)"
|
||||
:class="labelDotClasses(lb)"
|
||||
:title="`Color: ${NOTE_COLOR_LABELS[resolveLabelColor(lb)]}`"
|
||||
aria-label="Change tag color"
|
||||
@click="openColor(lb.id)"
|
||||
@@ -150,7 +143,7 @@ async function doMerge(sourceId: string, targetId: string) {
|
||||
>
|
||||
<span
|
||||
class="h-2.5 w-2.5 shrink-0 rounded-full border border-black/10 dark:border-white/15"
|
||||
:class="labelDot(t)"
|
||||
:class="labelDotClasses(t)"
|
||||
></span>
|
||||
<span class="truncate">{{ t.name }}</span>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import type { Note } from "../stores/notes";
|
||||
import Icon from "./Icon.vue";
|
||||
|
||||
// A note's own actions, as the card and the editor both offer them. Pin and archive
|
||||
// are everyone's own (#5176); share and trash are the owner's. A note shared with
|
||||
// you is never in your Trash, so restore and delete forever are the owner's too.
|
||||
// The default slot sits after Share, for the editor's history toggle.
|
||||
const props = defineProps<{ note: Note; own: boolean; canShare: boolean }>();
|
||||
const emit = defineEmits<{
|
||||
/** A pin, archive, trash, restore or delete landed. The editor closes on it. */
|
||||
(e: "acted"): void;
|
||||
}>();
|
||||
|
||||
const notes = useNotesStore();
|
||||
const ui = useUiStore();
|
||||
|
||||
async function act(fn: (id: string) => Promise<unknown>): Promise<void> {
|
||||
await fn(props.note.id);
|
||||
emit("acted");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="canShare && !note.trashed"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="note.shared ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Share"
|
||||
aria-label="Share"
|
||||
@click="ui.openShare(note.id)"
|
||||
>
|
||||
<Icon name="users" />
|
||||
</button>
|
||||
<slot />
|
||||
<template v-if="note.trashed">
|
||||
<template v-if="own">
|
||||
<button type="button" class="icon-btn" title="Restore" aria-label="Restore" @click="act(notes.restore)">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
aria-label="Delete forever"
|
||||
@click="act(notes.deleteForever)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="note.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-label="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-pressed="note.pinned"
|
||||
@click="act((id) => notes.setPinned(id, !note.pinned))"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="note.archived ? 'Unarchive' : 'Archive'"
|
||||
:aria-label="note.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="act((id) => notes.setArchived(id, !note.archived))"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<button
|
||||
v-if="own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
aria-label="Move to trash"
|
||||
@click="act(notes.trash)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
@@ -7,6 +7,8 @@ import type { Note } from "../stores/notes";
|
||||
import Icon from "./Icon.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
import MarkdownText from "./MarkdownText.vue";
|
||||
import NoteActions from "./NoteActions.vue";
|
||||
import ReminderActions from "./ReminderActions.vue";
|
||||
import {
|
||||
cardIdAt,
|
||||
draggingId,
|
||||
@@ -14,9 +16,8 @@ import {
|
||||
endCardDrag,
|
||||
overId,
|
||||
} from "../composables/useCardDrag";
|
||||
import { formatReminder, formatTrashCountdown, isOverdue, trashDaysLeft } from "../notes/datetime";
|
||||
import { formatShortDateTime, formatTrashCountdown, isOverdue, trashDaysLeft } from "../notes/datetime";
|
||||
import { useConfigStore } from "../stores/config";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import { canEditText, isOwnNote } from "../notes/sharing";
|
||||
|
||||
const props = defineProps<{ note: Note; reorderable?: boolean; active?: boolean }>();
|
||||
@@ -34,7 +35,6 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
const notes = useNotesStore();
|
||||
const config = useConfigStore();
|
||||
const ui = useUiStore();
|
||||
|
||||
// --- Sharing (#5174). Someone else's note shows who shared it and offers none of
|
||||
// the owner's controls; at `view` its checkboxes are inert too. The desktop offers
|
||||
@@ -193,15 +193,7 @@ 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");
|
||||
}
|
||||
|
||||
// Only the tags the BODY is not already showing. `via_tag` means exactly "backed by
|
||||
// text still in the note" since M311, so a chip for one printed the same tag twice —
|
||||
@@ -385,24 +377,14 @@ const tagColors = computed<Record<string, string>>(() => {
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
{{ formatReminder(note.remind_at) }}
|
||||
{{ formatShortDateTime(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>
|
||||
<ReminderActions :note-id="note.id" @changed="emit('reminder-changed')" />
|
||||
</div>
|
||||
|
||||
<div v-if="trashCountdown" class="mt-2">
|
||||
@@ -459,68 +441,11 @@ const tagColors = computed<Record<string, string>>(() => {
|
||||
<Icon name="grip" />
|
||||
</button>
|
||||
|
||||
<!-- Pin and archive are everyone's own (#5176); share and trash are the owner's.
|
||||
A note shared with you is never in your Trash, so the trashed set is too. -->
|
||||
<div
|
||||
v-if="own || !note.trashed"
|
||||
class="note-actions-set hover-reveal pointer-events-none flex items-center gap-0.5 rounded-full bg-white/85 p-0.5 opacity-0 shadow-sm ring-1 ring-black/5 backdrop-blur-sm transition focus-within:pointer-events-auto focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 dark:bg-neutral-900/85 dark:ring-white/10"
|
||||
>
|
||||
<template v-if="note.trashed">
|
||||
<button type="button" class="icon-btn" title="Restore" aria-label="Restore" @click="notes.restore(note.id)">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
aria-label="Delete forever"
|
||||
@click="notes.deleteForever(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canShare"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Share"
|
||||
aria-label="Share"
|
||||
@click="ui.openShare(note.id)"
|
||||
>
|
||||
<Icon name="users" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="note.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-label="note.pinned ? 'Unpin' : 'Pin'"
|
||||
:aria-pressed="note.pinned"
|
||||
@click="notes.setPinned(note.id, !note.pinned)"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="note.archived ? 'Unarchive' : 'Archive'"
|
||||
:aria-label="note.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="notes.setArchived(note.id, !note.archived)"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<button
|
||||
v-if="own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
aria-label="Move to trash"
|
||||
@click="notes.trash(note.id)"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
<NoteActions :note="note" :own="own" :can-share="canShare" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,16 @@ import { useNotesStore } from "../stores/notes";
|
||||
import { errorMessage } from "../api/errors";
|
||||
import { rendersInline } from "../notes/attachments";
|
||||
import Icon from "./Icon.vue";
|
||||
import NoteActions from "./NoteActions.vue";
|
||||
import ReminderActions from "./ReminderActions.vue";
|
||||
import LabelPicker from "./LabelPicker.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
import { fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import { formatShortDateTime, fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
||||
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
||||
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
||||
import { labelChipClasses } from "../notes/colors";
|
||||
import { canEditText, isOwnNote } from "../notes/sharing";
|
||||
import { useUiStore } from "../stores/ui";
|
||||
import {
|
||||
afterEnter,
|
||||
type EditorBlock,
|
||||
@@ -119,7 +120,6 @@ const bodyPlaceholder = "Take a note…";
|
||||
// Sharing (#5174). Someone else's note: at `edit` its text and checklist are theirs to
|
||||
// change and nothing else is; at `view` it is read-only. The server enforces both —
|
||||
// this only stops the editor offering what would be refused.
|
||||
const ui = useUiStore();
|
||||
const own = computed(() => isOwnNote(liveNote.value));
|
||||
const editable = computed(() => canEditText(liveNote.value));
|
||||
// On the desktop too: linked, it shares through the server; unlinked, the dialog
|
||||
@@ -427,12 +427,6 @@ async function onRecurrenceChange(e: Event) {
|
||||
if (!id) return;
|
||||
await notes.setRecurrence(id, (e.target as HTMLSelectElement).value || null);
|
||||
}
|
||||
async function completeReminder() {
|
||||
if (noteId.value) await notes.completeReminder(noteId.value);
|
||||
}
|
||||
async function snoozeReminder(minutes: number) {
|
||||
if (noteId.value) await notes.snoozeReminder(noteId.value, minutes);
|
||||
}
|
||||
|
||||
// ---- labels ----
|
||||
async function onLabelsChange(next: NoteLabel[]) {
|
||||
@@ -516,12 +510,6 @@ async function onPaste(e: ClipboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- edit-mode lifecycle actions (pin/archive/trash/restore/delete) ----
|
||||
async function act(fn: () => Promise<void>) {
|
||||
await fn();
|
||||
await finish();
|
||||
}
|
||||
|
||||
// ---- version history (modal edit only) ----
|
||||
const showHistory = ref(false);
|
||||
const revisions = ref<NoteRevision[]>([]);
|
||||
@@ -549,10 +537,6 @@ async function restoreRevisionAt(revId: string) {
|
||||
baseline.value = { body: updated.body };
|
||||
void loadRevisions(); // the pre-restore state became a new revision
|
||||
}
|
||||
function revLabel(iso: string | null): string {
|
||||
if (!iso) return "";
|
||||
return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
||||
}
|
||||
function revPreview(rev: NoteRevision): string {
|
||||
const s = rev.body.trim().replace(/\s+/g, " ");
|
||||
if (!s) return "(empty)";
|
||||
@@ -770,19 +754,7 @@ function revPreview(rev: NoteRevision): string {
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-neutral-600 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
@click="completeReminder"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(60)">
|
||||
Snooze 1h
|
||||
</button>
|
||||
<button type="button" class="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200" @click="snoozeReminder(1440)">
|
||||
1d
|
||||
</button>
|
||||
<ReminderActions :note-id="liveNote.id" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -797,7 +769,7 @@ function revPreview(rev: NoteRevision): string {
|
||||
</p>
|
||||
<ul v-else class="flex max-h-40 flex-col gap-0.5 overflow-y-auto">
|
||||
<li v-for="rev in revisions" :key="rev.id" class="flex items-center gap-2 rounded-md px-1 py-1 text-xs">
|
||||
<span class="shrink-0 tabular-nums text-neutral-400">{{ revLabel(rev.created_at) }}</span>
|
||||
<span class="shrink-0 tabular-nums text-neutral-400">{{ formatShortDateTime(rev.created_at) }}</span>
|
||||
<span class="min-w-0 flex-1 truncate text-neutral-600 dark:text-neutral-300">{{ revPreview(rev) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -842,71 +814,20 @@ function revPreview(rev: NoteRevision): string {
|
||||
:model-value="labelList"
|
||||
@update:model-value="onLabelsChange"
|
||||
/>
|
||||
<button
|
||||
v-if="canShare && !liveNote.trashed"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="liveNote.shared ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Share"
|
||||
aria-label="Share"
|
||||
@click="ui.openShare(liveNote.id)"
|
||||
>
|
||||
<Icon name="users" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!isCreate && own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="showHistory ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Version history"
|
||||
aria-label="Version history"
|
||||
:aria-pressed="showHistory"
|
||||
@click="toggleHistory"
|
||||
>
|
||||
<Icon name="history" />
|
||||
</button>
|
||||
<!-- Pin and archive are everyone's own on a shared note (#5176); trash is the owner's. -->
|
||||
<template v-if="!isCreate && !liveNote.trashed">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:class="liveNote.pinned ? 'text-brand-700 dark:text-brand' : ''"
|
||||
:title="liveNote.pinned ? 'Unpin' : 'Pin'"
|
||||
@click="act(() => notes.setPinned(liveNote.id, !liveNote.pinned))"
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
:title="liveNote.archived ? 'Unarchive' : 'Archive'"
|
||||
@click="act(() => notes.setArchived(liveNote.id, !liveNote.archived))"
|
||||
>
|
||||
<Icon name="archive" />
|
||||
</button>
|
||||
<NoteActions v-if="!isCreate" :note="liveNote" :own="own" :can-share="canShare" @acted="finish">
|
||||
<button
|
||||
v-if="own"
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Move to trash"
|
||||
@click="act(() => notes.trash(liveNote.id))"
|
||||
:class="showHistory ? 'text-brand-700 dark:text-brand' : ''"
|
||||
title="Version history"
|
||||
aria-label="Version history"
|
||||
:aria-pressed="showHistory"
|
||||
@click="toggleHistory"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
<Icon name="history" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else-if="!isCreate && liveNote.trashed">
|
||||
<button type="button" class="icon-btn" title="Restore" @click="act(() => notes.restore(liveNote.id))">
|
||||
<Icon name="restore" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
title="Delete forever"
|
||||
@click="act(() => notes.deleteForever(liveNote.id))"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</template>
|
||||
</NoteActions>
|
||||
<button
|
||||
type="button"
|
||||
:class="
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from "./Icon.vue";
|
||||
|
||||
// The top of a page that sits outside the board (Settings, Sync, Linked devices):
|
||||
// a way back to the board, and the page's name.
|
||||
defineProps<{ title: string }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="mb-8 flex items-center gap-3">
|
||||
<RouterLink to="/" class="icon-btn" title="Back to board" aria-label="Back to board">
|
||||
<Icon name="back" />
|
||||
</RouterLink>
|
||||
<h1 class="text-xl font-bold tracking-tight">{{ title }}</h1>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
|
||||
// Done, snooze an hour, snooze a day: the upkeep a due reminder asks for, one tap
|
||||
// each wherever the reminder shows (card and editor). Always visible rather than
|
||||
// hover-revealed: a finger cannot hover, and these are the point of a due note.
|
||||
const props = defineProps<{ noteId: string }>();
|
||||
const emit = defineEmits<{
|
||||
/** Completed or snoozed. For a host holding its own list (the reminders lens). */
|
||||
(e: "changed"): void;
|
||||
}>();
|
||||
|
||||
const notes = useNotesStore();
|
||||
|
||||
async function complete(): Promise<void> {
|
||||
await notes.completeReminder(props.noteId);
|
||||
emit("changed");
|
||||
}
|
||||
|
||||
async function snooze(minutes: number): Promise<void> {
|
||||
await notes.snoozeReminder(props.noteId, minutes);
|
||||
emit("changed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="chip-btn" title="Mark this reminder done" @click.stop="complete">Done</button>
|
||||
<button type="button" class="chip-btn" title="Snooze 1 hour" @click.stop="snooze(60)">1h</button>
|
||||
<button type="button" class="chip-btn" title="Snooze 1 day" @click.stop="snooze(1440)">1d</button>
|
||||
</template>
|
||||
Reference in New Issue
Block a user