From 48c5aa91a726db0fb09a5dd8c04fb1f1aafe5693 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:13:40 -0500 Subject: [PATCH 01/10] feat: add automated announcement banner for new benchmark data --- .../src/components/GlobalFilterContext.tsx | 37 ++++- .../src/components/announcement-banner.tsx | 85 ++++++++++ .../app/src/components/dashboard-shell.tsx | 2 + packages/app/src/hooks/useUrlState.ts | 35 +++- packages/app/src/lib/banner-data.test.ts | 150 ++++++++++++++++++ packages/app/src/lib/banner-data.ts | 95 +++++++++++ packages/app/src/lib/url-state.ts | 48 ++++++ 7 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 packages/app/src/components/announcement-banner.tsx create mode 100644 packages/app/src/lib/banner-data.test.ts create mode 100644 packages/app/src/lib/banner-data.ts diff --git a/packages/app/src/components/GlobalFilterContext.tsx b/packages/app/src/components/GlobalFilterContext.tsx index 4894cfd3..a7c2c7ac 100644 --- a/packages/app/src/components/GlobalFilterContext.tsx +++ b/packages/app/src/components/GlobalFilterContext.tsx @@ -108,7 +108,7 @@ function buildRunInfo(data: WorkflowInfoResponse): Record { } export function GlobalFilterProvider({ children }: { children: ReactNode }) { - const { hasUrlParam, getUrlParam, setUrlParams } = useUrlState(); + const { hasUrlParam, getUrlParam, setUrlParams, latestParams } = useUrlState(); // ── Core filter state ───────────────────────────────────────────────────── const [selectedModel, setSelectedModel] = useState(() => { @@ -145,6 +145,38 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) { const [selectedRunId, setSelectedRunId] = useState(() => getUrlParam('g_runid') || ''); + // When true, keep the user's date if available; otherwise always use latest + const userPickedDateRef = useRef(Boolean(getUrlParam('g_rundate'))); + + // ── Apply URL params from client-side navigations (e.g. banner clicks) ─── + const appliedParamsRef = useRef(latestParams); + useEffect(() => { + if (latestParams === appliedParamsRef.current) return; + appliedParamsRef.current = latestParams; + + const model = latestParams.g_model; + if (model && Object.values(Model).includes(model as Model)) { + setSelectedModel(model as Model); + } + const seq = latestParams.i_seq; + if (seq && Object.values(Sequence).includes(seq as Sequence)) { + setSelectedSequence(seq as Sequence); + } + const prec = latestParams.i_prec; + if (prec) { + const precs = prec.split(',').filter((p) => PRECISION_OPTIONS.includes(p as Precision)); + if (precs.length > 0) setSelectedPrecisions(precs); + } + const runDate = latestParams.g_rundate; + if (runDate) { + userPickedDateRef.current = true; + setSelectedRunDateBase(runDate); + setSelectedRunDateRev((v) => v + 1); + } + const runId = latestParams.g_runid; + if (runId) setSelectedRunId(runId); + }, [latestParams, setSelectedPrecisions]); + // ── Availability data ───────────────────────────────────────────────────── const { data: availabilityRows } = useAvailability(); @@ -213,9 +245,6 @@ export function GlobalFilterProvider({ children }: { children: ReactNode }) { return [...new Set(rows.map((r) => r.date))].toSorted(); }, [availabilityRows, modelRows, effectiveSequence, effectivePrecisions]); - // When true, keep the user's date if available; otherwise always use latest - const userPickedDateRef = useRef(Boolean(getUrlParam('g_rundate'))); - const setSelectedRunDateManual = useCallback((date: string) => { userPickedDateRef.current = true; setSelectedRunDateBase(date); diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx new file mode 100644 index 00000000..9cddf00f --- /dev/null +++ b/packages/app/src/components/announcement-banner.tsx @@ -0,0 +1,85 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { ChevronRight, Megaphone, X } from 'lucide-react'; + +import { track } from '@/lib/analytics'; +import { fetchAvailability, fetchWorkflowInfo } from '@/lib/api'; +import { + type BannerInfo, + buildBannerFromWorkflowInfo, + dismiss, + isDismissed, +} from '@/lib/banner-data'; + +export function AnnouncementBanner() { + const [banner, setBanner] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const rows = await fetchAvailability(); + if (cancelled || rows.length === 0) return; + + // Find the most recent date across all models (normalize to YYYY-MM-DD) + const latestDate = rows + .reduce((max, r) => (r.date > max ? r.date : max), rows[0].date) + .slice(0, 10); + if (isDismissed(`changelog-${latestDate}`)) return; + + const data = await fetchWorkflowInfo(latestDate); + if (cancelled) return; + + const info = buildBannerFromWorkflowInfo(latestDate, data); + if (!info || isDismissed(info.id)) return; + + setBanner(info); + track('banner_viewed', { banner_id: info.id }); + } catch { + // Silently fail — banner is non-critical + } + })(); + return () => { + cancelled = true; + }; + }, []); + + if (!banner) return null; + + const handleDismiss = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + dismiss(banner.id); + track('banner_dismissed', { banner_id: banner.id }); + setBanner(null); + }; + + return ( + track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref })} + className="mb-2 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" + > +
+ + {banner.message} + + {banner.date} + +
+
+ View + + +
+ + ); +} diff --git a/packages/app/src/components/dashboard-shell.tsx b/packages/app/src/components/dashboard-shell.tsx index 84bd217f..c36e77de 100644 --- a/packages/app/src/components/dashboard-shell.tsx +++ b/packages/app/src/components/dashboard-shell.tsx @@ -1,5 +1,6 @@ 'use client'; +import { AnnouncementBanner } from '@/components/announcement-banner'; import { ExportNudge } from '@/components/export-nudge'; import { GlobalFilterProvider } from '@/components/GlobalFilterContext'; import { GradientLabelNudge } from '@/components/gradient-label-nudge'; @@ -16,6 +17,7 @@ export function DashboardShell({ children }: { children: React.ReactNode }) {
+ {children}
diff --git a/packages/app/src/hooks/useUrlState.ts b/packages/app/src/hooks/useUrlState.ts index b440350f..3ad8e501 100644 --- a/packages/app/src/hooks/useUrlState.ts +++ b/packages/app/src/hooks/useUrlState.ts @@ -1,18 +1,19 @@ 'use client'; -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { type UrlStateKey, type UrlStateParams, readUrlParams, + refreshUrlParams, writeUrlParams, } from '@/lib/url-state'; /** * React hook for URL state synchronization. - * Reads URL params once on mount (cached in ref), and provides - * functions to write params back to the URL. + * Reads URL params on mount and on client-side navigations (pushState / popstate), + * clears them from the URL bar, and exposes fresh values via `latestParams`. */ export function useUrlState() { const initialParams = useRef(null); @@ -22,6 +23,33 @@ export function useUrlState() { initialParams.current = readUrlParams(); } + const [latestParams, setLatestParams] = useState(initialParams.current); + + useEffect(() => { + // pushState handler: params already read & cleared centrally, just apply the result + const pushHandler = (e: Event) => { + const fresh = (e as CustomEvent).detail; + if (fresh && Object.keys(fresh).length > 0) { + initialParams.current = { ...initialParams.current, ...fresh }; + setLatestParams((prev) => ({ ...prev, ...fresh })); + } + }; + // popstate handler: browser back/forward — need to read & clear ourselves + const popHandler = () => { + const fresh = refreshUrlParams(); + if (Object.keys(fresh).length > 0) { + initialParams.current = { ...initialParams.current, ...fresh }; + setLatestParams((prev) => ({ ...prev, ...fresh })); + } + }; + window.addEventListener('urlparamschange', pushHandler); + window.addEventListener('popstate', popHandler); + return () => { + window.removeEventListener('urlparamschange', pushHandler); + window.removeEventListener('popstate', popHandler); + }; + }, []); + const hasUrlParam = useCallback((key: UrlStateKey): boolean => { const value = initialParams.current?.[key]; return value !== undefined && value !== ''; @@ -42,6 +70,7 @@ export function useUrlState() { return { initialParams: initialParams.current, + latestParams, hasUrlParam, getUrlParam, setUrlParam, diff --git a/packages/app/src/lib/banner-data.test.ts b/packages/app/src/lib/banner-data.test.ts new file mode 100644 index 00000000..c8db5d6a --- /dev/null +++ b/packages/app/src/lib/banner-data.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import type { WorkflowInfoResponse } from '@/lib/api'; + +import { buildBannerFromWorkflowInfo, isDismissed, dismiss } from './banner-data'; + +const MOCK_WORKFLOW: WorkflowInfoResponse = { + runs: [], + changelogs: [ + { + workflow_run_id: 1, + date: '2026-04-07', + base_ref: 'abc', + head_ref: 'def', + config_keys: ['kimik2.5-fp4-gb200-dynamo-vllm'], + description: + 'Add Kimi K2.5 NVFP4 GB200 disaggregated multinode vLLM benchmark via Dynamo frontend\nImage: vllm/vllm-openai:v0.18.0', + pr_link: null, + }, + ], + configs: [], +}; + +describe('buildBannerFromWorkflowInfo', () => { + it('builds a banner from a changelog entry', () => { + const banner = buildBannerFromWorkflowInfo('2026-04-07', MOCK_WORKFLOW); + expect(banner).not.toBeNull(); + expect(banner!.id).toBe('changelog-2026-04-07'); + expect(banner!.message).toMatch(/^New data: /); + expect(banner!.message).toContain('Kimi-K2.5'); + expect(banner!.linkHref).toContain('/inference'); + expect(banner!.linkHref).toContain('g_model=Kimi-K2.5'); + expect(banner!.linkHref).toContain('g_rundate=2026-04-07'); + expect(banner!.linkHref).toContain('i_prec=fp4'); + expect(banner!.date).toMatch(/Apr 7, 2026/); + }); + + it('returns null when there are no changelogs', () => { + const banner = buildBannerFromWorkflowInfo('2026-04-07', { + runs: [], + changelogs: [], + configs: [], + }); + expect(banner).toBeNull(); + }); + + it('returns null when changelog has no config keys', () => { + const banner = buildBannerFromWorkflowInfo('2026-04-07', { + runs: [], + changelogs: [ + { + workflow_run_id: 1, + date: '2026-04-07', + base_ref: 'a', + head_ref: 'b', + config_keys: [], + description: 'Empty', + pr_link: null, + }, + ], + configs: [], + }); + expect(banner).toBeNull(); + }); + + it('always uses "New run:" prefix with formatted config label', () => { + const data: WorkflowInfoResponse = { + runs: [], + changelogs: [ + { + workflow_run_id: 1, + date: '2026-04-07', + base_ref: 'a', + head_ref: 'b', + config_keys: ['dsr1-fp8-b200-sglang'], + description: 'Add DeepSeek R1 FP8 B200 SGLang', + pr_link: null, + }, + ], + configs: [], + }; + const banner = buildBannerFromWorkflowInfo('2026-04-07', data); + expect(banner!.message).toMatch(/^New data: /); + expect(banner!.message).toContain('B200'); + expect(banner!.linkHref).toContain('g_model=DeepSeek-R1-0528'); + }); + + it('includes count when multiple changelogs exist', () => { + const data: WorkflowInfoResponse = { + runs: [], + changelogs: [ + { + workflow_run_id: 1, + date: '2026-04-07', + base_ref: 'a', + head_ref: 'b', + config_keys: ['dsr1-fp8-b200-sglang'], + description: + 'This is a very long description that exceeds one hundred characters and should fall back to the formatted config key label instead of using the raw description text', + pr_link: null, + }, + { + workflow_run_id: 2, + date: '2026-04-07', + base_ref: 'c', + head_ref: 'd', + config_keys: ['dsr1-fp4-h200-sglang'], + description: 'Another change', + pr_link: null, + }, + ], + configs: [], + }; + const banner = buildBannerFromWorkflowInfo('2026-04-07', data); + expect(banner!.message).toContain('+1 more'); + }); +}); + +describe('isDismissed / dismiss', () => { + let storage: Record; + + beforeEach(() => { + storage = {}; + vi.stubGlobal('window', {}); + vi.stubGlobal('localStorage', { + getItem: (key: string) => storage[key] ?? null, + setItem: (key: string, value: string) => { + storage[key] = value; + }, + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('returns false for a banner that has not been dismissed', () => { + expect(isDismissed('changelog-2026-04-07')).toBe(false); + }); + + it('returns true after dismissing a banner', () => { + dismiss('changelog-2026-04-07'); + expect(isDismissed('changelog-2026-04-07')).toBe(true); + }); + + it('does not affect other banners when one is dismissed', () => { + dismiss('changelog-2026-04-07'); + expect(isDismissed('changelog-2026-04-06')).toBe(false); + }); +}); diff --git a/packages/app/src/lib/banner-data.ts b/packages/app/src/lib/banner-data.ts new file mode 100644 index 00000000..c2d2f7a5 --- /dev/null +++ b/packages/app/src/lib/banner-data.ts @@ -0,0 +1,95 @@ +/** + * Announcement banner helpers. + * + * The banner is fully automated — it fetches the latest changelog entry from the + * workflow-info API and displays it. No manual configuration needed. + * + * Dismissals are tracked per changelog date in localStorage so users see each + * new day's announcements once. + */ + +import { DB_MODEL_TO_DISPLAY } from '@semianalysisai/inferencex-constants'; + +import type { ChangelogRow, WorkflowInfoResponse } from '@/lib/api'; +import { type Precision, MODEL_PREFIX_MAPPING, getPrecisionLabel } from '@/lib/data-mappings'; +import { getFrameworkLabel } from '@/lib/utils'; + +const DISMISS_KEY_PREFIX = 'banner-dismissed-'; + +/** Check if a banner has been dismissed by the user. */ +export function isDismissed(id: string): boolean { + if (typeof window === 'undefined') return false; + return localStorage.getItem(`${DISMISS_KEY_PREFIX}${id}`) === '1'; +} + +/** Mark a banner as dismissed. */ +export function dismiss(id: string): void { + if (typeof window === 'undefined') return; + localStorage.setItem(`${DISMISS_KEY_PREFIX}${id}`, '1'); +} + +export interface BannerInfo { + /** Dismiss key — e.g. "changelog-2026-04-07" */ + id: string; + /** Human-readable summary, e.g. "New data: Kimi-K2.5 FP4 GB200 (Dynamo vLLM)" */ + message: string; + /** Display date, e.g. "Apr 7, 2026" */ + date: string; + /** Deep-link to the inference tab with the relevant model/precision pre-selected. */ + linkHref: string; +} + +/** + * Extract a banner from a workflow-info response. + * Takes the most recent changelog entry and builds a human-readable message. + */ +export function buildBannerFromWorkflowInfo( + date: string, + data: WorkflowInfoResponse, +): BannerInfo | null { + if (!data.changelogs || data.changelogs.length === 0) return null; + + // Take the most recent entry (last in the array) + const entry: ChangelogRow = data.changelogs.at(-1)!; + const configKey = entry.config_keys[0]; + if (!configKey) return null; + + // Parse config key: model-precision-gpu-framework[-variant] + const parts = configKey.split('-'); + const modelPrefix = parts[0] ?? ''; + const precision = parts[1] ?? ''; + const gpu = parts[2] ?? ''; + const framework = parts.slice(3).join('-'); + const displayModel = DB_MODEL_TO_DISPLAY[modelPrefix]; + + // Build human-readable label: Model | Precision | GPU | Framework + const model = MODEL_PREFIX_MAPPING[modelPrefix] ?? modelPrefix; + const precLabel = getPrecisionLabel(precision as Precision); + const isMtp = framework.endsWith('-mtp'); + const baseFramework = isMtp ? framework.slice(0, -4) : framework; + const fwLabel = isMtp + ? `${getFrameworkLabel(baseFramework)}, MTP` + : getFrameworkLabel(baseFramework); + const label = `${model} ${precLabel} ${gpu.toUpperCase()} (${fwLabel})`; + const extra = data.changelogs.length > 1 ? ` (+${data.changelogs.length - 1} more)` : ''; + + const linkParams = new URLSearchParams(); + if (displayModel) linkParams.set('g_model', displayModel); + linkParams.set('g_rundate', date); + if (precision) linkParams.set('i_prec', precision); + const search = linkParams.toString(); + + // Format date as "Apr 7, 2026" + const displayDate = new Date(`${date}T00:00:00`).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + return { + id: `changelog-${date}`, + message: `New data: ${label}${extra}`, + date: displayDate, + linkHref: `/inference?${search}`, + }; +} diff --git a/packages/app/src/lib/url-state.ts b/packages/app/src/lib/url-state.ts index 3947488f..94939e78 100644 --- a/packages/app/src/lib/url-state.ts +++ b/packages/app/src/lib/url-state.ts @@ -118,6 +118,22 @@ if (typeof window !== 'undefined') { const s = sp.toString(); window.history.replaceState(null, '', `${window.location.pathname}${s ? `?${s}` : ''}`); }, 0); + + // Intercept pushState so client-side navigations (e.g. Next.js ) + // read, clear, and broadcast any share-link params in the new URL. + if (typeof history !== 'undefined' && history.pushState) { + const origPushState = history.pushState.bind(history); + history.pushState = (...args: Parameters) => { + origPushState(...args); + // Defer so the event fires after Next.js's useInsertionEffect completes + setTimeout(() => { + const fresh = refreshUrlParams(); + if (Object.keys(fresh).length > 0) { + window.dispatchEvent(new CustomEvent('urlparamschange', { detail: fresh })); + } + }, 0); + }; + } } /** Returns the share-link params that were in the URL at page load. */ @@ -125,6 +141,38 @@ export function readUrlParams(): UrlStateParams { return _initialParams; } +/** + * Re-read share-link params from the current URL, update internal state, + * clear them from the URL bar, and return the fresh params. + * Use this to pick up params from client-side navigations. + */ +export function refreshUrlParams(): UrlStateParams { + if (typeof window === 'undefined') return {}; + const searchParams = new URLSearchParams(window.location.search); + const fresh: UrlStateParams = {}; + let found = false; + for (const key of URL_STATE_KEYS) { + const value = searchParams.get(key); + if (value !== null) { + fresh[key] = value; + currentState[key] = value; + found = true; + } + } + if (!found) return {}; + + // Update cached initial params so getUrlParam() returns fresh values + Object.assign(_initialParams, fresh); + + // Clear share-link params from URL + const sp = new URLSearchParams(window.location.search); + for (const key of URL_STATE_KEYS) sp.delete(key); + const s = sp.toString(); + window.history.replaceState(null, '', `${window.location.pathname}${s ? `?${s}` : ''}`); + + return fresh; +} + /** Check whether the current URL has any share-link params. */ export function hasAnyUrlParams(): boolean { if (typeof window === 'undefined') return false; From 4ca150a67f94ac8050da119bfaaf5d3f8f1832db Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:18:05 -0500 Subject: [PATCH 02/10] feat: detect eval changelogs and link to evaluation tab --- packages/app/src/lib/banner-data.test.ts | 26 ++++++++++++++++++++++++ packages/app/src/lib/banner-data.ts | 8 ++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/app/src/lib/banner-data.test.ts b/packages/app/src/lib/banner-data.test.ts index c8db5d6a..311ab2d9 100644 --- a/packages/app/src/lib/banner-data.test.ts +++ b/packages/app/src/lib/banner-data.test.ts @@ -85,6 +85,32 @@ describe('buildBannerFromWorkflowInfo', () => { expect(banner!.linkHref).toContain('g_model=DeepSeek-R1-0528'); }); + it('links to /evaluation for eval-related changelogs', () => { + const data: WorkflowInfoResponse = { + runs: [], + changelogs: [ + { + workflow_run_id: 1, + date: '2026-03-28', + base_ref: 'a', + head_ref: 'b', + config_keys: ['qwen3.5-fp8-b200-sglang'], + description: 'Redo qwen eval', + pr_link: null, + }, + ], + configs: [], + }; + const banner = buildBannerFromWorkflowInfo('2026-03-28', data); + expect(banner!.linkHref).toMatch(/^\/evaluation\?/); + expect(banner!.linkHref).not.toContain('i_prec'); + }); + + it('links to /inference for non-eval changelogs', () => { + const banner = buildBannerFromWorkflowInfo('2026-04-07', MOCK_WORKFLOW); + expect(banner!.linkHref).toMatch(/^\/inference\?/); + }); + it('includes count when multiple changelogs exist', () => { const data: WorkflowInfoResponse = { runs: [], diff --git a/packages/app/src/lib/banner-data.ts b/packages/app/src/lib/banner-data.ts index c2d2f7a5..4f9f2e22 100644 --- a/packages/app/src/lib/banner-data.ts +++ b/packages/app/src/lib/banner-data.ts @@ -73,10 +73,14 @@ export function buildBannerFromWorkflowInfo( const label = `${model} ${precLabel} ${gpu.toUpperCase()} (${fwLabel})`; const extra = data.changelogs.length > 1 ? ` (+${data.changelogs.length - 1} more)` : ''; + // Detect eval-only changelogs by description text + const isEval = entry.description.toLowerCase().includes('eval'); + const tab = isEval ? 'evaluation' : 'inference'; + const linkParams = new URLSearchParams(); if (displayModel) linkParams.set('g_model', displayModel); linkParams.set('g_rundate', date); - if (precision) linkParams.set('i_prec', precision); + if (!isEval && precision) linkParams.set('i_prec', precision); const search = linkParams.toString(); // Format date as "Apr 7, 2026" @@ -90,6 +94,6 @@ export function buildBannerFromWorkflowInfo( id: `changelog-${date}`, message: `New data: ${label}${extra}`, date: displayDate, - linkHref: `/inference?${search}`, + linkHref: `/${tab}?${search}`, }; } From 6d9382989673cb50a8b2a21d321797839a1ed554 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:19:08 -0500 Subject: [PATCH 03/10] fix: dismiss banner on click, X button dismiss-only without navigation --- packages/app/src/components/announcement-banner.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx index 9cddf00f..17b65e3f 100644 --- a/packages/app/src/components/announcement-banner.tsx +++ b/packages/app/src/components/announcement-banner.tsx @@ -59,7 +59,11 @@ export function AnnouncementBanner() { return ( track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref })} + onClick={() => { + dismiss(banner.id); + track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref }); + setBanner(null); + }} className="mb-2 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" >
From b251a7dbf00bd723b61896c658e5477a788afe0f Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:19:56 -0500 Subject: [PATCH 04/10] fix: equal spacing above and below banner via parent gap --- packages/app/src/components/announcement-banner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx index 17b65e3f..6efc64e5 100644 --- a/packages/app/src/components/announcement-banner.tsx +++ b/packages/app/src/components/announcement-banner.tsx @@ -64,7 +64,7 @@ export function AnnouncementBanner() { track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref }); setBanner(null); }} - className="mb-2 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" + className="bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" >
From 1f74dafee4875f5014e933a04cb76658fe013065 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:20:36 -0500 Subject: [PATCH 05/10] fix: reduce banner bottom spacing on mobile --- packages/app/src/components/announcement-banner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx index 6efc64e5..dce90a98 100644 --- a/packages/app/src/components/announcement-banner.tsx +++ b/packages/app/src/components/announcement-banner.tsx @@ -64,7 +64,7 @@ export function AnnouncementBanner() { track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref }); setBanner(null); }} - className="bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" + className="-mb-2 lg:mb-0 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" >
From a387f177e918106eab02dc9fec05cb01720a733a Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:21:36 -0500 Subject: [PATCH 06/10] fix: cancel parent gap on mobile for even banner spacing --- packages/app/src/components/announcement-banner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx index dce90a98..5849afb6 100644 --- a/packages/app/src/components/announcement-banner.tsx +++ b/packages/app/src/components/announcement-banner.tsx @@ -64,7 +64,7 @@ export function AnnouncementBanner() { track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref }); setBanner(null); }} - className="-mb-2 lg:mb-0 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" + className="-mb-4 lg:mb-0 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" >
From 138c39a2c5fa22917e24c43bea238b515e8d0414 Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Wed, 8 Apr 2026 12:23:03 -0500 Subject: [PATCH 07/10] fix: remove redundant TabNav spacing, let parent gap-4 handle layout --- packages/app/src/components/announcement-banner.tsx | 2 +- packages/app/src/components/tab-nav.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/announcement-banner.tsx b/packages/app/src/components/announcement-banner.tsx index 5849afb6..6efc64e5 100644 --- a/packages/app/src/components/announcement-banner.tsx +++ b/packages/app/src/components/announcement-banner.tsx @@ -64,7 +64,7 @@ export function AnnouncementBanner() { track('banner_clicked', { banner_id: banner.id, link_href: banner.linkHref }); setBanner(null); }} - className="-mb-4 lg:mb-0 bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" + className="bg-brand/15 border border-brand/30 rounded-lg px-4 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-brand/25" >
diff --git a/packages/app/src/components/tab-nav.tsx b/packages/app/src/components/tab-nav.tsx index 2fc9774b..3bfeb111 100644 --- a/packages/app/src/components/tab-nav.tsx +++ b/packages/app/src/components/tab-nav.tsx @@ -102,8 +102,7 @@ export function TabNav() { return ( <> {/* Mobile: Dropdown */} -
-
+
@@ -128,7 +127,7 @@ export function TabNav() {
{/* Desktop: Nav links */} -
+