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:
2026-09-09 00:38:53 +00:00
parent 6d01eecccc
commit f0f7a0b524
38 changed files with 1998 additions and 765 deletions
+75 -39
View File
@@ -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>
)}