polish(pagespeed): mini gauges, animated tab switcher, filmstrip title
- Replace compact dot+number scores with 64px ScoreGauge circles - ScoreGauge scales font/stroke/spacing for small sizes - Add "Page Load Timeline" header to filmstrip section - Replace pill toggle with animated underline tabs (matches dashboard)
This commit is contained in:
@@ -6,6 +6,7 @@ import { useParams } from 'next/navigation'
|
|||||||
import { useSite, usePageSpeedConfig, usePageSpeedLatest, usePageSpeedHistory } from '@/lib/swr/dashboard'
|
import { useSite, usePageSpeedConfig, usePageSpeedLatest, usePageSpeedHistory } from '@/lib/swr/dashboard'
|
||||||
import { updatePageSpeedConfig, triggerPageSpeedCheck, getPageSpeedLatest, type PageSpeedCheck, type AuditSummary } from '@/lib/api/pagespeed'
|
import { updatePageSpeedConfig, triggerPageSpeedCheck, getPageSpeedLatest, type PageSpeedCheck, type AuditSummary } from '@/lib/api/pagespeed'
|
||||||
import { toast, Button } from '@ciphera-net/ui'
|
import { toast, Button } from '@ciphera-net/ui'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
import ScoreGauge from '@/components/pagespeed/ScoreGauge'
|
import ScoreGauge from '@/components/pagespeed/ScoreGauge'
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -306,27 +307,29 @@ export default function PageSpeedPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Mobile / Desktop toggle */}
|
{/* Mobile / Desktop toggle */}
|
||||||
<div className="flex gap-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg p-1">
|
<div className="flex gap-1" role="tablist" aria-label="Strategy tabs">
|
||||||
<button
|
{(['mobile', 'desktop'] as const).map(tab => (
|
||||||
onClick={() => setStrategy('mobile')}
|
<button
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
key={tab}
|
||||||
strategy === 'mobile'
|
onClick={() => setStrategy(tab)}
|
||||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
role="tab"
|
||||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
aria-selected={strategy === tab}
|
||||||
}`}
|
className={`relative px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-orange rounded cursor-pointer ${
|
||||||
>
|
strategy === tab
|
||||||
Mobile
|
? 'text-white'
|
||||||
</button>
|
: 'text-neutral-400 dark:text-neutral-500 hover:text-neutral-300'
|
||||||
<button
|
}`}
|
||||||
onClick={() => setStrategy('desktop')}
|
>
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
{tab === 'mobile' ? 'Mobile' : 'Desktop'}
|
||||||
strategy === 'desktop'
|
{strategy === tab && (
|
||||||
? 'bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm'
|
<motion.div
|
||||||
: 'text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
|
layoutId="pagespeedStrategyTab"
|
||||||
}`}
|
className="absolute inset-x-0 -bottom-px h-0.5 bg-brand-orange"
|
||||||
>
|
transition={{ type: 'spring', stiffness: 500, damping: 35 }}
|
||||||
Desktop
|
/>
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
@@ -360,20 +363,9 @@ export default function PageSpeedPage() {
|
|||||||
|
|
||||||
{/* Center — Compact Scores + Meta */}
|
{/* Center — Compact Scores + Meta */}
|
||||||
<div className="flex-1 flex flex-col justify-center gap-4 min-w-0">
|
<div className="flex-1 flex flex-col justify-center gap-4 min-w-0">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-wrap gap-4">
|
||||||
{compactScores.map(({ label, score }) => (
|
{compactScores.map(({ label, score }) => (
|
||||||
<div key={label} className="flex items-center gap-2.5">
|
<ScoreGauge key={label} score={score} label={label} size={64} />
|
||||||
<span
|
|
||||||
className="inline-block w-2.5 h-2.5 rounded-full flex-shrink-0"
|
|
||||||
style={{ backgroundColor: getScoreColor(score) }}
|
|
||||||
/>
|
|
||||||
<span className="text-lg font-semibold tabular-nums leading-tight" style={{ color: getScoreColor(score) }}>
|
|
||||||
{score !== null ? Math.round(score) : '--'}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -422,6 +414,9 @@ export default function PageSpeedPage() {
|
|||||||
{/* Filmstrip — page load progression */}
|
{/* Filmstrip — page load progression */}
|
||||||
{currentCheck?.filmstrip && currentCheck.filmstrip.length > 0 && (
|
{currentCheck?.filmstrip && currentCheck.filmstrip.length > 0 && (
|
||||||
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 relative">
|
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 sm:p-8 mb-6 relative">
|
||||||
|
<h3 className="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider mb-4">
|
||||||
|
Page Load Timeline
|
||||||
|
</h3>
|
||||||
<div className="flex items-center overflow-x-auto gap-1 scrollbar-none">
|
<div className="flex items-center overflow-x-auto gap-1 scrollbar-none">
|
||||||
{currentCheck.filmstrip.map((frame, idx) => (
|
{currentCheck.filmstrip.map((frame, idx) => (
|
||||||
<div key={idx} className="flex-shrink-0 text-center">
|
<div key={idx} className="flex-shrink-0 text-center">
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ export default function ScoreGauge({ score, label, size = 120 }: ScoreGaugeProps
|
|||||||
const offset = hasScore ? CIRCUMFERENCE * (1 - score / 100) : CIRCUMFERENCE
|
const offset = hasScore ? CIRCUMFERENCE * (1 - score / 100) : CIRCUMFERENCE
|
||||||
const color = hasScore ? getColor(score) : '#6b7280'
|
const color = hasScore ? getColor(score) : '#6b7280'
|
||||||
|
|
||||||
const fontSize = size >= 160 ? 'text-4xl' : 'text-2xl'
|
const fontSize = size >= 160 ? 'text-4xl' : size >= 100 ? 'text-2xl' : size >= 80 ? 'text-lg' : 'text-xs'
|
||||||
|
const labelSize = size >= 100 ? 'text-sm' : 'text-[10px]'
|
||||||
|
const strokeWidth = size >= 100 ? 8 : 6
|
||||||
|
const gap = size >= 100 ? 'gap-2' : 'gap-1'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className={`flex flex-col items-center ${gap}`}>
|
||||||
<div className="relative" style={{ width: size, height: size }}>
|
<div className="relative" style={{ width: size, height: size }}>
|
||||||
<svg
|
<svg
|
||||||
className="w-full h-full -rotate-90"
|
className="w-full h-full -rotate-90"
|
||||||
@@ -38,7 +41,7 @@ export default function ScoreGauge({ score, label, size = 120 }: ScoreGaugeProps
|
|||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
className="text-neutral-200 dark:text-neutral-700"
|
className="text-neutral-200 dark:text-neutral-700"
|
||||||
strokeWidth="8"
|
strokeWidth={strokeWidth}
|
||||||
/>
|
/>
|
||||||
{/* Filled arc */}
|
{/* Filled arc */}
|
||||||
<circle
|
<circle
|
||||||
@@ -47,7 +50,7 @@ export default function ScoreGauge({ score, label, size = 120 }: ScoreGaugeProps
|
|||||||
r={RADIUS}
|
r={RADIUS}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeWidth="8"
|
strokeWidth={strokeWidth}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeDasharray={CIRCUMFERENCE}
|
strokeDasharray={CIRCUMFERENCE}
|
||||||
strokeDashoffset={offset}
|
strokeDashoffset={offset}
|
||||||
@@ -66,7 +69,7 @@ export default function ScoreGauge({ score, label, size = 120 }: ScoreGaugeProps
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
<span className={`${labelSize} font-medium text-neutral-600 dark:text-neutral-400 text-center`}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user