feat: add notes UI components and routes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 16:13:22 -05:00
parent da73b25b39
commit edbfe02ee0
11 changed files with 1068 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card/index.js';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Pin } from '@lucide/svelte';
import type { Note } from '$lib/triplit/schema';
import NoteActionMenu from './NoteActionMenu.svelte';
import { Carta, Markdown } from 'carta-md';
import DOMPurify from 'isomorphic-dompurify';
let { note, view = 'grid', onclick }: { note: Note; view?: 'grid' | 'list'; onclick?: () => void } = $props();
const carta = new Carta({ sanitizer: DOMPurify.sanitize });
// Get first line of content as title if title is empty
const displayTitle = $derived(note.title || note.content.split('\n')[0] || 'Untitled');
// Truncate content for preview
const previewContent = $derived(() => {
const lines = view === 'grid' ? 3 : 2;
const contentLines = note.content.split('\n').slice(0, lines);
const preview = contentLines.join('\n');
return preview.length > 150 ? preview.substring(0, 150) + '...' : preview;
});
// Format relative time
function formatRelativeTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (years > 0) return `${years}y ago`;
if (months > 0) return `${months}mo ago`;
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return 'Just now';
}
const tags = $derived(Array.from(note.tags));
</script>
<Card.Root
class="cursor-pointer transition-all hover:shadow-md gap-2 {view === 'list' ? 'flex flex-row' : ''}"
role="button"
tabindex="0"
{onclick}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onclick?.();
}
}}
>
<div class="flex items-start justify-between gap-2 px-6">
<div class="flex items-center gap-2 flex-1 min-w-0">
{#if note.pinned}
<Pin class="h-4 w-4 text-muted-foreground shrink-0" />
{/if}
<h3 class="font-semibold text-base line-clamp-1">{displayTitle}</h3>
</div>
<div onclick={(e) => e.stopPropagation()}>
<NoteActionMenu {note} />
</div>
</div>
<Card.Content class="flex-1">
<!-- Content Preview -->
{#if note.content}
<div class="note-preview text-sm text-muted-foreground {view === 'grid' ? 'line-clamp-3' : 'line-clamp-2'} overflow-hidden">
<Markdown {carta} value={previewContent()} />
</div>
{/if}
<!-- Tags -->
{#if tags.length > 0}
<div class="flex flex-wrap gap-1 mt-3">
{#each tags.slice(0, 3) as tag (tag)}
<Badge variant="outline" class="text-xs">
{tag}
</Badge>
{/each}
{#if tags.length > 3}
<Badge variant="outline" class="text-xs">
+{tags.length - 3}
</Badge>
{/if}
</div>
{/if}
</Card.Content>
<Card.Footer>
<span class="text-xs text-muted-foreground">
{formatRelativeTime(note.updatedAt)}
</span>
</Card.Footer>
</Card.Root>
<style>
.note-preview :global(*) {
margin: 0;
}
</style>