diff --git a/configs/app/ui.ts b/configs/app/ui.ts index 73308ede5b..f4ae2ff6e2 100644 --- a/configs/app/ui.ts +++ b/configs/app/ui.ts @@ -1,8 +1,9 @@ import type { ContractCodeIde } from 'types/client/contract'; import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation'; -import type { ChainIndicatorId } from 'types/homepage'; +import type { ChainIndicatorId, HeroBannerConfig } from 'types/homepage'; import type { NetworkExplorer } from 'types/networks'; import type { ColorThemeId } from 'types/settings'; +import type { FontFamily } from 'types/ui'; import { COLOR_THEMES } from 'lib/settings/colorTheme'; @@ -34,9 +35,6 @@ const defaultColorTheme = (() => { return COLOR_THEMES.find((theme) => theme.id === envValue); })(); -// eslint-disable-next-line max-len -const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; - const UI = Object.freeze({ navigation: { logo: { @@ -60,9 +58,10 @@ const UI = Object.freeze({ }, homepage: { charts: parseEnvJson>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [], + heroBanner: parseEnvJson(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')), plate: { - background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT, - textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white', + background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND'), + textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR'), }, showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true, }, @@ -88,6 +87,10 @@ const UI = Object.freeze({ colorTheme: { 'default': defaultColorTheme, }, + fonts: { + heading: parseEnvJson(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_HEADING')), + body: parseEnvJson(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_BODY')), + }, }); export default UI; diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index 9770ea6399..d2c35ba50a 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -20,6 +20,7 @@ async function run() { return result; }, {} as Record); + printDeprecationWarning(appEnvs); await checkPlaceholdersCongruity(appEnvs); await validateEnvs(appEnvs); @@ -135,3 +136,15 @@ function getEnvsPlaceholders(filePath: string): Promise> { }); }); } + +function printDeprecationWarning(envsMap: Record) { + if ( + envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR || + envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND + ) { + console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗'); + // eslint-disable-next-line max-len + console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.'); + console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n'); + } +} diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index c06ec69e9e..fa1e1d72b4 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -30,9 +30,10 @@ import type { WalletType } from '../../../types/client/wallets'; import { SUPPORTED_WALLETS } from '../../../types/client/wallets'; import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks'; import { CHAIN_INDICATOR_IDS } from '../../../types/homepage'; -import type { ChainIndicatorId } from '../../../types/homepage'; +import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig } from '../../../types/homepage'; import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks'; import { COLOR_THEME_IDS } from '../../../types/settings'; +import type { FontFamily } from '../../../types/ui'; import type { AddressViewId } from '../../../types/views/address'; import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address'; import { BLOCK_FIELDS_IDS } from '../../../types/views/block'; @@ -390,6 +391,34 @@ const navItemExternalSchema: yup.ObjectSchema = yup url: yup.string().test(urlTest).required(), }); +const fontFamilySchema: yup.ObjectSchema = yup + .object() + .transform(replaceQuotes) + .json() + .shape({ + name: yup.string().required(), + url: yup.string().test(urlTest).required(), + }); + +const heroBannerButtonStateSchema: yup.ObjectSchema = yup.object({ + background: yup.array().max(2).of(yup.string()), + text_color: yup.array().max(2).of(yup.string()), +}); + +const heroBannerSchema: yup.ObjectSchema = yup.object() + .transform(replaceQuotes) + .json() + .shape({ + background: yup.array().max(2).of(yup.string()), + text_color: yup.array().max(2).of(yup.string()), + border: yup.array().max(2).of(yup.string()), + button: yup.object({ + _default: heroBannerButtonStateSchema, + _hover: heroBannerButtonStateSchema, + _selected: heroBannerButtonStateSchema, + }), + }); + const footerLinkSchema: yup.ObjectSchema = yup .object({ text: yup.string().required(), @@ -540,6 +569,23 @@ const schema = yup .of(yup.string().oneOf(CHAIN_INDICATOR_IDS)), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), + NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup + .mixed() + .test( + 'shape', + (ctx) => { + try { + heroBannerSchema.validateSync(ctx.originalValue); + throw new Error('Unknown validation error'); + } catch (error: unknown) { + const message = typeof error === 'object' && error !== null && 'errors' in error && Array.isArray(error.errors) ? error.errors.join(', ') : ''; + return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : ''); + } + }, + (data) => { + const isUndefined = data === undefined; + return isUndefined || heroBannerSchema.isValidSync(data); + }), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), // b. sidebar @@ -634,6 +680,18 @@ const schema = yup NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(), NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(), NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS), + NEXT_PUBLIC_FONT_FAMILY_HEADING: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => { + const isUndefined = data === undefined; + return isUndefined || fontFamilySchema.isValidSync(data); + }), + NEXT_PUBLIC_FONT_FAMILY_BODY: yup + .mixed() + .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => { + const isUndefined = data === undefined; + return isUndefined || fontFamilySchema.isValidSync(data); + }), // 5. Features configuration NEXT_PUBLIC_API_SPEC_URL: yup diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 607ff38d36..064bb48199 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -28,6 +28,8 @@ NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps'] NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal +NEXT_PUBLIC_FONT_FAMILY_HEADING={'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'} +NEXT_PUBLIC_FONT_FAMILY_BODY={'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'} NEXT_PUBLIC_FOOTER_LINKS=https://example.com NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false @@ -35,6 +37,7 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff' NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)' +NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']} NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_GAS_TRACKER_ENABLED=true NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] diff --git a/docs/ENVS.md b/docs/ENVS.md index 6909cdc1c7..c2397ce2ed 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -118,9 +118,21 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ | -| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | v1.0.x+ | -| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ | +| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `white` | `\#DCFE76` | v1.0.x+ | +| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | +| NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ | + +#### Hero banner configuration properties + +_Note_ Here, all values are arrays of up to two strings. The first string represents the value for the light color mode, and the second string represents the value for the dark color mode. If the array contains only one string, it will be used for both color modes. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| background | `[string, string]` | Banner background (could be a solid color, gradient or picture). The string should be a valid `background` CSS property value. | - | `['radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)']` | `['lightpink','no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)']` | +| text_color | `[string, string]` | Banner text background. The string should be a valid `color` CSS property value. | - | `['white']` | `['lightpink','#DCFE76']` | +| border | `[string, string]` | Banner border. The string should be a valid `border` CSS property value. | - | - | `['1px solid yellow','4px dashed #DCFE76']` | +| button | `Partial>` | The button on the banner. It has three possible states: `_default`, `_hover`, and `_selected`. The `_selected` state reflects when the user is logged in or their wallet is connected to the app. | - | - | `{'_default':{'background':['deeppink'],'text_color':['white']}}` |   @@ -286,6 +298,8 @@ Settings for meta tags, OG tags and SEO | NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ | | NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ | | NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ | +| NEXT_PUBLIC_FONT_FAMILY_HEADING | `FontFamily`, see full description [below](#font-family-configuration-properties) | Special typeface to use in page headings (`

