diff --git a/configs/app/features/growthBook.ts b/configs/app/features/growthBook.ts
index af672c5ac9..651e628ca6 100644
--- a/configs/app/features/growthBook.ts
+++ b/configs/app/features/growthBook.ts
@@ -7,7 +7,7 @@ const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
const title = 'GrowthBook feature flagging and A/B testing';
const config: Feature<{ clientKey: string }> = (() => {
- if (clientKey) {
+ if (clientKey && clientKey !== 'xxx') {
return Object.freeze({
title,
isEnabled: true,
diff --git a/configs/app/ui.ts b/configs/app/ui.ts
index 674580d944..5873aa85de 100644
--- a/configs/app/ui.ts
+++ b/configs/app/ui.ts
@@ -49,7 +49,7 @@ const highlightedRoutes = (() => {
const defaultColorTheme = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
- return COLOR_THEMES.find((theme) => theme.id === envValue);
+ return COLOR_THEMES.find((theme) => theme.id === envValue) ?? COLOR_THEMES.find((theme) => theme.id === 'dark');
})();
const UI = Object.freeze({
diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx
index 7f6ac5e01b..0d7238664a 100644
--- a/lib/hooks/useNavItems.tsx
+++ b/lib/hooks/useNavItems.tsx
@@ -300,10 +300,6 @@ export default function useNavItems(): ReturnType {
url: 'https://hub.opengradient.ai',
icon: 'apps',
},
- {
- text: 'Testnet V1 Explorer',
- url: 'https://testnetv1.opengradient.ai',
- },
].filter(Boolean);
const accountNavItems: ReturnType['accountNavItems'] = [
diff --git a/nextjs/csp/policies/app.ts b/nextjs/csp/policies/app.ts
index e445281572..ae0c3ecbd0 100644
--- a/nextjs/csp/policies/app.ts
+++ b/nextjs/csp/policies/app.ts
@@ -115,6 +115,7 @@ export function app(): CspDev.DirectiveDescriptor {
],
'font-src': [
+ KEY_WORDS.SELF,
KEY_WORDS.DATA,
...MAIN_DOMAINS,
...(externalFontsDomains || []),
diff --git a/nextjs/headers.js b/nextjs/headers.js
index c38f294879..1d35384404 100644
--- a/nextjs/headers.js
+++ b/nextjs/headers.js
@@ -22,7 +22,7 @@ async function headers() {
},
{
key: 'Cross-Origin-Opener-Policy',
- value: 'same-origin',
+ value: 'same-origin-allow-popups',
},
{
key: 'Referrer-Policy',
diff --git a/pages/_document.tsx b/pages/_document.tsx
index 61ee1a3284..c3ca6875e2 100644
--- a/pages/_document.tsx
+++ b/pages/_document.tsx
@@ -34,17 +34,19 @@ class MyDocument extends Document {
{ /* FONTS */ }
+
+
{ /* eslint-disable-next-line @next/next/no-sync-scripts */ }
diff --git a/toolkit/chakra/input-group.tsx b/toolkit/chakra/input-group.tsx
index 7f493ade9e..e835fd3d64 100644
--- a/toolkit/chakra/input-group.tsx
+++ b/toolkit/chakra/input-group.tsx
@@ -70,8 +70,8 @@ export const InputGroup = React.forwardRef(
return React.cloneElement(child, {
...(startElement && { ps: startOffset ?? (inlinePaddings?.start ? `${ inlinePaddings.start }px` : undefined) }),
...(endElement && { pe: endOffset ?? (inlinePaddings?.end ? `${ inlinePaddings.end }px` : undefined) }),
- // hide input value and placeholder for the first render
- value: inlinePaddings ? child.props.value : undefined,
+ // keep value controlled while hiding text until paddings are measured
+ value: inlinePaddings ? child.props.value : '',
placeholder: inlinePaddings ? child.props.placeholder : undefined,
});
}) }
diff --git a/toolkit/chakra/table.tsx b/toolkit/chakra/table.tsx
index c970792347..a5f95581bd 100644
--- a/toolkit/chakra/table.tsx
+++ b/toolkit/chakra/table.tsx
@@ -95,12 +95,21 @@ export const TableHeaderSticky = (props: TableHeaderProps) => {
};
}, [ handleScroll ]);
+ const stickyTop = (() => {
+ if (!isStuck) {
+ return undefined;
+ }
+
+ return top ? `${ top }px` : 0;
+ })();
+
return (
{
flexWrap="nowrap"
alignItems="center"
whiteSpace="nowrap"
- bgColor={{ _light: 'white', _dark: 'black' }}
+ bgColor={{ _light: 'rgba(244, 252, 254, 0.94)', _dark: 'rgba(10, 15, 25, 0.94)' }}
+ backdropFilter="blur(16px)"
// initially our cut is 0 and we don't want to show the list
// but we want to keep all items in the tabs row so it won't collapse
// that's why we only change opacity but not the position itself
diff --git a/toolkit/theme/foundations/animations.ts b/toolkit/theme/foundations/animations.ts
index 1764e175af..eac59ef696 100644
--- a/toolkit/theme/foundations/animations.ts
+++ b/toolkit/theme/foundations/animations.ts
@@ -17,4 +17,8 @@ export const keyframes = {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.7 },
},
+ signalScan: {
+ from: { backgroundPosition: '0 0, 0 0, 0 0' },
+ to: { backgroundPosition: '120px 0, 0 120px, 160px 160px' },
+ },
};
diff --git a/toolkit/theme/foundations/colors.ts b/toolkit/theme/foundations/colors.ts
index a957d1c37c..86354b5716 100644
--- a/toolkit/theme/foundations/colors.ts
+++ b/toolkit/theme/foundations/colors.ts
@@ -11,17 +11,32 @@ const colors = {
'800': { value: '#22543D' },
'900': { value: '#1C4532' },
},
+ // OpenGradient brand cyan reused as "blue" — Blockscout uses blue.* for many UI accents
blue: {
- '50': { value: '#EBF8FF' },
- '100': { value: '#BEE3F8' },
- '200': { value: '#90CDF4' },
- '300': { value: '#63B3ED' },
- '400': { value: '#4299E1' },
- '500': { value: '#3182CE' },
- '600': { value: '#2B6CB0' },
- '700': { value: '#2C5282' },
- '800': { value: '#2A4365' },
- '900': { value: '#1A365D' },
+ '50': { value: '#f4fcfe' },
+ '100': { value: '#e9f8fc' },
+ '200': { value: '#bdebf7' },
+ '300': { value: '#a7e4f4' },
+ '400': { value: '#50c9e9' },
+ '500': { value: '#24bce3' },
+ '600': { value: '#1d96b6' },
+ '700': { value: '#167188' },
+ '800': { value: '#0e4b5b' },
+ '900': { value: '#041317' },
+ },
+ // OpenGradient brand secondary — nautical blue-gray (registered as a new palette)
+ navy: {
+ '50': { value: '#dde2ec' },
+ '100': { value: '#bfc8dc' },
+ '200': { value: '#9ba9c4' },
+ '300': { value: '#7889ad' },
+ '400': { value: '#546a95' },
+ '500': { value: '#314a7d' },
+ '600': { value: '#273b64' },
+ '700': { value: '#1d2c4b' },
+ '800': { value: '#141e32' },
+ '900': { value: '#0f1626' },
+ '950': { value: '#0a0f19' },
},
red: {
'50': { value: '#FFF5F5' },
@@ -83,17 +98,18 @@ const colors = {
'800': { value: '#234E52' },
'900': { value: '#1D4044' },
},
+ // OpenGradient brand cyan (formerly Chakra "cyan")
cyan: {
- '50': { value: '#EDFDFD' },
- '100': { value: '#C4F1F9' },
- '200': { value: '#9DECF9' },
- '300': { value: '#76E4F7' },
- '400': { value: '#0BC5EA' },
- '500': { value: '#00B5D8' },
- '600': { value: '#00A3C4' },
- '700': { value: '#0987A0' },
- '800': { value: '#086F83' },
- '900': { value: '#065666' },
+ '50': { value: '#f4fcfe' },
+ '100': { value: '#e9f8fc' },
+ '200': { value: '#bdebf7' },
+ '300': { value: '#a7e4f4' },
+ '400': { value: '#50c9e9' },
+ '500': { value: '#24bce3' },
+ '600': { value: '#1d96b6' },
+ '700': { value: '#167188' },
+ '800': { value: '#0e4b5b' },
+ '900': { value: '#041317' },
},
purple: {
'50': { value: '#FAF5FF' },
diff --git a/toolkit/theme/foundations/semanticTokens.ts b/toolkit/theme/foundations/semanticTokens.ts
index 33cf8a5cb0..8e697728ee 100644
--- a/toolkit/theme/foundations/semanticTokens.ts
+++ b/toolkit/theme/foundations/semanticTokens.ts
@@ -162,8 +162,8 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
popover: {
DEFAULT: {
- bg: { value: { _light: '{colors.white}', _dark: '{colors.gray.900}' } },
- shadow: { value: { _light: '{colors.blackAlpha.200}', _dark: '{colors.whiteAlpha.300}' } },
+ bg: { value: { _light: '#fcfdfe', _dark: '#0f1626' } },
+ shadow: { value: { _light: 'rgba(36, 188, 227, 0.25)', _dark: 'rgba(36, 188, 227, 0.35)' } },
},
},
progressCircle: {
@@ -180,31 +180,31 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
tabs: {
solid: {
fg: {
- DEFAULT: { value: { _light: '{colors.blue.700}', _dark: '{colors.blue.100}' } },
- selected: { value: { _light: '{colors.blue.700}', _dark: '{colors.gray.50}' } },
+ DEFAULT: { value: { _light: '#314a7d', _dark: 'rgba(189, 235, 247, 0.66)' } },
+ selected: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
bg: {
- selected: { value: { _light: '{colors.blue.50}', _dark: '{colors.whiteAlpha.100}' } },
+ selected: { value: { _light: 'rgba(36, 188, 227, 0.12)', _dark: 'rgba(36, 188, 227, 0.12)' } },
},
},
secondary: {
fg: {
- DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
+ DEFAULT: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
bg: {
- selected: { value: { _light: '{colors.blue.50}', _dark: '{colors.whiteAlpha.100}' } },
+ selected: { value: { _light: 'rgba(36, 188, 227, 0.12)', _dark: 'rgba(36, 188, 227, 0.12)' } },
},
border: {
- DEFAULT: { value: { _light: '{colors.gray.300}', _dark: '{colors.gray.600}' } },
+ DEFAULT: { value: { _light: 'rgba(36, 188, 227, 0.22)', _dark: 'rgba(189, 235, 247, 0.16)' } },
},
},
segmented: {
fg: {
- DEFAULT: { value: { _light: '{colors.blue.600}', _dark: '{colors.blue.300}' } },
- selected: { value: { _light: '{colors.blue.700}', _dark: '{colors.gray.50}' } },
+ DEFAULT: { value: { _light: '#1d96b6', _dark: '#50c9e9' } },
+ selected: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
border: {
- DEFAULT: { value: { _light: '{colors.blue.50}', _dark: '{colors.gray.800}' } },
+ DEFAULT: { value: { _light: 'rgba(36, 188, 227, 0.18)', _dark: 'rgba(36, 188, 227, 0.18)' } },
},
},
},
@@ -244,23 +244,23 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
input: {
fg: {
- DEFAULT: { value: { _light: '{colors.gray.800}', _dark: '{colors.gray.50}' } },
+ DEFAULT: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
error: { value: '{colors.text.error}' },
},
bg: {
- DEFAULT: { value: { _light: '{colors.white}', _dark: '{colors.black}' } },
- readOnly: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.800}' } },
+ DEFAULT: { value: { _light: '#ffffff', _dark: 'rgba(15, 22, 38, 0.6)' } },
+ readOnly: { value: { _light: '#e9f8fc', _dark: 'rgba(15, 22, 38, 0.4)' } },
},
border: {
- DEFAULT: { value: { _light: '{colors.gray.100}', _dark: '{colors.gray.700}' } },
- hover: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.500}' } },
- focus: { value: '{colors.blue.400}' },
- filled: { value: { _light: '{colors.gray.300}', _dark: '{colors.gray.600}' } },
- readOnly: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.800}' } },
+ DEFAULT: { value: { _light: 'rgba(36, 188, 227, 0.18)', _dark: 'rgba(36, 188, 227, 0.18)' } },
+ hover: { value: { _light: 'rgba(36, 188, 227, 0.45)', _dark: 'rgba(36, 188, 227, 0.45)' } },
+ focus: { value: '#24bce3' },
+ filled: { value: { _light: 'rgba(36, 188, 227, 0.25)', _dark: 'rgba(36, 188, 227, 0.22)' } },
+ readOnly: { value: { _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.12)' } },
error: { value: '{colors.red.500}' },
},
placeholder: {
- DEFAULT: { value: '{colors.gray.500}' },
+ DEFAULT: { value: { _light: 'rgba(14, 75, 91, 0.4)', _dark: 'rgba(189, 235, 247, 0.35)' } },
error: { value: '{colors.red.500}' },
},
},
@@ -273,26 +273,26 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
dialog: {
bg: {
- DEFAULT: { value: { _light: '{colors.white}', _dark: '{colors.gray.900}' } },
+ DEFAULT: { value: { _light: '#fcfdfe', _dark: '#0f1626' } },
},
fg: {
- DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
+ DEFAULT: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
},
drawer: {
bg: {
- DEFAULT: { value: { _light: '{colors.white}', _dark: '{colors.gray.900}' } },
+ DEFAULT: { value: { _light: '#fcfdfe', _dark: '#0f1626' } },
},
},
select: {
trigger: {
outline: {
- fg: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
+ fg: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
},
item: {
bg: {
- highlighted: { value: { _light: '{colors.blue.50}', _dark: '{colors.whiteAlpha.100}' } },
+ highlighted: { value: { _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.10)' } },
},
},
indicator: {
@@ -310,7 +310,7 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
menu: {
item: {
bg: {
- highlighted: { value: { _light: '{colors.blue.50}', _dark: '{colors.whiteAlpha.100}' } },
+ highlighted: { value: { _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.12)' } },
},
},
},
@@ -333,12 +333,12 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
fg: { value: { _light: '{colors.red.500}', _dark: '{colors.red.200}' } },
},
purple: {
- bg: { value: { _light: '{colors.purple.50}', _dark: '{colors.purple.800}' } },
- fg: { value: { _light: '{colors.purple.500}', _dark: '{colors.purple.100}' } },
+ bg: { value: { _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.15)' } },
+ fg: { value: { _light: '#1d96b6', _dark: '#50c9e9' } },
},
purple_alt: {
- bg: { value: { _light: '{colors.purple.100}', _dark: '{colors.purple.800}' } },
- fg: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
+ bg: { value: { _light: 'rgba(36, 188, 227, 0.14)', _dark: 'rgba(36, 188, 227, 0.18)' } },
+ fg: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
orange: {
bg: { value: { _light: '{colors.orange.50}', _dark: '{colors.orange.800}' } },
@@ -389,8 +389,8 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
table: {
header: {
- bg: { value: { _light: '{colors.blackAlpha.100}', _dark: '{colors.whiteAlpha.200}' } },
- fg: { value: { _light: '{colors.blackAlpha.700}', _dark: '{colors.whiteAlpha.700}' } },
+ bg: { value: { _light: 'rgba(233, 248, 252, 0.78)', _dark: 'rgba(36, 188, 227, 0.08)' } },
+ fg: { value: { _light: '#314a7d', _dark: 'rgba(189, 235, 247, 0.72)' } },
},
},
checkbox: {
@@ -422,15 +422,15 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
highlighted: { value: '{colors.yellow.400}' },
},
heading: {
- DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
+ DEFAULT: { value: { _light: '#0e4b5b', _dark: '#bdebf7' } },
},
text: {
- primary: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
- secondary: { value: { _light: '{colors.gray.500}', _dark: '{colors.gray.400}' } },
+ primary: { value: { _light: '#0e4b5b', _dark: 'rgba(189, 235, 247, 0.92)' } },
+ secondary: { value: { _light: '#314a7d', _dark: 'rgba(189, 235, 247, 0.55)' } },
error: { value: '{colors.red.500}' },
},
border: {
- divider: { value: { _light: '{colors.blackAlpha.100}', _dark: '{colors.whiteAlpha.100}' } },
+ divider: { value: { _light: 'rgba(36, 188, 227, 0.14)', _dark: 'rgba(189, 235, 247, 0.10)' } },
error: { value: '{colors.red.500}' },
},
icon: {
@@ -446,7 +446,7 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
global: {
body: {
- bg: { value: { _light: '{colors.white}', _dark: '{colors.black}' } },
+ bg: { value: { _light: '#f4fcfe', _dark: '#0a0f19' } },
fg: { value: '{colors.text.primary}' },
},
mark: {
diff --git a/toolkit/theme/foundations/typography.ts b/toolkit/theme/foundations/typography.ts
index 8d859e7540..acc1203ee4 100644
--- a/toolkit/theme/foundations/typography.ts
+++ b/toolkit/theme/foundations/typography.ts
@@ -4,12 +4,13 @@ import type { ExcludeUndefined } from 'types/utils';
import config from 'configs/app';
-export const BODY_TYPEFACE = config.UI.fonts.body?.name ?? 'Inter';
-export const HEADING_TYPEFACE = config.UI.fonts.heading?.name ?? 'Poppins';
+export const BODY_TYPEFACE = config.UI.fonts.body?.name ?? 'Geist';
+export const HEADING_TYPEFACE = config.UI.fonts.heading?.name ?? 'Geist';
export const fonts: ExcludeUndefined['fonts'] = {
- heading: { value: `${ HEADING_TYPEFACE }, sans-serif` },
- body: { value: `${ BODY_TYPEFACE }, sans-serif` },
+ heading: { value: `${ HEADING_TYPEFACE }, system-ui, -apple-system, sans-serif` },
+ body: { value: `${ BODY_TYPEFACE }, system-ui, -apple-system, sans-serif` },
+ mono: { value: '"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace' },
};
export const textStyles: ThemingConfig['textStyles'] = {
@@ -19,7 +20,7 @@ export const textStyles: ThemingConfig['textStyles'] = {
fontSize: '32px',
lineHeight: '40px',
fontWeight: '500',
- letterSpacing: '-0.5px',
+ letterSpacing: '0',
fontFamily: 'heading',
},
},
diff --git a/toolkit/theme/globalCss.ts b/toolkit/theme/globalCss.ts
index 5dec641b57..b3322c8f4d 100644
--- a/toolkit/theme/globalCss.ts
+++ b/toolkit/theme/globalCss.ts
@@ -6,7 +6,7 @@ import scrollbar from './globals/scrollbar';
const webkitAutofillOverrides = {
WebkitTextFillColor: 'var(--chakra-colors-input-fg)',
- '-webkit-box-shadow': '0 0 0px 1000px var(--chakra-colors-input-bg) inset',
+ WebkitBoxShadow: '0 0 0px 1000px var(--chakra-colors-input-bg) inset',
transition: 'background-color 5000s ease-in-out 0s',
};
@@ -20,10 +20,15 @@ const globalCss: SystemConfig['globalCss'] = {
body: {
bg: 'global.body.bg',
color: 'global.body.fg',
+ fontFamily: 'body',
WebkitTapHighlightColor: 'transparent',
fontVariantLigatures: 'no-contextual',
focusRingStyle: 'hidden',
},
+ '::selection': {
+ bg: 'blue.200',
+ color: 'blue.900',
+ },
mark: {
bg: 'global.mark.bg',
color: 'inherit',
diff --git a/toolkit/theme/recipes/table.recipe.ts b/toolkit/theme/recipes/table.recipe.ts
index 89511ce483..1da6c91ad3 100644
--- a/toolkit/theme/recipes/table.recipe.ts
+++ b/toolkit/theme/recipes/table.recipe.ts
@@ -12,16 +12,22 @@ export const recipe = defineSlotRecipe({
textAlign: 'start',
verticalAlign: 'top',
overflow: 'unset',
+ bg: 'transparent',
},
cell: {
textAlign: 'start',
alignItems: 'center',
verticalAlign: 'top',
fontWeight: 'medium',
+ color: 'text.primary',
},
columnHeader: {
- fontWeight: 'medium',
+ fontFamily: 'mono',
+ fontSize: 'xs',
+ fontWeight: '700',
+ letterSpacing: '0',
textAlign: 'start',
+ textTransform: 'uppercase',
},
},
@@ -43,7 +49,12 @@ export const recipe = defineSlotRecipe({
borderColor: 'border.divider',
},
row: {
- bg: 'bg',
+ bg: 'transparent',
+ transitionProperty: 'background-color,color',
+ transitionDuration: 'fast',
+ _hover: {
+ bg: { _light: 'rgba(36, 188, 227, 0.045)', _dark: 'rgba(36, 188, 227, 0.055)' },
+ },
},
},
},
diff --git a/ui/blocks/BlocksContent.tsx b/ui/blocks/BlocksContent.tsx
index d71fffd66d..8f684b4e5a 100644
--- a/ui/blocks/BlocksContent.tsx
+++ b/ui/blocks/BlocksContent.tsx
@@ -17,12 +17,13 @@ import BlocksTable from 'ui/blocks/BlocksTable';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import IconSvg from 'ui/shared/IconSvg';
+import ExplorerPageSurface from 'ui/shared/Page/ExplorerPageSurface';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
const OVERLOAD_COUNT = 75;
-const TABS_HEIGHT = 88;
+const TABS_HEIGHT = 68;
interface Props {
type?: BlockType;
@@ -114,7 +115,7 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
) : null;
const actionBar = isMobile ? (
-
+
Block countdown
@@ -130,7 +131,11 @@ const BlocksContent = ({ type, query, enableSocket = true, top }: Props) => {
emptyText="There are no blocks."
actionBar={ actionBar }
>
- { content }
+ { content && (
+
+ { content }
+
+ ) }
);
};
diff --git a/ui/blocks/BlocksTabSlot.tsx b/ui/blocks/BlocksTabSlot.tsx
index 55d9c75fec..1d856e6426 100644
--- a/ui/blocks/BlocksTabSlot.tsx
+++ b/ui/blocks/BlocksTabSlot.tsx
@@ -10,6 +10,7 @@ import { nbsp } from 'lib/html-entities';
import { HOMEPAGE_STATS } from 'stubs/stats';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import IconSvg from 'ui/shared/IconSvg';
import Pagination from 'ui/shared/pagination/Pagination';
@@ -28,10 +29,10 @@ const BlocksTabSlot = ({ pagination }: Props) => {
{ statsQuery.data?.network_utilization_percentage !== undefined && (
-
+
Network utilization (last 50 blocks):{ nbsp }
-
+
{ statsQuery.data.network_utilization_percentage.toFixed(2) }%
diff --git a/ui/gasTracker/GasTrackerChart.tsx b/ui/gasTracker/GasTrackerChart.tsx
index f3f4d6357f..990d5914d3 100644
--- a/ui/gasTracker/GasTrackerChart.tsx
+++ b/ui/gasTracker/GasTrackerChart.tsx
@@ -1,67 +1,124 @@
import { Box, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
+import { Resolution } from '@blockscout/stats-types';
+import type { Block, BlocksResponse } from 'types/api/block';
+import type { TimeChartItem } from 'ui/shared/chart/types';
+
import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
-import { STATS_CHARTS } from 'stubs/stats';
import { Link } from 'toolkit/chakra/link';
-import ContentLoader from 'ui/shared/ContentLoader';
-import DataFetchAlert from 'ui/shared/DataFetchAlert';
-import ChartWidgetContainer from 'ui/stats/ChartWidgetContainer';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
+import ChartWidget from 'ui/shared/chart/ChartWidget';
+import useChartQuery from 'ui/shared/chart/useChartQuery';
const GAS_PRICE_CHART_ID = 'averageGasPrice';
+const DEFAULT_GAS_PRICE_CHART = {
+ title: 'Average gas price',
+ description: 'Average gas price for the period',
+ units: 'Gwei',
+};
+const RECENT_BLOCK_GAS_CHART = {
+ title: 'Recent block gas price',
+ description: 'Base fee per gas from latest blocks',
+ units: 'Gwei',
+};
+const RECENT_BLOCK_USAGE_CHART = {
+ title: 'Recent block utilization',
+ description: 'Gas used across latest blocks',
+ units: '%',
+};
+const BLOCKS_PLACEHOLDER: BlocksResponse = {
+ items: [],
+ next_page_params: null,
+};
const GasTrackerChart = () => {
- const [ isChartLoadingError, setChartLoadingError ] = React.useState(false);
- const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
+ const { items: statsItems, lineQuery } = useChartQuery(GAS_PRICE_CHART_ID, Resolution.DAY, 'oneMonth');
+ const shouldUseRecentBlocks = lineQuery.isError || (!lineQuery.isPlaceholderData && (!statsItems || statsItems.length < 3));
+
+ const blocksQuery = useApiQuery('blocks', {
queryOptions: {
- placeholderData: STATS_CHARTS,
+ enabled: shouldUseRecentBlocks,
+ placeholderData: BLOCKS_PLACEHOLDER,
},
});
- const handleLoadingError = React.useCallback(() => {
- setChartLoadingError(true);
- }, []);
-
- const content = (() => {
- if (isPlaceholderData) {
- return ;
- }
-
- if (isChartLoadingError || isError) {
- return ;
- }
-
- const chart = data?.sections.map((section) => section.charts.find((chart) => chart.id === GAS_PRICE_CHART_ID)).filter(Boolean)?.[0];
-
- if (!chart) {
- return ;
- }
-
- return (
-
- );
- })();
+ const recentBlockFallback = React.useMemo(() => buildRecentBlockFallback(blocksQuery.data?.items), [ blocksQuery.data?.items ]);
+ const chart = shouldUseRecentBlocks ? recentBlockFallback.chart : DEFAULT_GAS_PRICE_CHART;
+ const chartItems = shouldUseRecentBlocks ? recentBlockFallback.items : statsItems;
+ const isLoading = lineQuery.isPlaceholderData || (shouldUseRecentBlocks && blocksQuery.isPlaceholderData);
+ const isError = shouldUseRecentBlocks && blocksQuery.isError;
return (
- Gas price history
+ Gas activity
Charts & stats
- { content }
+
);
};
export default React.memo(GasTrackerChart);
+
+function buildRecentBlockFallback(blocks?: Array): { chart: typeof RECENT_BLOCK_GAS_CHART; items?: Array } {
+ const priceItems = blocksToChartItems(blocks, (block) => Number(block.base_fee_per_gas ?? 0) / 1_000_000_000);
+ const hasGasPriceMovement = Boolean(priceItems?.some((item) => item.value > 0));
+
+ if (hasGasPriceMovement) {
+ return {
+ chart: RECENT_BLOCK_GAS_CHART,
+ items: priceItems,
+ };
+ }
+
+ return {
+ chart: RECENT_BLOCK_USAGE_CHART,
+ items: blocksToChartItems(blocks, getBlockUtilization),
+ };
+}
+
+function blocksToChartItems(blocks: Array | undefined, getValue: (block: Block) => number): Array | undefined {
+ const items = blocks
+ ?.filter((block) => block.type === 'block')
+ .slice()
+ .reverse()
+ .map((block) => {
+ const value = getValue(block);
+
+ return {
+ date: new Date(block.timestamp),
+ date_to: new Date(block.timestamp),
+ value: Number.isFinite(value) ? value : 0,
+ };
+ });
+
+ return items && items.length > 2 ? items : undefined;
+}
+
+function getBlockUtilization(block: Block): number {
+ if (typeof block.gas_used_percentage === 'number') {
+ return block.gas_used_percentage;
+ }
+
+ const gasUsed = Number(block.gas_used ?? 0);
+ const gasLimit = Number(block.gas_limit);
+
+ if (!Number.isFinite(gasUsed) || !Number.isFinite(gasLimit) || gasLimit === 0) {
+ return 0;
+ }
+
+ return (gasUsed / gasLimit) * 100;
+}
diff --git a/ui/gasTracker/GasTrackerNetworkUtilization.tsx b/ui/gasTracker/GasTrackerNetworkUtilization.tsx
index 9a13af35d3..6ef84f5c41 100644
--- a/ui/gasTracker/GasTrackerNetworkUtilization.tsx
+++ b/ui/gasTracker/GasTrackerNetworkUtilization.tsx
@@ -1,8 +1,8 @@
import { chakra } from '@chakra-ui/react';
import React from 'react';
-import { mdash } from 'lib/html-entities';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
interface Props {
percentage: number;
@@ -10,29 +10,10 @@ interface Props {
}
const GasTrackerNetworkUtilization = ({ percentage, isLoading }: Props) => {
- const load = (() => {
- if (percentage > 80) {
- return 'high';
- }
-
- if (percentage > 50) {
- return 'medium';
- }
-
- return 'low';
- })();
-
- const colors = {
- high: 'red.600',
- medium: 'orange.600',
- low: 'green.600',
- };
- const color = colors[load];
-
return (
Network utilization
- { percentage.toFixed(2) }% { mdash } { load } load
+ { percentage.toFixed(2) }%
);
};
diff --git a/ui/gasTracker/GasTrackerPriceSnippet.tsx b/ui/gasTracker/GasTrackerPriceSnippet.tsx
index 9858a31c6b..74a8b049de 100644
--- a/ui/gasTracker/GasTrackerPriceSnippet.tsx
+++ b/ui/gasTracker/GasTrackerPriceSnippet.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex } from '@chakra-ui/react';
+import { Box, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo, GasPrices } from 'types/api/stats';
@@ -6,6 +6,7 @@ import type { GasPriceInfo, GasPrices } from 'types/api/stats';
import { SECOND } from 'lib/consts';
import { asymp } from 'lib/html-entities';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import GasPrice from 'ui/shared/gas/GasPrice';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
@@ -28,43 +29,114 @@ const ICONS: Record = {
};
const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => {
- const bgColors = {
- fast: 'transparent',
- average: { _light: 'gray.50', _dark: 'whiteAlpha.200' },
- slow: { _light: 'gray.50', _dark: 'whiteAlpha.200' },
+ const accentColors = {
+ fast: OPENGRADIENT_BRAND.text.accent,
+ average: { _light: '#24bce3', _dark: '#24bce3' },
+ slow: { _light: 'rgba(29, 150, 182, 0.52)', _dark: 'rgba(189, 235, 247, 0.34)' },
+ };
+ const iconBgColors = {
+ fast: { _light: 'rgba(36, 188, 227, 0.12)', _dark: 'rgba(36, 188, 227, 0.14)' },
+ average: { _light: 'rgba(36, 188, 227, 0.09)', _dark: 'rgba(36, 188, 227, 0.10)' },
+ slow: { _light: 'rgba(14, 75, 91, 0.06)', _dark: 'rgba(189, 235, 247, 0.07)' },
};
- const borderColor = { _light: 'gray.200', _dark: 'whiteAlpha.300' };
return (
- { TITLES[type] }
-
-
+
+
+
+ { TITLES[type] }
+
+
+
+
+
+
+
+
-
+
-
+
+
{ data.price !== null && data.fiat_price !== null && }
per transaction
{ typeof data.time === 'number' && data.time > 0 && / { (data.time / SECOND).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s }
-
- { typeof data.base_fee === 'number' && Base { data.base_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) } }
- { typeof data.base_fee === 'number' && typeof data.priority_fee === 'number' && / }
- { typeof data.priority_fee === 'number' && Priority { data.priority_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) } }
+
+
+
+ { typeof data.base_fee === 'number' && (
+
+ Base { data.base_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }
+
+ ) }
+ { typeof data.priority_fee === 'number' && (
+
+ Priority { data.priority_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }
+
+ ) }
+
);
diff --git a/ui/gasTracker/GasTrackerPrices.tsx b/ui/gasTracker/GasTrackerPrices.tsx
index 3c6485654b..3cad62fa23 100644
--- a/ui/gasTracker/GasTrackerPrices.tsx
+++ b/ui/gasTracker/GasTrackerPrices.tsx
@@ -1,4 +1,4 @@
-import { Flex } from '@chakra-ui/react';
+import { SimpleGrid } from '@chakra-ui/react';
import React from 'react';
import type { GasPrices } from 'types/api/stats';
@@ -12,18 +12,15 @@ interface Props {
const GasTrackerPrices = ({ prices, isLoading }: Props) => {
return (
-
{ prices.fast && }
{ prices.average && }
{ prices.slow && }
-
+
);
};
diff --git a/ui/home/HeroBanner.tsx b/ui/home/HeroBanner.tsx
index ca5b3fd403..a19b97ce17 100644
--- a/ui/home/HeroBanner.tsx
+++ b/ui/home/HeroBanner.tsx
@@ -1,39 +1,93 @@
-// we use custom heading size for hero banner
-
import { Box, Flex, VStack, Text, Grid } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { route } from 'nextjs-routes';
-import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
+import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY } from 'lib/opengradient/contracts/teeRegistry';
import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats';
import { LinkBox, LinkOverlay } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
-import IconSvg from 'ui/shared/IconSvg';
+import { PLACEHOLDER_TEE_REGISTRY_STATS, PLACEHOLDER_TEE_TYPES } from 'ui/opengradient/teeRegistry/placeholders';
+import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
-export const BACKGROUND_DEFAULT = { _light: 'gray.900', _dark: 'gray.800' };
+import { HOME_BRAND } from './brand';
+import isStatsMicroserviceEnabled from './utils/isStatsMicroserviceEnabled';
-const isStatsFeatureEnabled = config.features.stats.isEnabled;
+export const BACKGROUND_DEFAULT = { _light: '#e9f8fc', _dark: '#0a0f19' };
-const HeroBanner = () => {
- const configBackgroundLight = config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background;
- const configBackgroundDark = config.UI.homepage.heroBanner?.background?.[1] ||
- config.UI.homepage.heroBanner?.background?.[0] ||
- config.UI.homepage.plate.background;
+const { colors, fonts, panel, text } = HOME_BRAND;
+
+type MetricCardProps = {
+ href: string;
+ external?: boolean;
+ label: string;
+ iconName: IconName;
+ value: React.ReactNode;
+ loading?: boolean;
+};
+
+const MetricCard = ({ href, external, label, iconName, value, loading }: MetricCardProps) => {
+ const valueText = (
+
+ { value }
+
+ );
- const hasConfigBackground = Boolean(configBackgroundLight);
- const backgroundValue = hasConfigBackground ?
- { _light: configBackgroundLight, _dark: configBackgroundDark } :
- { _light: '#ffffff', _dark: '#0a0a0a' };
+ return (
+
+
+
+
+ { label }
+
+
+
+ { loading ? { valueText } : valueText }
+
+ );
+};
- // Fetch stats for hero metrics
+const HeroBanner = () => {
const statsQuery = useApiQuery('stats_main', {
queryOptions: {
refetchOnMount: false,
- placeholderData: isStatsFeatureEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined,
- enabled: isStatsFeatureEnabled,
+ placeholderData: isStatsMicroserviceEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined,
+ enabled: isStatsMicroserviceEnabled,
},
});
@@ -44,6 +98,16 @@ const HeroBanner = () => {
},
});
+ const teeRegistryQuery = useQuery({
+ queryKey: TEE_REGISTRY_QUERY_KEY,
+ queryFn: getTEERegistryOverview,
+ placeholderData: {
+ types: PLACEHOLDER_TEE_TYPES,
+ stats: PLACEHOLDER_TEE_REGISTRY_STATS,
+ nodesByType: {},
+ },
+ });
+
const settlementContractAddress = '0xAa3bB22c5Ef24fe3837134A25A4D801308E2516d';
const settlementContractAddressV2 = '0xf1dc0d5Dcf2A01924faC78185B9227CF3EC839A5';
const settlementQuery = useApiQuery('address_counters', {
@@ -60,8 +124,8 @@ const HeroBanner = () => {
});
const totalTransactions = React.useMemo(() => {
- const statsData = statsQuery.data;
- const apiData = apiQuery.data;
+ const statsData = statsQuery.isPlaceholderData ? undefined : statsQuery.data;
+ const apiData = apiQuery.isPlaceholderData ? undefined : apiQuery.data;
if (statsData?.total_transactions?.value) {
return Number(statsData.total_transactions.value);
}
@@ -69,19 +133,7 @@ const HeroBanner = () => {
return Number(apiData.total_transactions);
}
return null;
- }, [ statsQuery.data, apiQuery.data ]);
-
- const totalBlocks = React.useMemo(() => {
- const statsData = statsQuery.data;
- const apiData = apiQuery.data;
- if (statsData?.total_blocks?.value) {
- return Number(statsData.total_blocks.value);
- }
- if (apiData?.total_blocks) {
- return Number(apiData.total_blocks);
- }
- return null;
- }, [ statsQuery.data, apiQuery.data ]);
+ }, [ statsQuery.data, statsQuery.isPlaceholderData, apiQuery.data, apiQuery.isPlaceholderData ]);
const llmBatchSettlementsCount = React.useMemo(() => {
const countersData = settlementQuery.data;
@@ -93,9 +145,11 @@ const HeroBanner = () => {
}
return v1Count + v2Count;
}, [ settlementQuery.data, settlementQueryV2.data ]);
+ const isSettlementCountLoading = llmBatchSettlementsCount === null && (settlementQuery.isPending || settlementQueryV2.isPending);
+ const teeStats = teeRegistryQuery.data?.stats ?? PLACEHOLDER_TEE_REGISTRY_STATS;
const formatNumber = (num: number | null, decimals: number = 2): string => {
- if (num === null) return '—';
+ if (num === null) return '-';
if (num >= 1_000_000) return `${ (num / 1_000_000).toFixed(decimals) }M`;
if (num >= 1_000) return `${ (num / 1_000).toFixed(decimals) }K`;
return num.toLocaleString();
@@ -106,26 +160,29 @@ const HeroBanner = () => {
position="relative"
w="100%"
overflow="hidden"
- background={ backgroundValue }
- border="none"
+ background={{ _light: '#e9f8fc', _dark: '#0a0f19' }}
+ borderBottom="1px solid"
+ borderColor={ panel.border }
borderRadius="0"
- borderColor={{ _light: 'rgba(0, 0, 0, 0.06)', _dark: 'rgba(64, 209, 219, 0.1)' }}
minH={{ base: 'auto', lg: 'auto' }}
>
- { /* Subtle grid pattern - very minimal */ }
{
maxW={{ base: '100%', xl: '1600px' }}
mx="auto"
px={{ base: 4, lg: 8, xl: 12 }}
- pt={{ base: 6, lg: 10, xl: 12 }}
- pb={{ base: 4, lg: 6, xl: 7 }}
+ pt={{ base: 5, lg: 6, xl: 7 }}
+ pb={{ base: 5, lg: 6 }}
>
- { /* Large diffuse radial gradient behind entire top section */ }
-
- { /* Left: Search + Title */ }
- { /* Title Section */ }
+ / AI Execution Network Explorer
+
+
+
- OpenGradient Explorer
+
+ OpenGradient AI
+
+
+ { ' ' }Explorer
+
- Find transactions, blocks, addresses, models, and AI workflows on the OpenGradient network.
+ Trace model inference, x402 settlements, TEE attestations, and AI workflow activity across the OpenGradient network.
- { /* Search Bar - Premium, polished */ }
- { /* Right: Live Metrics Dashboard */ }
{
- { /* Section Header */ }
- Network Stats
+ Live network stats
- { /* Metrics Grid */ }
- { /* Models Hosted */ }
-
-
-
-
- Models Hosted
-
-
-
-
- 2000+
-
-
-
- { /* Total Blocks */ }
-
-
-
-
- Total Blocks
-
-
-
-
-
- { formatNumber(totalBlocks, 1) }
-
-
-
-
- { /* LLM Settlement Txns */ }
-
-
-
-
- x402 Txns
-
-
-
-
-
- { formatNumber(llmBatchSettlementsCount) }
-
-
-
-
- { /* Transactions */ }
-
-
-
-
- Transactions
-
-
-
-
-
- { formatNumber(totalTransactions) }
-
-
-
+
+
+
+
diff --git a/ui/home/LatestBlocks.tsx b/ui/home/LatestBlocks.tsx
index 077a8be93a..712436ae16 100644
--- a/ui/home/LatestBlocks.tsx
+++ b/ui/home/LatestBlocks.tsx
@@ -16,8 +16,11 @@ import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import { Link } from 'toolkit/chakra/link';
+import { HOME_BRAND } from './brand';
import LatestBlocksItem from './LatestBlocksItem';
+const { colors, fonts, panel, text } = HOME_BRAND;
+
const LatestBlocks = () => {
const isMobile = useIsMobile();
const blocksMaxCount = isMobile ? 4 : 6;
@@ -70,8 +73,8 @@ const LatestBlocks = () => {
No data. Please reload the page.
@@ -88,6 +91,7 @@ const LatestBlocks = () => {
align="stretch"
gap={ 2.5 }
px={{ base: 3, lg: 4 }}
+ pt={ 3 }
pb={ 4 }
>
{ dataToShow.map(((block, index) => (
@@ -111,8 +115,8 @@ const LatestBlocks = () => {
fontWeight={ 500 }
letterSpacing="0.08em"
textTransform="uppercase"
- color={{ _light: 'rgba(0, 0, 0, 0.4)', _dark: 'rgba(255, 255, 255, 0.4)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ color={ text.muted }
+ fontFamily={ fonts.mono }
width="100%"
display="block"
textAlign="center"
@@ -121,7 +125,7 @@ const LatestBlocks = () => {
_hover={{
textDecoration: 'none',
opacity: 0.7,
- color: { _light: 'rgba(0, 0, 0, 0.6)', _dark: 'rgba(255, 255, 255, 0.6)' },
+ color: text.accent,
}}
>
View all blocks
@@ -133,27 +137,43 @@ const LatestBlocks = () => {
return (
- { /* Premium Header with Status Indicator */ }
+
- Latest blocks
+ / Latest blocks
{ statsQueryResult.data?.celo && (
@@ -161,14 +181,15 @@ const LatestBlocks = () => {
Current epoch
@@ -176,8 +197,8 @@ const LatestBlocks = () => {
#{ statsQueryResult.data.celo.epoch_number }
diff --git a/ui/home/LatestBlocksItem.tsx b/ui/home/LatestBlocksItem.tsx
index e65085fbe2..9692588051 100644
--- a/ui/home/LatestBlocksItem.tsx
+++ b/ui/home/LatestBlocksItem.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Text, HStack, VStack } from '@chakra-ui/react';
+import { Box, Flex, Text, HStack } from '@chakra-ui/react';
import React from 'react';
import type { Block } from 'types/api/block';
@@ -8,13 +8,13 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import { LinkBox, LinkOverlay } from 'toolkit/chakra/link';
-import { Skeleton } from 'toolkit/chakra/skeleton';
-import { Tooltip } from 'toolkit/chakra/tooltip';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
-import BlockEntity from 'ui/shared/entities/block/BlockEntity';
-import IconSvg from 'ui/shared/IconSvg';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
+import { HOME_BRAND } from './brand';
+
+const { colors, fonts, text } = HOME_BRAND;
+
type Props = {
block: Block;
isLoading?: boolean;
@@ -24,211 +24,120 @@ type Props = {
const LatestBlocksItem = ({ block, isLoading, animation, isFirst = false }: Props) => {
const blockUrl = route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: block.height.toString() } });
+ const txCount = block.transaction_count;
+ const barPct = Math.min(100, Math.round((Math.log(Math.max(txCount, 1) + 1) / Math.log(400)) * 100));
return (
-
-
-
+
+
+
+ { isFirst && (
+
+ ) }
+
+ #{ block.height.toLocaleString() }
+
+
+
-
+
+
+
- { /* Visual Block Indicator */ }
+
+
+ { txCount.toLocaleString() } { txCount === 1 ? 'tx' : 'txs' }
+
+
+
+ { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.miner && (
+
+
-
+
+
- { /* New block indicator for first block */ }
- { isFirst && (
-
- ) }
- { /* Epoch indicator */ }
- { block.celo?.is_epoch_block && (
-
-
-
-
-
- ) }
-
- { /* Content Section */ }
-
- { /* Header: Block Number & Time */ }
-
-
-
-
-
- { /* Stats Row */ }
-
- { /* Transaction Count - Prominent Display */ }
-
-
-
- Txn
-
-
- { block.transaction_count.toLocaleString() }
-
-
-
-
- { /* Miner/Validator */ }
- { !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.miner && (
-
-
-
- { getNetworkValidatorTitle() }
-
-
-
-
-
-
- ) }
-
-
-
-
-
+
+ ) }
+
);
};
diff --git a/ui/home/LatestTxs.tsx b/ui/home/LatestTxs.tsx
index 6d82e32672..81bdf911d0 100644
--- a/ui/home/LatestTxs.tsx
+++ b/ui/home/LatestTxs.tsx
@@ -10,56 +10,72 @@ import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import { Link } from 'toolkit/chakra/link';
+import { HOME_BRAND } from './brand';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemMobile from './LatestTxsItemMobile';
+const { colors, fonts, panel, text } = HOME_BRAND;
+
const LatestTransactions = () => {
const isMobile = useIsMobile();
- const txsCount = isMobile ? 2 : 6;
+ const txsCount = isMobile ? 2 : 11;
const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs', {
queryOptions: {
placeholderData: Array(txsCount).fill(TX),
},
});
+ const validatedTxsQuery = useApiQuery('txs_validated', {
+ queryParams: {
+ filter: 'validated',
+ },
+ queryOptions: {
+ placeholderData: {
+ items: Array(txsCount).fill(TX),
+ next_page_params: null,
+ },
+ },
+ });
const { num, socketAlert } = useNewTxsSocket();
const txsUrl = route({ pathname: '/txs' });
let content;
- if (isError) {
+ const txs = isMobile ? data : validatedTxsQuery.data?.items;
+ const isTxsPlaceholderData = isMobile ? isPlaceholderData : validatedTxsQuery.isPlaceholderData;
+ const isTxsError = isMobile ? isError : validatedTxsQuery.isError;
+
+ if (isTxsError) {
content = (
No data. Please reload the page.
);
- }
-
- if (data) {
+ } else if (txs) {
content = (
<>
- { data.slice(0, txsCount).map(((tx, index) => (
+ { txs.slice(0, txsCount).map(((tx, index) => (
))) }
@@ -68,36 +84,21 @@ const LatestTransactions = () => {
- { /* Subtle grid pattern overlay */ }
-
-
- { data.slice(0, txsCount).map(((tx, index) => (
+
+ { txs.slice(0, txsCount).map(((tx, index) => (
))) }
@@ -105,10 +106,10 @@ const LatestTransactions = () => {
{
fontWeight={ 500 }
letterSpacing="0.08em"
textTransform="uppercase"
- color={{ _light: 'rgba(0, 0, 0, 0.4)', _dark: 'rgba(255, 255, 255, 0.4)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ color={ text.muted }
+ fontFamily={ fonts.mono }
width="100%"
display="block"
textAlign="center"
@@ -126,7 +127,7 @@ const LatestTransactions = () => {
_hover={{
textDecoration: 'none',
opacity: 0.7,
- color: { _light: 'rgba(0, 0, 0, 0.6)', _dark: 'rgba(255, 255, 255, 0.6)' },
+ color: text.accent,
}}
>
View all transactions
@@ -139,38 +140,46 @@ const LatestTransactions = () => {
const statusText = socketAlert || (num ? `${ num.toLocaleString() } new txns` : 'Monitoring...');
return (
-
- { /* Premium Header Section */ }
+
- Latest transactions
+ / Latest transactions
- { (num || socketAlert) && !isPlaceholderData && (
+ { (num || socketAlert) && !isTxsPlaceholderData && (
{
px={ 2.5 }
py={ 1 }
borderRadius="full"
- bg={{ _light: 'rgba(59, 130, 246, 0.1)', _dark: 'rgba(59, 130, 246, 0.2)' }}
+ bg={{ _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.16)' }}
border="1px solid"
- borderColor={{ _light: 'rgba(59, 130, 246, 0.2)', _dark: 'rgba(59, 130, 246, 0.3)' }}
+ borderColor={{ _light: 'rgba(36, 188, 227, 0.24)', _dark: 'rgba(80, 201, 233, 0.28)' }}
fontSize="10px"
fontWeight={ 500 }
- color={{ _light: 'rgba(59, 130, 246, 0.9)', _dark: 'rgba(147, 197, 253, 0.9)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ color={ text.accent }
+ fontFamily={ fonts.mono }
transition="all 0.2s ease"
_hover={{
- bg: { _light: 'rgba(59, 130, 246, 0.15)', _dark: 'rgba(59, 130, 246, 0.25)' },
- borderColor: { _light: 'rgba(59, 130, 246, 0.3)', _dark: 'rgba(59, 130, 246, 0.4)' },
+ bg: { _light: 'rgba(36, 188, 227, 0.16)', _dark: 'rgba(36, 188, 227, 0.24)' },
+ borderColor: { _light: 'rgba(36, 188, 227, 0.34)', _dark: 'rgba(80, 201, 233, 0.42)' },
textDecoration: 'none',
- transform: 'scale(1.05)',
}}
>
{ statusText }
diff --git a/ui/home/LatestTxsItem.tsx b/ui/home/LatestTxsItem.tsx
index 59466982e7..2143ce35f6 100644
--- a/ui/home/LatestTxsItem.tsx
+++ b/ui/home/LatestTxsItem.tsx
@@ -1,12 +1,4 @@
-import {
- Box,
- Flex,
- Grid,
- GridItem,
- HStack,
- Text,
- VStack,
-} from '@chakra-ui/react';
+import { Box, Flex, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
@@ -14,23 +6,23 @@ import type { Transaction } from 'types/api/transaction';
import { route } from 'nextjs-routes';
import { SUPPORTED_INFERENCE_ADDRESSES } from 'lib/inferences/address';
-import { Badge } from 'toolkit/chakra/badge';
import { LinkBox, LinkOverlay, Link } from 'toolkit/chakra/link';
-import { Skeleton } from 'toolkit/chakra/skeleton';
-import AddressFromTo from 'ui/shared/address/AddressFromTo';
-import TxEntity from 'ui/shared/entities/tx/TxEntity';
-import TxStatus from 'ui/shared/statusTag/TxStatus';
+import AddressEntity from 'ui/shared/entities/address/AddressEntity';
+import IconSvg from 'ui/shared/IconSvg';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
-import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
-import TxType from 'ui/txs/TxType';
+import { HOME_BRAND } from './brand';
import useInferenceType from './useInferenceType';
+const { colors, fonts, text } = HOME_BRAND;
+
type Props = {
tx: Transaction;
isLoading?: boolean;
};
+const formatHash = (hash: string) => `${ hash.slice(0, 10) }...${ hash.slice(-4) }`;
+
const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract;
const hasInference = tx.to?.hash === SUPPORTED_INFERENCE_ADDRESSES.InferenceHub ||
@@ -38,198 +30,131 @@ const LatestTxsItem = ({ tx, isLoading }: Props) => {
const inferenceInfo = useInferenceType(tx, isLoading || false);
const txUrl = route({ pathname: '/tx/[hash]', query: { hash: tx.hash } });
+ const isContractCreation = !tx.to && tx.created_contract;
+ const txKind = (() => {
+ if (isContractCreation) {
+ return 'Deploy';
+ }
+
+ return tx.to?.is_contract ? 'Call' : 'Send';
+ })();
+
return (
-
- { /* Transaction Hash & Metadata Column */ }
-
-
- { /* Hash Row */ }
-
-
-
-
- { /* Badges Row */ }
-
-
- { tx.status !== 'error' && (
-
- ) }
-
-
-
-
+
+
+ { txKind }
+
- { /* Address Column - Premium Styling */ }
-
-
-
- Addresses
-
-
-
-
-
-
+
+
+ { isLoading ? '0x000000...0000' : formatHash(tx.hash) }
+
+
+
+
+
+
+
+
+
+
+
+
+ { dataTo ? (
+
+ ) : (
+ -
+ ) }
+
+
- { /* Inference Column - Premium Styling */ }
-
- { hasInference ? (
-
-
+ { hasInference && inferenceInfo && !inferenceInfo.isLoading ? (
+
+
- Inference
-
- { /* eslint-disable-next-line no-nested-ternary */ }
- { inferenceInfo ? (
- inferenceInfo.isLoading ? (
-
- Loading...
-
- ) : (
- <>
-
-
- { inferenceInfo.type || 'AI Inference' }
-
-
- { inferenceInfo.modelCID && (
-
-
- Model:{ ' ' }
-
- { inferenceInfo.modelCID.slice(0, 8) }...
-
-
-
- ) }
- >
- )
- ) : (
-
+ { inferenceInfo.modelCID && (
+
- —
-
+ { inferenceInfo.modelCID.slice(0, 6) }...
+
) }
-
+
) : (
-
-
- Inference
-
-
- No inference
-
-
+
+ -
+
) }
-
-
+
+
);
};
diff --git a/ui/home/LatestTxsItemMobile.tsx b/ui/home/LatestTxsItemMobile.tsx
index 75dc7654fd..cfbf2f01fe 100644
--- a/ui/home/LatestTxsItemMobile.tsx
+++ b/ui/home/LatestTxsItemMobile.tsx
@@ -22,8 +22,11 @@ import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
import TxWatchListTags from 'ui/shared/tx/TxWatchListTags';
import TxType from 'ui/txs/TxType';
+import { HOME_BRAND } from './brand';
import useInferenceType from './useInferenceType';
+const { fonts, text } = HOME_BRAND;
+
type Props = {
tx: Transaction;
isLoading?: boolean;
@@ -43,14 +46,15 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
px={ 4 }
transition="all 0.2s cubic-bezier(0.4, 0, 0.2, 1)"
position="relative"
+ borderRadius="6px"
_hover={{
- bg: { _light: 'rgba(0, 0, 0, 0.02)', _dark: 'rgba(64, 209, 219, 0.05)' },
+ bg: { _light: 'rgba(36, 188, 227, 0.05)', _dark: 'rgba(36, 188, 227, 0.06)' },
+ boxShadow: { _light: 'inset 0 0 0 1px rgba(36, 188, 227, 0.18)', _dark: 'inset 0 0 0 1px rgba(36, 188, 227, 0.22)' },
}}
display={{ base: 'block', lg: 'none' }}
>
- { /* Header: Hash & Time */ }
{
hash={ tx.hash }
fontWeight={ 500 }
fontSize="sm"
- fontFamily="system-ui, -apple-system, sans-serif"
- color={{ _light: 'rgba(0, 0, 0, 0.95)', _dark: 'rgba(255, 255, 255, 0.98)' }}
+ fontFamily={ fonts.mono }
+ color={ text.primary }
truncation="dynamic"
tailLength={ 8 }
noIcon
@@ -72,15 +76,14 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
timestamp={ tx.timestamp }
enableIncrement
isLoading={ isLoading }
- color={{ _light: 'rgba(0, 0, 0, 0.4)', _dark: 'rgba(255, 255, 255, 0.4)' }}
+ color={ text.muted }
fontSize="11px"
fontWeight={ 400 }
- fontFamily="system-ui, -apple-system, sans-serif"
+ fontFamily={ fonts.mono }
flexShrink={ 0 }
/>
- { /* Badges Row */ }
{ tx.status !== 'error' && (
@@ -96,8 +99,8 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
px={ 2 }
py={ 0.5 }
minH="6"
- fontFamily="system-ui, -apple-system, sans-serif"
- letterSpacing="0.02em"
+ fontFamily={ fonts.mono }
+ letterSpacing="0.08em"
>
{ inferenceInfo?.type || 'AI Inference' }
@@ -110,8 +113,8 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
px={ 2 }
py={ 0.5 }
minH="6"
- fontFamily="system-ui, -apple-system, sans-serif"
- letterSpacing="0.02em"
+ fontFamily={ fonts.mono }
+ letterSpacing="0.08em"
>
{ inferenceInfo.mode }
@@ -121,22 +124,21 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
- { /* Addresses Section */ }
Addresses
{
- { /* Inference Details */ }
{ hasInference && (
{
fontWeight={ 500 }
letterSpacing="0.08em"
textTransform="uppercase"
- color={{ _light: 'rgba(0, 0, 0, 0.35)', _dark: 'rgba(255, 255, 255, 0.35)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ color={ text.muted }
+ fontFamily={ fonts.mono }
mb={ 1.5 }
>
AI Inference
@@ -165,17 +166,17 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
{ !inferenceInfo && (
- —
+ -
) }
{ inferenceInfo && inferenceInfo.isLoading && (
Loading...
@@ -184,8 +185,8 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
@@ -196,10 +197,10 @@ const LatestTxsItemMobile = ({ tx, isLoading }: Props) => {
fontSize="11px"
fontWeight={ 500 }
fontFamily="mono"
- color={{ _light: 'rgba(0, 0, 0, 0.7)', _dark: 'rgba(255, 255, 255, 0.7)' }}
+ color={ text.secondary }
_hover={{
textDecoration: 'underline',
- color: { _light: 'rgba(0, 0, 0, 0.9)', _dark: 'rgba(255, 255, 255, 0.9)' },
+ color: text.accent,
}}
lineClamp={ 1 }
>
diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx
index be42756d8b..4c495afcf3 100644
--- a/ui/home/Stats.tsx
+++ b/ui/home/Stats.tsx
@@ -4,7 +4,6 @@ import React from 'react';
import { route } from 'nextjs-routes';
-import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { getAllTasks } from 'lib/opengradient/contracts/scheduler';
import { HOMEPAGE_STATS, HOMEPAGE_STATS_MICROSERVICE } from 'stubs/stats';
@@ -12,15 +11,17 @@ import { LinkBox, LinkOverlay } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg';
-const isStatsFeatureEnabled = config.features.stats.isEnabled;
+import { HOME_BRAND } from './brand';
+import isStatsMicroserviceEnabled from './utils/isStatsMicroserviceEnabled';
+
+const { fonts, text } = HOME_BRAND;
const Stats = () => {
- // Fetch stats data
const statsQuery = useApiQuery('stats_main', {
queryOptions: {
refetchOnMount: false,
- placeholderData: isStatsFeatureEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined,
- enabled: isStatsFeatureEnabled,
+ placeholderData: isStatsMicroserviceEnabled ? HOMEPAGE_STATS_MICROSERVICE : undefined,
+ enabled: isStatsMicroserviceEnabled,
},
});
@@ -31,19 +32,15 @@ const Stats = () => {
},
});
- const isPlaceholderData = statsQuery.isPlaceholderData || apiQuery.isPlaceholderData;
-
- // Fetch active AI workflows
const workflowsQuery = useQuery({
queryKey: [ 'opengradient', 'getAllTasks' ],
queryFn: getAllTasks,
refetchOnMount: false,
});
- // Get total transactions from either microservice or regular API
const totalTransactions = React.useMemo(() => {
- const statsData = statsQuery.data;
- const apiData = apiQuery.data;
+ const statsData = statsQuery.isPlaceholderData ? undefined : statsQuery.data;
+ const apiData = apiQuery.isPlaceholderData ? undefined : apiQuery.data;
if (statsData?.total_transactions?.value) {
return Number(statsData.total_transactions.value);
@@ -54,19 +51,17 @@ const Stats = () => {
}
return null;
- }, [ statsQuery.data, apiQuery.data ]);
+ }, [ statsQuery.data, statsQuery.isPlaceholderData, apiQuery.data, apiQuery.isPlaceholderData ]);
- // Calculate active AI workflows count
const activeWorkflowsCount = React.useMemo(() => {
const tasks = workflowsQuery.data ?? [];
const now = BigInt(Math.floor(Date.now() / 1000));
return tasks.filter((t) => t.endTime > now).length;
}, [ workflowsQuery.data ]);
- // Format number with compact notation
const formatTransactionCount = (count: number | null): string => {
if (count === null) {
- return '—';
+ return '-';
}
if (count >= 1_000_000) {
@@ -90,7 +85,7 @@ const Stats = () => {
{
label: 'Transactions',
value: formatTransactionCount(totalTransactions),
- isLoading: isPlaceholderData,
+ isLoading: totalTransactions === null && (statsQuery.isPending || apiQuery.isPending),
href: route({ pathname: '/txs' }),
external: false,
},
@@ -128,10 +123,10 @@ const Stats = () => {
{ metric.label }
@@ -139,7 +134,7 @@ const Stats = () => {
) }
@@ -149,11 +144,11 @@ const Stats = () => {
>
{ metric.value }
@@ -165,7 +160,7 @@ const Stats = () => {
p: { base: 3, lg: 4 },
borderBottom: { base: !isLastItem ? '1px solid' : 'none', md: !isLastRow ? '1px solid' : 'none' },
borderRight: { base: 'none', md: isEvenIndex && !isLastItem ? '1px solid' : 'none' },
- borderColor: { _light: 'rgba(0, 0, 0, 0.06)', _dark: 'rgba(255, 255, 255, 0.08)' },
+ borderColor: { _light: 'rgba(36, 188, 227, 0.15)', _dark: 'rgba(36, 188, 227, 0.10)' },
};
if (metric.href) {
diff --git a/ui/home/Transactions.tsx b/ui/home/Transactions.tsx
index f5b720b648..d4b3d1f259 100644
--- a/ui/home/Transactions.tsx
+++ b/ui/home/Transactions.tsx
@@ -9,9 +9,11 @@ import LatestTxs from 'ui/home/LatestTxs';
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
import useAuth from 'ui/snippets/auth/useIsAuth';
+import { HOME_BRAND } from './brand';
import LatestArbitrumDeposits from './latestDeposits/LatestArbitrumDeposits';
const rollupFeature = config.features.rollup;
+const { fonts, text } = HOME_BRAND;
const TransactionsHome = () => {
const isAuth = useAuth();
@@ -19,9 +21,9 @@ const TransactionsHome = () => {
const tabs = [
{ id: 'txn', title: 'Latest txn', component: },
rollupFeature.isEnabled && rollupFeature.type === 'optimistic' &&
- { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: },
+ { id: 'deposits', title: 'Deposits (L1 -> L2 txn)', component: },
rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' &&
- { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: },
+ { id: 'deposits', title: 'Deposits (L1 -> L2 txn)', component: },
isAuth && { id: 'watchlist', title: 'Watch list', component: },
].filter(Boolean);
return (
@@ -31,7 +33,9 @@ const TransactionsHome = () => {
level="3"
fontSize={{ base: 'xl', lg: '2xl' }}
fontWeight={ 700 }
- letterSpacing="-0.02em"
+ letterSpacing="0"
+ color={ text.primary }
+ fontFamily={ fonts.sans }
mb={ 4 }
>
Transactions
diff --git a/ui/home/TrustedExecution.tsx b/ui/home/TrustedExecution.tsx
new file mode 100644
index 0000000000..34ca1a026e
--- /dev/null
+++ b/ui/home/TrustedExecution.tsx
@@ -0,0 +1,335 @@
+import { Box, Flex, Grid, HStack, Text, VStack } from '@chakra-ui/react';
+import { useQuery } from '@tanstack/react-query';
+import React from 'react';
+
+import { route } from 'nextjs-routes';
+
+import dayjs from 'lib/date/dayjs';
+import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY, TEE_REGISTRY_ADDRESS, type TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry';
+import { Link } from 'toolkit/chakra/link';
+import { Skeleton } from 'toolkit/chakra/skeleton';
+import { PLACEHOLDER_TEE_REGISTRY_STATS, PLACEHOLDER_TEE_TYPES } from 'ui/opengradient/teeRegistry/placeholders';
+import IconSvg from 'ui/shared/IconSvg';
+
+import { HOME_BRAND } from './brand';
+
+const { colors, fonts, panel, text } = HOME_BRAND;
+
+const formatHash = (hash: string, head = 6, tail = 4) => {
+ if (!hash) return 'N/A';
+ if (hash.length <= head + tail + 3) return hash;
+ return `${ hash.slice(0, head) }...${ hash.slice(-tail) }`;
+};
+
+const formatTimeAgo = (timestamp: bigint) => {
+ if (timestamp === BigInt(0)) return 'Never';
+ return dayjs.unix(Number(timestamp)).fromNow();
+};
+
+const SectionLabel = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ { children }
+
+
+);
+
+const RegistryMetric = ({ label, value, helper, loading }: { label: string; value: React.ReactNode; helper: string; loading?: boolean }) => (
+
+
+ { label }
+
+
+
+ { value }
+
+
+
+ { helper }
+
+
+);
+
+const NodePreview = ({ node, loading }: { node: TEENodeWithStatus; loading?: boolean }) => (
+
+
+
+
+
+
+ { formatHash(node.teeId, 8, 4) }
+
+
+
+ { node.endpoint || 'No endpoint published' }
+
+
+
+
+
+ { formatTimeAgo(node.lastHeartbeatAt) }
+
+
+
+);
+
+const TrustedExecution = () => {
+ const query = useQuery({
+ queryKey: TEE_REGISTRY_QUERY_KEY,
+ queryFn: getTEERegistryOverview,
+ placeholderData: {
+ types: PLACEHOLDER_TEE_TYPES,
+ stats: PLACEHOLDER_TEE_REGISTRY_STATS,
+ nodesByType: {},
+ },
+ });
+
+ const stats = query.data?.stats ?? PLACEHOLDER_TEE_REGISTRY_STATS;
+ const types = query.data?.types ?? PLACEHOLDER_TEE_TYPES;
+ const nodes = React.useMemo(() => {
+ const nodesByType = query.data?.nodesByType ?? {};
+ return Object.values(nodesByType).flat().sort((a, b) => Number(b.lastHeartbeatAt - a.lastHeartbeatAt));
+ }, [ query.data?.nodesByType ]);
+ const primaryType = types[0] ?? PLACEHOLDER_TEE_TYPES[0];
+ const visibleNodes = nodes.slice(0, 3);
+ const isLoading = query.isPlaceholderData;
+
+ return (
+
+
+
+ Trusted AI execution
+
+ Attested TEE operators for model inference
+
+
+ OpenGradient routes AI workloads through registered enclaves with approved PCR identities, live heartbeats, and on-chain operator records.
+
+
+
+
+ Open registry
+
+
+
+ Contract
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Primary registry type
+
+
+
+ { primaryType.name }
+
+
+
+
+ Active
+ { primaryType.activeNodes }/{ primaryType.totalNodes }
+
+
+ Enabled
+ { primaryType.enabledNodes }
+
+
+ PCRs
+ { primaryType.approvedPCRs }
+
+
+
+ 0 ? `${ Math.round((primaryType.activeNodes / primaryType.totalNodes) * 100) }%` : '0' }
+ minW={ primaryType.activeNodes > 0 ? '18px' : '0' }
+ bg={ colors.cyan }
+ borderRadius="2px"
+ />
+
+
+
+
+
+
+ Live operators
+
+
+ { stats.activeNodes } active
+
+
+
+ { visibleNodes.length > 0 ? visibleNodes.map((node) => (
+
+ )) : (
+
+ Registry nodes will appear here as they are indexed.
+
+ ) }
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(TrustedExecution);
diff --git a/ui/home/brand.ts b/ui/home/brand.ts
new file mode 100644
index 0000000000..0a11fde6e5
--- /dev/null
+++ b/ui/home/brand.ts
@@ -0,0 +1,30 @@
+export const HOME_BRAND = {
+ fonts: {
+ sans: '"Geist", system-ui, -apple-system, sans-serif',
+ mono: '"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
+ },
+ colors: {
+ cyan: '#24bce3',
+ cyanSoft: '#bdebf7',
+ cyanBright: '#50c9e9',
+ teal: '#0e4b5b',
+ tealMid: '#1d96b6',
+ navy: '#0a0f19',
+ navySoft: '#0f1626',
+ slate: '#314a7d',
+ },
+ panel: {
+ bg: { _light: 'rgba(255, 255, 255, 0.82)', _dark: 'rgba(10, 15, 25, 0.72)' },
+ border: { _light: 'rgba(36, 188, 227, 0.16)', _dark: 'rgba(189, 235, 247, 0.12)' },
+ shadow: {
+ _light: '0 14px 44px rgba(14, 75, 91, 0.08)',
+ _dark: '0 22px 70px rgba(0, 0, 0, 0.28)',
+ },
+ },
+ text: {
+ primary: { _light: '#0e4b5b', _dark: 'rgba(255, 255, 255, 0.94)' },
+ secondary: { _light: '#314a7d', _dark: 'rgba(189, 235, 247, 0.58)' },
+ muted: { _light: 'rgba(14, 75, 91, 0.50)', _dark: 'rgba(189, 235, 247, 0.42)' },
+ accent: { _light: '#1d96b6', _dark: '#50c9e9' },
+ },
+} as const;
diff --git a/ui/home/indicators/ChainIndicatorChartContent.tsx b/ui/home/indicators/ChainIndicatorChartContent.tsx
index 867708a2af..4bfe51a259 100644
--- a/ui/home/indicators/ChainIndicatorChartContent.tsx
+++ b/ui/home/indicators/ChainIndicatorChartContent.tsx
@@ -22,14 +22,13 @@ const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 };
const ChainIndicatorChartContent = ({ data, isEmpty = false }: Props) => {
const overlayRef = React.useRef(null);
- // Use subtle colors matching the design system
const lineColor = useColorModeValue(
- 'rgba(79, 172, 254, 0.5)',
- 'rgba(96, 165, 250, 0.5)',
+ 'rgba(29, 150, 182, 0.78)',
+ 'rgba(80, 201, 233, 0.78)',
);
const areaColor = useColorModeValue(
- 'rgba(79, 172, 254, 0.15)',
- 'rgba(96, 165, 250, 0.15)',
+ 'rgba(36, 188, 227, 0.22)',
+ 'rgba(36, 188, 227, 0.18)',
);
const axesConfig = React.useMemo(() => {
@@ -47,7 +46,6 @@ const ChainIndicatorChartContent = ({ data, isEmpty = false }: Props) => {
const hasData = !isEmpty && data[0]?.items.length > 0;
- // For ghost chart, render a flat line at the baseline (0 or middle of domain)
const ghostLineY = React.useMemo(() => {
if (!isEmpty) return null;
const domain = axes.y.scale.domain();
@@ -58,7 +56,6 @@ const ChainIndicatorChartContent = ({ data, isEmpty = false }: Props) => {
return (
@@ -145,9 +152,8 @@ const ChainIndicators = () => {
);
})();
- // Create a grid layout similar to Stats.tsx
const gridItems = indicators.map((indicator) => {
- const { value, valueDiff: diff } = getIndicatorValues(indicator, statsMicroserviceQueryResult?.data, statsApiQueryResult?.data);
+ const { value, valueDiff: diff } = getIndicatorValues(indicator, statsMicroserviceData, statsApiData);
const isSelected = selectedIndicator === indicator.id;
return {
@@ -161,40 +167,58 @@ const ChainIndicators = () => {
});
return (
-
- { /* Main selected indicator display */ }
+
+
- { title }
+ / { title }
{ hint && }
-
+
{ valueTitle }
{ valueDiff }
-
+
- { /* Indicator selector grid */ }
{ indicators.length > 1 && (
{
return (
no data
@@ -220,11 +245,12 @@ const ChainIndicators = () => {
}
return (
-
+
{ item.value }
@@ -238,11 +264,11 @@ const ChainIndicators = () => {
}
const diffColor = item.valueDiff >= 0 ?
- { _light: 'rgba(34, 197, 94, 0.7)', _dark: 'rgba(74, 222, 128, 0.7)' } :
- { _light: 'rgba(239, 68, 68, 0.7)', _dark: 'rgba(248, 113, 113, 0.7)' };
+ { _light: '#2e9e66', _dark: '#61d199' } :
+ { _light: '#bf0d0d', _dark: '#f66f6f' };
return (
-
+
{
fontSize="xs"
fontWeight={ 500 }
color={ diffColor }
+ fontFamily={ fonts.mono }
>
{ Math.abs(item.valueDiff) }%
@@ -265,15 +292,15 @@ const ChainIndicators = () => {
@@ -284,10 +311,10 @@ const ChainIndicators = () => {
{ item.label }
diff --git a/ui/home/indicators/useChartDataQuery.tsx b/ui/home/indicators/useChartDataQuery.tsx
index e30d01126e..32abe762fe 100644
--- a/ui/home/indicators/useChartDataQuery.tsx
+++ b/ui/home/indicators/useChartDataQuery.tsx
@@ -4,6 +4,7 @@ import type { TimeChartData, TimeChartDataItem, TimeChartItemRaw } from 'ui/shar
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
+import isStatsMicroserviceEnabled from '../utils/isStatsMicroserviceEnabled';
import prepareChartItems from './utils/prepareChartItems';
const CHART_ITEMS: Record> = {
@@ -33,8 +34,6 @@ const CHART_ITEMS: Record data.daily_new_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || [],
},
});
@@ -61,7 +60,7 @@ export default function useChartDataQuery(indicatorId: ChainIndicatorId): UseFet
const statsDailyOperationalTxsQuery = useApiQuery('stats_main', {
queryOptions: {
refetchOnMount: false,
- enabled: isStatsFeatureEnabled && indicatorId === 'daily_operational_txs',
+ enabled: isStatsMicroserviceEnabled && indicatorId === 'daily_operational_txs',
select: (data) => data.daily_new_operational_transactions?.chart.map((item) => ({ date: new Date(item.date), value: Number(item.value) })) || [],
},
});
@@ -69,7 +68,7 @@ export default function useChartDataQuery(indicatorId: ChainIndicatorId): UseFet
const apiDailyTxsQuery = useApiQuery('stats_charts_txs', {
queryOptions: {
refetchOnMount: false,
- enabled: !isStatsFeatureEnabled && indicatorId === 'daily_txs',
+ enabled: indicatorId === 'daily_txs',
select: (data) => data.chart_data.map((item) => ({ date: new Date(item.date), value: item.transaction_count })),
},
});
@@ -126,11 +125,14 @@ export default function useChartDataQuery(indicatorId: ChainIndicatorId): UseFet
switch (indicatorId) {
case 'daily_txs': {
- const query = isStatsFeatureEnabled ? statsDailyTxsQuery : apiDailyTxsQuery;
+ const statsMicroserviceData = isStatsMicroserviceEnabled && !statsDailyTxsQuery.isPlaceholderData ?
+ statsDailyTxsQuery.data :
+ undefined;
+ const data = statsMicroserviceData?.length ? statsMicroserviceData : apiDailyTxsQuery.data || [];
return {
- data: getChartData(indicatorId, query.data || []),
- isError: query.isError,
- isPending: query.isPending,
+ data: getChartData(indicatorId, data),
+ isError: apiDailyTxsQuery.isError && (!isStatsMicroserviceEnabled || statsDailyTxsQuery.isError),
+ isPending: !data.length && (statsDailyTxsQuery.isPending || apiDailyTxsQuery.isPending),
};
}
case 'daily_operational_txs': {
diff --git a/ui/home/utils/isStatsMicroserviceEnabled.ts b/ui/home/utils/isStatsMicroserviceEnabled.ts
new file mode 100644
index 0000000000..dde9a27b88
--- /dev/null
+++ b/ui/home/utils/isStatsMicroserviceEnabled.ts
@@ -0,0 +1,11 @@
+import { getFeaturePayload } from 'configs/app/features/types';
+
+import config from 'configs/app';
+
+const statsFeature = getFeaturePayload(config.features.stats);
+
+const isStatsMicroserviceEnabled = Boolean(
+ statsFeature && (statsFeature.api.endpoint !== config.api.endpoint || statsFeature.api.basePath),
+);
+
+export default isStatsMicroserviceEnabled;
diff --git a/ui/opengradient/brand.ts b/ui/opengradient/brand.ts
new file mode 100644
index 0000000000..abfde6d4ad
--- /dev/null
+++ b/ui/opengradient/brand.ts
@@ -0,0 +1 @@
+export { HOME_BRAND as OPENGRADIENT_BRAND } from 'ui/home/brand';
diff --git a/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx b/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx
index c842641f37..9c3c890fb6 100644
--- a/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx
+++ b/ui/opengradient/teeRegistry/TEENodeDetailDrawer.tsx
@@ -4,6 +4,7 @@ import React from 'react';
import dayjs from 'lib/date/dayjs';
import type { TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry';
import { DrawerBackdrop, DrawerBody, DrawerCloseTrigger, DrawerContent, DrawerHeader, DrawerRoot, DrawerTitle } from 'toolkit/chakra/drawer';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as EntityBase from 'ui/shared/entities/base/components';
@@ -13,19 +14,21 @@ type Props = {
onClose: () => void;
};
+const { colors, fonts, panel, text } = OPENGRADIENT_BRAND;
+
const DetailRow = ({ label, children }: { label: string; children: React.ReactNode }) => (
{ label }
@@ -35,15 +38,15 @@ const DetailRow = ({ label, children }: { label: string; children: React.ReactNo
);
function getStatusBg(isActive: boolean, enabled: boolean) {
- if (isActive) return 'green.500';
- if (enabled) return 'orange.400';
- return 'gray.400';
+ if (isActive) return '#61d199';
+ if (enabled) return '#d6a33d';
+ return '#708195';
}
function getStatusColor(isActive: boolean, enabled: boolean) {
- if (isActive) return { _light: 'rgba(22, 163, 74, 0.9)', _dark: 'rgba(34, 197, 94, 0.95)' };
- if (enabled) return { _light: 'rgba(217, 119, 6, 0.8)', _dark: 'rgba(245, 158, 11, 0.9)' };
- return { _light: 'rgba(0, 0, 0, 0.4)', _dark: 'rgba(255, 255, 255, 0.4)' };
+ if (isActive) return { _light: '#23824f', _dark: '#61d199' };
+ if (enabled) return { _light: '#9d6d10', _dark: '#d6a33d' };
+ return text.muted;
}
function getStatusLabel(isActive: boolean, enabled: boolean) {
@@ -73,7 +76,11 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
return (
-
+
@@ -82,8 +89,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
TEE Node Details
@@ -100,7 +107,7 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
fontSize="xs"
fontWeight={ 500 }
color={ getStatusColor(node.isActive, node.enabled) }
- fontFamily="system-ui, -apple-system, sans-serif"
+ fontFamily={ fonts.mono }
>
{ getStatusLabel(node.isActive, node.enabled) }
@@ -109,10 +116,10 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
fontWeight={ 500 }
px={ 2 }
py={ 0.5 }
- borderRadius="sm"
- bg={{ _light: 'rgba(124, 58, 237, 0.06)', _dark: 'rgba(139, 92, 246, 0.1)' }}
- color={{ _light: 'rgba(124, 58, 237, 0.8)', _dark: 'rgba(139, 92, 246, 0.9)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ borderRadius="6px"
+ bg={{ _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.13)' }}
+ color={ text.accent }
+ fontFamily={ fonts.mono }
>
{ typeName }
@@ -128,8 +135,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ node.teeId }
@@ -158,8 +165,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ node.endpoint || 'N/A' }
@@ -171,8 +178,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ node.pcrHash }
@@ -186,8 +193,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ truncateBytes(node.publicKey, 40) }
@@ -200,8 +207,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ truncateBytes(node.tlsCertificate, 40) }
@@ -213,8 +220,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ formatTimestamp(node.registeredAt) }
@@ -224,8 +231,8 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ formatTimestamp(node.lastHeartbeatAt) }
@@ -234,7 +241,7 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
w="6px"
h="6px"
borderRadius="50%"
- bg="green.500"
+ bgColor={ colors.cyan }
boxShadow="0 0 4px rgba(34, 197, 94, 0.5)"
/>
) }
@@ -245,11 +252,11 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
Chain of Trust
@@ -257,18 +264,19 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ [
- { label: 'AWS Nitro Hardware', desc: 'Root of trust', color: 'rgba(124, 58, 237' },
- { label: 'Attestation Document', desc: 'Hardware-signed proof', color: 'rgba(6, 182, 212' },
- { label: 'PCR Verification', desc: 'Enclave code identity check', color: 'rgba(22, 163, 74' },
- { label: 'Key Binding', desc: 'Signing key + TLS cert bound to enclave', color: 'rgba(30, 58, 138' },
- { label: 'On-Chain Record', desc: 'Immutable registration', color: 'rgba(217, 119, 6' },
- { label: 'Heartbeat Liveness', desc: 'Ongoing cryptographic proof', color: 'rgba(22, 163, 74' },
+ { label: 'AWS Nitro Hardware', desc: 'Root of trust', color: '#50c9e9' },
+ { label: 'Attestation Document', desc: 'Hardware-signed proof', color: '#24bce3' },
+ { label: 'PCR Verification', desc: 'Enclave code identity check', color: '#61d199' },
+ { label: 'Key Binding', desc: 'Signing key + TLS cert bound to enclave', color: '#1d96b6' },
+ { label: 'On-Chain Record', desc: 'Immutable registration', color: '#d6a33d' },
+ { label: 'Heartbeat Liveness', desc: 'Ongoing cryptographic proof', color: '#61d199' },
].map((step, i) => (
{
w="8px"
h="8px"
borderRadius="50%"
- bg={{ _light: `${ step.color }, 0.3)`, _dark: `${ step.color }, 0.5)` }}
+ bg={ step.color }
border="1.5px solid"
- borderColor={{ _light: `${ step.color }, 0.5)`, _dark: `${ step.color }, 0.7)` }}
+ borderColor={ step.color }
+ opacity={ 0.8 }
/>
{ i < 5 && (
) }
@@ -296,15 +305,15 @@ const TEENodeDetailDrawer = ({ node, typeName, onClose }: Props) => {
{ step.label }
{ step.desc }
diff --git a/ui/opengradient/teeRegistry/TEENodesTable.tsx b/ui/opengradient/teeRegistry/TEENodesTable.tsx
index dda96848ce..b039cf44fb 100644
--- a/ui/opengradient/teeRegistry/TEENodesTable.tsx
+++ b/ui/opengradient/teeRegistry/TEENodesTable.tsx
@@ -3,9 +3,9 @@ import React from 'react';
import dayjs from 'lib/date/dayjs';
import type { TEENodeWithStatus, TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry';
-import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableBody, TableCell, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
@@ -16,66 +16,70 @@ type Props = {
onNodeClick: (node: TEENodeWithStatus) => void;
};
-const StatusIndicator = ({ isActive, enabled }: { isActive: boolean; enabled: boolean }) => {
- if (isActive) {
- return (
-
-
-
- Active
-
-
- );
- }
-
- if (enabled) {
- return (
-
-
-
- Enabled
-
-
- );
- }
+const { colors, fonts, panel, text } = OPENGRADIENT_BRAND;
+
+const STATUS_STYLE = {
+ active: {
+ label: 'Active',
+ dot: '#61d199',
+ fg: { _light: '#23824f', _dark: '#61d199' },
+ bg: { _light: 'rgba(46, 158, 102, 0.10)', _dark: 'rgba(97, 209, 153, 0.10)' },
+ border: { _light: 'rgba(46, 158, 102, 0.18)', _dark: 'rgba(97, 209, 153, 0.22)' },
+ },
+ enabled: {
+ label: 'Enabled',
+ dot: '#d6a33d',
+ fg: { _light: '#9d6d10', _dark: '#d6a33d' },
+ bg: { _light: 'rgba(214, 163, 61, 0.10)', _dark: 'rgba(214, 163, 61, 0.10)' },
+ border: { _light: 'rgba(214, 163, 61, 0.20)', _dark: 'rgba(214, 163, 61, 0.24)' },
+ },
+ disabled: {
+ label: 'Disabled',
+ dot: '#708195',
+ fg: text.muted,
+ bg: { _light: 'rgba(49, 74, 125, 0.06)', _dark: 'rgba(189, 235, 247, 0.05)' },
+ border: panel.border,
+ },
+};
+
+const getStatus = (node: TEENodeWithStatus) => {
+ if (node.isActive) return STATUS_STYLE.active;
+ if (node.enabled) return STATUS_STYLE.enabled;
+ return STATUS_STYLE.disabled;
+};
+
+const StatusPill = ({ node }: { node: TEENodeWithStatus }) => {
+ const status = getStatus(node);
return (
-
+
- Disabled
+ { status.label }
);
@@ -86,6 +90,30 @@ function formatTimeAgo(timestamp: bigint) {
return dayjs.unix(Number(timestamp)).fromNow();
}
+const formatHash = (hash: string, head = 8, tail = 4) => {
+ if (!hash) return 'N/A';
+ if (hash.length <= head + tail + 3) return hash;
+ return `${ hash.slice(0, head) }...${ hash.slice(-tail) }`;
+};
+
+const HeaderCell = ({ children, w }: { children?: React.ReactNode; w?: string }) => (
+
+ { children }
+
+);
+
const NodeRow = ({ node, typeNameMap, isLoading, onNodeClick }: {
node: TEENodeWithStatus;
typeNameMap: Record;
@@ -100,103 +128,117 @@ const NodeRow = ({ node, typeNameMap, isLoading, onNodeClick }: {
-
-
-
+
+
+
-
+
-
+
+
+
+ ID { formatHash(node.teeId, 6, 4) }
+
+
-
-
+
+
{ typeNameMap[node.teeType] ?? `Type ${ node.teeType }` }
-
-
+
+
- { node.pcrHash ? `${ node.pcrHash.slice(0, 6) }...${ node.pcrHash.slice(-4) }` : 'N/A' }
+ { formatHash(node.pcrHash, 8, 5) }
-
+
{ node.endpoint || 'N/A' }
-
-
+
+
{ formatTimeAgo(node.lastHeartbeatAt) }
-
-
-
+
+
+
{ formatTimeAgo(node.registeredAt) }
-
-
-
-
-
+
+
+
@@ -223,31 +265,33 @@ const TEENodesTable = ({ nodes, types, isLoading, onNodeClick }: Props) => {
[ nodes ]);
return (
-
-
-
- Status
- Owner
- Type
- PCR Hash
- Endpoint
- Last Heartbeat
- Registered
-
-
-
-
- { sortedNodes.map((node) => (
-
- )) }
-
-
+
+
+
+
+ Status
+ Node / Owner
+ Type
+ PCR Hash
+ Endpoint
+ Heartbeat
+ Registered
+
+
+
+
+ { sortedNodes.map((node) => (
+
+ )) }
+
+
+
);
};
diff --git a/ui/opengradient/teeRegistry/TEETypeCard.tsx b/ui/opengradient/teeRegistry/TEETypeCard.tsx
index 9f5fa6bf20..eb81ace351 100644
--- a/ui/opengradient/teeRegistry/TEETypeCard.tsx
+++ b/ui/opengradient/teeRegistry/TEETypeCard.tsx
@@ -1,8 +1,9 @@
-import { Box, Flex, Text } from '@chakra-ui/react';
+import { Box, Flex, Grid, Text } from '@chakra-ui/react';
import React from 'react';
import type { TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
type Props = {
type: TEETypeSummary;
@@ -11,87 +12,115 @@ type Props = {
onClick: (typeId: number) => void;
};
-const TEETypeCard = ({ type, isSelected, isLoading, onClick }: Props) => {
+const { colors, fonts, panel, text } = OPENGRADIENT_BRAND;
+
+const Metric = ({ label, value, isLoading }: { label: string; value: string; isLoading?: boolean }) => (
+
+
+ { label }
+
+
+
+ { value }
+
+
+
+);
+const TEETypeCard = ({ type, isSelected, isLoading, onClick }: Props) => {
const handleClick = React.useCallback(() => {
onClick(type.typeId);
}, [ onClick, type.typeId ]);
+ const activePct = type.totalNodes > 0 ? Math.round((type.activeNodes / type.totalNodes) * 100) : 0;
+
return (
- { /* Active indicator bar at top */ }
-
-
- { /* Name + Stats */ }
-
+
{ type.name }
-
-
- { type.activeNodes }/{ type.totalNodes } active
-
-
-
-
- { type.approvedPCRs } approved PCR{ type.approvedPCRs !== 1 ? 's' : '' }
-
-
+
+
+
+
+
+
+
+
+
+ 0 ? '18px' : '0' }
+ bg={ colors.cyan }
+ borderRadius="2px"
+ transition="width 0.2s ease"
+ />
+
);
};
diff --git a/ui/opengradient/teeRegistry/placeholders.ts b/ui/opengradient/teeRegistry/placeholders.ts
new file mode 100644
index 0000000000..cdc706079b
--- /dev/null
+++ b/ui/opengradient/teeRegistry/placeholders.ts
@@ -0,0 +1,15 @@
+import type { TEERegistryStats, TEETypeSummary } from 'lib/opengradient/contracts/teeRegistry';
+
+export const PLACEHOLDER_TEE_REGISTRY_STATS: TEERegistryStats = {
+ totalTypes: 3,
+ totalNodes: 12,
+ activeNodes: 8,
+ enabledNodes: 10,
+ approvedPCRs: 5,
+};
+
+export const PLACEHOLDER_TEE_TYPES: Array = [
+ { typeId: 0, name: 'LLM Inference', totalNodes: 5, enabledNodes: 4, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) },
+ { typeId: 1, name: 'Agent Execution', totalNodes: 4, enabledNodes: 3, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) },
+ { typeId: 2, name: 'Model Training', totalNodes: 3, enabledNodes: 3, activeNodes: 2, approvedPCRs: 1, addedAt: BigInt(0) },
+];
diff --git a/ui/pages/Accounts.tsx b/ui/pages/Accounts.tsx
index d86a04f72b..597bc87a57 100644
--- a/ui/pages/Accounts.tsx
+++ b/ui/pages/Accounts.tsx
@@ -9,7 +9,8 @@ import AddressesListItem from 'ui/addresses/AddressesListItem';
import AddressesTable from 'ui/addresses/AddressesTable';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
-import PageTitle from 'ui/shared/Page/PageTitle';
+import ExplorerPageSurface from 'ui/shared/Page/ExplorerPageSurface';
+import ExplorerPageTitle from 'ui/shared/Page/ExplorerPageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
@@ -33,7 +34,7 @@ const Accounts = () => {
});
const actionBar = pagination.isVisible && (
-
+
);
@@ -72,14 +73,23 @@ const Accounts = () => {
return (
<>
-
+
- { content }
+ { content && (
+
+ { content }
+
+ ) }
>
);
diff --git a/ui/pages/Blocks.tsx b/ui/pages/Blocks.tsx
index dac25f2f46..5c274d316e 100644
--- a/ui/pages/Blocks.tsx
+++ b/ui/pages/Blocks.tsx
@@ -10,14 +10,14 @@ import { generateListStub } from 'stubs/utils';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
-import PageTitle from 'ui/shared/Page/PageTitle';
+import ExplorerPageTitle from 'ui/shared/Page/ExplorerPageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
const TAB_LIST_PROPS = {
marginBottom: 0,
- pt: 6,
- pb: 6,
- marginTop: -5,
+ pt: 2,
+ pb: 5,
+ marginTop: 0,
};
const BlocksPageContent = () => {
@@ -77,7 +77,12 @@ const BlocksPageContent = () => {
return (
<>
-
+
{
const { data, isPlaceholderData, isError, error, dataUpdatedAt } = useApiQuery('stats', {
@@ -83,12 +84,13 @@ const GasTracker = () => {
return (
<>
-
- { `Track ${ config.chain.name } gas fees` }
+ Current gas fees
{ snippets }
{ config.features.stats.isEnabled && (
diff --git a/ui/pages/Home.tsx b/ui/pages/Home.tsx
index 5fa86baac1..a102530562 100644
--- a/ui/pages/Home.tsx
+++ b/ui/pages/Home.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Container } from '@chakra-ui/react';
+import { Box, Flex, Container, VStack } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
@@ -8,6 +8,7 @@ import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batch
import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches';
import LatestBlocks from 'ui/home/LatestBlocks';
import Transactions from 'ui/home/Transactions';
+import TrustedExecution from 'ui/home/TrustedExecution';
import AdBanner from 'ui/shared/ad/AdBanner';
const rollupFeature = config.features.rollup;
@@ -31,41 +32,43 @@ const Home = () => {
-
+
+
+
- { /* Left Column: Chain Indicators + Latest Blocks */ }
-
+
-
- { leftWidget }
+ { leftWidget }
+
- { /* Right Column: Transactions */ }
diff --git a/ui/pages/InternalTxs.tsx b/ui/pages/InternalTxs.tsx
index 89468c2a0e..62e144bcb6 100644
--- a/ui/pages/InternalTxs.tsx
+++ b/ui/pages/InternalTxs.tsx
@@ -8,7 +8,8 @@ import InternalTxsList from 'ui/internalTxs/InternalTxsList';
import InternalTxsTable from 'ui/internalTxs/InternalTxsTable';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
-import PageTitle from 'ui/shared/Page/PageTitle';
+import ExplorerPageSurface from 'ui/shared/Page/ExplorerPageSurface';
+import ExplorerPageTitle from 'ui/shared/Page/ExplorerPageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
@@ -35,7 +36,7 @@ const InternalTxs = () => {
});
const actionBar = (!isMobile || pagination.isVisible) ? (
-
+
) : null;
@@ -53,8 +54,10 @@ const InternalTxs = () => {
return (
<>
-
{
emptyText="There are no internal transactions."
actionBar={ actionBar }
>
- { content }
+ { content && (
+
+ { content }
+
+ ) }
>
);
diff --git a/ui/pages/Transactions.tsx b/ui/pages/Transactions.tsx
index b1a8820799..7b8abf0a6f 100644
--- a/ui/pages/Transactions.tsx
+++ b/ui/pages/Transactions.tsx
@@ -17,7 +17,7 @@ import { generateListStub } from 'stubs/utils';
import { Link } from 'toolkit/chakra/link';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import IconSvg from 'ui/shared/IconSvg';
-import PageTitle from 'ui/shared/Page/PageTitle';
+import ExplorerPageTitle from 'ui/shared/Page/ExplorerPageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import useIsAuth from 'ui/snippets/auth/useIsAuth';
@@ -27,11 +27,11 @@ import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
- pt: 6,
- pb: 6,
- marginTop: -5,
+ pt: 2,
+ pb: 5,
+ marginTop: 0,
};
-const TABS_HEIGHT = 88;
+const TABS_HEIGHT = 68;
const Transactions = () => {
const verifiedTitle = capitalize(getNetworkValidationActionText());
@@ -181,8 +181,10 @@ const Transactions = () => {
return (
<>
-
diff --git a/ui/pages/VerifiedContracts.tsx b/ui/pages/VerifiedContracts.tsx
index e8f2f6241c..638973b848 100644
--- a/ui/pages/VerifiedContracts.tsx
+++ b/ui/pages/VerifiedContracts.tsx
@@ -15,7 +15,8 @@ import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import FilterInput from 'ui/shared/filters/FilterInput';
-import PageTitle from 'ui/shared/Page/PageTitle';
+import ExplorerPageSurface from 'ui/shared/Page/ExplorerPageSurface';
+import ExplorerPageTitle from 'ui/shared/Page/ExplorerPageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
@@ -91,7 +92,9 @@ const VerifiedContracts = () => {
const filterInput = (
{
{ filterInput }
{ (!isMobile || pagination.isVisible) && (
-
+
{ typeFilter }
{ filterInput }
@@ -141,8 +144,10 @@ const VerifiedContracts = () => {
return (
-
@@ -156,7 +161,11 @@ const VerifiedContracts = () => {
}}
actionBar={ actionBar }
>
- { content }
+ { content && (
+
+ { content }
+
+ ) }
);
diff --git a/ui/pages/opengradient/TEERegistry.tsx b/ui/pages/opengradient/TEERegistry.tsx
index 42edfdd466..9da00426d3 100644
--- a/ui/pages/opengradient/TEERegistry.tsx
+++ b/ui/pages/opengradient/TEERegistry.tsx
@@ -1,33 +1,99 @@
-import { Box, Flex, Grid, Text, VStack } from '@chakra-ui/react';
+import { Box, Flex, Grid, HStack, Text } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { route } from 'nextjs-routes';
import { getTEERegistryOverview, TEE_REGISTRY_QUERY_KEY, TEE_REGISTRY_ADDRESS } from 'lib/opengradient/contracts/teeRegistry';
-import type { TEERegistryStats, TEETypeSummary, TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry';
+import type { TEENodeWithStatus } from 'lib/opengradient/contracts/teeRegistry';
import { Checkbox } from 'toolkit/chakra/checkbox';
-import { Heading } from 'toolkit/chakra/heading';
-import { Link, LinkBox } from 'toolkit/chakra/link';
+import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
+import { PLACEHOLDER_TEE_REGISTRY_STATS, PLACEHOLDER_TEE_TYPES } from 'ui/opengradient/teeRegistry/placeholders';
import TEENodeDetailDrawer from 'ui/opengradient/teeRegistry/TEENodeDetailDrawer';
import TEENodesTable from 'ui/opengradient/teeRegistry/TEENodesTable';
import TEETypeCard from 'ui/opengradient/teeRegistry/TEETypeCard';
-import IconSvg from 'ui/shared/IconSvg';
-
-const PLACEHOLDER_STATS: TEERegistryStats = {
- totalTypes: 3,
- totalNodes: 12,
- activeNodes: 8,
- enabledNodes: 10,
- approvedPCRs: 5,
+import IconSvg, { type IconName } from 'ui/shared/IconSvg';
+
+const { colors, fonts, panel, text } = OPENGRADIENT_BRAND;
+
+type MetricCardProps = {
+ label: string;
+ value: number;
+ iconName: IconName;
+ helper: string;
+ loading?: boolean;
};
-const PLACEHOLDER_TYPES: Array = [
- { typeId: 0, name: 'LLM Inference', totalNodes: 5, enabledNodes: 4, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) },
- { typeId: 1, name: 'Agent Execution', totalNodes: 4, enabledNodes: 3, activeNodes: 3, approvedPCRs: 2, addedAt: BigInt(0) },
- { typeId: 2, name: 'Model Training', totalNodes: 3, enabledNodes: 3, activeNodes: 2, approvedPCRs: 1, addedAt: BigInt(0) },
-];
+const SectionLabel = ({ children }: { children: React.ReactNode }) => (
+
+
+
+ { children }
+
+
+);
+
+const MetricCard = ({ label, value, iconName, helper, loading }: MetricCardProps) => (
+
+
+
+ { label }
+
+
+
+
+
+ { value.toLocaleString() }
+
+
+
+ { helper }
+
+
+);
const TEERegistry = () => {
const [ selectedNode, setSelectedNode ] = React.useState(null);
@@ -37,32 +103,30 @@ const TEERegistry = () => {
queryKey: TEE_REGISTRY_QUERY_KEY,
queryFn: getTEERegistryOverview,
placeholderData: {
- types: PLACEHOLDER_TYPES,
- stats: PLACEHOLDER_STATS,
+ types: PLACEHOLDER_TEE_TYPES,
+ stats: PLACEHOLDER_TEE_REGISTRY_STATS,
nodesByType: {},
},
});
- const stats = query.data?.stats ?? PLACEHOLDER_STATS;
- const types = query.data?.types ?? PLACEHOLDER_TYPES;
+ const stats = query.data?.stats ?? PLACEHOLDER_TEE_REGISTRY_STATS;
+ const types = query.data?.types ?? PLACEHOLDER_TEE_TYPES;
- // All nodes flat list for the table
const allNodes = React.useMemo(() => {
- const nbt = query.data?.nodesByType ?? {};
+ const nodesByType = query.data?.nodesByType ?? {};
if (selectedType !== null) {
- return nbt[selectedType] ?? [];
+ return nodesByType[selectedType] ?? [];
}
- return Object.values(nbt).flat();
+ return Object.values(nodesByType).flat();
}, [ query.data?.nodesByType, selectedType ]);
const hasNonDisabledNodes = React.useMemo(
- () => allNodes.some((n) => n.isActive || n.enabled),
+ () => allNodes.some((node) => node.isActive || node.enabled),
[ allNodes ],
);
const [ showDisabled, setShowDisabled ] = React.useState(null);
- // Reset when allNodes changes (e.g. type filter changed)
React.useEffect(() => {
setShowDisabled(null);
}, [ selectedType ]);
@@ -70,10 +134,23 @@ const TEERegistry = () => {
const resolvedShowDisabled = showDisabled ?? !hasNonDisabledNodes;
const filteredNodes = React.useMemo(
- () => resolvedShowDisabled ? allNodes : allNodes.filter((n) => n.isActive || n.enabled),
+ () => resolvedShowDisabled ? allNodes : allNodes.filter((node) => node.isActive || node.enabled),
[ allNodes, resolvedShowDisabled ],
);
+ const tableTitle = selectedType !== null ?
+ `${ types.find((type) => type.typeId === selectedType)?.name ?? 'Selected' } nodes` :
+ 'All nodes';
+
+ const tableSubtitle = resolvedShowDisabled ?
+ 'Showing active, enabled, and disabled registry records.' :
+ 'Showing active and enabled registry records.';
+
+ const activeVisibleCount = React.useMemo(
+ () => filteredNodes.filter((node) => node.isActive).length,
+ [ filteredNodes ],
+ );
+
const handleToggleShowDisabled = React.useCallback(() => {
setShowDisabled((prev) => {
const current = prev ?? !hasNonDisabledNodes;
@@ -98,314 +175,137 @@ const TEERegistry = () => {
}, []);
return (
- <>
-
- TEE Registry
-
-
- { /* Description */ }
-
+
-
-
-
-
-
-
- Hardware-rooted chain of trust from AWS Nitro enclaves to on-chain verification.
- Each TEE node is cryptographically attested, with its signing key and TLS certificate
- bound to verified enclave code. Heartbeat liveness proofs ensure nodes are actively running
- approved software. Click on a node to inspect its attestation details.
-
-
- View Registry Contract
-
-
-
-
-
-
- { /* Hero Stats Section */ }
-
-
- { /* Metrics Grid */ }
-
- { /* TEE Types */ }
-
-
-
- TEE Types
-
-
-
-
-
- { stats.totalTypes.toLocaleString() }
-
-
-
-
- { /* Approved PCRs */ }
-
-
-
- Approved PCRs
-
-
-
-
-
- { stats.approvedPCRs.toLocaleString() }
-
-
-
-
- { /* Active Nodes */ }
-
-
-
- Active Nodes
-
-
-
-
-
- { stats.activeNodes.toLocaleString() }
-
-
-
-
- { /* Enabled Nodes */ }
-
-
-
- Registered Nodes
-
-
-
-
-
- { stats.enabledNodes.toLocaleString() }
-
-
-
-
-
-
-
-
- { /* TEE Types Grid */ }
-
-
+
- TEE Types
+ / Trusted execution registry
+
+ TEE Registry
+
+
+ Monitor attested OpenGradient TEE operators, approved PCR identities, endpoints, and heartbeat liveness from the on-chain registry.
+
+
+
+
+ Registry contract
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TEE types
{ selectedType !== null && (
Clear filter
) }
{ types.map((type) => (
{
- { /* Nodes Table */ }
-
-
-
-
- { selectedType !== null ?
- `${ types.find((t) => t.typeId === selectedType)?.name ?? '' } Nodes` :
- 'All Nodes' }
-
-
- { filteredNodes.length > 0 ? `(${ filteredNodes.length })` : '' }
+
+
+
+
+ { tableTitle }
+
+ { filteredNodes.length } shown / { allNodes.length } total
+
+
+
+ { tableSubtitle } { activeVisibleCount > 0 ? `${ activeVisibleCount } currently active.` : '' }
-
+
+
Show disabled
+
+
{ !query.isPlaceholderData && filteredNodes.length === 0 && (
-
- No TEE nodes registered yet.
+
+ No TEE nodes match the current filter.
) }
- { /* Node Detail Drawer */ }
{ selectedNode && (
t.typeId === selectedNode.teeType)?.name ?? `Type ${ selectedNode.teeType }` }
+ typeName={ types.find((type) => type.typeId === selectedNode.teeType)?.name ?? `Type ${ selectedNode.teeType }` }
onClose={ handleCloseDrawer }
/>
) }
- >
+
);
};
diff --git a/ui/shared/ActionBar.tsx b/ui/shared/ActionBar.tsx
index 46f2f2ff24..43a9da0802 100644
--- a/ui/shared/ActionBar.tsx
+++ b/ui/shared/ActionBar.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsSticky from 'lib/hooks/useIsSticky';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
type Props = {
children: React.ReactNode;
@@ -27,7 +28,10 @@ const ActionBar = ({ children, className, showShadow }: Props) => {
return (
{
+ return (
+
+ { children }
+
+ );
+};
+
+export default ExplorerPageSurface;
diff --git a/ui/shared/Page/ExplorerPageTitle.tsx b/ui/shared/Page/ExplorerPageTitle.tsx
new file mode 100644
index 0000000000..dbf686c41c
--- /dev/null
+++ b/ui/shared/Page/ExplorerPageTitle.tsx
@@ -0,0 +1,62 @@
+import { Box, Flex, Text } from '@chakra-ui/react';
+import React from 'react';
+
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
+
+import PageTitle from './PageTitle';
+
+type Props = {
+ title: string;
+ eyebrow?: string;
+ description?: React.ReactNode;
+ secondRow?: React.ReactNode;
+ withTextAd?: boolean;
+};
+
+const ExplorerPageTitle = ({ title, eyebrow, description, secondRow, withTextAd }: Props) => {
+ const composedSecondRow = description || secondRow ? (
+
+ { description && (
+
+ { description }
+
+ ) }
+ { secondRow }
+
+ ) : undefined;
+
+ return (
+
+ { eyebrow && (
+
+ / { eyebrow }
+
+ ) }
+
+
+ );
+};
+
+export default React.memo(ExplorerPageTitle);
diff --git a/ui/shared/Page/PageTitle.tsx b/ui/shared/Page/PageTitle.tsx
index 590039eb3b..66d7ae483c 100644
--- a/ui/shared/Page/PageTitle.tsx
+++ b/ui/shared/Page/PageTitle.tsx
@@ -7,6 +7,7 @@ import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import TextAd from 'ui/shared/ad/TextAd';
import ButtonBackTo from 'ui/shared/buttons/ButtonBackTo';
@@ -113,7 +114,6 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
ref={ headingRef }
level="1"
whiteSpace="normal"
- wordBreak="break-all"
style={{
WebkitLineClamp: TEXT_MAX_LINES,
WebkitBoxOrient: 'vertical',
@@ -124,12 +124,14 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
onMouseEnter={ tooltip.onOpen }
onMouseLeave={ tooltip.onClose }
onClick={ isMobile ? tooltip.onToggle : undefined }
- fontSize={{ base: '30px', md: '40px', lg: '44px', xl: '46px' }}
- fontWeight={ 200 }
- letterSpacing="-0.04em"
- lineHeight="1.15"
- color={{ _light: 'rgba(0, 0, 0, 0.95)', _dark: 'rgba(255, 255, 255, 0.98)' }}
- fontFamily="system-ui, -apple-system, sans-serif"
+ fontSize={{ base: '28px', md: '34px', lg: '38px' }}
+ fontWeight={ 600 }
+ letterSpacing="0"
+ lineHeight="1.14"
+ color={ OPENGRADIENT_BRAND.text.primary }
+ fontFamily={ OPENGRADIENT_BRAND.fonts.sans }
+ wordBreak="normal"
+ overflowWrap="anywhere"
>
{ title }
diff --git a/ui/shared/chart/ChartArea.tsx b/ui/shared/chart/ChartArea.tsx
index f3171ebb5b..22bca2abf2 100644
--- a/ui/shared/chart/ChartArea.tsx
+++ b/ui/shared/chart/ChartArea.tsx
@@ -18,7 +18,8 @@ interface Props extends React.SVGProps {
const ChartArea = ({ id, xScale, yScale, color, data, noAnimation, ...props }: Props) => {
const ref = React.useRef(null);
- const gradientColorId = `${ id || 'gradient' }-${ color }-color`;
+ const gradientColorKey = color?.replace(/[^\w-]/g, '') || 'custom';
+ const gradientColorId = `${ id || 'gradient' }-${ gradientColorKey }-color`;
const gradientStopColor = useToken('colors', useColorModeValue('whiteAlpha.200', 'blackAlpha.100'));
const defaultGradient = {
startColor: useToken('colors', useColorModeValue('blue.100', 'blue.400')),
diff --git a/ui/shared/chart/ChartWatermarkIcon.tsx b/ui/shared/chart/ChartWatermarkIcon.tsx
index 0cf7fd990c..a8b6ae6e57 100644
--- a/ui/shared/chart/ChartWatermarkIcon.tsx
+++ b/ui/shared/chart/ChartWatermarkIcon.tsx
@@ -1,23 +1,20 @@
-import type { IconProps } from '@chakra-ui/react';
-import { Icon } from '@chakra-ui/react';
+import { chakra, type BoxProps } from '@chakra-ui/react';
import React from 'react';
-// eslint-disable-next-line no-restricted-imports
-import logoIcon from 'icons/networks/logo-placeholder.svg';
-
-const ChartWatermarkIcon = (props: IconProps) => {
+const ChartWatermarkIcon = (props: BoxProps) => {
return (
-
);
};
diff --git a/ui/shared/chart/ChartWidget.tsx b/ui/shared/chart/ChartWidget.tsx
index 95df9b7816..ba038be2d9 100644
--- a/ui/shared/chart/ChartWidget.tsx
+++ b/ui/shared/chart/ChartWidget.tsx
@@ -10,6 +10,7 @@ import { IconButton } from 'toolkit/chakra/icon-button';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import IconSvg from 'ui/shared/IconSvg';
import ChartMenu from './ChartMenu';
@@ -95,9 +96,12 @@ const ChartWidget = ({
ref={ ref }
flexDir="column"
padding={{ base: 3, lg: 4 }}
- borderRadius="lg"
+ bg={ OPENGRADIENT_BRAND.panel.bg }
+ backdropFilter="blur(10px)"
+ borderRadius="8px"
borderWidth="1px"
- borderColor={{ _light: 'gray.200', _dark: 'gray.600' }}
+ borderColor={ OPENGRADIENT_BRAND.panel.border }
+ boxShadow={ OPENGRADIENT_BRAND.panel.shadow }
className={ className }
>
diff --git a/ui/shared/layout/LayoutApp.tsx b/ui/shared/layout/LayoutApp.tsx
index f3e72502cd..ab211db4ab 100644
--- a/ui/shared/layout/LayoutApp.tsx
+++ b/ui/shared/layout/LayoutApp.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
+import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components';
@@ -22,6 +23,7 @@ const LayoutDefault = ({ children }: Props) => {
paddingBottom={ 0 }
paddingX={{ base: 4, lg: 6 }}
>
+
{ children }
diff --git a/ui/shared/layout/LayoutHome.tsx b/ui/shared/layout/LayoutHome.tsx
index 3d549551ba..e20a9313c8 100644
--- a/ui/shared/layout/LayoutHome.tsx
+++ b/ui/shared/layout/LayoutHome.tsx
@@ -4,6 +4,7 @@ import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import HeaderAlert from 'ui/snippets/header/HeaderAlert';
+import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components';
@@ -19,6 +20,7 @@ const LayoutHome = ({ children }: Props) => {
paddingTop={{ base: 3, lg: 6 }}
>
+
{ children }
diff --git a/ui/shared/layout/components/Container.tsx b/ui/shared/layout/components/Container.tsx
index 11d7f5f5da..cf3400e117 100644
--- a/ui/shared/layout/components/Container.tsx
+++ b/ui/shared/layout/components/Container.tsx
@@ -10,9 +10,10 @@ const Container = ({ children, className }: Props) => {
return (
{ children }
diff --git a/ui/shared/layout/components/MainColumn.tsx b/ui/shared/layout/components/MainColumn.tsx
index 75133c9651..bad65859ba 100644
--- a/ui/shared/layout/components/MainColumn.tsx
+++ b/ui/shared/layout/components/MainColumn.tsx
@@ -14,6 +14,7 @@ const MainColumn = ({ children, className }: Props) => {
className={ className }
flexDir="column"
flexGrow={ 1 }
+ minW={ 0 }
w={{ base: '100%', lg: config.UI.navigation.layout === 'horizontal' ? '100%' : 'auto' }}
paddingX={{ base: 3, lg: config.UI.navigation.layout === 'horizontal' ? 6 : 12 }}
paddingRight={{ '2xl': 6 }}
diff --git a/ui/shared/stats/StatsWidget.tsx b/ui/shared/stats/StatsWidget.tsx
index ee326b5e70..1c081f85e4 100644
--- a/ui/shared/stats/StatsWidget.tsx
+++ b/ui/shared/stats/StatsWidget.tsx
@@ -6,6 +6,7 @@ import { route } from 'nextjs-routes';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
+import { OPENGRADIENT_BRAND } from 'ui/opengradient/brand';
import Hint from 'ui/shared/Hint';
import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue';
@@ -58,36 +59,37 @@ const StatsWidget = ({
{ icon && (
) }
@@ -127,7 +129,7 @@ const StatsWidget = ({
{ typeof hint === 'string' ? (
-
+
) : hint }
diff --git a/ui/snippets/colorMode/ColorModeToggle.tsx b/ui/snippets/colorMode/ColorModeToggle.tsx
new file mode 100644
index 0000000000..ce5bda8b1b
--- /dev/null
+++ b/ui/snippets/colorMode/ColorModeToggle.tsx
@@ -0,0 +1,101 @@
+import { Text } from '@chakra-ui/react';
+import React from 'react';
+
+import * as cookies from 'lib/cookies';
+import { COLOR_THEMES } from 'lib/settings/colorTheme';
+import { Button } from 'toolkit/chakra/button';
+import { useColorMode } from 'toolkit/chakra/color-mode';
+import { IconButton } from 'toolkit/chakra/icon-button';
+import { Tooltip } from 'toolkit/chakra/tooltip';
+import IconSvg from 'ui/shared/IconSvg';
+
+type Props = {
+ variant?: 'icon' | 'full';
+};
+
+const getTheme = (mode: 'light' | 'dark') => {
+ if (mode === 'light') {
+ return COLOR_THEMES.find((theme) => theme.id === 'light') ?? COLOR_THEMES[0];
+ }
+
+ return COLOR_THEMES.find((theme) => theme.id === 'dark') ?? COLOR_THEMES.filter((theme) => theme.colorMode === 'dark').slice(-1)[0];
+};
+
+const persistColorMode = (mode: 'light' | 'dark') => {
+ const theme = getTheme(mode);
+ const varName = theme.colorMode === 'light' ? '--chakra-colors-white' : '--chakra-colors-black';
+
+ window.document.documentElement.style.setProperty(varName, theme.hex);
+ cookies.set(cookies.NAMES.COLOR_MODE_HEX, theme.hex);
+ cookies.set(cookies.NAMES.COLOR_MODE, theme.colorMode);
+ window.localStorage.setItem(cookies.NAMES.COLOR_MODE, theme.colorMode);
+};
+
+const ColorModeToggle = ({ variant = 'icon' }: Props) => {
+ const { colorMode, setColorMode } = useColorMode();
+ const nextMode = colorMode === 'light' ? 'dark' : 'light';
+ const label = colorMode === 'light' ? 'Dark mode' : 'Light mode';
+ const iconName = colorMode === 'light' ? 'moon' : 'sun';
+
+ const handleClick = React.useCallback(() => {
+ setColorMode(nextMode);
+ persistColorMode(nextMode);
+ }, [ nextMode, setColorMode ]);
+
+ if (variant === 'full') {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default React.memo(ColorModeToggle);
diff --git a/ui/snippets/header/HeaderDesktop.tsx b/ui/snippets/header/HeaderDesktop.tsx
index 29f301e237..53d4580a09 100644
--- a/ui/snippets/header/HeaderDesktop.tsx
+++ b/ui/snippets/header/HeaderDesktop.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import config from 'configs/app';
import RewardsButton from 'ui/rewards/RewardsButton';
+import ColorModeToggle from 'ui/snippets/colorMode/ColorModeToggle';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import UserProfileDesktop from 'ui/snippets/user/profile/UserProfileDesktop';
import UserWalletDesktop from 'ui/snippets/user/wallet/UserWalletDesktop';
@@ -21,8 +22,9 @@ const HeaderDesktop = ({ hideSearchBar, renderSearchBar }: Props) => {
as="header"
display={{ base: 'none', lg: 'flex' }}
width="100%"
+ minH="50px"
alignItems="center"
- justifyContent="center"
+ justifyContent={ hideSearchBar ? 'flex-end' : 'center' }
gap={ 6 }
>
{ !hideSearchBar && (
@@ -30,15 +32,14 @@ const HeaderDesktop = ({ hideSearchBar, renderSearchBar }: Props) => {
{ searchBar }
) }
- { config.UI.navigation.layout === 'vertical' && (
-
- { config.features.rewards.isEnabled && }
- {
- (config.features.account.isEnabled && ) ||
- (config.features.blockchainInteraction.isEnabled && )
- }
-
- ) }
+
+
+ { config.features.rewards.isEnabled && }
+ {
+ (config.features.account.isEnabled && ) ||
+ (config.features.blockchainInteraction.isEnabled && )
+ }
+
);
};
diff --git a/ui/snippets/header/HeaderMobile.tsx b/ui/snippets/header/HeaderMobile.tsx
index 4db200e123..1f4652cba3 100644
--- a/ui/snippets/header/HeaderMobile.tsx
+++ b/ui/snippets/header/HeaderMobile.tsx
@@ -5,6 +5,7 @@ import { useInView } from 'react-intersection-observer';
import config from 'configs/app';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import RewardsButton from 'ui/rewards/RewardsButton';
+import ColorModeToggle from 'ui/snippets/colorMode/ColorModeToggle';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import UserProfileMobile from 'ui/snippets/user/profile/UserProfileMobile';
@@ -49,6 +50,7 @@ const HeaderMobile = ({ hideSearchBar, renderSearchBar }: Props) => {
+
{ config.features.rewards.isEnabled && }
{
(config.features.account.isEnabled && ) ||
diff --git a/ui/snippets/navigation/useNavLinkStyleProps.tsx b/ui/snippets/navigation/useNavLinkStyleProps.tsx
index d1d4d4e410..f33f618ebc 100644
--- a/ui/snippets/navigation/useNavLinkStyleProps.tsx
+++ b/ui/snippets/navigation/useNavLinkStyleProps.tsx
@@ -22,8 +22,8 @@ export default function useNavLinkStyleProps({ isActive }: Props) {
fontWeight: isActive ? 500 : 400,
lineHeight: '1.3',
opacity: 1,
- fontFamily: 'system-ui, -apple-system, sans-serif',
- letterSpacing: '0.02em',
+ fontFamily: '"Geist Mono", ui-monospace, monospace',
+ letterSpacing: '0.08em',
textTransform: 'uppercase',
},
};
diff --git a/ui/snippets/navigation/vertical/NavLink.tsx b/ui/snippets/navigation/vertical/NavLink.tsx
index 3abcc86ca7..bb4c0df1eb 100644
--- a/ui/snippets/navigation/vertical/NavLink.tsx
+++ b/ui/snippets/navigation/vertical/NavLink.tsx
@@ -37,16 +37,24 @@ const NavLink = ({ item, onClick, isDisabled }: Props) => {
aria-label={ `${ item.text } link` }
whiteSpace="nowrap"
onClick={ onClick }
- bgColor="transparent"
+ bgColor={ isInternalLink && item.isActive ?
+ { _light: 'rgba(36, 188, 227, 0.09)', _dark: 'rgba(36, 188, 227, 0.12)' } :
+ 'transparent'
+ }
+ borderRadius="8px"
color={ isInternalLink && item.isActive ?
- { _light: 'rgba(0, 0, 0, 0.95)', _dark: 'rgba(255, 255, 255, 0.98)' } :
- { _light: 'rgba(0, 0, 0, 0.65)', _dark: 'rgba(255, 255, 255, 0.75)' }
+ { _light: '#0e4b5b', _dark: '#bdebf7' } :
+ { _light: 'rgba(14, 75, 91, 0.65)', _dark: 'rgba(189, 235, 247, 0.55)' }
}
transition="all 0.15s ease-out"
_hover={{
color: isDisabled ? undefined : {
- _light: 'rgba(0, 0, 0, 0.9)',
- _dark: 'rgba(255, 255, 255, 0.95)',
+ _light: '#1d96b6',
+ _dark: '#50c9e9',
+ },
+ bgColor: isDisabled ? undefined : {
+ _light: 'rgba(36, 188, 227, 0.07)',
+ _dark: 'rgba(36, 188, 227, 0.10)',
},
}}
>
@@ -75,7 +83,8 @@ const NavLink = ({ item, onClick, isDisabled }: Props) => {
w="6px"
h="6px"
borderRadius="50%"
- bg={{ _light: 'cyan.600', _dark: 'cyan.300' }}
+ bg="#24bce3"
+ boxShadow="0 0 8px rgba(36, 188, 227, 0.7)"
ml={ 2 }
flexShrink={ 0 }
/>
diff --git a/ui/snippets/navigation/vertical/NavLinkGroup.tsx b/ui/snippets/navigation/vertical/NavLinkGroup.tsx
index 1f6b8d6d16..1bbab9bf79 100644
--- a/ui/snippets/navigation/vertical/NavLinkGroup.tsx
+++ b/ui/snippets/navigation/vertical/NavLinkGroup.tsx
@@ -26,19 +26,20 @@ const NavLinkGroup = ({ item }: Props) => {
@@ -51,7 +52,7 @@ const NavLinkGroup = ({ item }: Props) => {
mb: 2,
pb: 2,
borderBottomWidth: '1px',
- borderColor: { _light: 'rgba(0, 0, 0, 0.08)', _dark: 'rgba(255, 255, 255, 0.12)' },
+ borderColor: { _light: 'rgba(36, 188, 227, 0.14)', _dark: 'rgba(189, 235, 247, 0.10)' },
}}
>
{ subItem.map(subSubItem => ) }
@@ -79,8 +80,8 @@ const NavLinkGroup = ({ item }: Props) => {
contentProps={{
p: 0,
boxShadow: {
- _light: '0 8px 24px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1)',
- _dark: '0 8px 24px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4)',
+ _light: '0 18px 50px rgba(14, 75, 91, 0.14)',
+ _dark: '0 18px 52px rgba(0, 0, 0, 0.46)',
},
}}
>
@@ -89,10 +90,14 @@ const NavLinkGroup = ({ item }: Props) => {
aria-label={ `${ item.text } link group` }
position="relative"
cursor="pointer"
- bgColor="transparent"
+ bgColor={ item.isActive ?
+ { _light: 'rgba(36, 188, 227, 0.09)', _dark: 'rgba(36, 188, 227, 0.12)' } :
+ 'transparent'
+ }
+ borderRadius="8px"
color={ item.isActive ?
- { _light: 'rgba(0, 0, 0, 0.95)', _dark: 'rgba(255, 255, 255, 0.98)' } :
- { _light: 'rgba(0, 0, 0, 0.65)', _dark: 'rgba(255, 255, 255, 0.75)' }
+ { _light: '#0e4b5b', _dark: '#bdebf7' } :
+ { _light: 'rgba(14, 75, 91, 0.65)', _dark: 'rgba(189, 235, 247, 0.55)' }
}
transition="all 0.15s ease-out"
py={ 3 }
@@ -101,8 +106,12 @@ const NavLinkGroup = ({ item }: Props) => {
{ ...(item.isActive ? { 'data-selected': true } : {}) }
_hover={{
color: {
- _light: 'rgba(0, 0, 0, 0.9)',
- _dark: 'rgba(255, 255, 255, 0.95)',
+ _light: '#1d96b6',
+ _dark: '#50c9e9',
+ },
+ bgColor: {
+ _light: 'rgba(36, 188, 227, 0.07)',
+ _dark: 'rgba(36, 188, 227, 0.10)',
},
}}
>
diff --git a/ui/snippets/navigation/vertical/NavigationDesktop.tsx b/ui/snippets/navigation/vertical/NavigationDesktop.tsx
index 91876c113a..ba91c511df 100644
--- a/ui/snippets/navigation/vertical/NavigationDesktop.tsx
+++ b/ui/snippets/navigation/vertical/NavigationDesktop.tsx
@@ -22,14 +22,13 @@ const NavigationDesktop = () => {
position="relative"
flexDirection="column"
alignItems="stretch"
- bgColor={{
- _light: 'linear-gradient(180deg, #ffffff 0%, #fafbfc 100%)',
- _dark: '#0a0a0a',
- }}
+ bgColor={{ _light: '#fcfdfe', _dark: '#0a0f19' }}
backgroundImage={{
- _light: 'linear-gradient(180deg, #ffffff 0%, #fafbfc 100%)',
- _dark: '#0a0a0a',
+ _light: 'linear-gradient(180deg, #fcfdfe 0%, #f4fcfe 100%)',
+ _dark: 'linear-gradient(180deg, #0a0f19 0%, #0f1626 100%)',
}}
+ borderRight="1px solid"
+ borderColor={{ _light: 'rgba(36, 188, 227, 0.10)', _dark: 'rgba(36, 188, 227, 0.10)' }}
px={ 6 }
py={ 10 }
width="260px"
@@ -47,7 +46,7 @@ const NavigationDesktop = () => {
mb={ 10 }
pb={ 8 }
borderBottom="1px solid"
- borderColor={{ _light: 'rgba(0, 0, 0, 0.08)', _dark: 'rgba(255, 255, 255, 0.12)' }}
+ borderColor={{ _light: 'rgba(36, 188, 227, 0.14)', _dark: 'rgba(189, 235, 247, 0.12)' }}
position="relative"
_after={{
content: '""',
@@ -57,8 +56,8 @@ const NavigationDesktop = () => {
right: 0,
height: '1px',
background: {
- _light: 'linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.1) 50%, transparent 100%)',
- _dark: 'linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%)',
+ _light: 'linear-gradient(90deg, transparent 0%, rgba(36, 188, 227, 0.28) 50%, transparent 100%)',
+ _dark: 'linear-gradient(90deg, transparent 0%, rgba(80, 201, 233, 0.24) 50%, transparent 100%)',
},
}}
>
@@ -96,7 +95,7 @@ const NavigationDesktop = () => {
{
right: 0,
height: '1px',
background: {
- _light: 'linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.1) 50%, transparent 100%)',
- _dark: 'linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%)',
+ _light: 'linear-gradient(90deg, transparent 0%, rgba(36, 188, 227, 0.28) 50%, transparent 100%)',
+ _dark: 'linear-gradient(90deg, transparent 0%, rgba(80, 201, 233, 0.24) 50%, transparent 100%)',
},
}}
>
diff --git a/ui/snippets/networkMenu/NetworkLogo.tsx b/ui/snippets/networkMenu/NetworkLogo.tsx
index 82449d2756..f5cd467bae 100644
--- a/ui/snippets/networkMenu/NetworkLogo.tsx
+++ b/ui/snippets/networkMenu/NetworkLogo.tsx
@@ -47,12 +47,16 @@ const NetworkLogo = ({ isCollapsed, onClick, className }: Props) => {
, 'onChange'> {
onClear: () => void;
isHomepage?: boolean;
isSuggestOpen?: boolean;
- value: string;
+ value?: string;
}
const SearchBarInput = (
@@ -141,39 +141,45 @@ const SearchBarInput = (
return { _light: 'blackAlpha.100', _dark: 'whiteAlpha.200' };
}
if (isFocused) {
- return { _light: 'rgba(64, 209, 219, 0.8)', _dark: 'rgba(64, 209, 219, 0.9)' };
+ return { _light: 'rgba(36, 188, 227, 0.75)', _dark: 'rgba(80, 201, 233, 0.82)' };
}
- return { _light: 'rgba(0, 0, 0, 0.1)', _dark: 'rgba(255, 255, 255, 0.15)' };
+ return { _light: 'rgba(36, 188, 227, 0.20)', _dark: 'rgba(189, 235, 247, 0.16)' };
};
const getInputBoxShadow = () => {
if (!isHomepage) {
return undefined;
}
- return 'none';
+ return isFocused ?
+ { _light: '0 0 0 3px rgba(36, 188, 227, 0.12)', _dark: '0 0 0 3px rgba(80, 201, 233, 0.14)' } :
+ { _light: '0 10px 26px rgba(14, 75, 91, 0.06)', _dark: '0 12px 30px rgba(0, 0, 0, 0.20)' };
};
const inputBorderColor = getInputBorderColor();
const inputBoxShadow = getInputBoxShadow();
+ const safeValue = value ?? '';
const startElement = undefined;
const endElement = (
<>
- 0 } mx={ isHomepage ? { base: 3, md: 4 } : 2 }/>
+ 0 } mx={ isHomepage ? { base: 3, md: 4 } : 2 }/>
-
+
>
);
@@ -206,57 +212,52 @@ const SearchBarInput = (
>
| QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
@@ -101,7 +101,6 @@ const TxsContent = ({
const actionBar = isMobile ? (
- { content }
+ { content && (
+
+ { content }
+
+ ) }
);
};