UI redesign: data-dense professional treatment across entire app
Foundation: - Rewrote CSS design tokens for light/dark modes (compact density default) - Added JetBrains Mono for monospace accents on numeric data/timestamps - Vibrant semantic color palette (tasks=blue, habits=green, projects=purple, notes=amber) - Removed accent color toggle system, pinned interactive accent blue - New shell surface tokens (sidebar-bg, sidebar-border, topbar-bg) Shell: - Sidebar: colored entity-type icons, always-visible shortcuts, left accent border - Topbar: reduced to h-10, backdrop blur, monospace search shortcut Pages (all 12 existing): - Border treatment (no shadows), hover:bg-muted/20 transitions - font-mono for all numeric data, timestamps, IDs, status codes - Tighter spacing (gap-3, space-y-3), text-xl headers New pages: - Inbox (/inbox) - focused attention view with overdue/today habits/recent notes - Reports (/reports) - richer analytics with stat cards, charts, CSV export - Templates (/templates) - localStorage-based task/note template management Entity detail pages: - Consistent border-t pt-3 dividers, monospace timestamps - Added TagManager to projects (consistency fix) - Removed Card wrappers from activity/comments (flat sections) Login: split-layout with branding panel and floating colored dots Shared components: colored EmptyState icons, monospace error messages Command palette & shortcuts: new nav items for Inbox/Reports/Analytics Cleanup: removed all hover:shadow-md and ACCENT_PALETTE references
This commit is contained in:
@@ -44,11 +44,17 @@ const NoteTitleInput = memo(function NoteTitleInput({ noteId, initialTitle }: {
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onBlur={handleBlur}
|
||||
autoFocus
|
||||
className="flex h-9 w-full rounded-md border-0 bg-transparent px-0 py-1 text-lg font-semibold text-base shadow-none transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="flex h-9 w-full rounded-md border-0 bg-transparent px-0 py-1 text-lg font-semibold shadow-none transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Count words in HTML content (rough estimate)
|
||||
function countWords(html: string | null): number {
|
||||
if (!html) return 0;
|
||||
return html.replace(/<[^>]+>/g, "").trim().split(/\s+/).filter(Boolean).length;
|
||||
}
|
||||
|
||||
// Memoized right pane - only re-renders when note changes, not on parent re-renders
|
||||
const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete, onOpenNote }: { note: Note; onDelete: (id: string) => void; onOpenNote: (note: Note) => void }) {
|
||||
const [showBacklinks, setShowBacklinks] = useState(false);
|
||||
@@ -67,19 +73,19 @@ const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete, onOpenNote
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 p-3 border-b">
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b">
|
||||
<NoteTitleInput key={note.id} noteId={note.id} initialTitle={note.title} />
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowBacklinks(!showBacklinks)} aria-label="Backlinks">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setShowBacklinks(!showBacklinks)} aria-label="Backlinks">
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => { setShowVersions(!showVersions); if (!showVersions) { api.get<{ items: { id: string; createdAt: string; action?: string; changes?: Record<string, unknown> | null }[] }>("/notes/" + note.id + "/versions").then((data) => setVersions(data.items || [])).catch(() => setVersions([])); } }} aria-label="Version history">
|
||||
<History className="h-4 w-4" />
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => { setShowVersions(!showVersions); if (!showVersions) { api.get<{ items: { id: string; createdAt: string; action?: string; changes?: Record<string, unknown> | null }[] }>("/notes/" + note.id + "/versions").then((data) => setVersions(data.items || [])).catch(() => setVersions([])); } }} aria-label="Version history">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive" aria-label="Delete note">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" aria-label="Delete note">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
@@ -95,20 +101,45 @@ const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete, onOpenNote
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metadata strip */}
|
||||
<div className="flex items-center gap-3 px-3 py-1.5 border-b text-xs text-muted-foreground">
|
||||
<span className="font-mono">{format(parseISO(note.updatedAt), "yyyy-MM-dd HH:mm")}</span>
|
||||
<span className="text-border">·</span>
|
||||
<span className="font-mono">{countWords(note.content)} words</span>
|
||||
{note.isPinned && (
|
||||
<>
|
||||
<span className="text-border">·</span>
|
||||
<span className="font-mono text-amber-500">pinned</span>
|
||||
</>
|
||||
)}
|
||||
{note.tags && note.tags.length > 0 && (
|
||||
<>
|
||||
<span className="text-border">·</span>
|
||||
<span className="font-mono">{note.tags.length} tag{note.tags.length !== 1 ? 's' : ''}</span>
|
||||
</>
|
||||
)}
|
||||
{note.backlinks && note.backlinks.length > 0 && (
|
||||
<>
|
||||
<span className="text-border">·</span>
|
||||
<span className="font-mono">{note.backlinks.length} backlink{note.backlinks.length !== 1 ? 's' : ''}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<NoteEditor key={note.id} initialContent={note.content || ''} onSave={handleSave} />
|
||||
</div>
|
||||
{/* Backlinks section */}
|
||||
{showBacklinks && note.backlinks && note.backlinks.length > 0 && (
|
||||
<div className="border-t p-3">
|
||||
<h4 className="text-sm font-semibold mb-2">Linked from</h4>
|
||||
<div className="space-y-1">
|
||||
<div className="border-t px-3 py-2">
|
||||
<h4 className="text-xs font-semibold mb-1 uppercase tracking-wider text-muted-foreground">Linked from</h4>
|
||||
<div className="space-y-0.5">
|
||||
{note.backlinks.map((bl) => (
|
||||
<div
|
||||
key={bl.id}
|
||||
className="text-sm text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground cursor-pointer flex items-center gap-1.5"
|
||||
onClick={() => { const linked: Note = { ...note, id: bl.id, title: bl.title }; onOpenNote(linked); }}
|
||||
>
|
||||
<LinkIcon className="h-3 w-3 shrink-0" />
|
||||
{bl.title}
|
||||
</div>
|
||||
))}
|
||||
@@ -117,16 +148,16 @@ const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete, onOpenNote
|
||||
)}
|
||||
{/* Version history */}
|
||||
{showVersions && (
|
||||
<div className="border-t p-3">
|
||||
<h4 className="text-sm font-semibold mb-2">Version History</h4>
|
||||
<div className="border-t px-3 py-2">
|
||||
<h4 className="text-xs font-semibold mb-1 uppercase tracking-wider text-muted-foreground">Version History</h4>
|
||||
{versions.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No versions yet.</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
{versions.map((v) => (
|
||||
<div key={v.id} className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{format(parseISO(v.createdAt), "MMM d, yyyy HH:mm")}</span>
|
||||
{v.action && <Badge variant="secondary" className="text-[10px] capitalize">{v.action}</Badge>}
|
||||
<span className="font-mono">{format(parseISO(v.createdAt), "yyyy-MM-dd HH:mm")}</span>
|
||||
{v.action && <Badge variant="secondary" className="text-[10px] capitalize font-mono">{v.action}</Badge>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -214,48 +245,53 @@ function NotesPage() {
|
||||
<div className="flex flex-col md:flex-row h-auto min-h-[calc(100vh-8rem)] md:h-[calc(100vh-8rem)] -m-4 md:-m-6">
|
||||
{/* Left pane - note list */}
|
||||
<div className="w-full md:w-72 h-64 md:h-auto border-b md:border-b-0 md:border-r flex flex-col shrink-0">
|
||||
<div className="p-3 border-b">
|
||||
<div className="px-3 py-2 border-b">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search notes..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-8" aria-label="Search notes" />
|
||||
<Search className="absolute left-2.5 top-2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input placeholder="Search notes..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-8 h-8 text-sm" aria-label="Search notes" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<Button size="sm" className="w-full" onClick={() => createMutation.mutate()} aria-label="New note">
|
||||
<Plus className="h-4 w-4 mr-2" />New Note
|
||||
<div className="px-3 py-2 border-b">
|
||||
<Button size="sm" className="w-full h-8 text-xs" onClick={() => createMutation.mutate()} aria-label="New note">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />New Note
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">Loading...</div>
|
||||
<div className="px-3 py-2 text-xs text-muted-foreground">Loading...</div>
|
||||
) : notes.length === 0 ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">No notes yet.</div>
|
||||
<div className="px-3 py-4 text-center">
|
||||
<FileText className="h-8 w-8 mx-auto mb-2 text-amber-400" />
|
||||
<p className="text-xs text-muted-foreground">No notes yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5 p-2">
|
||||
<div>
|
||||
{notes.map((note) => (
|
||||
<button
|
||||
key={note.id}
|
||||
onClick={() => selectNote(note)}
|
||||
className={cn(
|
||||
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
|
||||
"w-full text-left px-3 py-1.5 text-sm transition-colors border-b border-border/50 last:border-b-0",
|
||||
selectedNoteId === note.id ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{note.isPinned && <Pin className="h-3 w-3 shrink-0 text-muted-foreground" />}
|
||||
<span className="truncate font-medium">{note.title}</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{note.isPinned && <Pin className="h-3 w-3 shrink-0 text-amber-500" />}
|
||||
<span className="truncate font-medium text-xs">{note.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5 text-[10px] text-muted-foreground font-mono">
|
||||
<span>{new Date(note.updatedAt).toLocaleDateString()}</span>
|
||||
<span>·</span>
|
||||
<span>{countWords(note.content)}w</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{new Date(note.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{hasMoreNotes && (
|
||||
<div className="p-2">
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={loadMoreNotes} disabled={loadingMoreNotes}>
|
||||
{loadingMoreNotes ? "Loading..." : "Load more notes"}
|
||||
<div className="px-3 py-1.5">
|
||||
<Button variant="outline" size="sm" className="w-full h-7 text-xs" onClick={loadMoreNotes} disabled={loadingMoreNotes}>
|
||||
{loadingMoreNotes ? "Loading..." : "Load more"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -269,8 +305,8 @@ function NotesPage() {
|
||||
) : (
|
||||
<div className="flex items-center justify-center flex-1 text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p>Select a note or create a new one</p>
|
||||
<FileText className="h-10 w-10 mx-auto mb-2 text-amber-400" />
|
||||
<p className="text-sm text-muted-foreground">Select a note or create a new one</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user