From 2776c803f179f047993e4261903d740e0659ef7b Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Tue, 10 Mar 2026 00:08:09 +0100 Subject: [PATCH] fix: use focus-visible for all button/tab/link focus rings across app Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 ++ app/faq/page.tsx | 2 +- app/integrations/page.tsx | 6 ++--- app/page.tsx | 4 +-- app/sites/[id]/realtime/page.tsx | 4 +-- app/sites/[id]/settings/page.tsx | 16 +++++------ app/sites/new/page.tsx | 2 +- app/welcome/page.tsx | 12 ++++----- components/Footer.tsx | 24 ++++++++--------- components/PricingSection.tsx | 4 +-- components/dashboard/Campaigns.tsx | 2 +- components/dashboard/ContentStats.tsx | 2 +- components/dashboard/GoalStats.tsx | 2 +- components/dashboard/Locations.tsx | 2 +- components/dashboard/PerformanceStats.tsx | 4 +-- components/dashboard/TechSpecs.tsx | 2 +- components/settings/OrganizationSettings.tsx | 28 ++++++++++---------- components/sites/SiteList.tsx | 2 +- 18 files changed, 61 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc7bc63..6adf351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Beautiful funnel visualization.** Funnel reports now show a smooth, animated funnel shape instead of a plain bar chart. Each step flows into the next with curved segments, hover effects, and labels showing visitor counts and conversion percentages at a glance. - **Tidier dashboard layout.** The tab navigation (Dashboard, Uptime, Funnels, Settings) now sits above your site name and controls, keeping the tabs front and center. - **Instant tab switching.** Clicking between Dashboard, Uptime, Funnels, and Settings now feels instant — the tab bar stays in place while the page content loads below it, instead of the whole screen flashing with a loading skeleton. +- **Smooth tab animations.** Switching tabs now plays a sliding indicator animation on the active tab and a subtle crossfade on the page content, making navigation feel polished and responsive. +- **Cleaner focus styles.** Buttons, tabs, and links no longer show an orange outline when you click them — the focus ring now only appears when navigating with the keyboard, keeping the interface clean. - **Smoother theme switching.** Toggling between light and dark mode now plays a satisfying circular reveal animation that expands from the toggle button, instead of everything just flipping instantly. - **Cleaner site navigation.** Dashboard, Uptime, Funnels, and Settings now use an underline tab bar instead of floating buttons. The active section is highlighted with an orange underline, making it easy to see where you are and switch between views. - **Consistent icon style.** All dashboard icons now use a single, unified icon set for a cleaner look across Technology, Locations, Campaigns, and Referrers panels. diff --git a/app/faq/page.tsx b/app/faq/page.tsx index 51393f8..f1bcdca 100644 --- a/app/faq/page.tsx +++ b/app/faq/page.tsx @@ -58,7 +58,7 @@ function FAQItem({ faq, index }: { faq: typeof faqs[0]; index: number }) { > @@ -152,7 +152,7 @@ export default function RealtimePage() { exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.2 }} onClick={() => handleSelectVisitor(visitor)} - className={`w-full text-left p-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-inset ${ + className={`w-full text-left p-4 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-inset ${ selectedVisitor?.session_id === visitor.session_id ? 'bg-neutral-50 dark:bg-neutral-800/50 ring-1 ring-inset ring-neutral-200 dark:ring-neutral-700' : '' }`} > diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index c72fdbe..c17945d 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -428,7 +428,7 @@ export default function SiteSettingsPage() { onClick={() => setActiveTab('general')} role="tab" aria-selected={activeTab === 'general'} - className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 ${ + className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 ${ activeTab === 'general' ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800' @@ -441,7 +441,7 @@ export default function SiteSettingsPage() { onClick={() => setActiveTab('visibility')} role="tab" aria-selected={activeTab === 'visibility'} - className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 ${ + className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 ${ activeTab === 'visibility' ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800' @@ -454,7 +454,7 @@ export default function SiteSettingsPage() { onClick={() => setActiveTab('data')} role="tab" aria-selected={activeTab === 'data'} - className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 ${ + className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 ${ activeTab === 'data' ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800' @@ -467,7 +467,7 @@ export default function SiteSettingsPage() { onClick={() => setActiveTab('goals')} role="tab" aria-selected={activeTab === 'goals'} - className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 ${ + className={`w-full flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 ${ activeTab === 'goals' ? 'bg-brand-orange/10 text-brand-orange' : 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800' @@ -568,7 +568,7 @@ export default function SiteSettingsPage() { @@ -616,7 +616,7 @@ export default function SiteSettingsPage() { @@ -682,7 +682,7 @@ export default function SiteSettingsPage() { diff --git a/app/sites/new/page.tsx b/app/sites/new/page.tsx index da34c13..1638fd1 100644 --- a/app/sites/new/page.tsx +++ b/app/sites/new/page.tsx @@ -133,7 +133,7 @@ export default function NewSitePage() { diff --git a/app/welcome/page.tsx b/app/welcome/page.tsx index 759c6fb..a61de13 100644 --- a/app/welcome/page.tsx +++ b/app/welcome/page.tsx @@ -475,7 +475,7 @@ function WelcomeContent() {

) : (

- + View pricing

@@ -631,7 +631,7 @@ function WelcomeContent() { diff --git a/components/Footer.tsx b/components/Footer.tsx index ed6b2cd..7a0ff24 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -52,16 +52,16 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate © 2024-{year} Ciphera. All rights reserved.
- + Why {appName} - + Changelog - + Pricing - + FAQ
@@ -106,7 +106,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate href="https://github.com/ciphera-net" target="_blank" rel="noopener noreferrer" - className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange" + className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange" aria-label="GitHub" > @@ -115,7 +115,7 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate href="https://x.com/cipheranet" target="_blank" rel="noopener noreferrer" - className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange" + className="w-9 h-9 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange" aria-label="X (Twitter)" > @@ -134,14 +134,14 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate href={link.href} target="_blank" rel="noopener noreferrer" - className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange focus:rounded" + className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus:rounded" > {link.name} ) : ( {link.name} @@ -162,14 +162,14 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate href={link.href} target="_blank" rel="noopener noreferrer" - className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange focus:rounded" + className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus:rounded" > {link.name} ) : ( {link.name} @@ -190,14 +190,14 @@ export function Footer({ LinkComponent = Link, appName = 'Pulse', isAuthenticate href={link.href} target="_blank" rel="noopener noreferrer" - className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange focus:rounded" + className="text-sm text-neutral-600 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus:rounded" > {link.name} ) : ( {link.name} diff --git a/components/PricingSection.tsx b/components/PricingSection.tsx index 42b7663..048f942 100644 --- a/components/PricingSection.tsx +++ b/components/PricingSection.tsx @@ -267,7 +267,7 @@ export default function PricingSection() { onClick={() => setIsYearly(false)} role="radio" aria-checked={!isYearly} - className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-brand-orange ${ + className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange ${ !isYearly ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white' @@ -279,7 +279,7 @@ export default function PricingSection() { onClick={() => setIsYearly(true)} role="radio" aria-checked={isYearly} - className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-brand-orange ${ + className={`min-w-[88px] px-4 py-2 rounded-lg text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange ${ isYearly ? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm' : 'text-neutral-500 hover:text-neutral-900 dark:hover:text-white' diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index 9f0ffe0..5676e1c 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -200,7 +200,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp

Learn more diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index 33ac2ad..0bb095b 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -106,7 +106,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, onClick={() => setActiveTab(tab)} role="tab" aria-selected={activeTab === tab} - className={`px-2.5 py-1 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-brand-orange rounded cursor-pointer border-b-2 ${ + className={`px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer border-b-2 ${ activeTab === tab ? 'border-brand-orange text-neutral-900 dark:text-white' : 'border-transparent text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' diff --git a/components/dashboard/GoalStats.tsx b/components/dashboard/GoalStats.tsx index 3207393..f641af7 100644 --- a/components/dashboard/GoalStats.tsx +++ b/components/dashboard/GoalStats.tsx @@ -54,7 +54,7 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)

Read documentation diff --git a/components/dashboard/Locations.tsx b/components/dashboard/Locations.tsx index b1d3cfe..076bceb 100644 --- a/components/dashboard/Locations.tsx +++ b/components/dashboard/Locations.tsx @@ -209,7 +209,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = ' onClick={() => setActiveTab(tab)} role="tab" aria-selected={activeTab === tab} - className={`px-2.5 py-1 text-xs font-medium transition-colors capitalize focus:outline-none focus:ring-2 focus:ring-brand-orange rounded cursor-pointer border-b-2 ${ + className={`px-2.5 py-1 text-xs font-medium transition-colors capitalize focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer border-b-2 ${ activeTab === tab ? 'border-brand-orange text-neutral-900 dark:text-white' : 'border-transparent text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' diff --git a/components/dashboard/PerformanceStats.tsx b/components/dashboard/PerformanceStats.tsx index 58af809..11ec7d2 100644 --- a/components/dashboard/PerformanceStats.tsx +++ b/components/dashboard/PerformanceStats.tsx @@ -114,7 +114,7 @@ export default function PerformanceStats({ stats, performanceByPage, siteId, sta @@ -1077,14 +1077,14 @@ export default function OrganizationSettings() { {invoice.invoice_pdf && ( + className="inline-flex items-center gap-2 px-2.5 py-1.5 text-xs font-medium text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange" title="Download PDF"> Download PDF )} {invoice.hosted_invoice_url && ( setIsSavingNotificationSettings(false)) }} disabled={isSavingNotificationSettings} - className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-orange focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${ + className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${ notificationSettings[cat.id] !== false ? 'bg-brand-orange' : 'bg-neutral-200 dark:bg-neutral-700' }`} > @@ -1461,7 +1461,7 @@ export default function OrganizationSettings() { diff --git a/components/sites/SiteList.tsx b/components/sites/SiteList.tsx index 0e79116..718386b 100644 --- a/components/sites/SiteList.tsx +++ b/components/sites/SiteList.tsx @@ -99,7 +99,7 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr