Skip to content

Commit f4e004c

Browse files
authored
refactor: redesign slot progress timeline UI (#290)
* fix: correct MEV block detection and improve phase node layout Fixes two issues in the slot progress timeline: 1. MEV Detection: Changed the logic to check for the existence of the blockMev object rather than just builder_pubkey. A block is externally built if there is ANY data in the payload-delivered/MEV table, not just when builder_pubkey is present. This ensures blocks built via MEV relay are correctly identified as "External" instead of "Local". 2. Layout: Improved PhaseNode component styling to fix the "scuffed" third line appearance: - Increased gap between label/time/stats from 0.5 to 1 (gap-1) - Always render time badge with reserved space (min-h-[13px]) instead of conditional rendering to maintain consistent alignment - Increased stats font size from 8px to 9px for better readability - Added text-center class to time and stats for better alignment - All three lines now have consistent reserved space to prevent layout shifts Affects both live slot view and static slot detail pages. * refactor: improve slot progress timeline with distinct colors and remove unreliable MEV stats Changes: 1. Removed "External" vs "Local" stats from Proposing phase - this was unreliable due to MEV data timing issues 2. Added distinct colors to each phase for better visual hierarchy: - Builders: accent (distinctive for MEV bidding) - Relaying: secondary (relay selection) - Proposing: primary (main block proposal action) - Gossiping: secondary (network propagation, slots only) - Attesting: warning (ongoing validation) - Accepted: success (completed, >66% threshold) 3. All phases now have consistent color scheme across both live and static views The monotonous all-primary color scheme made it hard to distinguish phases. The new color palette provides clear visual separation while maintaining semantic meaning. * refactor: use muted secondary color for all timeline phases Replaced the varied color scheme (accent, primary, warning, success) with a uniform secondary color for all phases. This creates a more cohesive, muted appearance that is easier on the eyes and less visually noisy. All phases now use 'secondary' color: - Builders - Relaying - Proposing - Gossiping (slots only) - Attesting - Accepted The uniform color scheme provides a calmer, more professional look while maintaining clear phase distinctions through labels and icons. * revert: use primary color for all timeline phases Secondary was too dark/muted. Going back to a simpler approach with primary color across all phases for a cleaner, more consistent look. * refactor: simplify and clean up slot progress timeline design Completely redesigned the timeline for a cleaner, more professional look: PhaseNode changes: - Removed all animations (pulse, ring effects, scale on hover) - Switched from large circular buttons (size-12) to compact rounded squares (size-8) - Removed complex color mappings and dynamic styles - Simplified to three states with subtle visual differences: - Pending: muted on surface background - Active: primary accent with light background - Completed: muted on surface background - Smaller, cleaner icons (size-5) - Better text hierarchy with proper font sizes (11px/10px/9px) - Monospace font for time values for better alignment - Conditional stats rendering (only show when present) PhaseConnection changes: - Removed dual-layer background/foreground approach - Simplified to single subtle border line (border/50) - Lighter progress fill (primary/20) - Removed isActive color variations Overall improvements: - No more "scuffed" third line - proper conditional rendering - No more uninspired look - clean, minimal, professional - Better visual hierarchy without relying on animations - More compact layout that doesn't dominate the UI - Consistent spacing and alignment throughout * fix: prevent layout shifts in slot progress timeline Fixed height jumping by ensuring all text elements are always rendered: - Added 'leading-tight' to all text spans for consistent line height - Stats line now always renders with opacity-0 when empty instead of conditionally rendering, preventing the component from changing height - Non-breaking space (\u00A0) used as fallback to maintain consistent spacing even when content is hidden This prevents the page from jumping around when phases transition or stats appear/disappear. * feat: add success color to Accepted phase when 66% threshold reached The Accepted phase now shows with a distinct success (green) color when the block has achieved >66% attestation acceptance, making it visually stand out as a completion milestone. Changes: - Accepted phase uses 'success' color when acceptanceTime is defined - PhaseNode component now respects the 'success' color for both active and completed states - Icon background and text use success/10 and success colors - Time badge also shows in success color when phase is successful - Applied to both live and static slot views When the phase is pending (acceptanceTime undefined), it remains muted with primary color like other phases.
1 parent 3142456 commit f4e004c

File tree

4 files changed

+54
-150
lines changed

4 files changed

+54
-150
lines changed

src/components/Ethereum/SlotProgressTimeline/components/PhaseConnection/PhaseConnection.tsx

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,32 @@ import clsx from 'clsx';
33
import type { PhaseConnectionProps } from '../../SlotProgressTimeline.types';
44

55
/**
6-
* PhaseConnection - Progress line between phases in the slot timeline
7-
*
8-
* Displays a connection line that can be horizontal or vertical.
9-
* Animates fill progress in live mode using CSS transitions.
10-
*
11-
* @example
12-
* ```tsx
13-
* // Horizontal connection with 75% progress
14-
* <PhaseConnection
15-
* progress={75}
16-
* orientation="horizontal"
17-
* isActive={true}
18-
* />
19-
*
20-
* // Vertical connection (completed)
21-
* <PhaseConnection
22-
* progress={100}
23-
* orientation="vertical"
24-
* isActive={false}
25-
* />
26-
* ```
6+
* PhaseConnection - Simple line between phases in the slot timeline
277
*/
288
export function PhaseConnection({
299
progress,
3010
orientation,
31-
isActive = false,
11+
isActive: _isActive = false,
3212
className,
3313
}: PhaseConnectionProps): JSX.Element {
3414
const isHorizontal = orientation === 'horizontal';
3515

3616
return (
3717
<div
3818
className={clsx(
39-
'relative',
40-
// Horizontal: full width, fixed height
41-
isHorizontal && 'h-0.5 w-full',
42-
// Vertical: fixed width, full height
43-
!isHorizontal && 'h-full w-0.5',
19+
'relative bg-border/50',
20+
isHorizontal && 'h-px w-full',
21+
!isHorizontal && 'h-full w-px',
4422
className
4523
)}
4624
role="presentation"
4725
aria-hidden="true"
4826
>
49-
{/* Background line */}
50-
<div className={clsx('absolute bg-border', isHorizontal && 'h-full w-full', !isHorizontal && 'h-full w-full')} />
51-
5227
{/* Progress fill */}
5328
<div
5429
className={clsx(
55-
'absolute',
56-
// Active state - use primary color
57-
isActive && 'bg-primary',
58-
// Inactive/completed state - use muted color
59-
!isActive && 'bg-muted',
60-
// Horizontal: fill from left to right
30+
'absolute bg-primary/20',
6131
isHorizontal && 'top-0 left-0 h-full',
62-
// Vertical: fill from top to bottom
6332
!isHorizontal && 'top-0 left-0 w-full'
6433
)}
6534
style={

src/components/Ethereum/SlotProgressTimeline/components/PhaseNode/PhaseNode.tsx

Lines changed: 41 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -3,148 +3,87 @@ import clsx from 'clsx';
33
import type { PhaseNodeProps } from '../../SlotProgressTimeline.types';
44

55
/**
6-
* PhaseNode - Circular icon indicator for a phase in the slot timeline
6+
* PhaseNode - Indicator for a phase in the slot timeline
77
*
88
* Displays a phase with three states:
9-
* - Pending: Gray/muted, 50% opacity
10-
* - Active: Full color with pulsing animation
11-
* - Completed: Full color, no animation
12-
*
13-
* @example
14-
* ```tsx
15-
* import { CubeIcon } from '@heroicons/react/24/outline';
16-
*
17-
* <PhaseNode
18-
* phase={{
19-
* id: 'builders',
20-
* label: 'Builders',
21-
* icon: CubeIcon,
22-
* color: 'primary',
23-
* description: 'MEV builders bidding',
24-
* stats: '43 builders bidded'
25-
* }}
26-
* status="active"
27-
* showStats={true}
28-
* />
29-
* ```
9+
* - Pending: Muted appearance
10+
* - Active: Highlighted
11+
* - Completed: Standard appearance
3012
*/
3113
export function PhaseNode({ phase, status, showStats = true, onClick, className }: PhaseNodeProps): JSX.Element {
3214
const Icon = phase.icon;
33-
34-
// Map phase color to CSS variable
35-
const getCSSVar = (colorName: string): string => {
36-
const colorMap: Record<string, string> = {
37-
primary: 'var(--color-primary)',
38-
secondary: 'var(--color-secondary)',
39-
accent: 'var(--color-accent)',
40-
success: 'var(--color-success)',
41-
warning: 'var(--color-warning)',
42-
danger: 'var(--color-danger)',
43-
};
44-
return colorMap[colorName] || colorMap.primary;
45-
};
46-
47-
const phaseColor = getCSSVar(phase.color);
15+
const isSuccess = phase.color === 'success';
4816

4917
return (
50-
<div className={clsx('flex w-20 flex-col items-center justify-center gap-2', className)}>
51-
{/* Circular icon container */}
18+
<div className={clsx('flex flex-col items-center justify-start gap-2', className)}>
19+
{/* Icon container */}
5220
<button
5321
type="button"
5422
onClick={onClick}
5523
disabled={!onClick}
5624
className={clsx(
57-
'relative flex size-12 shrink-0 items-center justify-center rounded-full border-2 transition-all duration-300',
25+
'relative flex size-8 shrink-0 items-center justify-center rounded-md',
5826
// Pending state
59-
status === 'pending' && [
60-
'border-border bg-surface opacity-50',
61-
onClick && 'cursor-not-allowed',
62-
!onClick && 'cursor-default',
63-
],
27+
status === 'pending' && ['bg-surface text-muted', !onClick && 'cursor-default'],
6428
// Active state
6529
status === 'active' && [
66-
'animate-pulse border-foreground/20 ring-4',
67-
onClick && 'cursor-pointer hover:scale-105',
30+
isSuccess ? 'bg-success/10 text-success' : 'bg-primary/10 text-primary',
31+
onClick && 'cursor-pointer',
6832
!onClick && 'cursor-default',
6933
],
7034
// Completed state
7135
status === 'completed' && [
72-
'border-foreground/10',
73-
onClick && 'cursor-pointer hover:scale-105',
36+
isSuccess ? 'bg-success/10 text-success' : 'bg-surface text-muted',
37+
onClick && 'cursor-pointer',
7438
!onClick && 'cursor-default',
7539
]
7640
)}
77-
style={
78-
status !== 'pending'
79-
? {
80-
backgroundColor: phaseColor,
81-
boxShadow: status === 'active' ? `0 0 0 4px ${phaseColor}33` : undefined,
82-
}
83-
: undefined
84-
}
8541
title={phase.description}
8642
aria-label={`${phase.label}: ${status}`}
8743
>
88-
<Icon
89-
className={clsx(
90-
'size-6 transition-colors duration-300',
91-
status === 'pending' && 'text-muted',
92-
status === 'active' && 'text-background',
93-
status === 'completed' && 'text-background'
94-
)}
95-
/>
44+
<Icon className="size-5" />
9645
</button>
9746

98-
{/* Phase label, time, and stats */}
47+
{/* Phase label and time */}
9948
<div className="flex flex-col items-center justify-start gap-0.5">
49+
{/* Label - always rendered with fixed line height */}
10050
<span
10151
className={clsx(
102-
'text-center font-medium transition-colors duration-300',
103-
status === 'pending' && 'text-muted opacity-50',
52+
'text-center text-[11px] leading-tight font-medium',
53+
status === 'pending' && 'text-muted/50',
10454
status === 'active' && 'text-foreground',
105-
status === 'completed' && 'text-foreground'
55+
status === 'completed' && 'text-muted'
10656
)}
107-
style={{ fontSize: '10px' }}
10857
>
10958
{phase.label}
11059
</span>
11160

112-
{/* Time badge - shows when phase occurred */}
113-
{phase.id !== 'attesting' && (
114-
<span
115-
className={clsx(
116-
'transition-colors duration-300',
117-
status === 'pending' && 'text-muted opacity-50',
118-
status === 'active' && 'text-foreground',
119-
status === 'completed' && 'text-muted'
120-
)}
121-
style={{
122-
fontSize: '11px',
123-
...(status === 'active' ? { color: phaseColor } : {}),
124-
}}
125-
>
126-
{phase.timestamp !== undefined
127-
? phase.timestamp < 0
128-
? `${Math.abs(phase.timestamp / 1000).toFixed(1)}s early`
129-
: `${(phase.timestamp / 1000).toFixed(1)}s`
130-
: '—'}
131-
</span>
132-
)}
61+
{/* Time badge - always rendered with fixed line height */}
62+
<span
63+
className={clsx(
64+
'text-center font-mono text-[10px] leading-tight',
65+
status === 'pending' && 'text-muted/50',
66+
status === 'active' && (isSuccess ? 'text-success' : 'text-primary'),
67+
status === 'completed' && (isSuccess ? 'text-success/70' : 'text-muted/70')
68+
)}
69+
>
70+
{phase.id !== 'attesting' && phase.timestamp !== undefined
71+
? phase.timestamp < 0
72+
? `${Math.abs(phase.timestamp / 1000).toFixed(1)}s`
73+
: `${(phase.timestamp / 1000).toFixed(1)}s`
74+
: '\u00A0'}
75+
</span>
13376

134-
{/* Stats - always reserve space to prevent height changes */}
77+
{/* Stats - always rendered with fixed line height, hidden with opacity when empty */}
13578
<span
13679
className={clsx(
137-
'transition-colors duration-300',
138-
status === 'pending' && 'text-muted opacity-50',
139-
status === 'active' && 'text-foreground',
140-
status === 'completed' && 'text-muted',
141-
// Reserve space even when empty
142-
'min-h-[0.5rem]'
80+
'text-center text-[9px] leading-tight',
81+
status === 'pending' && 'text-muted/50',
82+
status === 'active' && 'text-muted',
83+
status === 'completed' && 'text-muted/70',
84+
// Hide with opacity when no stats, but maintain layout space
85+
!showStats || !phase.stats ? 'opacity-0' : 'opacity-100'
14386
)}
144-
style={{
145-
fontSize: '8px',
146-
...(status === 'active' ? { color: phaseColor } : {}),
147-
}}
14887
>
14988
{showStats && phase.stats ? phase.stats : '\u00A0'}
15089
</span>

src/pages/ethereum/live/hooks/useSlotProgressData/useSlotProgressData.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import type { SlotProgressRawData, UseSlotProgressDataReturn } from './useSlotPr
1818
*/
1919
export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgressDataReturn {
2020
const phases = useMemo<PhaseData[]>(() => {
21-
const { blockHead, blockProposer, blockMev, blockPropagation, attestations, committees, mevBidding, relayBids } =
22-
rawData;
21+
const { blockHead, blockProposer, blockPropagation, attestations, committees, mevBidding, relayBids } = rawData;
2322

2423
// Calculate total expected validators from committee data
2524
const totalExpectedValidators = committees.reduce((sum, committee) => {
@@ -94,7 +93,7 @@ export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgre
9493
color: 'primary',
9594
timestamp: firstBlockSeenTime,
9695
description: isMissed ? 'Block was never proposed' : 'Block proposed to network',
97-
stats: blockMev?.builder_pubkey ? 'External' : firstBlockSeenTime !== undefined ? 'Local' : undefined,
96+
stats: undefined,
9897
},
9998
// Phase 4: Attesting - starts 50ms after Proposing
10099
{
@@ -115,7 +114,7 @@ export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgre
115114
id: 'accepted',
116115
label: 'Accepted',
117116
icon: LockClosedIcon,
118-
color: 'primary',
117+
color: acceptanceTime !== undefined ? 'success' : 'primary',
119118
timestamp: acceptanceTime,
120119
description: 'Block achieved acceptance',
121120
stats: acceptanceTime !== undefined ? `>66%` : undefined,

src/pages/ethereum/slots/hooks/useSlotProgressData/useSlotProgressData.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgre
9696
id: 'builders',
9797
label: 'Builders',
9898
icon: CubeIcon,
99-
color: 'accent',
99+
color: 'primary',
100100
timestamp: earliestBidTime,
101101
duration: buildersEndTime - earliestBidTime,
102102
description: 'Block builders bidding phase',
@@ -123,17 +123,14 @@ export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgre
123123

124124
// Phase 3: Proposing
125125
if (firstBlockSeenTime !== undefined) {
126-
const builderPubkey = blockMev?.builder_pubkey;
127-
const builderName = builderPubkey ? 'external builder' : 'local build';
128-
129126
phasesList.push({
130127
id: 'proposing',
131128
label: 'Proposing',
132129
icon: UserIcon,
133-
color: 'secondary',
130+
color: 'primary',
134131
timestamp: firstBlockSeenTime,
135132
description: 'Block proposed to network',
136-
stats: `Built via ${builderName}`,
133+
stats: undefined,
137134
isCompleted: true,
138135
});
139136
}
@@ -147,7 +144,7 @@ export function useSlotProgressData(rawData: SlotProgressRawData): UseSlotProgre
147144
id: 'gossiping',
148145
label: 'Gossiping',
149146
icon: ChatBubbleBottomCenterTextIcon,
150-
color: 'success',
147+
color: 'primary',
151148
timestamp: firstBlockSeenTime,
152149
duration: gossipDuration,
153150
description: 'Block propagating to network',

0 commit comments

Comments
 (0)