"Track 3 wrote the clip. Track 4 reads the clip. The side panel ends as a real library — searchable, copy-to-clipboard, links back to the source. Two letters of CRUD now on the board."
What 'R' Means Here
The R in CRUD is not just "display." A real read view earns its surface by being scannable, searchable, and actionable. For ClipDeck specifically:
- Newest clip at the top (sorted by
savedAtdescending). - Each row shows the source title (clickable link), the relative time ("5m ago"), and the clip text (truncated with a read-more affordance for long clips).
- A search box that filters as the user types (substring match against clip text and title).
- A "copy text" button per row that writes to the system clipboard.
- Empty state that explains what to do: "No clips yet. Select text on any page and press Ctrl+Shift+K."
The Render Loop
The pattern from Track 2 carries straight through:
- Panel mounts. Read clips once from
chrome.storage.local. - Render, including current search filter from the search input.
- Subscribe to
chrome.storage.onChanged. Whenclipschanges, re-render. - Subscribe to the search input's
inputevent. On every keystroke, re-render with the new filter.
No state-management library needed. Storage is the source of truth; the panel is a derived view. If you find yourself maintaining a parallel in-memory clips array in the panel, you're duplicating state — read from storage every render, or cache the array but rebuild it on every onChanged fire.
Relative Time
Modern browsers (Chrome 71+) include Intl.RelativeTimeFormat for human-friendly time strings. Used right, it handles localization automatically — Korean shows "5분 전," English shows "5 minutes ago," no extra code.
The bucket logic is straightforward: compute the difference in seconds, fall through buckets (seconds → minutes → hours → days), and format with the appropriate unit. Refresh the rendered times every minute via setInterval, or accept that they go stale until the user interacts.
Copy to Clipboard
The modern clipboard API (navigator.clipboard.writeText) works in extension pages with no extra permission, but it requires a user gesture — the button click handler counts. The legacy document.execCommand('copy') path also works as a fallback for older Chrome, but most installs are recent enough that you can skip it.
The pattern: click the copy button, await navigator.clipboard.writeText(clip.text), then show a brief confirmation toast ("Copied!") that auto-fades after 1.5 seconds. Resist the urge to use an alert — alerts steal focus from the page the user is reading and break the persistent-panel UX.
The Filter
For a few hundred clips, simple substring matching on each render is fast enough — no need for an index. Lowercase both sides, check for inclusion, done. Past a few thousand clips it might be worth building a lazy-loaded list or a fuzzy-search index, but that is well past v1 scope.
Open Source URL
The clip's title is a link to the original URL. Clicking it opens a new tab (target="_blank" in the anchor, or programmatically via chrome.tabs.create({ url, active: false })). The active: false variant opens the tab in the background so the user does not lose their place in the side panel — usually the right default for a library view.
escapeHtml helper from earlier lessons (or DOMPurify for richer HTML) is the bare-minimum baseline; for v1 we render text only, so the escape helper is sufficient.