fix: mobile responsiveness across all pages

- SiteNav: add horizontal scroll for 8 tabs on mobile
- NotificationCenter: full-width dropdown on mobile
- ContentStats/Locations/TechSpecs: scrollable tab bars
- FrustrationTable: fix selector text overflow
- FrustrationByPageTable: horizontal scroll on mobile
- CDN: better stat card grid breakpoints
- Home: reduce stat card height, prevent button wrap
- Billing: shorter invoice labels on mobile
- Bump @ciphera-net/ui to 0.2.6 (AppLauncher mobile fix)
This commit is contained in:
Usman Baig
2026-03-15 18:15:06 +01:00
parent 7103a39273
commit 24c71f7991
13 changed files with 30 additions and 21 deletions

View File

@@ -400,7 +400,7 @@ export default function HomePage() {
) : null ) : null
})() ?? ( })() ?? (
<Link href="/sites/new"> <Link href="/sites/new">
<Button variant="primary" className="text-sm"> <Button variant="primary" className="text-sm whitespace-nowrap">
Add New Site Add New Site
</Button> </Button>
</Link> </Link>
@@ -409,11 +409,11 @@ export default function HomePage() {
{/* * Global Overview - min-h ensures no layout shift when Plan & usage loads */} {/* * Global Overview - min-h ensures no layout shift when Plan & usage loads */}
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="flex min-h-[160px] flex-col rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900"> <div className="flex min-h-[100px] sm:min-h-[160px] flex-col rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
<p className="text-sm text-neutral-500 dark:text-neutral-400">Total Sites</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Total Sites</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-white">{sites.length}</p> <p className="text-2xl font-bold text-neutral-900 dark:text-white">{sites.length}</p>
</div> </div>
<div className="flex min-h-[160px] flex-col rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900"> <div className="flex min-h-[100px] sm:min-h-[160px] flex-col rounded-2xl border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
<p className="text-sm text-neutral-500 dark:text-neutral-400">Total Visitors (24h)</p> <p className="text-sm text-neutral-500 dark:text-neutral-400">Total Visitors (24h)</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-white"> <p className="text-2xl font-bold text-neutral-900 dark:text-white">
{sites.length === 0 || Object.keys(siteStats).length < sites.length {sites.length === 0 || Object.keys(siteStats).length < sites.length

View File

@@ -143,7 +143,7 @@ export default function CDNPage() {
</div> </div>
<SkeletonLine className="h-9 w-36 rounded-lg" /> <SkeletonLine className="h-9 w-36 rounded-lg" />
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 mb-8"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
<StatCardSkeleton /> <StatCardSkeleton />
<StatCardSkeleton /> <StatCardSkeleton />
<StatCardSkeleton /> <StatCardSkeleton />
@@ -249,7 +249,7 @@ export default function CDNPage() {
</div> </div>
{/* Overview cards */} {/* Overview cards */}
<div className="grid grid-cols-2 sm:grid-cols-5 gap-4 mb-8"> <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
<OverviewCard <OverviewCard
label="Bandwidth" label="Bandwidth"
value={overview ? formatBytes(overview.total_bandwidth) : '-'} value={overview ? formatBytes(overview.total_bandwidth) : '-'}

View File

@@ -45,7 +45,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
{loading ? ( {loading ? (
<SkeletonRows /> <SkeletonRows />
) : hasData ? ( ) : hasData ? (
<div> <div className="overflow-x-auto -mx-6 px-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-2 -mx-2 mb-2 text-xs font-medium text-neutral-400 dark:text-neutral-500 uppercase tracking-wider"> <div className="flex items-center justify-between px-2 -mx-2 mb-2 text-xs font-medium text-neutral-400 dark:text-neutral-500 uppercase tracking-wider">
<span>Page</span> <span>Page</span>
@@ -72,7 +72,7 @@ export default function FrustrationByPageTable({ pages, loading }: FrustrationBy
style={{ width: `${barWidth}%` }} style={{ width: `${barWidth}%` }}
/> />
<span <span
className="relative text-sm text-neutral-900 dark:text-white truncate max-w-[300px]" className="relative text-sm text-neutral-900 dark:text-white truncate max-w-[200px] sm:max-w-[300px]"
title={page.page_path} title={page.page_path}
> >
{page.page_path} {page.page_path}

View File

@@ -81,7 +81,7 @@ function Row({
return ( return (
<div className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors"> <div className="flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 min-w-0 overflow-hidden">
<SelectorCell selector={item.selector} /> <SelectorCell selector={item.selector} />
<span <span
className="text-xs text-neutral-400 dark:text-neutral-500 truncate shrink-0" className="text-xs text-neutral-400 dark:text-neutral-500 truncate shrink-0"

View File

@@ -115,7 +115,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
</button> </button>
)} )}
</div> </div>
<div className="flex gap-1" role="tablist" aria-label="Pages view tabs" onKeyDown={handleTabKeyDown}> <div className="flex gap-1 overflow-x-auto scrollbar-hide" role="tablist" aria-label="Pages view tabs" onKeyDown={handleTabKeyDown}>
{(['top_pages', 'entry_pages', 'exit_pages'] as Tab[]).map((tab) => ( {(['top_pages', 'entry_pages', 'exit_pages'] as Tab[]).map((tab) => (
<button <button
key={tab} key={tab}

View File

@@ -233,7 +233,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
</button> </button>
)} )}
</div> </div>
<div className="flex gap-1" role="tablist" aria-label="Location view tabs" onKeyDown={handleTabKeyDown}> <div className="flex gap-1 overflow-x-auto scrollbar-hide" role="tablist" aria-label="Location view tabs" onKeyDown={handleTabKeyDown}>
{(['map', 'globe', 'countries', 'regions', 'cities'] as Tab[]).map((tab) => ( {(['map', 'globe', 'countries', 'regions', 'cities'] as Tab[]).map((tab) => (
<button <button
key={tab} key={tab}

View File

@@ -36,7 +36,7 @@ export default function SiteNav({ siteId }: SiteNavProps) {
return ( return (
<div className="border-b border-neutral-200 dark:border-neutral-800 mb-6"> <div className="border-b border-neutral-200 dark:border-neutral-800 mb-6">
<nav className="flex gap-1" role="tablist" aria-label="Site navigation" onKeyDown={handleTabKeyDown}> <nav className="flex gap-1 overflow-x-auto scrollbar-hide" role="tablist" aria-label="Site navigation" onKeyDown={handleTabKeyDown}>
{tabs.map((tab) => ( {tabs.map((tab) => (
<Link <Link
key={tab.href} key={tab.href}
@@ -44,7 +44,7 @@ export default function SiteNav({ siteId }: SiteNavProps) {
role="tab" role="tab"
aria-selected={isActive(tab.href)} aria-selected={isActive(tab.href)}
tabIndex={isActive(tab.href) ? 0 : -1} tabIndex={isActive(tab.href) ? 0 : -1}
className={`relative px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded-t cursor-pointer -mb-px ${ className={`relative shrink-0 whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded-t cursor-pointer -mb-px ${
isActive(tab.href) isActive(tab.href)
? 'text-neutral-900 dark:text-white' ? 'text-neutral-900 dark:text-white'
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300' : 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300'

View File

@@ -145,7 +145,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
</button> </button>
)} )}
</div> </div>
<div className="flex gap-1" role="tablist" aria-label="Technology view tabs" onKeyDown={handleTabKeyDown}> <div className="flex gap-1 overflow-x-auto scrollbar-hide" role="tablist" aria-label="Technology view tabs" onKeyDown={handleTabKeyDown}>
{(['browsers', 'os', 'devices', 'screens'] as Tab[]).map((tab) => ( {(['browsers', 'os', 'devices', 'screens'] as Tab[]).map((tab) => (
<button <button
key={tab} key={tab}

View File

@@ -152,7 +152,7 @@ export default function NotificationCenter() {
id="notification-dropdown" id="notification-dropdown"
role="dialog" role="dialog"
aria-label="Notifications" aria-label="Notifications"
className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100]" className="fixed left-4 right-4 top-16 sm:absolute sm:left-auto sm:right-0 sm:top-full sm:mt-2 sm:w-96 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100]"
> >
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700"> <div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3> <h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3>

View File

@@ -1098,7 +1098,7 @@ export default function OrganizationSettings() {
<a href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer" <a href={invoice.invoice_pdf} target="_blank" rel="noopener noreferrer"
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"> 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">
<DownloadIcon className="w-3.5 h-3.5" /> <DownloadIcon className="w-3.5 h-3.5" />
Download PDF <span className="hidden sm:inline">Download</span> PDF
</a> </a>
)} )}
{invoice.hosted_invoice_url && ( {invoice.hosted_invoice_url && (
@@ -1110,7 +1110,7 @@ export default function OrganizationSettings() {
}`} }`}
title={invoice.status === 'open' ? 'Pay now' : 'View invoice'}> title={invoice.status === 'open' ? 'Pay now' : 'View invoice'}>
<ExternalLinkIcon className="w-3.5 h-3.5" /> <ExternalLinkIcon className="w-3.5 h-3.5" />
{invoice.status === 'open' ? 'Pay now' : 'View invoice'} {invoice.status === 'open' ? 'Pay now' : <><span className="hidden sm:inline">View </span>Invoice</>}
</a> </a>
)} )}
</div> </div>

8
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "pulse-frontend", "name": "pulse-frontend",
"version": "0.15.0-alpha", "version": "0.15.0-alpha",
"dependencies": { "dependencies": {
"@ciphera-net/ui": "^0.2.5", "@ciphera-net/ui": "^0.2.6",
"@ducanh2912/next-pwa": "^10.2.9", "@ducanh2912/next-pwa": "^10.2.9",
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",
@@ -1667,9 +1667,9 @@
} }
}, },
"node_modules/@ciphera-net/ui": { "node_modules/@ciphera-net/ui": {
"version": "0.2.5", "version": "0.2.6",
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.5/01371025a6706621b7a2c353cb1b07a239961fc3", "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.2.6/85b0deed2ec86461502209b098f64e028b49e63e",
"integrity": "sha512-Ybd3zZLqpdv/dktNylT/jOm9OTMVST35+19QY+DTvDeluF3B4bN2YA7S85V7PpXGmZBmnPQX3U8qP4t2HwyyMw==", "integrity": "sha512-mNlK4FNwWAYiScMcyTP7Y5EEZjSUf8H63EdQUlcTEWoFo4km5ZPrlJcfPsbUsN65YB9OtT+iAiu/XRG4dI0/Gg==",
"dependencies": { "dependencies": {
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",

View File

@@ -12,7 +12,7 @@
"test:watch": "vitest" "test:watch": "vitest"
}, },
"dependencies": { "dependencies": {
"@ciphera-net/ui": "^0.2.5", "@ciphera-net/ui": "^0.2.6",
"@ducanh2912/next-pwa": "^10.2.9", "@ducanh2912/next-pwa": "^10.2.9",
"@phosphor-icons/react": "^2.1.10", "@phosphor-icons/react": "^2.1.10",
"@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/browser": "^13.2.2",

View File

@@ -82,6 +82,15 @@
transform-style: preserve-3d; transform-style: preserve-3d;
} }
/* * Scrollbar hide - for horizontal scroll navs */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* * Animations */ /* * Animations */
@keyframes float { @keyframes float {
0%, 100% { transform: translateY(0px); } 0%, 100% { transform: translateY(0px); }