From 723bdd2aaa64f5e1a0fe82e6288d0cd72c330877 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 22 Jul 2026 11:50:45 +0100 Subject: [PATCH] Present post-update What's New highlights in a dialog instead of a banner The curated highlights are multi-bullet reading content, which the old full-width top banner rendered as a cramped, edge-to-edge wall of text that pushed the whole app down and even scrolled internally. The shared Dialog gives the content a readable measure, a proper header and footer, and leaves the first post-update paint of the dashboard unobstructed. Show/dismiss gating is unchanged: once per release, only when the release has a Highlights section, and any close path records the version as seen. Contract-Neutral: presentation-only change: post-update What's New highlights move from a top banner strip to the shared Dialog; show/dismiss gating, highlights extraction, and release-note transport are untouched --- .../src/components/WhatsNewCard.tsx | 111 +++++++++++------- .../__tests__/WhatsNewCard.test.tsx | 13 +- 2 files changed, 73 insertions(+), 51 deletions(-) diff --git a/frontend-modern/src/components/WhatsNewCard.tsx b/frontend-modern/src/components/WhatsNewCard.tsx index fb00049b1..f9dafcdb0 100644 --- a/frontend-modern/src/components/WhatsNewCard.tsx +++ b/frontend-modern/src/components/WhatsNewCard.tsx @@ -1,7 +1,11 @@ import { Show, createEffect, createSignal } from 'solid-js'; +import XIcon from 'lucide-solid/icons/x'; import { updateStore } from '@/stores/updates'; import { UpdatesAPI } from '@/api/updates'; import { STORAGE_KEYS } from '@/utils/localStorage'; +import { ActionIconButton, Button } from '@/components/shared/Button'; +import { Dialog } from '@/components/shared/Dialog'; +import { ExternalTextLink } from '@/components/shared/ExternalTextLink'; import { buildReleaseNotesUrl, normalizeReleaseVersion } from '@/components/updateVersion'; import { extractHighlights, isReleaseVersion } from '@/components/whatsNewModel'; import { renderMarkdown } from '@/components/AI/aiChatUtils'; @@ -19,15 +23,15 @@ const markVersionSeen = (version: string) => { try { localStorage.setItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN, version); } catch { - // Private mode / storage disabled: the banner simply won't persist state. + // Private mode / storage disabled: the dialog simply won't persist state. } }; /** - * Post-update "What's New" banner. Shows once after the running version + * Post-update "What's New" dialog. Shows once after the running version * changes, and only when that release has a curated `## Highlights` section * in its GitHub release notes. Dismissing (or a highlights-free release) - * records the version so the banner stays quiet until the next update. + * records the version so the dialog stays quiet until the next update. */ export function WhatsNewCard() { const [visible, setVisible] = createSignal(false); @@ -58,7 +62,7 @@ export function WhatsNewCard() { return; } // Transient failure: leave last-seen untouched so the next load retries. - logger.warn("Failed to load release notes for What's New banner", error); + logger.warn("Failed to load release notes for What's New dialog", error); } }; @@ -77,7 +81,7 @@ export function WhatsNewCard() { const lastSeen = readLastSeenVersion(); if (!lastSeen) { // First run (fresh install or first load after this feature shipped): - // record the baseline silently instead of greeting users with a banner. + // record the baseline silently instead of greeting users with a dialog. markVersionSeen(currentVersion); return; } @@ -88,6 +92,7 @@ export function WhatsNewCard() { void loadNotes(currentVersion); }); + // Any close path (button, backdrop, Escape) counts as seen. const dismiss = () => { markVersionSeen(version()); setVisible(false); @@ -95,54 +100,72 @@ export function WhatsNewCard() { return ( -
-
-
-
- {/* Sparkle icon */} - +
+
+
+ {/* Sparkle icon */} + +
+

+ What's new in v{version()} +

+

Pulse updated successfully

+
+
+ - - - - - Pulse updated to v{version()} — here's what's new - - +
-
+
+ +
+ + Full release notes → + + +
-
+ ); } diff --git a/frontend-modern/src/components/__tests__/WhatsNewCard.test.tsx b/frontend-modern/src/components/__tests__/WhatsNewCard.test.tsx index 16a57a4cc..15a6ef8bc 100644 --- a/frontend-modern/src/components/__tests__/WhatsNewCard.test.tsx +++ b/frontend-modern/src/components/__tests__/WhatsNewCard.test.tsx @@ -44,7 +44,7 @@ describe('WhatsNewCard', () => { await renderCard(); - expect(screen.queryByTestId('whats-new-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument(); expect(getReleaseNotesMock).not.toHaveBeenCalled(); expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.1.0-rc.1'); }); @@ -66,11 +66,10 @@ describe('WhatsNewCard', () => { await renderCard(); await waitFor(() => { - expect(screen.getByTestId('whats-new-banner')).toBeInTheDocument(); + expect(screen.getByTestId('whats-new-modal')).toBeInTheDocument(); }); - expect( - screen.getByText("Pulse updated to v6.1.0-rc.1 — here's what's new"), - ).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByText("What's new in v6.1.0-rc.1")).toBeInTheDocument(); expect(screen.getByText('Reviewed Actions inbox')).toBeInTheDocument(); expect(screen.queryByText('Internal work')).not.toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Full release notes →' })).toHaveAttribute( @@ -99,7 +98,7 @@ describe('WhatsNewCard', () => { fireEvent.click(screen.getByText('Got it')); await waitFor(() => { - expect(screen.queryByTestId('whats-new-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument(); }); expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.1.0-rc.1'); }); @@ -114,7 +113,7 @@ describe('WhatsNewCard', () => { await renderCard(); - expect(screen.queryByTestId('whats-new-banner')).not.toBeInTheDocument(); + expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument(); expect(getReleaseNotesMock).not.toHaveBeenCalled(); expect(localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN)).toBe('6.0.5'); });