/// /** * Bookmarks - A pattern for collecting and browsing URLs/bookmarks. * * Displays saved links in a searchable grid with rich previews using * ct-link-preview. Users can add URLs, search across titles/descriptions/URLs, * and remove bookmarks. * * Keywords: bookmarks, links, collection, urls, grid, preview, search */ import { action, computed, Default, NAME, pattern, UI, type VNode, Writable, } from "commontools"; // ===== Types ===== interface Bookmark { url: string; title: Default; description: Default; } interface BookmarksInput { bookmarks?: Writable>; } interface BookmarksOutput { [NAME]: string; [UI]: VNode; bookmarks: Bookmark[]; count: number; } // ===== The Pattern ===== export const Bookmarks = pattern( ({ bookmarks }) => { const searchQuery = Writable.of(""); const filteredBookmarks = computed(() => { const query = searchQuery.get().toLowerCase(); if (!query) return bookmarks.get() ?? []; return (bookmarks.get() ?? []).filter((b) => { const url = (b.url ?? "").toLowerCase(); const title = (b.title ?? "").toLowerCase(); const description = (b.description ?? "").toLowerCase(); return url.includes(query) || title.includes(query) || description.includes(query); }); }); const count = computed(() => (bookmarks.get() ?? []).length); const addBookmark = action(({ url }: { url: string }) => { const trimmed = url.trim(); if (!trimmed) return; bookmarks.push({ url: trimmed, title: "", description: "" }); }); const removeBookmark = action(({ index }: { index: number }) => { const current = bookmarks.get() ?? []; if (index < 0 || index >= current.length) return; bookmarks.set(current.toSpliced(index, 1)); }); return { [NAME]: computed(() => `🔖 Bookmarks (${count})`), [UI]: ( {/* Add URL input */} { const url = e.detail?.message; if (url) addBookmark.send({ url }); }} /> {/* Search */} {/* Grid of link previews */} {filteredBookmarks.map((bookmark: Bookmark, index: number) => ( ))} ), bookmarks, count, }; }, ); export default Bookmarks;