[website] - Add Wallet page (#419)

* feat: add wallet page and all its necessary components

* feat: border and address element

* feat: add navigation with responsive view

* feat: add floating menu for mobile version

* fix: floating menu mobile

* fix: menu floating and footer

* feat: adds prefooter and fixes footer and pages layouts

* fix: footer mobile border

* fix: pages with new layout

* fix: minor fixes

* fix: some minor fixes

* fix: changes from review

* fix: performance safari issue

* fix: changes from review

* feat: add grid hero component with proper variants

* fix: remove unnecessary prop

* feat: some improvements to shared components

* fix: nav-desktop z-index

* add passive option to scroll listener

* lint tailwind classnames

---------

Co-authored-by: Pavel Prichodko <14926950+prichodko@users.noreply.github.com>
This commit is contained in:
marcelines 2023-06-22 16:46:20 +01:00 committed by GitHub
parent b63515e65f
commit 9479b4bb2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1903 additions and 290 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,161 @@
import { cva } from 'class-variance-authority'
import Image from 'next/image'
import type { StaticImageData } from 'next/image'
// Variants for the grid hero class names
const biggerCardClassNames = cva(
[
'min-w-[350px] rounded-[40px]',
'px-[22px] py-6 sm:min-w-0 sm:px-[35px] sm:py-[48px] lg:px-5 xl:px-[34px] xl:py-[68px] 2xl:px-[73px]',
],
{
variants: {
color: {
yellow: ['bg-customisation-yellow/10'],
turquoise: ['bg-customisation-turquoise/10'],
purple: ['bg-customisation-purple/10'],
},
},
}
)
const imagesWithBorders = cva(['border-4', 'rounded-3xl'], {
variants: {
color: {
yellow: ['border-customisation-yellow/5'],
turquoise: ['border-customisation-turquoise/5'],
purple: ['border-customisation-purple/5'],
},
},
})
const imagesWithBordersTopOrBottom = cva(['border-4', 'rounded-3xl'], {
variants: {
color: {
yellow: ['border-customisation-yellow/5'],
turquoise: ['border-customisation-turquoise/5'],
purple: ['border-customisation-purple/5'],
},
alignment: {
top: ['border-t-0'],
bottom: ['border-b-0'],
},
},
})
const thirdCardClassNames = cva(
[
'flex min-w-[calc(50%-10px)] rounded-[40px]',
'px-[22px] sm:min-w-0 sm:px-[35px] lg:px-5 xl:px-[34px] 2xl:px-[73px]',
],
{
variants: {
color: {
yellow: ['bg-customisation-yellow/10'],
turquoise: ['bg-customisation-turquoise/10'],
purple: ['bg-customisation-purple/10'],
},
alignment: {
top: [
'pt-0',
'pb-6 sm:pb-[48px] xl:pb-[68px]',
'items-start',
'justify-start',
],
bottom: [
'pb-0',
'pt-6 sm:pt-[48px] xl:pt-[68px]',
'items-end',
'justify-end',
],
},
},
}
)
const fourthCardClassNames = cva(
[
'flex min-w-[calc(50%-10px)] rounded-[40px]',
'grow items-center justify-center px-0 sm:px-5 md:px-10 lg:px-5 2xl:px-10',
],
{
variants: {
color: {
yellow: ['bg-customisation-yellow/10'],
turquoise: ['bg-customisation-turquoise/10'],
purple: ['bg-customisation-purple/10'],
},
},
}
)
type Props = {
color: 'yellow' | 'turquoise' | 'purple'
cardOne: {
image: StaticImageData
alt: string
}
cardTwo: {
image: StaticImageData
alt: string
}
cardThree: {
image: StaticImageData
alt: string
alignment?: 'top' | 'bottom'
}
cardFour: {
image: StaticImageData
alt: string
}
}
const GridHero = (props: Props) => {
const { color, cardOne, cardTwo, cardThree, cardFour } = props
return (
<div className="relative z-[2] flex w-full max-w-[1504px] justify-start overflow-x-auto px-5 sm:justify-center sm:px-10">
<div className="flex min-w-[712px] flex-row gap-3 sm:min-w-fit sm:flex-col sm:gap-5 lg:flex-row">
<div className="flex flex-row gap-3 sm:gap-5">
<div className={biggerCardClassNames({ color })}>
<Image
src={cardOne.image}
alt={cardOne.alt}
className={imagesWithBorders({ color })}
/>
</div>
<div className={biggerCardClassNames({ color })}>
<Image
src={cardTwo.image}
alt={cardTwo.alt}
className={imagesWithBorders({ color })}
/>
</div>
</div>
<div className="flex min-w-[350px] flex-col gap-3 pr-5 sm:min-w-0 sm:flex-row sm:gap-5 sm:pr-0 lg:flex-col">
<div
className={thirdCardClassNames({
color,
alignment: cardThree.alignment || 'bottom',
})}
>
<Image
src={cardThree.image}
alt={cardThree.alt}
className={imagesWithBordersTopOrBottom({
color,
alignment: cardThree.alignment || 'bottom',
})}
/>
</div>
<div className={fourthCardClassNames({ color })}>
<Image src={cardFour.image} alt={cardFour.alt} />
</div>
</div>
</div>
</div>
)
}
export { GridHero }

View File

@ -0,0 +1,2 @@
export { GridHero } from './grid-hero'
export { Section } from './section'

View File

@ -0,0 +1,149 @@
import { Text } from '@status-im/components'
import { PopupIcon } from '@status-im/icons'
import { cva } from 'class-variance-authority'
import Image from 'next/image'
import { Border } from './border'
import type { StaticImageData } from 'next/image'
type Props = {
direction?: 'ltr' | 'rtl'
title: string
description: string
image: StaticImageData
imageAlt: string
imageSecondary: StaticImageData
imageSecondaryAlt: string
secondaryDescription: string
secondaryTitle: string
customNode?: React.ReactNode
color: 'yellow' | 'turquoise' | 'purple'
information?: string
}
const containerClassNames = cva(
[
'relative flex w-full justify-center',
'overflow-hidden',
'max-h-[854px]',
'max-w-[582px]',
'rounded-[32px]',
'py-[65px]',
],
{
variants: {
color: {
yellow: ['bg-customisation-yellow/10'],
turquoise: ['bg-customisation-turquoise/10'],
purple: ['bg-customisation-purple/10'],
},
},
}
)
const imageClassNames = cva(
['rounded-3xl', 'border-4', 'h-auto max-h-[724px] w-auto'],
{
variants: {
color: {
yellow: ['border-customisation-yellow/5'],
turquoise: ['border-customisation-turquoise/5'],
purple: ['border-customisation-purple/5'],
},
},
}
)
const borderContainerClassNames = cva(
['absolute left-0 top-0', 'w-full', 'h-[100%]'],
{
variants: {
color: {
yellow: ['text-customisation-yellow/5'],
turquoise: ['text-customisation-turquoise/5'],
purple: ['text-customisation-purple/5'],
},
},
}
)
const Section = (props: Props) => {
const {
title,
color,
description,
image,
imageAlt,
imageSecondary,
imageSecondaryAlt,
secondaryDescription,
secondaryTitle,
direction = 'ltr',
} = props
const directionOrder = direction === 'ltr' ? 'order-0' : 'order-1'
return (
<div className="flex justify-center">
<div className="relative z-[3] grid auto-cols-auto grid-flow-dense auto-rows-[1fr] grid-cols-1 gap-24 px-5 md:grid-cols-2 lg:gap-12 lg:px-[100px] xl:gap-[140px]">
<div
className={`${directionOrder} flex max-w-[1504px] justify-center overflow-hidden rounded-[32px]`}
>
<div className={containerClassNames({ color })}>
<div className={borderContainerClassNames({ color })}>
<Border />
</div>
<div className="absolute left-0 top-0 h-full w-full bg-[url('/assets/wallet/texture.png')] bg-contain bg-[left_top_0] bg-no-repeat" />
<Image
src={image}
alt={imageAlt}
className={imageClassNames({ color })}
/>
</div>
</div>
<div className="flex flex-col justify-start md:justify-center">
<div className="flex flex-col">
<Image
src={imageSecondary}
alt={imageSecondaryAlt}
width={48}
height={48}
/>
<div className="flex flex-col pt-4">
<Text size={27} weight="semibold">
{title}
</Text>
<div className="relative flex pt-1">
<Text size={27}>{description}</Text>
</div>
</div>
<div className="mt-16 flex flex-col rounded-[20px] border border-dashed border-neutral-80/20 p-4">
<Text size={19} weight="semibold">
{secondaryTitle}
</Text>
<div className="flex pt-1">
<Text size={19}>
{secondaryDescription}
{props.information && (
<span className="relative left-1 top-1 inline-block">
<PopupIcon size={20} color="$neutral-50" />
</span>
)}
</Text>
</div>
{props.customNode && (
<div className="mt-4 flex flex-col">{props.customNode}</div>
)}
</div>
</div>
</div>
</div>
</div>
)
}
export { Section }

File diff suppressed because one or more lines are too long

View File

@ -17,8 +17,8 @@ const DatePicker = (props: Props) => {
return ( return (
<div className="sticky bottom-5 flex justify-center"> <div className="sticky bottom-5 flex justify-center">
<Popover alignOffset={8} align="center" sideOffset={8}> <Popover alignOffset={8} align="center" sideOffset={8}>
<button className="border-neutral-80/5 bg-blur-neutral-5/70 active inline-flex min-h-[30px] cursor-pointer items-center justify-center gap-2 rounded-xl border-solid pl-3 pr-2 uppercase text-neutral-100 backdrop-blur-sm"> <button className="active inline-flex min-h-[30px] cursor-pointer items-center justify-center gap-2 rounded-xl border-solid border-neutral-80/5 bg-blur-neutral-5/70 pl-3 pr-2 uppercase text-neutral-100 backdrop-blur-sm">
<span className="text-blur-neutral-80/80 text-[13px] font-medium"> <span className="text-[13px] font-medium text-blur-neutral-80/80">
Filter between Filter between
</span> </span>
<span className="text-[13px] font-medium text-neutral-100"> <span className="text-[13px] font-medium text-neutral-100">
@ -26,7 +26,7 @@ const DatePicker = (props: Props) => {
selected?.to ? formatDate(selected.to) : 'End Date' selected?.to ? formatDate(selected.to) : 'End Date'
}`} }`}
</span> </span>
<div className="bg-neutral-80/5 h-full w-[1px]" /> <div className="h-full w-[1px] bg-neutral-80/5" />
<EditIcon size={20} color="$neutral-80-opa-40" /> <EditIcon size={20} color="$neutral-80-opa-40" />
</button> </button>
<Popover.Content> <Popover.Content>

View File

@ -13,7 +13,7 @@ export const ErrorPage = (props: Props) => {
// todo!: design review, not in designs // todo!: design review, not in designs
case ERROR_CODES.NOT_FOUND: case ERROR_CODES.NOT_FOUND:
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-white text-center"> <div className="bg-white flex h-full w-full flex-col items-center justify-center gap-8 text-center">
<div className="h-[160px] w-[160px] rounded-full bg-[#b3b3b3]" /> <div className="h-[160px] w-[160px] rounded-full bg-[#b3b3b3]" />
<Text size={27} weight="semibold"> <Text size={27} weight="semibold">
Page not found. Page not found.
@ -24,7 +24,7 @@ export const ErrorPage = (props: Props) => {
case ERROR_CODES.INTERNAL_SERVER_ERROR: case ERROR_CODES.INTERNAL_SERVER_ERROR:
default: default:
return ( return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-white text-center"> <div className="bg-white flex h-full w-full flex-col items-center justify-center gap-8 text-center">
<div className="h-[160px] w-[160px] rounded-full bg-[hsla(355,47%,50%,1)]" /> <div className="h-[160px] w-[160px] rounded-full bg-[hsla(355,47%,50%,1)]" />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Text size={27} weight="semibold"> <Text size={27} weight="semibold">

View File

@ -0,0 +1,29 @@
import { Button, Text } from '@status-im/components'
type Props = {
title: string
description: string
action: string
}
const ActionCard = (props: Props) => {
const { title, description, action } = props
return (
<div className="flex items-center rounded-[20px] border border-neutral-90 bg-neutral-95 px-5 py-3">
<div className="grid flex-1 gap-px">
<Text size={19} color="$white-100" weight="semibold">
{title}
</Text>
<Text size={15} color="$white-100">
{description}
</Text>
</div>
<Button size={32} variant="darkGrey">
{action}
</Button>
</div>
)
}
export { ActionCard }

View File

@ -0,0 +1,13 @@
export const Dot = () => (
<span className="flex self-center">
<svg
width="2"
height="2"
viewBox="0 0 2 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="1" cy="1" r="1" fill="#647084" />
</svg>
</span>
)

View File

@ -0,0 +1,31 @@
export const MessariIcon = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2650_62112)">
<path
d="M11.9531 4.24242L16.0019 0V12.2667L11.9531 15.9515V4.24242Z"
fill="#647084"
/>
<path
d="M5.97656 6.37524L7.92778 7.97523L10.0253 6.13281V12.2661L5.97656 15.951V6.37524Z"
fill="#647084"
/>
<path
d="M4.07317 4.24242L0 0V16L4.07317 12.5576V4.24242Z"
fill="#647084"
/>
</g>
<defs>
<clipPath id="clip0_2650_62112">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
)
}

View File

@ -0,0 +1,77 @@
import { Text } from '@status-im/components'
import { SOCIALS } from '@/config/links'
import { Logo } from '../logo'
import { AccordionMenu } from '../navigation/accordion-menu'
import { Dot } from './components/dot'
import { MessariIcon } from './components/messari-icon'
type Props = {
hasBorderTop?: boolean
}
export const FooterMobile = (props: Props) => {
const { hasBorderTop } = props
return (
<footer
className={`block border-dashed border-neutral-80 ${
hasBorderTop ? 'border-t' : 'border-t-0'
} pb-12 sm:hidden`}
>
<div className="">
<div className="flex flex-col px-2 pt-6">
<div className="px-3 pb-3">
<Logo />
</div>
<div className="pb-5">
<AccordionMenu />
</div>
</div>
<div className="flex flex-col items-center gap-2 border-t border-dashed border-neutral-80 px-6 pt-6">
<div className="flex w-full flex-col items-start gap-2 ">
<Text size={11} color="$neutral-50">
© Status Research & Development GmbH
</Text>
<div className="flex gap-3">
<Text size={11} color="$neutral-40">
Terms of use
</Text>
<Dot />
<Text size={11} color="$neutral-40">
Privacy policy
</Text>
<Dot />
<Text size={11} color="$neutral-40">
Cookies
</Text>
</div>
</div>
<div className="flex w-full flex-col items-start gap-3">
<div className="flex w-full items-center justify-start gap-3 py-2">
<MessariIcon />
<Text size={11} color="$neutral-50">
Messari Transparency Verified
</Text>
</div>
<div className="flex gap-3">
{Object.values(SOCIALS).map(social => {
const IconComponent = social.icon
return (
<IconComponent
key={social.name}
size={20}
color="$neutral-40"
/>
)
})}
</div>
</div>
</div>
</div>
</footer>
)
}

View File

@ -0,0 +1,94 @@
import { Text } from '@status-im/components'
import { cva } from 'class-variance-authority'
import { LINKS, SOCIALS } from '@/config/links'
import { Logo } from '../logo'
import { Dot } from './components/dot'
import { MessariIcon } from './components/messari-icon'
import { Section } from './section'
type Props = {
hasBorderTop?: boolean
}
const section = cva(
[
'border-neutral-80',
'mb-6',
'flex',
'items-start',
'border-dashed',
'pl-6',
'pt-6',
],
{
variants: {
hasBorderTop: {
true: ['border-t'],
false: ['border-t-0'],
},
},
}
)
export const Footer = (props: Props) => {
const { hasBorderTop } = props
return (
<footer className="hidden pb-3 sm:block">
<div className="grid grid-cols-4 gap-5 sm:gap-0 lg:mb-10 lg:grid-cols-8">
<div
className={section({
hasBorderTop,
})}
>
<Logo />
</div>
{Object.entries(LINKS).map(([title, links], index) => (
<Section
key={title}
title={title}
links={links}
hasBorderLeft={index !== 3}
hasBorderTop={hasBorderTop}
/>
))}
</div>
<div className="flex flex-col items-start justify-between gap-2 px-5 lg:px-6 md-lg:flex-row md-lg:items-center">
<div className="flex items-center gap-3">
<Text size={11} color="$neutral-50">
© Status Research & Development GmbH
</Text>
<Dot />
<div className="flex gap-3">
<Text size={11} color="$neutral-40">
Terms of use
</Text>
<Text size={11} color="$neutral-40">
Privacy policy
</Text>
<Text size={11} color="$neutral-40">
Cookies
</Text>
</div>
</div>
<div className="flex gap-3">
<div className="flex items-center gap-3">
<MessariIcon />
<Text size={11} color="$neutral-50">
Messari Transparency Verified
</Text>
<Dot />
</div>
{Object.values(SOCIALS).map(social => {
const IconComponent = social.icon
return (
<IconComponent key={social.name} size={20} color="$neutral-40" />
)
})}
</div>
</div>
</footer>
)
}

View File

@ -0,0 +1,79 @@
import { Text } from '@status-im/components'
import { cva } from 'class-variance-authority'
import { Link } from '../link'
import type { Links } from '@/config/links'
type Props = {
title: string
links: Links
hasBorderLeft?: boolean
hasBorderTop?: boolean
}
const section = cva(
[
'border-neutral-80',
'relative',
'grid',
'gap-3',
'border-dashed',
'px-5',
'pb-6',
'pt-6',
'lg:border-l',
'lg:pb-0',
],
{
variants: {
hasBorderTop: {
true: ['border-t'],
false: ['border-t-0'],
},
hasBorderLeft: {
true: ['border-l'],
false: ['border-l-0'],
},
},
}
)
const Section = (props: Props) => {
const { title, links, hasBorderLeft, hasBorderTop } = props
return (
<div>
<div
className={section({
hasBorderTop,
hasBorderLeft,
})}
>
<div
style={{
background:
'linear-gradient(180deg, transparent 0%, rgba(9 16 28 / 100%))',
}}
className=" absolute left-[-4px] top-0 h-full w-2"
/>
<Text size={13} color="$neutral-50">
{title}
</Text>
<ul className="grid gap-1">
{links.map(link => (
<li key={link.name}>
<Link href={link.href}>
<Text size={15} color="$white-100" weight="medium">
{link.name}
</Text>
</Link>
</li>
))}
</ul>
</div>
</div>
)
}
export { Section }

View File

@ -8,10 +8,11 @@ import logoLearnSrc from '../../public/images/logo/learn.svg'
type Props = { type Props = {
label?: boolean label?: boolean
isTopbarDesktop?: boolean
} }
export const Logo = (props: Props) => { export const Logo = (props: Props) => {
const { label = true } = props const { label = true, isTopbarDesktop } = props
const { pathname } = useRouter() const { pathname } = useRouter()
@ -39,7 +40,7 @@ export const Logo = (props: Props) => {
width="70" width="70"
height="16" height="16"
fill="none" fill="none"
className="mr-[10px]" className={`mr-[10px] ${isTopbarDesktop ? 'hidden lg:block' : ''}`}
> >
<path <path
fill="#fff" fill="#fff"

View File

@ -0,0 +1,73 @@
import { useState } from 'react'
import * as Accordion from '@radix-ui/react-accordion'
import { Text } from '@status-im/components'
import { ChevronRightIcon, ExternalIcon } from '@status-im/icons'
import { LINKS } from '@/config/links'
import { Link } from '../link'
const AccordionMenu = () => {
const [openLink, setOpenLink] = useState('')
const handleToggle = (value: string) => {
setOpenLink(value === openLink ? '' : value)
}
return (
<div className="flex flex-col px-3">
{Object.entries(LINKS).map(([name, links]) => (
<Accordion.Root
key={name}
type="single"
collapsible
value={openLink}
onValueChange={handleToggle}
className="accordion-root flex flex-col pb-3 first-of-type:pt-3"
>
<Accordion.Item key={name} value={name} className="accordion-item">
<div>
<Accordion.Trigger className="accordion-trigger">
<div className="accordion-chevron inline-flex h-5 w-5">
<ChevronRightIcon size={20} color="$white-100" />
</div>
<Text size={19} weight="medium" color={'$white-100'}>
{name}
</Text>
</Accordion.Trigger>
<Accordion.Content className="accordion-content pl-5">
<div className="overflow-hidden">
{links.map((link, index) => {
const external = link.href.startsWith('http')
return (
<div
key={link.name + index}
className={`pt-3 transition-opacity first-of-type:pt-5 last-of-type:pb-5 hover:opacity-50`}
>
<Link
href={link.href}
className="flex items-center gap-1"
>
<Text size={15} weight="medium" color="$white-100">
{link.name}
</Text>
{external && (
<ExternalIcon size={20} color="$white-100" />
)}
</Link>
</div>
)
})}
</div>
</Accordion.Content>
</div>
</Accordion.Item>
</Accordion.Root>
))}
</div>
)
}
export { AccordionMenu }

View File

@ -1,5 +1,3 @@
import { useEffect, useRef, useState } from 'react'
import * as NavigationMenu from '@radix-ui/react-navigation-menu' import * as NavigationMenu from '@radix-ui/react-navigation-menu'
import { Button, Text } from '@status-im/components' import { Button, Text } from '@status-im/components'
import { DownloadIcon, ExternalIcon } from '@status-im/icons' import { DownloadIcon, ExternalIcon } from '@status-im/icons'
@ -7,39 +5,22 @@ import { cx } from 'class-variance-authority'
import { LINKS } from '@/config/links' import { LINKS } from '@/config/links'
import { Link } from './link' import { Link } from '../link'
import { Logo } from './logo' import { Logo } from '../logo'
export const NavMenu = () => { type Props = {
const [visible, setVisible] = useState(false) visible: boolean
}
// Using ref to prevent re-running of useEffect const FloatingDesktop = (props: Props) => {
const visibleRef = useRef(visible) const { visible } = props
visibleRef.current = visible
useEffect(() => {
const handleScroll = () => {
if (window.scrollY > 60) {
visibleRef.current === false && setVisible(true)
} else {
visibleRef.current === true && setVisible(false)
}
}
handleScroll()
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return ( return (
<NavigationMenu.Root <NavigationMenu.Root
data-visible={visible} data-visible={visible}
className={cx([ className={cx([
'fixed left-1/2 top-5 z-10 min-w-[746px] -translate-x-1/2 overflow-hidden',
'rounded-2xl border border-neutral-80/5 bg-blur-neutral-80/80 backdrop-blur-md',
'data-[visible=false]:pointer-events-none', 'data-[visible=false]:pointer-events-none',
'data-[visible=true]:pointer-events-auto',
'opacity-0 transition-opacity data-[visible=true]:opacity-100', 'opacity-0 transition-opacity data-[visible=true]:opacity-100',
])} ])}
> >
@ -98,3 +79,5 @@ export const NavMenu = () => {
</NavigationMenu.Root> </NavigationMenu.Root>
) )
} }
export { FloatingDesktop }

View File

@ -0,0 +1,82 @@
import { useRef, useState } from 'react'
import { animated, useScroll } from '@react-spring/web'
import { cx } from 'class-variance-authority'
import { useLockScroll } from '@/hooks/use-lock-scroll'
import { useOutsideClick } from '@/hooks/use-outside-click'
import { FloatingDesktop } from './floating-desktop'
import { FloatingMobile } from './floating-mobile'
const FloatingMenu = (): JSX.Element => {
const [visible, setVisible] = useState(false)
const [open, setOpen] = useState(false)
const openRef = useRef(open)
openRef.current = open
// Using ref to prevent re-running of useEffect
const visibleRef = useRef(visible)
const scrollYRef = useRef(0)
visibleRef.current = visible
// Close menu on outside click
const ref = useOutsideClick(() => setOpen(false))
useLockScroll(open)
useScroll({
onChange: ({ value: { scrollYProgress } }) => {
const isMenuOpen = openRef.current
const isScrollingUp = scrollYProgress < scrollYRef.current
const detectionPoint = scrollYProgress > 0.005
if (detectionPoint && isScrollingUp) {
if (!visibleRef.current) {
setVisible(true)
}
} else {
if (!isMenuOpen) {
setVisible(false)
}
}
scrollYRef.current = scrollYProgress
},
default: {
immediate: true,
},
})
return (
<>
<animated.div
ref={ref}
style={{
opacity: visible ? 1 : 0,
}}
className={cx([
'fixed left-1/2 top-1 z-10 flex -translate-x-1/2 flex-col items-center justify-between p-2 pb-0 lg:hidden',
'bg-blur-neutral-80/80 border-neutral-80/5 rounded-2xl border backdrop-blur-md',
' w-[calc(100%-24px)]',
' opacity-0 transition-opacity data-[visible=true]:opacity-100',
'z-10',
])}
>
<FloatingMobile open={open} setOpen={setOpen} />
</animated.div>
<animated.div
style={{
opacity: visible ? 1 : 0,
}}
className={cx([
'fixed left-1/2 top-5 z-10 w-max min-w-[746px] -translate-x-1/2 overflow-hidden',
'bg-blur-neutral-80/80 border-neutral-80/5 rounded-2xl border backdrop-blur-md',
'md-lg:block hidden',
])}
>
<FloatingDesktop visible={visible} />
</animated.div>
</>
)
}
export { FloatingMenu }

View File

@ -0,0 +1,61 @@
import { Button, IconButton } from '@status-im/components'
import { CloseIcon, DownloadIcon, MenuIcon } from '@status-im/icons'
import { Link } from '../link'
import { Logo } from '../logo'
import { AccordionMenu } from './accordion-menu'
type Props = {
open: boolean
setOpen: (open: boolean) => void
}
const FloatingMobile = (props: Props) => {
const { open, setOpen } = props
return (
<div className="w-full">
<div className="z-10 flex w-full items-center justify-between">
<div className="flex">
<Link href="/">
<Logo />
</Link>
</div>
<IconButton
variant="outline"
icon={
open ? (
<CloseIcon size={20} color={'$white-100'} />
) : (
<MenuIcon size={20} />
)
}
onPress={() => setOpen(!open)}
/>
</div>
<div
style={{
height: open ? '100%' : 0,
opacity: open ? 1 : 0,
pointerEvents: open ? 'auto' : 'none',
overflow: open ? 'auto' : 'hidden',
}}
className={`z-10 flex w-full flex-col justify-between pt-2`}
>
<AccordionMenu />
<div className="flex justify-center py-3">
<Button
size={40}
variant="grey"
icon={<DownloadIcon size={20} />}
fullWidth
>
Sign up for early access
</Button>
</div>
</div>
</div>
)
}
export { FloatingMobile }

View File

@ -0,0 +1,88 @@
import * as NavigationMenu from '@radix-ui/react-navigation-menu'
import { Button, Text } from '@status-im/components'
import { DownloadIcon, ExternalIcon } from '@status-im/icons'
import { cx } from 'class-variance-authority'
import { Logo } from '@/components/logo'
import { LINKS } from '@/config/links'
import { Link } from '../link'
const NavDesktop = () => {
return (
<>
<NavigationMenu.Root className="relative z-10 hidden md-lg:block">
<div className="flex items-center px-6">
<div className="mr-5 flex shrink-0 ">
<Link href="/">
<Logo isTopbarDesktop />
</Link>
</div>
<div className="flex-1">
<NavigationMenu.List className="flex items-center">
{Object.entries(LINKS).map(([name, links]) => (
<NavigationMenu.Item key={name}>
<NavigationMenu.Trigger className="py-4 pr-5 aria-expanded:opacity-50">
<Text size={15} weight="medium" color="$white-100">
{name}
</Text>
</NavigationMenu.Trigger>
<NavigationMenu.Content className="grid gap-3 pb-8 pl-[164px] pt-3">
{links.map(link => {
const external = link.href.startsWith('http')
return (
<NavigationMenu.Link key={link.name} asChild>
<Link
href={link.href}
className="flex items-center gap-1"
>
<Text
size={27}
weight="semibold"
color="$white-100"
>
{link.name}
</Text>
{external && (
<ExternalIcon size={20} color="$white-100" />
)}
</Link>
</NavigationMenu.Link>
)
})}
</NavigationMenu.Content>
</NavigationMenu.Item>
))}
</NavigationMenu.List>
</div>
<div className="flex justify-end ">
<Button
size={32}
variant="darkGrey"
icon={<DownloadIcon size={20} />}
>
Sign up for early access
</Button>
</div>
</div>
<NavigationMenu.Viewport
className={cx([
'data-[state=closed]:animate-heightOut data-[state=open]:animate-heightIn',
'h-[var(--radix-navigation-menu-viewport-height)] transition-height',
// 'data-[state=open]:animate-heightIn animate-',
// 'data-[state=closed]:animate-heightOut',
// 'transition-height h-[var(--radix-navigation-menu-viewport-height)]',
// 'transition-height mb-8 h-[var(--radix-navigation-menu-viewport-height)] duration-1000',
// 'h-[var(--radix-navigation-menu-viewport-height)]',
// 'data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut relative mt-[10px] h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[6px] bg-white transition-[width,height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]',
])}
/>
</NavigationMenu.Root>
</>
)
}
export { NavDesktop }

View File

@ -0,0 +1,67 @@
import { useState } from 'react'
import { Button, IconButton } from '@status-im/components'
import { CloseIcon, DownloadIcon, MenuIcon } from '@status-im/icons'
import { Logo } from '@/components/logo'
import { useLockScroll } from '@/hooks/use-lock-scroll'
import { Link } from '../link'
import { AccordionMenu } from './accordion-menu'
const NavMobile = () => {
const [open, setOpen] = useState(false)
const screenHeight = typeof window !== 'undefined' ? window.innerHeight : 0
useLockScroll(open)
const handleToggle = () => {
setOpen(prevOpen => !prevOpen)
}
return (
<div className="z-10 flex flex-col items-center justify-between px-5 py-3 pb-1 md-lg:hidden">
<div className="z-10 flex w-full items-center justify-between">
<div className="flex">
<Link href="/">
<Logo />
</Link>
</div>
<IconButton
variant="outline"
icon={
open ? (
<CloseIcon size={20} color="$white-100" />
) : (
<MenuIcon size={20} color="$white-100" />
)
}
onPress={handleToggle}
/>
</div>
<div
style={{
height: open ? screenHeight - 100 : 0,
opacity: open ? 1 : 0,
pointerEvents: open ? 'auto' : 'none',
overflow: 'auto',
}}
className={`z-10 flex w-full flex-col justify-between bg-blur-neutral-100/70 pt-2 transition-all duration-300`}
>
<AccordionMenu />
<div className="flex justify-center py-3">
<Button
size={40}
variant="grey"
icon={<DownloadIcon size={20} />}
fullWidth
>
Sign up for early access
</Button>
</div>
</div>
</div>
)
}
export { NavMobile }

View File

@ -1,135 +0,0 @@
import { Button, Text } from '@status-im/components'
import Link from 'next/link'
import { LINKS, SOCIALS } from '@/config/links'
import { Logo } from './logo'
import type { Links } from '@/config/links'
export const PageFooter = () => {
return (
<footer className="px-6 pb-3 pt-14">
<div className="mb-6">
<Logo />
</div>
<div className="mb-10 grid grid-cols-2 lg:grid-cols-8">
{Object.entries(LINKS).map(([title, links]) => (
<Section key={title} title={title} links={links} />
))}
<div>
<Text size={13} color="$neutral-50">
{"Let's connect"}
</Text>
<div className="flex flex-col">
{Object.values(SOCIALS).map(social => (
<Text size={11} key={social.name}>
{social.name}
</Text>
))}
</div>
</div>
</div>
<div className="mb-4 grid gap-5 lg:grid-cols-[2fr_1fr]">
<ActionCard
title="Own a community?"
description="Don't give Discord and Telegram power over your community"
action="Take back control"
/>
<ActionCard
title="Give us feedback!"
description="Tell us how to make it better"
action="Upvote"
/>
</div>
<div className="flex items-center gap-2">
<Text size={11} color="$neutral-50">
© Status Research & Development GmbH
</Text>
<span>
<svg
width="2"
height="2"
viewBox="0 0 2 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="1" cy="1" r="1" fill="#647084" />
</svg>
</span>
<div className="flex gap-3">
<Text size={11} color="$neutral-40">
Terms of use
</Text>
<Text size={11} color="$neutral-40">
Privacy policy
</Text>
<Text size={11} color="$neutral-40">
Cookies
</Text>
</div>
</div>
</footer>
)
}
type SectionProps = {
title: string
links: Links
}
const Section = (props: SectionProps) => {
const { title, links } = props
return (
<div>
<div className="grid gap-3">
<Text size={13} color="$neutral-50">
{title}
</Text>
<ul className="grid gap-1">
{links.map(link => (
<li key={link.name}>
<Link href={link.href}>
<Text size={15} color="$white-100" weight="medium">
{link.name}
</Text>
</Link>
</li>
))}
</ul>
</div>
</div>
)
}
type ActionCardProps = {
title: string
description: string
action: string
}
const ActionCard = (props: ActionCardProps) => {
const { title, description, action } = props
return (
<div className="bg-netural-95 flex items-center rounded-[20px] border border-neutral-90 px-5 py-3">
<div className="grid flex-1 gap-px">
<Text size={19} color="$white-100" weight="semibold">
{title}
</Text>
<Text size={15} color="$white-100">
{description}
</Text>
</div>
<Button size={32} variant="darkGrey">
{action}
</Button>
</div>
)
}

View File

@ -0,0 +1,48 @@
import { Button, Text } from '@status-im/components'
import { DownloadIcon } from '@status-im/icons'
import Image from 'next/image'
import logoSrc from '../../public/images/logo/default.svg'
import { ComingSoon } from './coming-soon'
const Prefooter = () => {
return (
<div className="bg-neutral-100 p-5 py-[120px]">
<div className="flex justify-center">
<div className="flex flex-col items-center justify-center">
<Image src={logoSrc} alt="Status logo" width={80} />
<h1 className="py-4 pb-3 pt-5 text-center text-[40px] font-bold leading-[44px] text-white-100 lg:pb-5 lg:text-[88px] lg:leading-[84px]">
Be unstoppable
</h1>
<span className="max-w-md text-center font-bold">
<Text size={19} color={'$white-100'}>
Use the open source, decentralized crypto communication super app.
</Text>
</span>
<div className="relative flex flex-col justify-center pt-8">
<div className="inline-flex justify-center">
<Button
size={40}
icon={<DownloadIcon size={20} />}
variant="yellow"
>
Sign up for early access
</Button>
</div>
<div className="relative max-w-xs pt-4 text-center leading-3">
<Text size={11} color={'$neutral-40'}>
Betas for Mac, Windows, Linux <br />
Alphas for iOS & Android
</Text>
<div className="absolute right-[-48px] top-9">
<ComingSoon />
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export { Prefooter }

View File

@ -334,8 +334,8 @@ export function PreviewPage(props: PreviewPageProps) {
style={!bannerURL ? getGradientStyles(data) : undefined} style={!bannerURL ? getGradientStyles(data) : undefined}
className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]" className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]"
> >
<div className="absolute left-0 right-0 top-0 xl:hidden"> <div className="absolute inset-x-0 top-0 xl:hidden">
<div className="from-white-100 to-white-60 absolute h-full w-full bg-gradient-to-t" /> <div className="absolute h-full w-full bg-gradient-to-t from-white-100 to-white-60" />
{bannerURL && ( {bannerURL && (
<img <img
className="aspect-video h-full w-full object-cover" className="aspect-video h-full w-full object-cover"
@ -435,7 +435,7 @@ export function PreviewPage(props: PreviewPageProps) {
{/* INSTRUCTIONS */} {/* INSTRUCTIONS */}
<div className="mb-6 grid gap-3"> <div className="mb-6 grid gap-3">
<div className="border-neutral-10 bg-white-100 rounded-2xl border px-4 py-3"> <div className="rounded-2xl border border-neutral-10 bg-white-100 px-4 py-3">
<h3 className="mb-2 text-[15px] font-semibold xl:text-[19px]"> <h3 className="mb-2 text-[15px] font-semibold xl:text-[19px]">
{INSTRUCTIONS_HEADING[type]} {INSTRUCTIONS_HEADING[type]}
</h3> </h3>
@ -475,7 +475,7 @@ export function PreviewPage(props: PreviewPageProps) {
</ul> </ul>
</div> </div>
<div className="border-neutral-10 bg-white-100 flex flex-col items-start gap-4 rounded-2xl border p-4 pt-3"> <div className="flex flex-col items-start gap-4 rounded-2xl border border-neutral-10 bg-white-100 p-4 pt-3">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Text size={15} weight="semibold"> <Text size={15} weight="semibold">
Have Status already? Have Status already?

View File

@ -26,8 +26,8 @@ export const QrDialog = (props: Props) => {
<Dialog.Content className="data-[state=open]:animate-contentShow fixed inset-0 flex items-center justify-center focus:outline-none"> <Dialog.Content className="data-[state=open]:animate-contentShow fixed inset-0 flex items-center justify-center focus:outline-none">
<div> <div>
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<div className="bg-white-5 aspect-square w-full max-w-[335px] rounded-2xl p-3"> <div className="aspect-square w-full max-w-[335px] rounded-2xl bg-white-5 p-3">
<div className="bg-white-100 rounded-xl p-3"> <div className="rounded-xl bg-white-100 p-3">
<QRCodeSVG value={value} height={286} width={286} /> <QRCodeSVG value={value} height={286} width={286} />
</div> </div>
</div> </div>

View File

@ -0,0 +1,178 @@
import { Button, Counter, Tag, Text } from '@status-im/components'
import { useParalax } from '@/hooks/use-parallax'
import { ParalaxCircle } from './parallax-circle'
const ComparisionSection = () => {
const { top, bottom } = useParalax({
initialTop: -690,
})
return (
<div className="relative">
<div className="relative grid grid-cols-4 border-t border-dashed border-neutral-80/20 bg-white-100 mix-blend-normal">
<div className="col-span-2 border-r border-dashed border-neutral-80/20 py-[160px] pl-10 pr-[60px]">
<div className="flex h-full flex-col justify-between">
<div className="inline-flex max-w-[646px] flex-col">
<div className="inline-flex">
<Tag label="Use case" size={24} />
</div>
<h2 className="pt-4 text-[40px] font-bold leading-[44px]">
Alice has 50 DAI on both Ethereum Mainnet and Optimism and wants
to send 100 DAI to Bob on Arbitrum.
</h2>
</div>
<div className="mt-16 flex max-w-[341px] flex-col rounded-[20px] border border-dashed border-neutral-80/20 p-4">
<Text size={19} weight="semibold">
Finally! Multi-chain done right!
</Text>
<div className="flex pt-1">
<Text size={19}>
Interested in how Statuss send with auto routing and bridging
works and helps users?{' '}
</Text>
</div>
<div className="inline-flex pt-1">
<Button variant="outline">Read more</Button>
</div>
</div>
</div>
</div>
<div className="border-r border-dashed border-neutral-80/20 px-10 py-[160px]">
<Text size={19} weight="semibold">
Other wallets
</Text>
<div className="flex flex-col pt-3">
{listOneData.map(item => (
<Item key={item.count} {...item} />
))}
</div>
</div>
<div className="px-10 py-[160px]">
<Text size={19} weight="semibold">
Status Wallet
</Text>
<div className="flex flex-col pt-3">
{listTwoData.map(item => (
<Item key={item.count} {...item} />
))}
</div>
</div>
<ParalaxCircle initialTop={-600} />
</div>
<p
style={{
top: `${top}px`,
bottom: `${bottom}px`,
left: -40,
// TODO: use font from design when it's ready
fontFamily: 'Menlo',
}}
className="absolute whitespace-nowrap text-[240px] font-bold leading-[212px] text-neutral-80/5"
>
eth:opt:arb:0xAgafhja
</p>
</div>
)
}
export { ComparisionSection }
const listOneData = [
{
count: 1,
label: 'Open dApp Browser',
},
{
count: 2,
label: 'Visit Bridge dApp',
},
{
count: 3,
label: 'Bridge DAI from Mainnet to Arbitrum',
},
{
count: 4,
label: 'Send DAI on Arbitrum',
},
{
count: 5,
label: 'Open dApp Browser',
},
{
count: 6,
label: 'Visit Bridge dApp',
},
{
count: 7,
label: 'Bridge DAI from Optimism to Arbitrum',
},
{
count: 8,
label: 'Send DAI on Arbitrum',
noBorder: true,
},
{
count: '😮‍💨',
label: 'Is that it?',
secondaryLabel: 'Did I use cheapest route and bridges?',
noBorder: true,
},
]
const listTwoData = [
{
count: 1,
label: 'Select the token',
},
{
count: 2,
label: 'Select the amount',
},
{
count: 3,
label: 'Send',
noBorder: true,
},
{
count: '🎉',
label: 'Thats it!',
secondaryLabel: 'Lowest possible cost!',
noBorder: true,
},
]
const Item = (props: {
count: string | number
label: string
secondaryLabel?: string
noBorder?: boolean
}) => {
const { count, label, secondaryLabel, noBorder } = props
const isNumber = typeof count === 'number'
return (
<div
className={`flex border-neutral-80/20 ${
noBorder ? 'border-b-0' : 'border-b'
} border-dashed py-3`}
>
{isNumber ? (
<Counter value={count} type="outline" />
) : (
<Text size={19}>{count}</Text>
)}
<div className="flex flex-col pl-[10px]">
<Text size={19} weight={isNumber ? 'regular' : 'semibold'}>
{label}
</Text>
{!isNumber && (
<Text size={19} weight="regular">
{secondaryLabel}
</Text>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,62 @@
import { Text } from '@status-im/components'
const HandsSection = () => {
return (
<div className="relative bg-[url('/assets/wallet/background-pattern.png')] bg-[length:100%_100%] bg-no-repeat py-[322px]">
<div className="mx-40 mb-24">
<div className="flex flex-col items-center justify-center">
<h2 className="max-w-[600px] py-4 pb-5 text-center text-[64px] font-bold leading-[68px]">
Take control
<br />
of your crypto
</h2>
<span className="max-w-xl text-center font-bold">
<Text size={27}>
No one (including Status!) has the power to freeze, lock-out or
stop a Status user from accessing and transacting their tokens.
</Text>
</span>
</div>
</div>
<div className="relative z-[2] flex justify-center pt-[320px]">
<div className="mr-24 flex max-w-[381px] flex-col items-center">
<img src="/assets/wallet/skull.png" alt="skull" width="48px" />
<div className="flex flex-col items-center pt-4 text-center">
<Text size={27} weight="semibold">
Ethereum based assets
</Text>
<Text size={19}>
We support all assets in the Uniswap Labs default tokenlist and
those minted by communities using Status.
</Text>
</div>
</div>
<div className="flex max-w-[381px] flex-col items-center">
<img src="/assets/wallet/nft.png" alt="nft" width="48px" />
<div className="flex flex-col items-center pt-4 text-center">
<Text size={27} weight="semibold">
NFTs and collectibles
</Text>
<Text size={19}>
We will display any NFTs or collectibles listed on OpenSea plus
those minted by communities using Status.
</Text>
</div>
</div>
</div>
<img
src="/assets/wallet/hands.png"
alt="hands"
className="absolute left-0 top-0 w-full"
/>
<img
src="/assets/wallet/gentleman.png"
alt="gentleman"
className="absolute bottom-0 left-0 mix-blend-multiply"
width={403}
/>
</div>
)
}
export { HandsSection }

View File

@ -0,0 +1,73 @@
import { Button, Tag, Text } from '@status-im/components'
import { DownloadIcon, WalletIcon } from '@status-im/icons'
import image1 from 'public/assets/wallet/1.png'
import image2 from 'public/assets/wallet/2.png'
import image3 from 'public/assets/wallet/3.png'
import image4 from 'public/assets/wallet/vegas.png'
import { GridHero } from '../cards/grid-hero'
import { ParalaxCircle } from './parallax-circle'
const HeroSection = () => {
return (
<div className="flex w-full flex-col items-center">
<div className="relative mb-16 grid px-5 lg:mb-24 lg:px-40">
<ParalaxCircle initialLeft={-206} initialTop={-170} />
<div className="relative flex flex-col items-center justify-center">
<div className="inline-flex">
<Tag
size={32}
icon={WalletIcon}
color="$yellow-50"
label="Wallet"
/>
</div>
<h1 className="max-w-[600px] py-4 text-center text-[48px] font-bold leading-[50px] lg:pb-5 lg:text-[88px] lg:leading-[84px]">
The future
<br />
is multi-chain
</h1>
<span className="max-w-md text-center font-bold">
<Text size={19}>
L2s made simple - send and manage your crypto easily and safely
across multiple networks.
</Text>
</span>
</div>
<div className="relative flex justify-center">
<div className="mt-6 inline-flex rounded-[20px] border border-dashed border-neutral-80/20 p-2 lg:mt-8">
<Button
size={40}
icon={<DownloadIcon size={20} />}
variant="yellow"
>
Sign up for early access
</Button>
</div>
</div>
</div>
<GridHero
color="yellow"
cardOne={{
alt: 'wallet-1',
image: image1,
}}
cardTwo={{
alt: 'wallet-2',
image: image2,
}}
cardThree={{
alt: 'wallet-3',
image: image3,
}}
cardFour={{
alt: 'wallet-4',
image: image4,
}}
/>
</div>
)
}
export { HeroSection }

View File

@ -0,0 +1,5 @@
export { ComparisionSection } from './comparision-section'
export { HandsSection } from './hands-section'
export { HeroSection } from './hero-section'
export { ParalaxCircle } from './parallax-circle'
export { VideoSection } from './video-section'

View File

@ -0,0 +1,28 @@
import { useParalax } from '@/hooks/use-parallax'
type ParalaxProps = {
initialLeft?: number
initialTop?: number
initialRight?: number
initialBottom?: number
}
const ParalaxCircle = (props: ParalaxProps) => {
const { top, bottom } = useParalax(props)
return (
<div
className="absolute z-[0] h-[676px] w-[676px] rounded-full bg-customisation-yellow-50 opacity-[0.10] blur-[260px] "
style={{
// Fixes the performance issue on safari with filter: blur() to force the browser use GPU acceleration for that particular element instead of the CPU.
transform: 'translate3d(0, 0, 0)',
left: `${props.initialLeft}px`,
top: `${top}px`,
right: `${props.initialRight}px`,
bottom: `${bottom}px`,
}}
/>
)
}
export { ParalaxCircle }

View File

@ -0,0 +1,76 @@
import { ContextTag, Text } from '@status-im/components'
import { ParalaxCircle } from './parallax-circle'
const VideoSection = () => {
return (
<div className="relative flex flex-col md:flex-row">
<ParalaxCircle initialLeft={-100} initialTop={-100} />
<div className="relative z-[1] flex flex-col px-5 pt-24 lg:px-[164px] lg:pt-[240px]">
<h2 className="text-[40px] font-bold leading-[44px] lg:text-[64px] lg:leading-[68px]">
Fully
<br />
Decentralized
<br />
Networks
</h2>
<div className="flex max-w-[462px] flex-col pt-4">
<Text size={27}>
Status supports blockchain networks that are fully committed to
decentralization.
</Text>
<div className="pt-8">
<Text size={13} color="$neutral-50" weight="medium">
Currently supported networks
</Text>
</div>
<div className="flex pt-3">
<div className="pr-[10px]">
<ContextTag
type="network"
network={{
name: 'Mainnet',
src: '/assets/wallet/ethereum.png',
}}
size={24}
/>
</div>
<div className="pr-[10px]">
<ContextTag
type="network"
network={{
name: 'Optmism',
src: '/assets/wallet/optimism.png',
}}
size={24}
/>
</div>
<div>
<ContextTag
type="network"
network={{
name: 'Arbitrum',
src: '/assets/wallet/arbitrum.png',
}}
size={24}
/>
</div>
</div>
</div>
</div>
<div className="relative right-[-5px] top-0 flex justify-center md:absolute">
<video autoPlay loop muted playsInline>
<source
src="/assets/wallet/vitalik.mp4"
type="video/mp4;codecs=hvc1"
/>
<source src="/assets/wallet/vitalik.webm" type="video/webm" />
</video>
</div>
</div>
)
}
export { VideoSection }

View File

@ -1,3 +1,10 @@
import {
AdvancedIcon,
AirdropIcon,
CommunitiesIcon,
TokenIcon,
} from '@status-im/icons'
export const LINKS = { export const LINKS = {
Features: [ Features: [
{ name: 'Communities', href: '/features/communities' }, { name: 'Communities', href: '/features/communities' },
@ -45,22 +52,27 @@ export const LINKS = {
], ],
} as const } as const
// TODO Update icons when available
export const SOCIALS = { export const SOCIALS = {
status: { status: {
name: 'Status', name: 'Status',
href: '<TODO>', href: '<TODO>',
icon: CommunitiesIcon,
}, },
twitter: { twitter: {
name: 'Twitter', name: 'Twitter',
href: 'https://twitter.com/ethstatus', href: 'https://twitter.com/ethstatus',
icon: TokenIcon,
}, },
github: { github: {
name: 'GitHub', name: 'GitHub',
href: 'https://github.com/status-im', href: 'https://github.com/status-im',
icon: AirdropIcon,
}, },
youtube: { youtube: {
name: 'YouTube', name: 'YouTube',
href: 'https://youtube.com/<TODO>', href: 'https://youtube.com/<TODO>',
icon: AdvancedIcon,
}, },
} }

View File

@ -0,0 +1,12 @@
export const useLockScroll = (open = false) => {
if (typeof document === 'undefined') {
return
}
// Adds the following code to disable scrolling when the menu is open
const rootElement = document.documentElement
if (open) {
rootElement.style.overflowY = 'hidden'
} else {
rootElement.style.overflowY = 'auto'
}
}

View File

@ -0,0 +1,21 @@
import { useEffect, useRef } from 'react'
export const useOutsideClick = (callback: () => void) => {
const ref = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const handleClick = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
callback()
}
}
document.addEventListener('click', handleClick, true)
return () => {
document.removeEventListener('click', handleClick, true)
}
}, [callback])
return ref
}

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from 'react'
type ParalaxProps = {
initialLeft?: number
initialTop?: number
initialRight?: number
initialBottom?: number
}
const PARALLAX_SPEED = 4
function useParalax(props?: ParalaxProps) {
const initialTop = props?.initialTop || 0
const initialBottom = props?.initialBottom || 0
const [top, setTop] = useState(initialTop)
const [bottom, setBottom] = useState(initialBottom)
useEffect(() => {
const handleScroll = () => {
const scrollY = window.scrollY
setTop(scrollY / PARALLAX_SPEED + initialTop)
setBottom(scrollY / PARALLAX_SPEED + initialBottom)
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [initialBottom, initialTop])
return { top, bottom }
}
export { useParalax }

View File

@ -1,100 +1,38 @@
import * as NavigationMenu from '@radix-ui/react-navigation-menu' import { Footer } from '@/components/footer/footer'
import { Button, Text } from '@status-im/components' import { FooterMobile } from '@/components/footer/footer-mobile'
import { DownloadIcon, ExternalIcon } from '@status-im/icons' import { FloatingMenu } from '@/components/navigation/floating-menu'
import { cx } from 'class-variance-authority' import { NavDesktop } from '@/components/navigation/nav-desktop'
import { NavMobile } from '@/components/navigation/nav-mobile'
import { Prefooter } from '@/components/pre-footer'
import { Logo } from '@/components/logo' import type { ReactElement } from 'react'
import { NavMenu } from '@/components/nav-menu'
import { PageFooter } from '@/components/page-footer'
import { LINKS } from '@/config/links'
import { Link } from '../components/link' type AppLayoutProps = {
hasPreFooter?: boolean
children: ReactElement
}
import type { PageLayout } from 'next' export const AppLayout: React.FC<AppLayoutProps> = ({
hasPreFooter = true,
export const AppLayout: PageLayout = page => { children,
}) => {
return ( return (
<> <>
<div className="hidden lg:block"> <FloatingMenu />
<NavMenu /> <div className="min-h-full w-full bg-neutral-100">
</div> <NavDesktop />
<div className="min-h-full bg-neutral-100"> <NavMobile />
<NavigationMenu.Root>
<div className="flex items-center px-6 py-3">
<div className="mr-5">
<Link href="/">
<Logo />
</Link>
</div>
<div className="hidden flex-1 lg:flex">
<NavigationMenu.List className="flex items-center">
{Object.entries(LINKS).map(([name, links]) => (
<NavigationMenu.Item key={name}>
<NavigationMenu.Trigger className="pr-5 aria-expanded:opacity-50">
<Text size={15} weight="medium" color="$white-100">
{name}
</Text>
</NavigationMenu.Trigger>
<NavigationMenu.Content className="grid gap-3 pb-8 pl-[164px] pt-3">
{links.map(link => {
const external = link.href.startsWith('http')
return (
<NavigationMenu.Link key={link.name} asChild>
<Link
href={link.href}
className="flex items-center gap-1"
>
<Text
size={27}
weight="semibold"
color="$white-100"
>
{link.name}
</Text>
{external && (
<ExternalIcon size={20} color="$white-100" />
)}
</Link>
</NavigationMenu.Link>
)
})}
</NavigationMenu.Content>
</NavigationMenu.Item>
))}
</NavigationMenu.List>
</div>
<div className="hidden justify-end lg:flex">
<Button
size={32}
variant="darkGrey"
icon={<DownloadIcon size={20} />}
>
Sign up for early access
</Button>
</div>
</div>
<NavigationMenu.Viewport
className={cx([
'data-[state=closed]:animate-heightOut data-[state=open]:animate-heightIn',
'h-[var(--radix-navigation-menu-viewport-height)] transition-height',
// 'data-[state=open]:animate-heightIn animate-',
// 'data-[state=closed]:animate-heightOut',
// 'transition-height h-[var(--radix-navigation-menu-viewport-height)]',
// 'transition-height mb-8 h-[var(--radix-navigation-menu-viewport-height)] duration-1000',
// 'h-[var(--radix-navigation-menu-viewport-height)]',
// 'data-[state=open]:animate-scaleIn data-[state=closed]:animate-scaleOut relative mt-[10px] h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[6px] bg-white transition-[width,height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]',
])}
/>
</NavigationMenu.Root>
{/* ROUNDED WHITE BG */} {/* ROUNDED WHITE BG */}
{/* <div className="bg-white-100 mx-1 min-h-[900px] rounded-3xl">{page}</div> */} <div className="flex justify-center lg:p-1">
{page} {/* TODO Check max-width to use */}
<div className="min-h-[900px] w-full rounded-3xl bg-white-100">
<PageFooter /> {children}
</div>
</div>
{hasPreFooter && <Prefooter />}
<Footer hasBorderTop={hasPreFooter} />
<FooterMobile hasBorderTop={hasPreFooter} />
</div> </div>
</> </>
) )

View File

@ -1,7 +1,7 @@
import { SideBar } from '../components' import { SideBar } from '../components'
import { AppLayout } from './app-layout' import { AppLayout } from './app-layout'
import type { PageLayout } from 'next' import type { ReactNode } from 'react'
// Eventually this will be fetched from the API, at least the nested links // Eventually this will be fetched from the API, at least the nested links
const MENU_LINKS = [ const MENU_LINKS = [
@ -81,11 +81,17 @@ const MENU_LINKS = [
}, },
] ]
export const InsightsLayout: PageLayout = page => { interface InsightsLayoutProps {
return AppLayout( children: ReactNode
<div className="relative mx-1 flex min-h-[calc(100vh-56px-4px)] rounded-3xl bg-white-100"> }
<SideBar data={MENU_LINKS} />
<main className="flex-1">{page}</main> export const InsightsLayout: React.FC<InsightsLayoutProps> = ({ children }) => {
</div> return (
<AppLayout hasPreFooter={false}>
<div className="relative mx-1 flex min-h-[calc(100vh-56px-4px)] w-full rounded-3xl bg-white-100">
<SideBar data={MENU_LINKS} />
<main className="flex-1">{children}</main>
</div>
</AppLayout>
) )
} }

View File

@ -248,6 +248,8 @@ const BlogDetailPage: Page<Props> = ({ post, relatedPosts }) => {
) )
} }
BlogDetailPage.getLayout = AppLayout BlogDetailPage.getLayout = function getLayout(page) {
return <AppLayout>{page}</AppLayout>
}
export default BlogDetailPage export default BlogDetailPage

View File

@ -222,6 +222,8 @@ export const PostCard = (props: PostCardProps) => {
) )
} }
BlogPage.getLayout = AppLayout BlogPage.getLayout = function getLayout(page) {
return <AppLayout>{page}</AppLayout>
}
export default BlogPage export default BlogPage

View File

@ -0,0 +1,124 @@
import { Text } from '@status-im/components'
import imageSection1 from 'public/assets/wallet/4.png'
import imageSection2 from 'public/assets/wallet/5.png'
import imageSection3 from 'public/assets/wallet/6.png'
import imageSection4 from 'public/assets/wallet/7.png'
import imageSection5 from 'public/assets/wallet/8.png'
import cubeImage from 'public/assets/wallet/cube.png'
import duckImage from 'public/assets/wallet/duck.png'
import megaphone from 'public/assets/wallet/megaphone.png'
import pizzaImage from 'public/assets/wallet/pizza.png'
import skullImage from 'public/assets/wallet/skull.png'
import { Section } from '@/components/cards'
import {
ComparisionSection,
HandsSection,
HeroSection,
ParalaxCircle,
VideoSection,
} from '@/components/wallet'
import { AppLayout } from '@/layouts/app-layout'
import type { Page } from 'next'
const WalletPage: Page = () => {
return (
<div className="overflow-hidden pt-16 lg:pt-32">
<HeroSection />
<VideoSection />
<div className="relative pt-0 md:pt-[220px]">
<ParalaxCircle initialRight={-100} initialTop={-100} />
<Section
color="yellow"
title="True multi-chain experience"
description=" All networks, all the time. See all your assets and NFTs in one place, even if spread across multiple blockchains."
image={imageSection1}
imageAlt="wallet-2"
imageSecondary={skullImage}
imageSecondaryAlt="skull"
secondaryDescription=" You can treat it like other wallets by selecting individual chains manually. But once youve tried the Status all chains, always way of doing things, you wont want to go back."
secondaryTitle=" Can be used as single chain at a time wallet"
information="This has some information so it will show the icon to open the dialog"
/>
</div>
<div className="relative pt-24 md:pt-[160px]">
<Section
color="yellow"
title="Simple token sending"
description=" Select token, amount and press “Send”. Need to bridge between blockchains? Status does this automatically."
image={imageSection2}
imageAlt="wallet-5"
imageSecondary={pizzaImage}
imageSecondaryAlt="pizza"
secondaryDescription="Status automatically calculates the cheapest way of moving tokens from A to B, considering: gas prices, chains your tokens are on, chains the recipient uses, and bridge costs. All in real time."
secondaryTitle="Cost efficient"
direction="rtl"
customNode={
<div className="relative rounded-2xl border border-neutral-80/5 px-4 py-3">
<Text size={15} color="$neutral-50">
EIP-3770 format:
</Text>
<p className="text-[19px] leading-[28px] text-neutral-100">
<span className="text-networks-ethereum">eth:</span>
<span className="text-networks-optimism">opt:</span>
<span className="text-networks-arbitrum">artb:</span>
0xAbc123...xyz789
</p>
</div>
}
/>
</div>
<div className="relative py-0 md:py-[160px]">
<Section
color="yellow"
title="Full control for power users"
description="Select exact amounts to send from each blockchain, and precisely set the chains the recipient will receive funds on."
image={imageSection3}
imageAlt="wallet-6"
imageSecondary={duckImage}
imageSecondaryAlt="duck"
secondaryDescription="Gas limit, max base fee, max priority fee, input data and nounce value for every transaction in each transaction bundle. If you really want to..."
secondaryTitle="Pro user? You can edit:"
/>
<ParalaxCircle initialRight={-200} initialTop={-300} />
</div>
<ComparisionSection />
<div className="relative rounded-[48px] border-t border-neutral-10 bg-white-100 py-0 mix-blend-normal shadow-[0_-2px_20px_rgba(9,16,28,0.04)] md:py-[160px]">
<Section
color="yellow"
title="New address format"
description="A way for wallets to automatically signal which chains they can receive funds on. So you dont have to."
image={imageSection4}
imageAlt="wallet-7"
imageSecondary={cubeImage}
imageSecondaryAlt="cube"
secondaryDescription="Status is working with other wallets to ensure EIP-3770 with multiple chain shortName prefixes becomes the new ethereum address standard."
secondaryTitle="On the path to adoption"
direction="rtl"
/>
<div className="relative pt-0 md:pt-[160px]">
<Section
color="yellow"
title="Total balance graphs"
description="Who doesnt want to see how their crypto portfolio number has gone up or down over time?"
image={imageSection5}
imageAlt="wallet-8"
imageSecondary={megaphone}
imageSecondaryAlt="megaphone"
secondaryDescription="Everything is taken into account: fluctuations in fiat exchange rates, daily valuation updates of every crypto asset you hodl, and how your balance changes when tokens are sent/received"
secondaryTitle="Graphs that do maths"
/>
</div>
</div>
<HandsSection />
</div>
)
}
WalletPage.getLayout = function getLayout(page) {
return <AppLayout>{page}</AppLayout>
}
export default WalletPage

View File

@ -8,8 +8,8 @@ import type { Page } from 'next'
const HomePage: Page = () => { const HomePage: Page = () => {
return ( return (
<> <>
<div className="mx-1 rounded-3xl bg-white-100 py-32"> <div className="px-5 py-32 lg:px-40">
<div className="mx-40 mb-40 grid gap-8"> <div className="mb-40 grid gap-8 px-5 ">
<div className="grid gap-6"> <div className="grid gap-6">
<h1 className="text-7xl font-bold"> <h1 className="text-7xl font-bold">
Make the Make the
@ -23,7 +23,7 @@ const HomePage: Page = () => {
</div> </div>
<div className="grid gap-4"> <div className="grid gap-4">
<div className="flex gap-3"> <div className="flex flex-col gap-3 lg:flex-col">
<Button size={40} icon={<DownloadIcon size={20} />}> <Button size={40} icon={<DownloadIcon size={20} />}>
Sign up for early access Sign up for early access
</Button> </Button>
@ -80,7 +80,7 @@ type FeatureSectionProps = {
const FeatureSection = ({ title, description }: FeatureSectionProps) => { const FeatureSection = ({ title, description }: FeatureSectionProps) => {
return ( return (
<section className="px-10"> <section className="px-10">
<div className="mb-24 flex justify-between"> <div className="mb-24 flex flex-col justify-between lg:flex-row">
<h2 className="flex-1 whitespace-pre-line text-6xl font-bold"> <h2 className="flex-1 whitespace-pre-line text-6xl font-bold">
{title} {title}
</h2> </h2>
@ -125,6 +125,8 @@ const FeatureGrid = () => {
) )
} }
HomePage.getLayout = AppLayout HomePage.getLayout = function getLayout(page) {
return <AppLayout>{page}</AppLayout>
}
export default HomePage export default HomePage

View File

@ -26,6 +26,8 @@ const EpicsDetailPage: Page = () => {
) )
} }
EpicsDetailPage.getLayout = InsightsLayout EpicsDetailPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default EpicsDetailPage export default EpicsDetailPage

View File

@ -59,12 +59,13 @@ const EpicsPage: Page = () => {
</Shadow> </Shadow>
))} ))}
</div> </div>
<DatePicker selected={selectedDates} onSelect={setSelectedDates} /> <DatePicker selected={selectedDates} onSelect={setSelectedDates} />
</div> </div>
) )
} }
EpicsPage.getLayout = InsightsLayout EpicsPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default EpicsPage export default EpicsPage

View File

@ -17,6 +17,8 @@ const OrphansPage: Page = () => {
) )
} }
OrphansPage.getLayout = InsightsLayout OrphansPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default OrphansPage export default OrphansPage

View File

@ -85,6 +85,8 @@ const ReposPage: Page = () => {
) )
} }
ReposPage.getLayout = InsightsLayout ReposPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default ReposPage export default ReposPage

View File

@ -26,6 +26,8 @@ const WorkstreamDetailPage: Page = () => {
) )
} }
WorkstreamDetailPage.getLayout = InsightsLayout WorkstreamDetailPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default WorkstreamDetailPage export default WorkstreamDetailPage

View File

@ -64,6 +64,8 @@ const WorkstreamsPage: Page = () => {
) )
} }
WorkstreamsPage.getLayout = InsightsLayout WorkstreamsPage.getLayout = function getLayout(page) {
return <InsightsLayout>{page}</InsightsLayout>
}
export default WorkstreamsPage export default WorkstreamsPage