`, `

`, etc.) | - | - | `{'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}` | v1.35.0+ | +| NEXT_PUBLIC_FONT_FAMILY_BODY | `FontFamily`, see full description [below](#font-family-configuration-properties) | Main typeface to use in page content elements. | - | - | `{'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}` | v1.35.0+ | #### Network explorer configuration properties @@ -306,6 +320,13 @@ Settings for meta tags, OG tags and SEO | url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` | | icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` | +#### Font family configuration properties + +| Variable | Type| Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| name | `string` | Font family name; used to define the `font-family` CSS property. | Required | - | `Montserrat` | +| url | `string` | URL for external font. Ensure the font supports the following weights: 400, 500, 600, and 700. | Required | - | `https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap` | +   ## App features diff --git a/nextjs/csp/policies/app.ts b/nextjs/csp/policies/app.ts index de6feff4f8..1b7a495284 100644 --- a/nextjs/csp/policies/app.ts +++ b/nextjs/csp/policies/app.ts @@ -30,6 +30,18 @@ const getCspReportUrl = () => { } }; +const externalFontsDomains = (() => { + try { + return [ + config.UI.fonts.heading?.url, + config.UI.fonts.body?.url, + ] + .filter(Boolean) + .map((urlString) => new URL(urlString)) + .map((url) => url.hostname); + } catch (error) {} +})(); + export function app(): CspDev.DirectiveDescriptor { return { 'default-src': [ @@ -116,6 +128,7 @@ export function app(): CspDev.DirectiveDescriptor { 'font-src': [ KEY_WORDS.DATA, ...MAIN_DOMAINS, + ...(externalFontsDomains || []), ], 'object-src': [ diff --git a/pages/_document.tsx b/pages/_document.tsx index fa7c1ab184..2e879910dd 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -6,6 +6,7 @@ import React from 'react'; import logRequestFromBot from 'nextjs/utils/logRequestFromBot'; import * as serverTiming from 'nextjs/utils/serverTiming'; +import config from 'configs/app'; import theme from 'theme/theme'; import * as svgSprite from 'ui/shared/IconSvg'; @@ -35,11 +36,11 @@ class MyDocument extends Document { { /* FONTS */ } diff --git a/theme/components/Button/Button.pw.tsx b/theme/components/Button/Button.pw.tsx index 5e50dbf2c8..e8b15075a7 100644 --- a/theme/components/Button/Button.pw.tsx +++ b/theme/components/Button/Button.pw.tsx @@ -13,6 +13,8 @@ test.use({ viewport: { width: 150, height: 350 } }); { variant: 'ghost', withDarkMode: true, states: [ 'default', 'hovered', 'active' ] }, { variant: 'subtle', states: [ 'default', 'hovered' ] }, { variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true }, + { variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true }, + { variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true }, ].forEach(({ variant, colorScheme, withDarkMode, states }) => { test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => { test('', async({ render }) => { diff --git a/theme/components/Button/Button.ts b/theme/components/Button/Button.ts index 0cac051ad2..85f97fb8c2 100644 --- a/theme/components/Button/Button.ts +++ b/theme/components/Button/Button.ts @@ -2,6 +2,8 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; import { mode } from '@chakra-ui/theme-tools'; import { runIfFn } from '@chakra-ui/utils'; +import config from 'configs/app'; + const variantSolid = defineStyle((props) => { const { colorScheme: c } = props; @@ -150,12 +152,76 @@ const variantSubtle = defineStyle((props) => { }; }); +// for buttons in the hero banner +const variantHero = defineStyle((props) => { + return { + bg: mode( + config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600', + config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600', + )(props), + color: mode( + config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white', + config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white', + )(props), + _hover: { + bg: mode( + config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400', + config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400', + )(props), + color: mode( + config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white', + config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white', + )(props), + }, + '&[data-selected=true]': { + bg: mode( + config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50', + config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50', + )(props), + color: mode( + config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800', + config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800', + )(props), + }, + }; +}); + +// for buttons in the page header +const variantHeader = defineStyle((props) => { + + return { + bgColor: 'transparent', + color: mode('blackAlpha.800', 'gray.400')(props), + borderColor: mode('gray.300', 'gray.600')(props), + borderWidth: props.borderWidth || '2px', + borderStyle: 'solid', + _hover: { + color: 'link_hovered', + borderColor: 'link_hovered', + }, + '&[data-selected=true]': { + bgColor: mode('blackAlpha.50', 'whiteAlpha.100')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + borderColor: 'transparent', + borderWidth: props.borderWidth || '0px', + }, + '&[data-selected=true][data-warning=true]': { + bgColor: mode('orange.100', 'orange.900')(props), + color: mode('blackAlpha.800', 'whiteAlpha.800')(props), + borderColor: 'transparent', + borderWidth: props.borderWidth || '0px', + }, + }; +}); + const variants = { solid: variantSolid, outline: variantOutline, simple: variantSimple, ghost: variantGhost, subtle: variantSubtle, + hero: variantHero, + header: variantHeader, }; const baseStyle = defineStyle({ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-header-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-header-dark-mode-1.png new file mode 100644 index 0000000000..58c21cf5c4 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-header-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-hero-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-hero-dark-mode-1.png new file mode 100644 index 0000000000..fd0b3b4ae3 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_dark-color-mode_variant-hero-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-header-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-header-dark-mode-1.png new file mode 100644 index 0000000000..0488f1594c Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-header-dark-mode-1.png differ diff --git a/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-hero-dark-mode-1.png b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-hero-dark-mode-1.png new file mode 100644 index 0000000000..3d3967a965 Binary files /dev/null and b/theme/components/Button/__screenshots__/Button.pw.tsx_default_variant-hero-dark-mode-1.png differ diff --git a/theme/foundations/typography.ts b/theme/foundations/typography.ts index 57e59c4191..b129403a93 100644 --- a/theme/foundations/typography.ts +++ b/theme/foundations/typography.ts @@ -1,7 +1,9 @@ import { theme } from '@chakra-ui/react'; -export const BODY_TYPEFACE = 'Inter'; -export const HEADING_TYPEFACE = 'Poppins'; +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'; const typography = { fonts: { diff --git a/types/homepage.ts b/types/homepage.ts index 2492134e97..6bc73ee8d3 100644 --- a/types/homepage.ts +++ b/types/homepage.ts @@ -1,2 +1,18 @@ export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const; export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number]; + +export interface HeroBannerButtonState { + background?: Array; + text_color?: Array; +} + +export interface HeroBannerConfig { + background?: Array; + text_color?: Array; + border?: Array; + button?: { + _default?: HeroBannerButtonState; + _hover?: HeroBannerButtonState; + _selected?: HeroBannerButtonState; + }; +} diff --git a/types/ui.ts b/types/ui.ts new file mode 100644 index 0000000000..119920c0f3 --- /dev/null +++ b/types/ui.ts @@ -0,0 +1,4 @@ +export interface FontFamily { + name: string; + url: string; +} diff --git a/ui/home/HeroBanner.pw.tsx b/ui/home/HeroBanner.pw.tsx new file mode 100644 index 0000000000..ea9735a78f --- /dev/null +++ b/ui/home/HeroBanner.pw.tsx @@ -0,0 +1,39 @@ +import type { BrowserContext } from '@playwright/test'; +import React from 'react'; + +import * as profileMock from 'mocks/user/profile'; +import { contextWithAuth } from 'playwright/fixtures/auth'; +import { test, expect } from 'playwright/lib'; +import * as pwConfig from 'playwright/utils/config'; + +import HeroBanner from './HeroBanner'; + +const authTest = test.extend<{ context: BrowserContext }>({ + context: contextWithAuth, +}); + +authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse, mockAssetResponse }) => { + const IMAGE_URL = 'https://localhost:3000/my-image.png'; + + await mockEnvs([ + // eslint-disable-next-line max-len + [ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ], + ]); + + await page.route(IMAGE_URL, (route) => { + return route.fulfill({ + status: 200, + path: './playwright/mocks/image_long.jpg', + }); + }); + + await mockApiResponse('user_info', profileMock.base); + await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg'); + + const component = await render(); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator(pwConfig.adsBannerSelector) ], + maskColor: pwConfig.maskColor, + }); +}); diff --git a/ui/home/HeroBanner.tsx b/ui/home/HeroBanner.tsx new file mode 100644 index 0000000000..9ad893a668 --- /dev/null +++ b/ui/home/HeroBanner.tsx @@ -0,0 +1,69 @@ +import { Box, Flex, Heading, useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; + +import config from 'configs/app'; +import AdBanner from 'ui/shared/ad/AdBanner'; +import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; +import SearchBar from 'ui/snippets/searchBar/SearchBar'; +import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; + +const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; +const TEXT_COLOR_DEFAULT = 'white'; +const BORDER_DEFAULT = 'none'; + +const HeroBanner = () => { + const background = useColorModeValue( + config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, + config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, + ); + + const textColor = useColorModeValue( + config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, + config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, + ); + + const border = useColorModeValue( + config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT, + config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT, + ); + + return ( + + + + + { + config.meta.seo.enhancedDataEnabled ? + `${ config.chain.name } blockchain explorer` : + `${ config.chain.name } explorer` + } + + { config.UI.navigation.layout === 'vertical' && ( + + { config.features.account.isEnabled && } + { config.features.blockchainInteraction.isEnabled && } + + ) } + + + + + + ); +}; + +export default React.memo(HeroBanner); diff --git a/ui/home/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png b/ui/home/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png new file mode 100644 index 0000000000..1c941071f9 Binary files /dev/null and b/ui/home/__screenshots__/HeroBanner.pw.tsx_dark-color-mode_customization-dark-mode-1.png differ diff --git a/ui/home/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png b/ui/home/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png new file mode 100644 index 0000000000..7f5eda5ad9 Binary files /dev/null and b/ui/home/__screenshots__/HeroBanner.pw.tsx_default_customization-dark-mode-1.png differ diff --git a/ui/pages/Home.pw.tsx b/ui/pages/Home.pw.tsx index 8e60696696..58287f4943 100644 --- a/ui/pages/Home.pw.tsx +++ b/ui/pages/Home.pw.tsx @@ -49,33 +49,6 @@ test.describe('default view', () => { }); }); -test.describe('custom hero plate background', () => { - const IMAGE_URL = 'https://localhost:3000/my-image.png'; - test.beforeEach(async({ mockEnvs }) => { - await mockEnvs([ - [ 'NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND', `no-repeat center/cover url(${ IMAGE_URL })` ], - ]); - }); - - test('default view', async({ render, page }) => { - await page.route(IMAGE_URL, (route) => { - return route.fulfill({ - status: 200, - path: './playwright/mocks/image_long.jpg', - }); - }); - - const component = await render(); - - const heroPlate = component.locator('div[data-label="hero plate"]'); - - await expect(heroPlate).toHaveScreenshot({ - mask: [ page.locator(pwConfig.adsBannerSelector) ], - maskColor: pwConfig.maskColor, - }); - }); -}); - // had to separate mobile test, otherwise all the tests fell on CI test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); diff --git a/ui/pages/Home.tsx b/ui/pages/Home.tsx index 7de01cbe63..42a45e6cae 100644 --- a/ui/pages/Home.tsx +++ b/ui/pages/Home.tsx @@ -1,7 +1,8 @@ -import { Box, Flex, Heading } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import React from 'react'; import config from 'configs/app'; +import HeroBanner from 'ui/home/HeroBanner'; import ChainIndicators from 'ui/home/indicators/ChainIndicators'; import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batches'; import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches'; @@ -9,50 +10,13 @@ import LatestBlocks from 'ui/home/LatestBlocks'; import Stats from 'ui/home/Stats'; import Transactions from 'ui/home/Transactions'; import AdBanner from 'ui/shared/ad/AdBanner'; -import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; -import SearchBar from 'ui/snippets/searchBar/SearchBar'; -import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; const rollupFeature = config.features.rollup; const Home = () => { return ( - - - - - { - config.meta.seo.enhancedDataEnabled ? - `${ config.chain.name } blockchain explorer` : - `${ config.chain.name } explorer` - } - - { config.UI.navigation.layout === 'vertical' && ( - - { config.features.account.isEnabled && } - { config.features.blockchainInteraction.isEnabled && } - - ) } - - - - - + diff --git a/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png b/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png deleted file mode 100644 index ebf021d6da..0000000000 Binary files a/ui/pages/__screenshots__/Home.pw.tsx_default_custom-hero-plate-background-default-view-1.png and /dev/null differ diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index 222a9423a2..d0e0e05fcd 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -9,8 +9,6 @@ import Popover from 'ui/shared/chakra/Popover'; import UserAvatar from 'ui/shared/UserAvatar'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; -import useMenuButtonColors from '../useMenuButtonColors'; - type Props = { isHomePage?: boolean; className?: string; @@ -21,7 +19,6 @@ type Props = { const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBoxSize }: Props) => { const { data, error, isPending } = useFetchProfileInfo(); const loginUrl = useLoginUrl(); - const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const [ hasMenu, setHasMenu ] = React.useState(false); React.useEffect(() => { @@ -50,29 +47,6 @@ const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBox }; })(); - const variant = React.useMemo(() => { - if (hasMenu) { - return 'subtle'; - } - return isHomePage ? 'solid' : 'outline'; - }, [ hasMenu, isHomePage ]); - - let iconButtonStyles: Partial = {}; - if (hasMenu) { - iconButtonStyles = { - bg: isHomePage ? 'blue.50' : themedBackground, - }; - } else if (isHomePage) { - iconButtonStyles = { - color: 'white', - }; - } else { - iconButtonStyles = { - borderColor: themedBorderColor, - color: themedColor, - }; - } - return ( } - variant={ variant } - colorScheme="blue" + variant={ isHomePage ? 'hero' : 'header' } + data-selected={ hasMenu } boxSize={ buttonBoxSize ?? '40px' } flexShrink={ 0 } { ...iconButtonProps } - { ...iconButtonStyles } /> diff --git a/ui/snippets/profileMenu/ProfileMenuMobile.tsx b/ui/snippets/profileMenu/ProfileMenuMobile.tsx index b50367595b..51feea6f4f 100644 --- a/ui/snippets/profileMenu/ProfileMenuMobile.tsx +++ b/ui/snippets/profileMenu/ProfileMenuMobile.tsx @@ -8,13 +8,10 @@ import * as mixpanel from 'lib/mixpanel/index'; import UserAvatar from 'ui/shared/UserAvatar'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; -import useMenuButtonColors from '../useMenuButtonColors'; - const ProfileMenuMobile = () => { const { isOpen, onOpen, onClose } = useDisclosure(); const { data, error, isPending } = useFetchProfileInfo(); const loginUrl = useLoginUrl(); - const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const [ hasMenu, setHasMenu ] = React.useState(false); const handleSignInClick = React.useCallback(() => { @@ -48,13 +45,10 @@ const ProfileMenuMobile = () => { } - variant={ data?.avatar ? 'subtle' : 'outline' } - colorScheme="gray" + variant="header" + data-selected={ hasMenu } boxSize="40px" flexShrink={ 0 } - bg={ data?.avatar ? themedBackground : undefined } - color={ themedColor } - borderColor={ !data?.avatar ? themedBorderColor : undefined } onClick={ hasMenu ? onOpen : undefined } { ...iconButtonProps } /> diff --git a/ui/snippets/useMenuButtonColors.tsx b/ui/snippets/useMenuButtonColors.tsx deleted file mode 100644 index cbd3e9bbd3..0000000000 --- a/ui/snippets/useMenuButtonColors.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useColorModeValue } from '@chakra-ui/react'; - -export default function useMenuColors() { - const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); - const themedBackgroundOrange = useColorModeValue('orange.100', 'orange.900'); - const themedBorderColor = useColorModeValue('gray.300', 'gray.700'); - const themedColor = useColorModeValue('blackAlpha.800', 'gray.400'); - - return { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor }; -} diff --git a/ui/snippets/walletMenu/WalletIdenticon.tsx b/ui/snippets/walletMenu/WalletIdenticon.tsx index 95a10a7334..3cdb29ffa2 100644 --- a/ui/snippets/walletMenu/WalletIdenticon.tsx +++ b/ui/snippets/walletMenu/WalletIdenticon.tsx @@ -1,12 +1,10 @@ -import { Box, Flex, chakra } from '@chakra-ui/react'; +import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react'; import React from 'react'; import useIsMobile from 'lib/hooks/useIsMobile'; import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import IconSvg from 'ui/shared/IconSvg'; -import useMenuButtonColors from '../useMenuButtonColors'; - type Props = { address: string; isAutoConnectDisabled?: boolean; @@ -14,8 +12,8 @@ type Props = { }; const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) => { - const { themedBackgroundOrange } = useMenuButtonColors(); const isMobile = useIsMobile(); + const borderColor = useColorModeValue('orange.100', 'orange.900'); return ( @@ -31,7 +29,7 @@ const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) = backgroundColor="rgba(16, 17, 18, 0.80)" borderRadius="full" border="1px solid" - borderColor={ themedBackgroundOrange } + borderColor={ borderColor } > { - const { themedBackgroundOrange } = useMenuButtonColors(); + const bgColor = useColorModeValue('orange.100', 'orange.900'); const [ isModalOpening, setIsModalOpening ] = React.useState(false); const onAddressClick = React.useCallback(() => { @@ -39,7 +37,7 @@ const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDi p={ 3 } mb={ 3 } alignItems="center" - backgroundColor={ themedBackgroundOrange } + backgroundColor={ bgColor } > { - const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors(); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false); const isMobile = useIsMobile(); const { isAutoConnectDisabled } = useMarketplaceContext(); @@ -51,36 +48,6 @@ export const WalletMenuDesktop = ({ }, }); - const variant = React.useMemo(() => { - if (isWalletConnected) { - return 'subtle'; - } - return isHomePage ? 'solid' : 'outline'; - }, [ isWalletConnected, isHomePage ]); - - const themedColorForOrangeBg = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); - let buttonStyles: Partial = {}; - if (isWalletConnected) { - const backgroundColor = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground; - const color = isAutoConnectDisabled ? themedColorForOrangeBg : themedColor; - buttonStyles = { - bg: isHomePage ? 'blue.50' : backgroundColor, - color: isHomePage ? 'blackAlpha.800' : color, - _hover: { - color: isHomePage ? 'blackAlpha.800' : color, - }, - }; - } else if (isHomePage) { - buttonStyles = { - color: 'white', - }; - } else { - buttonStyles = { - borderColor: themedBorderColor, - color: themedColor, - }; - } - const openPopover = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' }); setIsPopoverOpen.toggle(); @@ -103,8 +70,9 @@ export const WalletMenuDesktop = ({ >