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:
@@ -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
|
||||||
|
|||||||
@@ -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) : '-'}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
8
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user