Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ function NotesApp() {
duplicateNote,
moveNote,
setNoteStatus,
pinNote,
unpinNote,
} = useNoteMutations();

// Determine which notes to display
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -308,6 +324,7 @@ function NotesApp() {
onDelete={handleDeleteNote}
onArchive={handleArchiveNote}
onDuplicate={handleDuplicateNote}
onPin={handlePinNote}
onMove={handleMoveNote}
onSearch={handleSearch}
onNewNote={handleNewNote}
Expand Down
36 changes: 34 additions & 2 deletions apps/desktop/src/renderer/components/NoteList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
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';
import type { QuickFilterType } from './sidebar';
import { useTagColorsStore } from '../stores/tagColorsStore';

Check warning on line 17 in apps/desktop/src/renderer/components/NoteList.tsx

View workflow job for this annotation

GitHub Actions / lint

`../stores/tagColorsStore` import should occur before type import of `./sidebar`
import { NoteListContextMenu } from './NoteListContextMenu';
import { NotebookPicker } from './NotebookPicker';

Expand All @@ -20,6 +30,7 @@
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;
Expand Down Expand Up @@ -89,6 +100,7 @@
noteId: string;
notebookId: string | null;
isArchived: boolean;
isPinned: boolean;
x: number;
y: number;
}
Expand All @@ -111,6 +123,7 @@
onDelete,
onArchive,
onDuplicate,
onPin,
onMove,
onSearch,
onNewNote,
Expand Down Expand Up @@ -183,6 +196,7 @@
noteId: note.id,
notebookId: note.notebookId,
isArchived: note.isArchived,
isPinned: note.isPinned,
x: e.clientX,
y: e.clientY,
});
Expand Down Expand Up @@ -328,8 +342,10 @@
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}
Expand Down Expand Up @@ -366,6 +382,18 @@
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 (
<li
Expand All @@ -382,7 +410,11 @@
}}
tabIndex={0}
>
<div className="note-list-item-title">{note.title || 'Untitled'}</div>
<div className="note-list-item-title">
{note.isPinned && <Pin size={12} className="pin-icon" aria-label="Pinned" />}
{showUnpinEffect && <PinOff size={12} className="unpin-icon" aria-hidden="true" />}
{note.title || 'Untitled'}
</div>
<div className="note-list-item-meta">
<span className="timestamp">{formatRelativeTime(note.updatedAt)}</span>
{note.tags.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 */
Expand All @@ -28,8 +32,10 @@ export function NoteListContextMenu({
noteId,
currentNotebookId,
isArchived,
isPinned,
position,
onClose,
onPin,
onDuplicate,
onArchive,
onDelete,
Expand Down Expand Up @@ -85,6 +91,11 @@ export function NoteListContextMenu({
};
}, [onClose]);

const handlePin = useCallback(() => {
onPin(noteId);
onClose();
}, [noteId, onPin, onClose]);

const handleDuplicate = useCallback(() => {
onDuplicate(noteId);
onClose();
Expand Down Expand Up @@ -122,6 +133,12 @@ export function NoteListContextMenu({
<span className={styles.shortcut}>M</span>
</button>

{/* Pin / Unpin */}
<button type="button" className={styles.item} onClick={handlePin}>
{isPinned ? <PinOff size={14} /> : <Pin size={14} />}
<span className={styles.label}>{isPinned ? 'Unpin' : 'Pin'}</span>
</button>

{/* Duplicate */}
<button type="button" className={styles.item} onClick={handleDuplicate}>
<Copy size={14} />
Expand Down
77 changes: 50 additions & 27 deletions apps/desktop/src/renderer/components/editor/MarkdownPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useMemo, useRef, useImperativeHandle, forwardRef, useEffect, useCallback } from 'react';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Clock, CalendarPlus, ListChecks } from 'lucide-react';
import { remarkWikilink } from './remark-wikilink';

Check warning on line 5 in apps/desktop/src/renderer/components/editor/MarkdownPreview.tsx

View workflow job for this annotation

GitHub Actions / lint

`./remark-wikilink` import should occur after import of `../../utils/date`
import { countMarkdownTasks } from '../../utils/markdown';
import { formatDateTime } from '../../utils/date';

interface MarkdownPreviewProps {
readonly content: string;
Expand Down Expand Up @@ -81,29 +84,56 @@
},
}));

// Format dates for display
const formattedDates = useMemo(() => {
if (!createdAt && !updatedAt) return null;

const formatDate = (iso: string) => {
const date = new Date(iso);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};

return {
created: createdAt ? formatDate(createdAt) : null,
updated: updatedAt ? formatDate(updatedAt) : null,
};
}, [createdAt, updatedAt]);
// Count tasks for progress display
const tasks = useMemo(() => countMarkdownTasks(content), [content]);
const hasProgress = tasks.total > 0;
const progressPercent = hasProgress ? (tasks.completed / tasks.total) * 100 : 0;

return (
<div ref={containerRef} className="markdown-preview" onClick={handleClick}>
{/* Metadata header - Inkdrop style */}
<div className="preview-metadata-header">
{hasProgress && (
<div className="preview-meta-item">
<ListChecks size={20} className="preview-meta-icon" aria-hidden="true" />
<div className="preview-meta-content">
<span className="preview-meta-label">PROGRESS</span>
<div className="preview-meta-progress">
<div className="preview-progress-bar">
<div
className="preview-progress-fill"
style={{ width: `${progressPercent}%` }}
/>
</div>
<span className="preview-progress-text">
{tasks.completed} of {tasks.total} tasks
</span>
</div>
</div>
</div>
)}

{createdAt && (
<div className="preview-meta-item">
<Clock size={20} className="preview-meta-icon" aria-hidden="true" />
<div className="preview-meta-content">
<span className="preview-meta-label">CREATED AT</span>
<span className="preview-meta-value">{formatDateTime(createdAt)}</span>
</div>
</div>
)}

{updatedAt && (
<div className="preview-meta-item">
<CalendarPlus size={20} className="preview-meta-icon" aria-hidden="true" />
<div className="preview-meta-content">
<span className="preview-meta-label">UPDATED AT</span>
<span className="preview-meta-value">{formatDateTime(updatedAt)}</span>
</div>
</div>
)}
</div>

<Markdown
remarkPlugins={[remarkGfm, remarkWikilink]}
skipHtml={true}
Expand All @@ -119,13 +149,6 @@
>
{content}
</Markdown>

{formattedDates && (
<div className="markdown-preview-meta">
{formattedDates.created && <span>Created: {formattedDates.created}</span>}
{formattedDates.updated && <span>Updated: {formattedDates.updated}</span>}
</div>
)}
</div>
);
}
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop/src/renderer/styles/note-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@

/* Title */
.note-list-item-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
Expand All @@ -232,6 +235,35 @@
text-overflow: ellipsis;
}

/* Pin icon for pinned notes */
.note-list-item-title .pin-icon {
flex-shrink: 0;
color: var(--accent);
opacity: 0.8;
}

/* Unpin animation effect */
@keyframes unpin-fade-out {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.2);
}
100% {
opacity: 0;
transform: scale(0.8) translateY(-4px);
}
}

.note-list-item-title .unpin-icon {
flex-shrink: 0;
color: var(--text-muted);
animation: unpin-fade-out 0.6s ease-out forwards;
}

/* Meta Row (timestamp + tags) */
.note-list-item-meta {
display: flex;
Expand Down
Loading
Loading