View File

@ -30,6 +30,10 @@ module.exports = {
height: 'height', height: 'height',
}, },
screens: {
'md-lg': { raw: '(min-width: 868px)' },
},
keyframes: { keyframes: {
heightIn: { heightIn: {
from: { height: 0 }, from: { height: 0 },

View File

@ -19,6 +19,7 @@ type Props = PressableProps & {
icon?: React.ReactElement icon?: React.ReactElement
iconAfter?: React.ReactElement iconAfter?: React.ReactElement
disabled?: boolean disabled?: boolean
fullWidth?: boolean
} }
const textColors: MapVariant<typeof Base, 'variant'> = { const textColors: MapVariant<typeof Base, 'variant'> = {
@ -29,6 +30,19 @@ const textColors: MapVariant<typeof Base, 'variant'> = {
outline: '$neutral-100', outline: '$neutral-100',
ghost: '$neutral-100', ghost: '$neutral-100',
danger: '$white-100', danger: '$white-100',
blue: '$white-100',
purple: '$white-100',
orange: '$white-100',
army: '$white-100',
turquoise: '$white-100',
sky: '$white-100',
yellow: '$white-100',
pink: '$white-100',
cooper: '$white-100',
camel: '$white-100',
magenta: '$white-100',
yin: '$white-100',
yang: '$neutral-100',
} }
const textSizes: Record<NonNullable<Props['size']>, TextProps['size']> = { const textSizes: Record<NonNullable<Props['size']>, TextProps['size']> = {
@ -45,6 +59,7 @@ const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
children, children,
icon, icon,
iconAfter, iconAfter,
fullWidth,
...buttonProps ...buttonProps
} = props } = props
@ -62,8 +77,9 @@ const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
radius={shape === 'circle' ? 'full' : size} radius={shape === 'circle' ? 'full' : size}
size={size} size={size}
iconOnly={iconOnly} iconOnly={iconOnly}
width={fullWidth ? '100%' : 'auto'}
> >
{icon ? cloneElement(icon, { color: '$neutral-40' }) : null} {icon ? cloneElement(icon, { color: textColor || '$neutral-40' }) : null}
<Text weight="medium" color={textColor} size={textSize}> <Text weight="medium" color={textColor} size={textSize}>
{children} {children}
</Text> </Text>
@ -133,6 +149,72 @@ const Base = styled(Stack, {
// TODO: update background color // TODO: update background color
pressStyle: { backgroundColor: '$danger' }, pressStyle: { backgroundColor: '$danger' },
}, },
// TODO sync colors with the design foundation colors
blue: {
backgroundColor: '$blue-50',
hoverStyle: { backgroundColor: '$blue-60' },
pressStyle: { backgroundColor: '$blue-50' },
},
purple: {
backgroundColor: '$purple-50',
hoverStyle: { backgroundColor: '$purple-60' },
pressStyle: { backgroundColor: '$purple-50' },
},
orange: {
backgroundColor: '$orange-50',
hoverStyle: { backgroundColor: '$orange-60' },
pressStyle: { backgroundColor: '$orange-50' },
},
army: {
backgroundColor: '$indigo-50',
hoverStyle: { backgroundColor: '$indigo-60' },
pressStyle: { backgroundColor: '$indigo-50' },
},
turquoise: {
backgroundColor: '$turquoise-50',
hoverStyle: { backgroundColor: '$turquoise-60' },
pressStyle: { backgroundColor: '$turquoise-50' },
},
sky: {
backgroundColor: '$sky-50',
hoverStyle: { backgroundColor: '$sky-60' },
pressStyle: { backgroundColor: '$sky-50' },
},
yellow: {
backgroundColor: '$yellow-50',
hoverStyle: { backgroundColor: '$yellow-60' },
pressStyle: { backgroundColor: '$yellow-50' },
},
pink: {
backgroundColor: '$pink-50',
hoverStyle: { backgroundColor: '$pink-60' },
pressStyle: { backgroundColor: '$pink-50' },
},
cooper: {
backgroundColor: '$cooper-50',
hoverStyle: { backgroundColor: '$cooper-60' },
pressStyle: { backgroundColor: '$cooper-50' },
},
camel: {
backgroundColor: '$camel-50',
hoverStyle: { backgroundColor: '$camel-60' },
pressStyle: { backgroundColor: '$camel-50' },
},
magenta: {
backgroundColor: '$magenta',
hoverStyle: { backgroundColor: '$magenta-60' },
pressStyle: { backgroundColor: '$magenta-50' },
},
yin: {
backgroundColor: '$yin-50',
hoverStyle: { backgroundColor: '$yin-60' },
pressStyle: { backgroundColor: '$yin-50' },
},
yang: {
backgroundColor: '$yang-50',
hoverStyle: { backgroundColor: '$yang-60' },
pressStyle: { backgroundColor: '$yang-50' },
},
}, },
disabled: { disabled: {

View File

@ -42,7 +42,10 @@ const Tag = (props: Props) => {
return <Text size={textSizes[size]}>{icon}</Text> return <Text size={textSizes[size]}>{icon}</Text>
} }
return createElement(icon, { size: iconSizes[size] }) return createElement(icon, {
size: iconSizes[size],
color,
})
} }
return ( return (

View File

@ -14,9 +14,15 @@ import type { ComponentType, SVGProps } from 'react'
// } // }
// } // }
function isColorTokens(
value: `#${string}` | ColorTokens
): value is ColorTokens {
return typeof value === 'string' && value.startsWith('$')
}
export interface IconProps extends SVGProps<SVGSVGElement> { export interface IconProps extends SVGProps<SVGSVGElement> {
size: 12 | 16 | 20 size: 12 | 16 | 20
color?: ColorTokens color?: ColorTokens | `#${string}`
} }
export function createIcon<P extends SVGProps<SVGSVGElement>>( export function createIcon<P extends SVGProps<SVGSVGElement>>(
@ -25,7 +31,7 @@ export function createIcon<P extends SVGProps<SVGSVGElement>>(
const Icon = forwardRef<SVGElement, IconProps>((props, ref) => { const Icon = forwardRef<SVGElement, IconProps>((props, ref) => {
const { size, color = '$neutral-100', ...rest } = props const { size, color = '$neutral-100', ...rest } = props
const theme = useTheme() const theme = useTheme()
const token = theme[color]?.val const token = isColorTokens(color) ? theme[color]?.val : color
return createElement(Component, { return createElement(Component, {
ref, ref,