+ {selection && (
+ |
+ selection.onToggleAll?.()}
+ label={selection.allSelected ? t('notes.deselectAll') : t('notes.selectAll')}
+ />
+ |
+ )}
handleSort('title')}
@@ -407,6 +425,15 @@ export function NotesListViews({
role="button"
className="h-11 hover:bg-foreground/[0.02] cursor-pointer transition-colors group focus-visible:bg-foreground/[0.04]"
>
+ {selection && (
+ |
+ selection.onToggle(note.id)}
+ label={t('notes.selectNote')}
+ />
+ |
+ )}
{note.isPinned && }
@@ -464,6 +491,7 @@ export function NotesListViews({
onMoveToNotebook={onMoveToNotebook}
onNotePatch={onNotePatch}
onNoteIllustrationGenerated={onNoteIllustrationGenerated}
+ selection={selection}
/>
)}
@@ -479,6 +507,7 @@ export function NotesListViews({
onMoveToNotebook={onMoveToNotebook}
onNotePatch={onNotePatch}
onNoteIllustrationGenerated={onNoteIllustrationGenerated}
+ selection={selection}
/>
)}
@@ -497,6 +526,13 @@ function formatGridCardDate(date: Date | string, language: string): string {
return `${month.toUpperCase()} ${day}, ${year}`
}
+type NotesSelection = {
+ selectedIds: ReadonlySet
+ onToggle: (id: string) => void
+ onToggleAll?: () => void
+ allSelected?: boolean
+}
+
type GridCardSharedProps = {
note: Note
index: number
@@ -512,6 +548,7 @@ type GridCardSharedProps = {
onNoteIllustrationDeleted?: (noteId: string) => void | Promise
onOpenHistory?: (note: Note) => void
isOverlay?: boolean
+ selection?: NotesSelection
}
function NotesMasonryGrid({
@@ -624,6 +661,7 @@ function NotesGridSection({
onDeleteNote,
onMoveToNotebook,
onNoteIllustrationGenerated,
+ selection,
className,
}: Omit & {
notes: Note[]
@@ -650,6 +688,7 @@ function NotesGridSection({
onDeleteNote={onDeleteNote}
onMoveToNotebook={onMoveToNotebook}
onNoteIllustrationGenerated={onNoteIllustrationGenerated}
+ selection={selection}
/>
) : (
),
)}
@@ -721,6 +761,7 @@ const GridCard = memo(function GridCard({
onNoteIllustrationDeleted,
onOpenHistory,
isOverlay = false,
+ selection,
}: GridCardSharedProps) {
const router = useRouter()
const { t, language } = useLanguage()
@@ -760,8 +801,21 @@ const GridCard = memo(function GridCard({
onNoteIllustrationGenerated={onNoteIllustrationGenerated}
onNoteIllustrationDeleted={onNoteIllustrationDeleted}
/>
+ {selection && !isOverlay && (
+
+ selection.onToggle(note.id)}
+ label={t('notes.selectNote')}
+ className="shadow-sm"
+ />
+
+ )}
{note.isPinned && (
-
+
)}
diff --git a/memento-note/components/public-site-chrome.tsx b/memento-note/components/public-site-chrome.tsx
index 07970f6f..4024dc8a 100644
--- a/memento-note/components/public-site-chrome.tsx
+++ b/memento-note/components/public-site-chrome.tsx
@@ -229,15 +229,15 @@ export function PublicSiteChrome({
Memento
- {t('landing.footer.desc')}
+ {t('landing.footer.desc')}
{(['product', 'community', 'legal'] as const).map((section) => (
-
+
{t(`landing.footer.${section}.title`)}
-
+
{[0, 1, 2].map((j) => {
const label = t(`landing.footer.${section}.link${j}`)
const href = t(`landing.footer.${section}.link${j}Href`)
@@ -258,7 +258,7 @@ export function PublicSiteChrome({
))}
-
+
© 2026 Memento. {t('landing.footer.rights')}
diff --git a/memento-note/lib/notes/trash-toast.ts b/memento-note/lib/notes/trash-toast.ts
index 4839dccf..724f19e5 100644
--- a/memento-note/lib/notes/trash-toast.ts
+++ b/memento-note/lib/notes/trash-toast.ts
@@ -1,7 +1,7 @@
'use client'
import { toast } from 'sonner'
-import { restoreNote } from '@/app/actions/notes'
+import { restoreNote, restoreNotes } from '@/app/actions/notes'
import { emitNoteChange } from '@/lib/note-change-sync'
import type { Note } from '@/lib/types'
@@ -27,3 +27,31 @@ export function showNoteTrashedToast(
},
})
}
+
+export function showNotesTrashedToast(
+ notes: Note[],
+ t: (key: string, params?: Record) => string,
+ onRestored?: () => void,
+) {
+ if (notes.length === 1) {
+ showNoteTrashedToast(notes[0], t, onRestored)
+ return
+ }
+ toast.success(t('notes.bulkTrashedToast', { count: notes.length }), {
+ action: {
+ label: t('notes.undoDelete'),
+ onClick: async () => {
+ try {
+ await restoreNotes(notes.map((note) => note.id), { skipRevalidation: true })
+ for (const note of notes) {
+ emitNoteChange({ type: 'created', note: { ...note, trashedAt: null } })
+ }
+ onRestored?.()
+ toast.success(t('trash.noteRestored'))
+ } catch {
+ toast.error(t('general.error'))
+ }
+ },
+ },
+ })
+}
diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json
index 83ae1a5d..ff4b56b3 100644
--- a/memento-note/locales/en.json
+++ b/memento-note/locales/en.json
@@ -114,6 +114,17 @@
"title": "Notes",
"newNote": "New note",
"reorganize": "Reorganize notes",
+ "selectAll": "Select all",
+ "deselectAll": "Deselect all",
+ "selectedCount": "{count} notes selected",
+ "selectedCountOne": "1 note selected",
+ "bulkMove": "Move",
+ "bulkTrash": "Trash",
+ "selectNote": "Select this note",
+ "confirmBulkDeleteTitle": "Send to trash",
+ "confirmBulkDelete": "These notes will go to the trash. You can restore them later.",
+ "bulkTrashedToast": "{count} notes sent to trash.",
+ "bulkMovedToast": "{count} notes moved.",
"untitled": "Untitled",
"placeholder": "Take a note...",
"markdownPlaceholder": "Take a note... (Markdown supported)",
diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json
index 735ddc8f..f53847bb 100644
--- a/memento-note/locales/fr.json
+++ b/memento-note/locales/fr.json
@@ -114,6 +114,17 @@
"title": "Notes",
"newNote": "Nouvelle note",
"reorganize": "Réorganiser les notes",
+ "selectAll": "Tout sélectionner",
+ "deselectAll": "Tout désélectionner",
+ "selectedCount": "{count} notes sélectionnées",
+ "selectedCountOne": "1 note sélectionnée",
+ "bulkMove": "Déplacer",
+ "bulkTrash": "Corbeille",
+ "selectNote": "Sélectionner cette note",
+ "confirmBulkDeleteTitle": "Envoyer à la corbeille",
+ "confirmBulkDelete": "Ces notes iront dans la corbeille. Vous pourrez les récupérer ensuite.",
+ "bulkTrashedToast": "{count} notes envoyées à la corbeille.",
+ "bulkMovedToast": "{count} notes déplacées.",
"untitled": "Sans titre",
"placeholder": "Prenez une note...",
"markdownPlaceholder": "Prenez une note... (Markdown supporté)",
|