Skip to content

Commit 509fc76

Browse files
Dbochmanclaude
andauthored
style: add tabular-nums to all dynamic numeric displays (#255)
Prevents layout shift when numbers change by using equal-width digits across analytics cards, SLO tool, MTTR builder, and billing components. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 22a9309 commit 509fc76

11 files changed

Lines changed: 36 additions & 36 deletions

src/components/analytics/CoreWebVitalsCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export function CoreWebVitalsCard({ data }: CoreWebVitalsCardProps) {
9696
<div key={vital.key} className="space-y-1">
9797
<div className="flex justify-between text-sm">
9898
<span className="font-medium">{vital.label}</span>
99-
<span className={cn('font-mono', {
99+
<span className={cn('font-mono tabular-nums', {
100100
'text-emerald-500': status === 'good',
101101
'text-amber-500': status === 'warning',
102102
'text-red-500': status === 'critical',
@@ -112,7 +112,7 @@ export function CoreWebVitalsCard({ data }: CoreWebVitalsCardProps) {
112112
className="h-2"
113113
aria-label={`${vital.label}: ${rawValue}`}
114114
/>
115-
<div className="flex justify-between text-xs text-muted-foreground">
115+
<div className="flex justify-between text-xs text-muted-foreground tabular-nums">
116116
<span>Good: ≤{vital.thresholds.good}{vital.unit}</span>
117117
<span>Poor: &gt;{vital.thresholds.poor}{vital.unit}</span>
118118
</div>

src/components/analytics/GitHubBillingCard.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const GitHubBillingCard = memo(function GitHubBillingCard({ data }: GitHu
4242
<Clock className="h-4 w-4 text-muted-foreground" />
4343
</CardHeader>
4444
<CardContent>
45-
<div className="text-2xl font-bold">{summary.totalMinutes.toLocaleString()}</div>
45+
<div className="text-2xl font-bold tabular-nums">{summary.totalMinutes.toLocaleString()}</div>
4646
<p className="text-xs text-muted-foreground mt-1">{period.description}</p>
4747
</CardContent>
4848
</Card>
@@ -55,7 +55,7 @@ export const GitHubBillingCard = memo(function GitHubBillingCard({ data }: GitHu
5555
<DollarSign className="h-4 w-4 text-emerald-500" />
5656
</CardHeader>
5757
<CardContent>
58-
<div className="text-2xl font-bold text-emerald-500">
58+
<div className="text-2xl font-bold text-emerald-500 tabular-nums">
5959
${summary.totalDiscountAmount.toFixed(2)}
6060
</div>
6161
<p className="text-xs text-muted-foreground mt-1">{savingsPercent}% from free tier</p>
@@ -70,7 +70,7 @@ export const GitHubBillingCard = memo(function GitHubBillingCard({ data }: GitHu
7070
<DollarSign className="h-4 w-4 text-muted-foreground" />
7171
</CardHeader>
7272
<CardContent>
73-
<div className={`text-2xl font-bold ${summary.totalNetAmount === 0 ? 'text-emerald-500' : ''}`}>
73+
<div className={`text-2xl font-bold tabular-nums ${summary.totalNetAmount === 0 ? 'text-emerald-500' : ''}`}>
7474
${summary.totalNetAmount.toFixed(2)}
7575
</div>
7676
<p className="text-xs text-muted-foreground mt-1">actual spend</p>
@@ -111,7 +111,7 @@ export const GitHubBillingCard = memo(function GitHubBillingCard({ data }: GitHu
111111
<span className="text-sm font-medium capitalize">{runner} runners</span>
112112
<div className="text-right">
113113
<span className="text-sm tabular-nums">{stats.minutes.toLocaleString()} min</span>
114-
<span className="text-xs text-muted-foreground ml-2">
114+
<span className="text-xs text-muted-foreground ml-2 tabular-nums">
115115
(${stats.grossAmount.toFixed(2)})
116116
</span>
117117
</div>
@@ -122,7 +122,7 @@ export const GitHubBillingCard = memo(function GitHubBillingCard({ data }: GitHu
122122
<span className="text-sm font-medium">Artifact storage</span>
123123
<div className="text-right">
124124
<span className="text-sm tabular-nums">{storage.gbHours.toFixed(2)} GB-hrs</span>
125-
<span className="text-xs text-muted-foreground ml-2">
125+
<span className="text-xs text-muted-foreground ml-2 tabular-nums">
126126
(${storage.grossAmount.toFixed(2)})
127127
</span>
128128
</div>

src/components/analytics/LighthouseScoresTable.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function ScoreBadge({ score }: { score: number }) {
1111

1212
return (
1313
<span className={cn(
14-
'inline-flex items-center justify-center w-10 h-6 rounded text-xs font-medium',
14+
'inline-flex items-center justify-center w-10 h-6 rounded text-xs font-medium tabular-nums',
1515
{
1616
'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400': status === 'good',
1717
'bg-amber-500/20 text-amber-600 dark:text-amber-400': status === 'warning',
@@ -81,8 +81,8 @@ export function LighthouseScoresTable({ data }: LighthouseScoresTableProps) {
8181
<td className="text-center py-2">
8282
<ScoreBadge score={page.bestPractices} />
8383
</td>
84-
<td className="text-right py-2 font-mono text-xs">{page.lcp}</td>
85-
<td className="text-right py-2 font-mono text-xs">{page.cls}</td>
84+
<td className="text-right py-2 font-mono text-xs tabular-nums">{page.lcp}</td>
85+
<td className="text-right py-2 font-mono text-xs tabular-nums">{page.cls}</td>
8686
</tr>
8787
))}
8888
</tbody>

src/components/analytics/MetricCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const MetricCard = memo(function MetricCard({ title, value, icon: Icon, s
2828
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
2929
</CardHeader>
3030
<CardContent>
31-
<div className={cn('text-2xl font-bold', status && statusColors[status])}>
31+
<div className={cn('text-2xl font-bold tabular-nums', status && statusColors[status])}>
3232
{value}
3333
</div>
3434
<div className="flex items-center gap-2 mt-1">
@@ -45,7 +45,7 @@ export const MetricCard = memo(function MetricCard({ title, value, icon: Icon, s
4545
) : (
4646
<TrendingDown className="h-3 w-3 mr-1" />
4747
)}
48-
{Math.abs(trend).toFixed(0)}%
48+
<span className="tabular-nums">{Math.abs(trend).toFixed(0)}%</span>
4949
</div>
5050
)}
5151
</div>

src/components/analytics/RumWebVitalsCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function RumWebVitalsCard({ data }: RumWebVitalsCardProps) {
7878
<CardHeader>
7979
<CardTitle>Real User Metrics</CardTitle>
8080
<p className="text-sm text-muted-foreground">
81-
Field data from {data.metrics.LCP?.count || data.metrics.FCP?.count || 0}+ sessions
81+
Field data from <span className="tabular-nums">{data.metrics.LCP?.count || data.metrics.FCP?.count || 0}</span>+ sessions
8282
</p>
8383
</CardHeader>
8484
<CardContent className="space-y-4">
@@ -94,7 +94,7 @@ export function RumWebVitalsCard({ data }: RumWebVitalsCardProps) {
9494
<div key={vital.key} className="space-y-1">
9595
<div className="flex justify-between text-sm">
9696
<span className="font-medium">{vital.key}</span>
97-
<span className={cn('font-mono', {
97+
<span className={cn('font-mono tabular-nums', {
9898
'text-emerald-500': status === 'good',
9999
'text-amber-500': status === 'warning',
100100
'text-red-500': status === 'critical',
@@ -110,7 +110,7 @@ export function RumWebVitalsCard({ data }: RumWebVitalsCardProps) {
110110
className="h-2"
111111
aria-label={`${vital.key}: ${vital.format(value)}`}
112112
/>
113-
<div className="flex justify-between text-xs text-muted-foreground">
113+
<div className="flex justify-between text-xs text-muted-foreground tabular-nums">
114114
<span>Good: ≤{vital.key === 'CLS' ? vital.thresholds.good : `${vital.thresholds.good}ms`}</span>
115115
<span>Poor: &gt;{vital.key === 'CLS' ? vital.thresholds.poor : `${vital.thresholds.poor}ms`}</span>
116116
</div>

src/components/analytics/TrafficQualityCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function TrafficQualityCard({ topPages }: TrafficQualityCardProps) {
102102
<div className="space-y-1">
103103
<div className="flex justify-between text-xs text-muted-foreground">
104104
<span>Traffic Composition</span>
105-
<span>{analysis.humanPercentage.toFixed(1)}% human</span>
105+
<span className="tabular-nums">{analysis.humanPercentage.toFixed(1)}% human</span>
106106
</div>
107107
<div className="h-2 rounded-full bg-muted overflow-hidden flex">
108108
{analysis.humanSessions > 0 && (

src/components/blog/MTTRBuilder.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,12 @@ export function MTTRBuilder({ title = 'What Can You Achieve?' }: MTTRBuilderProp
134134
{/* Hero stats */}
135135
<div className="grid grid-cols-2 gap-4 mb-6">
136136
<div className="text-center p-4 bg-muted/50 rounded-lg border border-border">
137-
<span className="text-3xl font-bold text-foreground">{mttr}</span>
137+
<span className="text-3xl font-bold text-foreground tabular-nums">{mttr}</span>
138138
<span className="text-lg text-muted-foreground ml-1">min</span>
139139
<div className="text-sm text-muted-foreground mt-1">MTTR</div>
140140
</div>
141141
<div className={`text-center p-4 rounded-lg border ${sloBgColor}`}>
142-
<span className={`text-3xl font-bold ${sloColor}`}>
142+
<span className={`text-3xl font-bold tabular-nums ${sloColor}`}>
143143
{achievableSlo.toFixed(2)}%
144144
</span>
145145
<div className="text-sm text-muted-foreground mt-1">achievable SLO</div>
@@ -191,7 +191,7 @@ export function MTTRBuilder({ title = 'What Can You Achieve?' }: MTTRBuilderProp
191191
</div>
192192

193193
{/* Formula Display */}
194-
<div className="mt-4 p-3 bg-muted/50 rounded-lg border border-border font-mono text-xs sm:text-sm space-y-1">
194+
<div className="mt-4 p-3 bg-muted/50 rounded-lg border border-border font-mono text-xs sm:text-sm space-y-1 tabular-nums">
195195
<div className="text-muted-foreground">
196196
<span className="text-foreground">Effective downtime</span> = {incidents} × {mttr} min × {blastRadius}% ={' '}
197197
<span className="font-semibold text-foreground">{effectiveDowntime.toFixed(0)} min</span>

src/components/projects/slo-tool/AchievableTab.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function AchievableTab({ result, period }: AchievableTabProps) {
151151
</CardTitle>
152152
</CardHeader>
153153
<CardContent>
154-
<div className="text-xl font-bold">{formatDuration(result.mttrMinutes)}</div>
154+
<div className="text-xl font-bold tabular-nums">{formatDuration(result.mttrMinutes)}</div>
155155
</CardContent>
156156
</Card>
157157

@@ -162,7 +162,7 @@ export function AchievableTab({ result, period }: AchievableTabProps) {
162162
</CardTitle>
163163
</CardHeader>
164164
<CardContent>
165-
<div className="text-xl font-bold">{formatDuration(result.periodDowntimeMinutes)}</div>
165+
<div className="text-xl font-bold tabular-nums">{formatDuration(result.periodDowntimeMinutes)}</div>
166166
</CardContent>
167167
</Card>
168168

@@ -173,7 +173,7 @@ export function AchievableTab({ result, period }: AchievableTabProps) {
173173
</CardTitle>
174174
</CardHeader>
175175
<CardContent>
176-
<div className="text-xl font-bold">
176+
<div className="text-xl font-bold tabular-nums">
177177
{formatDuration(
178178
period === 'yearly'
179179
? result.periodDowntimeMinutes
@@ -192,7 +192,7 @@ export function AchievableTab({ result, period }: AchievableTabProps) {
192192
</CardTitle>
193193
</CardHeader>
194194
<CardContent>
195-
<div className="text-xl font-bold text-primary">{formatSlo(result.maxAchievableSlo)}</div>
195+
<div className="text-xl font-bold text-primary tabular-nums">{formatSlo(result.maxAchievableSlo)}</div>
196196
</CardContent>
197197
</Card>
198198
</div>

src/components/projects/slo-tool/BurndownTab.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export function BurndownTab({ calculation, config, incidents }: BurndownTabProps
9898
<div className="flex items-start justify-between">
9999
<div>
100100
<p className="text-sm text-muted-foreground">Budget Consumed</p>
101-
<p className="text-2xl font-bold">{formatDuration(consumedMinutes)}</p>
102-
<p className="text-sm text-muted-foreground">
101+
<p className="text-2xl font-bold tabular-nums">{formatDuration(consumedMinutes)}</p>
102+
<p className="text-sm text-muted-foreground tabular-nums">
103103
of {formatDuration(totalBudgetMinutes)} ({consumedPercent.toFixed(1)}%)
104104
</p>
105105
</div>
@@ -121,17 +121,17 @@ export function BurndownTab({ calculation, config, incidents }: BurndownTabProps
121121
<div className="flex items-start justify-between">
122122
<div>
123123
<p className="text-sm text-muted-foreground">Budget Remaining</p>
124-
<p className={cn('text-2xl font-bold', remainingMinutes <= 0 && 'text-destructive')}>
124+
<p className={cn('text-2xl font-bold tabular-nums', remainingMinutes <= 0 && 'text-destructive')}>
125125
{formatDuration(remainingMinutes)}
126126
</p>
127-
<p className="text-sm text-muted-foreground">{daysRemaining} days left in period</p>
127+
<p className="text-sm text-muted-foreground tabular-nums">{daysRemaining} days left in period</p>
128128
</div>
129129
<div className="rounded-full p-2 bg-primary/10">
130130
<Calendar className="h-5 w-5 text-primary" />
131131
</div>
132132
</div>
133133
<div className="mt-4 flex items-center text-sm text-muted-foreground">
134-
<span>
134+
<span className="tabular-nums">
135135
Day {daysElapsed} of {calculation.periodDays}
136136
</span>
137137
</div>
@@ -144,7 +144,7 @@ export function BurndownTab({ calculation, config, incidents }: BurndownTabProps
144144
<div className="flex items-start justify-between">
145145
<div>
146146
<p className="text-sm text-muted-foreground">Current Burn Rate</p>
147-
<p className={cn('text-2xl font-bold', burnRateColor)}>{burnMultiplier.toFixed(1)}x</p>
147+
<p className={cn('text-2xl font-bold tabular-nums', burnRateColor)}>{burnMultiplier.toFixed(1)}x</p>
148148
<p className="text-sm text-muted-foreground">
149149
{burnRateText}
150150
{burnMultiplier > 1 ? ' (faster than sustainable)' : ' burn rate'}
@@ -297,15 +297,15 @@ export function BurndownTab({ calculation, config, incidents }: BurndownTabProps
297297
{simulatedProjection.isOnTrack ? (
298298
<>
299299
At {simulatedMultiplier.toFixed(1)}x, you'll use{' '}
300-
<span className="font-medium text-foreground">
300+
<span className="font-medium text-foreground tabular-nums">
301301
{simulatedProjection.percentConsumedAtEnd.toFixed(0)}%
302302
</span>{' '}
303303
of budget by end of period
304304
</>
305305
) : simulatedProjection.exhaustionDate ? (
306306
<>
307307
At {simulatedMultiplier.toFixed(1)}x, budget exhausted in{' '}
308-
<span className="font-medium text-foreground">
308+
<span className="font-medium text-foreground tabular-nums">
309309
{Math.ceil(simulatedProjection.daysUntilExhaustion!)} days
310310
</span>{' '}
311311
({simulatedProjection.exhaustionDate.toLocaleDateString('en-US', {

src/components/projects/slo-tool/SloConfiguration.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export function SloConfiguration({ config, onChange }: SloConfigurationProps) {
165165
<span className="font-semibold text-foreground">{formatDuration(totalBudget)}</span>
166166
</p>
167167
<p className="text-xs text-muted-foreground mt-1">
168-
{((1 - config.target / 100) * 100).toFixed(3)}% of {config.period} period
168+
<span className="tabular-nums">{((1 - config.target / 100) * 100).toFixed(3)}%</span> of {config.period} period
169169
</p>
170170
</div>
171171
</CardContent>

0 commit comments

Comments
 (0)