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:
2026-10-07 20:02:18 -04:00
co-authored by Claude Opus 5.5
parent 05c82c2362
commit e75e3d37d0
17 changed files with 348 additions and 422 deletions
+87
View File
@@ -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>