diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index aa3929a..f5830a4 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@readied/desktop",
- "version": "0.1.3",
+ "version": "0.1.5",
"private": true,
"description": "Markdown-first, offline-forever note app for developers",
"author": {
diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx
index 9a711d4..fea29b6 100644
--- a/apps/desktop/src/renderer/App.tsx
+++ b/apps/desktop/src/renderer/App.tsx
@@ -85,6 +85,8 @@ function NotesApp() {
duplicateNote,
moveNote,
setNoteStatus,
+ pinNote,
+ unpinNote,
} = useNoteMutations();
// Determine which notes to display
@@ -203,6 +205,20 @@ function NotesApp() {
[duplicateNote, goToAllNotes]
);
+ // Pin/unpin note (toggle)
+ const handlePinNote = useCallback(
+ async (id: string) => {
+ const note = displayedNotes.find(n => n.id === id);
+ if (!note) return;
+ if (note.isPinned) {
+ await unpinNote.mutateAsync(id);
+ } else {
+ await pinNote.mutateAsync(id);
+ }
+ },
+ [displayedNotes, pinNote, unpinNote]
+ );
+
// Move note to notebook
const handleMoveNote = useCallback(
async (noteId: string, notebookId: string) => {
@@ -308,6 +324,7 @@ function NotesApp() {
onDelete={handleDeleteNote}
onArchive={handleArchiveNote}
onDuplicate={handleDuplicateNote}
+ onPin={handlePinNote}
onMove={handleMoveNote}
onSearch={handleSearch}
onNewNote={handleNewNote}
diff --git a/apps/desktop/src/renderer/components/NoteList.tsx b/apps/desktop/src/renderer/components/NoteList.tsx
index 8b943fd..9041cf9 100644
--- a/apps/desktop/src/renderer/components/NoteList.tsx
+++ b/apps/desktop/src/renderer/components/NoteList.tsx
@@ -1,5 +1,15 @@
import { useState, useCallback, useRef, useEffect } from 'react';
-import { Sparkles, Archive, Search, X, Check, SquarePen, ArrowUpDown } from 'lucide-react';
+import {
+ Sparkles,
+ Archive,
+ Search,
+ X,
+ Check,
+ SquarePen,
+ ArrowUpDown,
+ Pin,
+ PinOff,
+} from 'lucide-react';
import { useNotebookList, useNotebook } from '../hooks/useNotebooks';
import type { NoteWithExcerpt, SortBy, SortOrder } from '../hooks/useNavigation';
import { formatRelativeTime } from '../utils/date';
@@ -20,6 +30,7 @@ interface NoteListProps {
onDelete: (id: string) => void;
onArchive: (id: string) => void;
onDuplicate: (id: string) => void;
+ onPin: (id: string) => void;
onMove: (noteId: string, notebookId: string) => void;
onSearch: (query: string) => void;
onNewNote: () => void;
@@ -89,6 +100,7 @@ interface ContextMenuState {
noteId: string;
notebookId: string | null;
isArchived: boolean;
+ isPinned: boolean;
x: number;
y: number;
}
@@ -111,6 +123,7 @@ export function NoteList({
onDelete,
onArchive,
onDuplicate,
+ onPin,
onMove,
onSearch,
onNewNote,
@@ -183,6 +196,7 @@ export function NoteList({
noteId: note.id,
notebookId: note.notebookId,
isArchived: note.isArchived,
+ isPinned: note.isPinned,
x: e.clientX,
y: e.clientY,
});
@@ -328,8 +342,10 @@ export function NoteList({
noteId={contextMenu.noteId}
currentNotebookId={contextMenu.notebookId}
isArchived={contextMenu.isArchived}
+ isPinned={contextMenu.isPinned}
position={{ x: contextMenu.x, y: contextMenu.y }}
onClose={() => setContextMenu(null)}
+ onPin={onPin}
onDuplicate={onDuplicate}
onArchive={onArchive}
onDelete={onDelete}
@@ -366,6 +382,18 @@ function NoteListItem({
onContextMenu,
}: NoteListItemProps) {
const getColor = useTagColorsStore(state => state.getColor);
+ const [showUnpinEffect, setShowUnpinEffect] = useState(false);
+ const prevPinnedRef = useRef(note.isPinned);
+
+ // Detect unpin transition (pinned → unpinned)
+ useEffect(() => {
+ if (prevPinnedRef.current && !note.isPinned) {
+ setShowUnpinEffect(true);
+ const timer = setTimeout(() => setShowUnpinEffect(false), 600);
+ return () => clearTimeout(timer);
+ }
+ prevPinnedRef.current = note.isPinned;
+ }, [note.isPinned]);
return (
- {note.title || 'Untitled'}
+
+ {note.isPinned &&
}
+ {showUnpinEffect &&
}
+ {note.title || 'Untitled'}
+
{formatRelativeTime(note.updatedAt)}
{note.tags.length > 0 && (
diff --git a/apps/desktop/src/renderer/components/NoteListContextMenu/NoteListContextMenu.tsx b/apps/desktop/src/renderer/components/NoteListContextMenu/NoteListContextMenu.tsx
index 6750c1e..57ff62d 100644
--- a/apps/desktop/src/renderer/components/NoteListContextMenu/NoteListContextMenu.tsx
+++ b/apps/desktop/src/renderer/components/NoteListContextMenu/NoteListContextMenu.tsx
@@ -1,6 +1,6 @@
import { useRef, useEffect, useState, useCallback } from 'react';
import { createPortal } from 'react-dom';
-import { FolderOpen, Copy, Archive, ArchiveRestore, Trash2 } from 'lucide-react';
+import { FolderOpen, Copy, Archive, ArchiveRestore, Trash2, Pin, PinOff } from 'lucide-react';
import styles from './NoteListContextMenu.module.css';
export interface NoteListContextMenuProps {
@@ -10,10 +10,14 @@ export interface NoteListContextMenuProps {
currentNotebookId: string | null;
/** Whether the note is archived */
isArchived: boolean;
+ /** Whether the note is pinned */
+ isPinned: boolean;
/** Menu position */
position: { x: number; y: number };
/** Called when menu should close */
onClose: () => void;
+ /** Called when pin/unpin is clicked */
+ onPin: (id: string) => void;
/** Called when duplicate is clicked */
onDuplicate: (id: string) => void;
/** Called when archive/restore is clicked */
@@ -28,8 +32,10 @@ export function NoteListContextMenu({
noteId,
currentNotebookId,
isArchived,
+ isPinned,
position,
onClose,
+ onPin,
onDuplicate,
onArchive,
onDelete,
@@ -85,6 +91,11 @@ export function NoteListContextMenu({
};
}, [onClose]);
+ const handlePin = useCallback(() => {
+ onPin(noteId);
+ onClose();
+ }, [noteId, onPin, onClose]);
+
const handleDuplicate = useCallback(() => {
onDuplicate(noteId);
onClose();
@@ -122,6 +133,12 @@ export function NoteListContextMenu({
M
+ {/* Pin / Unpin */}
+
+
{/* Duplicate */}