Conversation
Redesign the homepage as a broadsheet front page with masthead, lead story, dispatches sidebar, and multi-column archives grid. Layout: - Centered masthead with Oxford double rules and dateline - Above-the-fold split: lead story (latest unread) + dispatches sidebar - Newsroom section (compose form) with progressive disclosure - Three-column archives grid with column rules and featured first article - Three-tier headline scale: lead (3rem) > featured (1.375rem) > regular (1.125rem) Data: - Store AI summary in ReviewHistoryEntry for article ledes - Backfill existing reviews from full JSON on first load - Article ledes clamped to 3 lines in grid, full in featured Polish (from /critique, /typeset, /clarify, /onboard, /arrange): - Delete confirmation dialogs with specific button labels - Time-gated lead labels: Breaking (<1h) → New (<24h) → Unread - Dropped forced metaphors (correspondent, classified, Gazette, editions) - Kept good ones (Archives, Dispatches, masthead, Oxford rules) - Welcome hero auto-focuses PR input on dismiss - Above-the-fold hidden for first-time users (no empty-state noise) - Input placeholder shows example URL format - Toggle descriptions in sans-serif (not monospace) - All inline styles eliminated, spacing consolidated into CSS - Sidebar hover no longer causes layout shift Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the update-available banner into the newspaper layout as an "EXTRA" notice between the masthead and content. The generic UpdateBanner is hidden on the home page to avoid duplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Redesigns the app’s home screen into a broadsheet/newspaper-style “front page”, and persists AI-generated review summaries into the reviews index so the homepage can show lede excerpts without reloading full review JSON.
Changes:
- Reworks
HomePagelayout into masthead + lead story + dispatches sidebar + newsroom composer + multi-column archives grid, and adds delete confirmation dialogs. - Adds update availability handling on the homepage via a newspaper-style “Extra” notice; hides the global update banner on the home page.
- Stores
summaryonReviewHistoryEntry, backfills it from per-review JSON on first read, and writes it into the reviews index.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/pages/HomePage.tsx |
Implements the newspaper layout, update “Extra” notice, archive ledes, and new delete-confirm UX. |
src/main.ts |
Backfills and persists summary into reviews-index.json; stores summary on review completion. |
src/globals.css |
Adds the broadsheet/homepage CSS (masthead rules, columns, archive grid, typography). |
src/App.tsx |
Prevents UpdateBanner from rendering on the home page (homepage uses “Extra” instead). |
lib/types.ts |
Extends ReviewHistoryEntry with optional summary for homepage ledes. |
| onClick={() => | ||
| setExpandedPRs((prev) => { | ||
| const next = new Set(prev); | ||
| if (next.has(group.prUrl)) next.delete(group.prUrl); | ||
| else next.add(group.prUrl); | ||
| return next; | ||
| }) | ||
| } |
There was a problem hiding this comment.
The “X reviews/Hide” toggle button doesn’t stop propagation, so it will also trigger the parent <article onClick> and open the latest review when the user is only trying to expand/collapse the list. Pass the click event and call e.stopPropagation() (or remove the parent click handler entirely).
| onClick={() => | |
| setExpandedPRs((prev) => { | |
| const next = new Set(prev); | |
| if (next.has(group.prUrl)) next.delete(group.prUrl); | |
| else next.add(group.prUrl); | |
| return next; | |
| }) | |
| } | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| setExpandedPRs((prev) => { | |
| const next = new Set(prev); | |
| if (next.has(group.prUrl)) next.delete(group.prUrl); | |
| else next.add(group.prUrl); | |
| return next; | |
| }); | |
| }} |
| return ( | ||
| <main className="min-h-screen overflow-y-auto px-10 lg:px-12 py-8"> | ||
| <main className="h-screen overflow-y-auto"> | ||
| <ShortcutOverlay open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} /> |
There was a problem hiding this comment.
<main> is now the scroll container (h-screen overflow-y-auto), but the page still calls window.scrollTo(...) in a few places (e.g., About/replay onboarding). That likely won’t scroll this container, so “scroll to top” can stop working. Consider using a ref to the <main> element and scrolling that element instead.
| const editionNumber = Math.floor( | ||
| (today.getTime() - new Date('2024-01-01').getTime()) / 86_400_000 |
There was a problem hiding this comment.
editionNumber is derived from today.getTime() (time-of-day) minus a date-only epoch, so the edition can tick over at UTC midnight and can be off-by-one relative to the local dateline. If you want “days since epoch” in local time, normalize both dates to local midnight before dividing; if you want UTC, compute both in UTC and ensure the dateline matches.
| const editionNumber = Math.floor( | |
| (today.getTime() - new Date('2024-01-01').getTime()) / 86_400_000 | |
| const todayAtLocalMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()); | |
| const editionEpoch = new Date(2024, 0, 1); | |
| const editionNumber = Math.floor( | |
| (todayAtLocalMidnight.getTime() - editionEpoch.getTime()) / 86_400_000 |
| function readReviewsIndex(): ReviewHistoryEntry[] { | ||
| try { | ||
| const entries = JSON.parse(fs.readFileSync(getReviewsIndexPath(), 'utf-8')) as ReviewHistoryEntry[]; | ||
| // Backward compat: entries without status are completed | ||
| return entries.map((e) => ({ ...e, status: e.status ?? 'completed' })); | ||
| let dirty = false; | ||
| const result = entries.map((e) => { | ||
| const patched = { ...e, status: e.status ?? ('completed' as const) }; | ||
| // Backfill summary from the full review JSON if missing | ||
| if (!patched.summary && patched.status === 'completed') { | ||
| try { | ||
| const reviewPath = path.join(getReviewsDir(), `${patched.id}.json`); | ||
| const review = JSON.parse(fs.readFileSync(reviewPath, 'utf-8')) as ReviewGuide; | ||
| if (review.summary) { | ||
| patched.summary = review.summary; | ||
| dirty = true; | ||
| } | ||
| } catch { | ||
| // Review file missing or unreadable — skip | ||
| } | ||
| } | ||
| return patched; | ||
| }); | ||
| // Persist backfilled summaries so we only do this once | ||
| if (dirty) { | ||
| fs.writeFileSync(getReviewsIndexPath(), JSON.stringify(result, null, 2)); | ||
| } |
There was a problem hiding this comment.
readReviewsIndex() now performs synchronous disk reads for each entry missing summary (and may synchronously write the index) every time it’s called. Since readReviewsIndex() is used in multiple IPC handlers, this can block the Electron main process and make the app feel janky on large histories. Consider doing a one-time migration step at startup (or caching the parsed index in memory) and/or performing backfill asynchronously.
| .newspaper-article *, | ||
| .newspaper-article--featured * { | ||
| cursor: pointer; | ||
| } | ||
|
|
There was a problem hiding this comment.
The universal cursor: pointer on .newspaper-article * forces a pointer cursor even for nested controls (including disabled buttons) and makes text-selection cursors impossible inside the article. This can be misleading and hurts accessibility cues. Consider removing the wildcard rule and only applying the pointer cursor to the actual clickable container/CTA elements.
| .newspaper-article *, | |
| .newspaper-article--featured * { | |
| cursor: pointer; | |
| } |
| <article | ||
| key={group.prUrl} | ||
| data-pr-url={group.prUrl} | ||
| className={`${idx === 0 ? 'newspaper-article--featured' : 'newspaper-article'} group`} | ||
| onClick={() => isClickable && handleLoadFromHistory(latestId)} | ||
| > | ||
| {/* Headline */} | ||
| <button | ||
| type="button" | ||
| onClick={() => setPrUrl(pr.url)} | ||
| className="flex items-baseline gap-3 text-left flex-1 min-w-0" | ||
| onClick={() => isClickable && handleLoadFromHistory(latestId)} | ||
| className="text-left w-full" |
There was a problem hiding this comment.
<article> is clickable and also contains a clickable <button> that calls handleLoadFromHistory. Clicking the headline will trigger both handlers due to event bubbling, which can load the review twice (and double-run side effects like marking unread). Prefer a single click target (e.g., remove the <article onClick> and keep the button, or keep the article but make inner controls call e.stopPropagation()).
- Remove headline button to prevent double-click with article onClick - Add stopPropagation to expand/collapse toggle - Remove wildcard cursor:pointer on article children (accessibility) - Use ref-based scrolling instead of window.scrollTo (main is scroll container) - Normalize edition number to local midnight (timezone fix) - Move summary backfill to one-time startup instead of every readReviewsIndex call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The featured (first) article now uses a proper two-column grid: headline + meta on the left, full AI summary on the right. Badges and actions span full width below. Removed line-clamp from all article ledes — show full summaries like a real newspaper front page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
/critique,/typeset,/clarify,/onboard,/arrangeWhat it looks like
Masthead: Centered "GNOSIS" with Oxford double rules, edition number, dateline, and user avatar
Above the fold: Latest unread review as a lead story (with time-gated labels: Breaking → New → Unread) alongside a dispatches sidebar for pending review requests
The Newsroom: Compose form with progressive disclosure for model, instructions, and preferences
The Archives: Three-column grid with column rules. First article spans full width as a featured piece. Article ledes (AI summaries) clamped to 3 lines in grid, shown in full for the featured article.
Design passes applied
/critique: Identified 5 priority issues (28/40 heuristics score, 20/25 anti-pattern scan)/typeset: Tightened masthead, clarified headline scale (four clean tiers), bumped lede readability, added hover states/clarify: Dropped forced metaphors (correspondent, classified, Gazette, editions), made error messages actionable, specific delete button labels, toggle descriptions in sans-serif/onboard: Welcome hero now auto-focuses input on dismiss, above-the-fold hidden for first-time users, example URL in placeholder, improved empty states/arrange: Eliminated all inline styles, consolidated spacing into CSS, fixed vertical rhythm, simplified featured article layout, fixed sidebar hover layout shiftTest plan
🤖 Generated with Claude Code