diff --git a/packages/app-commons/src/components/PageTitle/PageTitle.tsx b/packages/app-commons/src/components/PageTitle/PageTitle.tsx index 927e9dcf3..fc1af246e 100644 --- a/packages/app-commons/src/components/PageTitle/PageTitle.tsx +++ b/packages/app-commons/src/components/PageTitle/PageTitle.tsx @@ -1,72 +1,35 @@ 'use client'; -import type { BaseProps } from '@fuels/ui'; -import { Badge, HStack, Heading } from '@fuels/ui'; -import { tv } from 'tailwind-variants'; +import { Flex, HStack, HStackProps, Text } from '@fuels/ui'; -export type PageTitleProps = BaseProps<{ - as?: string; - size?: '1' | '2' | '3'; - children: React.ReactNode; - icon?: React.ReactNode; - rightElement?: React.ReactNode; -}>; +export type PageTitleProps = { + title: string; + children?: React.ReactNode; + subtitle?: React.ReactNode; + inverse?: boolean; + mb?: HStackProps['mb']; +}; export function PageTitle({ + title, children, - as = 'h2', - size = '1', - icon, - rightElement, - className, + subtitle, + inverse = false, + mb = '7', }: PageTitleProps) { - const classes = styles({ size }); return ( - - {icon && ( - - {icon} - - )} - {children} -
- {rightElement} -
-
+ + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + + {children} + ); } - -const styles = tv({ - slots: { - root: [ - 'first:mb-8 flex items-center justify-between gap-2', - 'border-border border-b', - 'tablet:flex-nowrap tablet:gap-4', - ], - icon: ['h-full flex-shrink-0 px-2', 'tablet:self-start tablet:mt-1.5'], - title: [ - 'items-center basis-full gap-3 order-3 flex-shrink-0 justify-between', - 'tablet:flex-col tablet:items-start tablet:gap-0', - 'text-[1.7rem] tablet:text-[2rem] laptop:text-[2.2rem]', - 'tablet:order-none tablet:flex-1 tablet:justify-start', - ], - }, - variants: { - size: { - '1': { - root: 'flex-wrap pb-2 tablet:pb-4', - title: 'text-[1.7rem] tablet:text-[2rem] laptop:text-[2.2rem]', - }, - '2': { - title: 'text-[1.4rem] tablet:text-[1.7rem] laptop:text-[1.9rem]', - root: 'pb-2 tablet:pb-4', - }, - '3': { - title: 'text-[1.2rem] tablet:text-[1.4rem] laptop:text-[1.6rem]', - root: 'pb-2', - }, - }, - }, - defaultVariants: { - size: '1', - }, -}); diff --git a/packages/app-explorer/.storybook/fonts.css b/packages/app-explorer/.storybook/fonts.css index 8191a3cd7..2f08c1a77 100644 --- a/packages/app-explorer/.storybook/fonts.css +++ b/packages/app-explorer/.storybook/fonts.css @@ -1,7 +1,7 @@ /* this is just for storybook */ @font-face { - font-family: 'GeistSans'; - src: url('/fonts/GeistVariableVF.woff2') format('woff2'); + font-family: 'Inter'; + src: url('/fonts/InterVariable.woff2') format('woff2'); font-weight: normal; font-style: normal; } @@ -14,7 +14,7 @@ } :root { - --font-geist-sans: 'GeistSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', + --font-inter: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; --font-geist-mono: 'GeistMono', 'SF Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Courier, monospace; diff --git a/packages/app-explorer/.storybook/preview.tsx b/packages/app-explorer/.storybook/preview.tsx index 7a27de3a5..8c107cee5 100644 --- a/packages/app-explorer/.storybook/preview.tsx +++ b/packages/app-explorer/.storybook/preview.tsx @@ -4,14 +4,13 @@ import './fonts.css'; import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; import type { Preview } from '@storybook/react'; -import React from 'react'; import { withThemeDecorator } from 'storybook-addon-theme'; import type { ReactNode } from 'react'; -import { Provider } from '../src/systems/Core/components/Provider'; +import { ThemeProvider } from '../src/systems/Core/components/Theme'; function ThemeWrapper({ children }: { children: ReactNode }) { - return {children}; + return {children}; } const preview: Preview = { diff --git a/packages/app-explorer/public/fonts/GeistVariableVF.woff2 b/packages/app-explorer/public/fonts/GeistVariableVF.woff2 deleted file mode 100644 index fa004e702..000000000 Binary files a/packages/app-explorer/public/fonts/GeistVariableVF.woff2 and /dev/null differ diff --git a/packages/app-explorer/public/fonts/InterVariable.woff2 b/packages/app-explorer/public/fonts/InterVariable.woff2 new file mode 100644 index 000000000..22a12b04e Binary files /dev/null and b/packages/app-explorer/public/fonts/InterVariable.woff2 differ diff --git a/packages/app-explorer/src/app/error.css b/packages/app-explorer/src/app/error.css index 8191a3cd7..2f08c1a77 100644 --- a/packages/app-explorer/src/app/error.css +++ b/packages/app-explorer/src/app/error.css @@ -1,7 +1,7 @@ /* this is just for storybook */ @font-face { - font-family: 'GeistSans'; - src: url('/fonts/GeistVariableVF.woff2') format('woff2'); + font-family: 'Inter'; + src: url('/fonts/InterVariable.woff2') format('woff2'); font-weight: normal; font-style: normal; } @@ -14,7 +14,7 @@ } :root { - --font-geist-sans: 'GeistSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', + --font-inter: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; --font-geist-mono: 'GeistMono', 'SF Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Courier, monospace; diff --git a/packages/app-explorer/src/app/global-error.tsx b/packages/app-explorer/src/app/global-error.tsx index 4a80b9825..9415af2b4 100644 --- a/packages/app-explorer/src/app/global-error.tsx +++ b/packages/app-explorer/src/app/global-error.tsx @@ -6,7 +6,7 @@ import './globals.css'; import Cookies from 'js-cookie'; import type { Metadata } from 'next'; import { ErrorPageComponent } from '~/systems/Core/components/ErrorPage/ErrorPage'; -import { Provider } from '~/systems/Core/components/Provider'; +import { ThemeProvider } from '~/systems/Core/components/Theme'; import { cx } from '~/systems/Core/utils/cx'; export const metadata: Metadata = { @@ -35,9 +35,9 @@ export default function Page() { - + - + ); diff --git a/packages/app-explorer/src/app/layout.tsx b/packages/app-explorer/src/app/layout.tsx index 0fec62be9..f3323df9f 100644 --- a/packages/app-explorer/src/app/layout.tsx +++ b/packages/app-explorer/src/app/layout.tsx @@ -3,13 +3,15 @@ import './globals.css'; import { Analytics } from '@vercel/analytics/react'; import { GeistMono } from 'geist/font/mono'; -import { GeistSans } from 'geist/font/sans'; import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; import { Layout } from '~/systems/Core/components/Layout/Layout'; -import { Provider } from '~/systems/Core/components/Provider'; -import { cx } from '~/systems/Core/utils/cx'; +import { PointsProgramEyebrow } from '~/systems/Core/components/PointsProgramEyebrow/PointsProgramEyebrow'; +import { ThemeProvider } from '~/systems/Core/components/Theme'; import { SafaryScript } from '~/systems/Safary/Safary'; +const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); + export const metadata: Metadata = { metadataBase: new URL('https://app.fuel.network'), title: 'Fuel Explorer', @@ -33,18 +35,19 @@ export default function RootLayout({ - + + {children} - + diff --git a/packages/app-explorer/src/systems/Account/components/AccountHeader.tsx b/packages/app-explorer/src/systems/Account/components/AccountHeader.tsx index 6c8b040f6..8997eedd5 100644 --- a/packages/app-explorer/src/systems/Account/components/AccountHeader.tsx +++ b/packages/app-explorer/src/systems/Account/components/AccountHeader.tsx @@ -1,5 +1,4 @@ import { Address } from '@fuels/ui'; -import { IconHash } from '@tabler/icons-react'; import { Suspense } from 'react'; import { PageTitle } from 'app-commons'; @@ -10,12 +9,9 @@ export function AccountHeader({ id }: { id: string }) { return ( <> } - className="border-b-gray-3" - > - Account -
- + title="Account" + subtitle={
} + /> }> diff --git a/packages/app-explorer/src/systems/Account/components/AccountTabs/AccountTabs.tsx b/packages/app-explorer/src/systems/Account/components/AccountTabs/AccountTabs.tsx index 90854f8dd..c95d1035e 100644 --- a/packages/app-explorer/src/systems/Account/components/AccountTabs/AccountTabs.tsx +++ b/packages/app-explorer/src/systems/Account/components/AccountTabs/AccountTabs.tsx @@ -27,7 +27,7 @@ export function AccountTabs({ return ( ( diff --git a/packages/app-explorer/src/systems/Account/components/AccountTitle/AccountTitle.tsx b/packages/app-explorer/src/systems/Account/components/AccountTitle/AccountTitle.tsx index f7d8cd4a3..1e42b8b3b 100644 --- a/packages/app-explorer/src/systems/Account/components/AccountTitle/AccountTitle.tsx +++ b/packages/app-explorer/src/systems/Account/components/AccountTitle/AccountTitle.tsx @@ -1,18 +1,15 @@ 'use client'; import { Address, useBreakpoints } from '@fuels/ui'; -import { IconHash } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; export function AccountTitle({ id }: { id: string }) { const { isLaptop } = useBreakpoints(); + return ( } - className="border-b-gray-3" - > - Account -
- + title="Account" + subtitle={
} + /> ); } diff --git a/packages/app-explorer/src/systems/Block/components/BlockHeader.tsx b/packages/app-explorer/src/systems/Block/components/BlockHeader.tsx index db962317f..55e29f78a 100644 --- a/packages/app-explorer/src/systems/Block/components/BlockHeader.tsx +++ b/packages/app-explorer/src/systems/Block/components/BlockHeader.tsx @@ -1,9 +1,7 @@ 'use client'; import { Address, LoadingBox, LoadingWrapper } from '@fuels/ui'; -import { IconCube } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; import { useParams } from 'next/navigation'; -import { PageSubtitle } from '~/systems/Core/components/PageSubtitle/PageSubtitle'; import { ViewMode } from '~/systems/Core/components/ViewMode/ViewMode'; import type { ViewModes } from '~/systems/Core/components/ViewMode/constants'; @@ -19,21 +17,22 @@ export function BlockHeader({ const { mode } = useParams<{ mode: ViewModes }>(); return ( } - rightElement={!isLoading && } + title="Block" + subtitle={ + } + regularEl={ + isValidAddress(id) ? ( +
+ ) : ( + <>#{id} + ) + } + /> + } > - Block - } - regularEl={ - isValidAddress(id) ? ( -
- ) : ( - #{id} - ) - } - /> + {!isLoading && } ); } diff --git a/packages/app-explorer/src/systems/Block/components/BlockScreenSimple.tsx b/packages/app-explorer/src/systems/Block/components/BlockScreenSimple.tsx index 52b229a3e..0b9964411 100644 --- a/packages/app-explorer/src/systems/Block/components/BlockScreenSimple.tsx +++ b/packages/app-explorer/src/systems/Block/components/BlockScreenSimple.tsx @@ -1,15 +1,7 @@ 'use client'; import type { GQLBlockFragment, Maybe } from '@fuel-explorer/graphql'; -import { - Address, - Grid, - Icon, - LoadingBox, - LoadingWrapper, - VStack, -} from '@fuels/ui'; -import { IconListDetails } from '@tabler/icons-react'; +import { Address, Grid, LoadingBox, LoadingWrapper, VStack } from '@fuels/ui'; import { PageTitle } from 'app-commons'; import NextLink from 'next/link'; import { Routes } from '~/routes'; @@ -65,9 +57,7 @@ export function BlockScreenSimple({ /> - }> - Transactions - + {isLoading ? ( ) : ( diff --git a/packages/app-explorer/src/systems/Bridge/components/BridgeScreenLoader.tsx b/packages/app-explorer/src/systems/Bridge/components/BridgeScreenLoader.tsx index 6a2ec8105..81e6181e3 100644 --- a/packages/app-explorer/src/systems/Bridge/components/BridgeScreenLoader.tsx +++ b/packages/app-explorer/src/systems/Bridge/components/BridgeScreenLoader.tsx @@ -1,23 +1,16 @@ import { Box, LoadingBox, VStack } from '@fuels/ui'; -import { IconArrowsShuffle } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; import { BridgeTxItemsLoading } from 'app-portal'; export function BridgeScreenLoader({ view }: { view: 'history' | 'bridge' }) { return ( - } - className="border-b-0 first:mb-0" - > - Fuel Bridge - + {view === 'history' ? ( ) : ( - + diff --git a/packages/app-explorer/src/systems/Contract/components/ContractHeader.tsx b/packages/app-explorer/src/systems/Contract/components/ContractHeader.tsx index 42a7bf0d2..4a5ab7222 100644 --- a/packages/app-explorer/src/systems/Contract/components/ContractHeader.tsx +++ b/packages/app-explorer/src/systems/Contract/components/ContractHeader.tsx @@ -1,7 +1,6 @@ 'use client'; import { Address } from '@fuels/ui'; -import { IconChecklist } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; import { ContractTabs } from './ContractTabs'; @@ -10,12 +9,9 @@ export function ContractHeader({ id }: { id: string }) { return ( <> } - className="border-b-gray-3" - > - Contract -
- + title="Contract" + subtitle={
} + /> ); diff --git a/packages/app-explorer/src/systems/Contract/components/ContractTitle.tsx b/packages/app-explorer/src/systems/Contract/components/ContractTitle.tsx index 75bab59fa..58bf840ae 100644 --- a/packages/app-explorer/src/systems/Contract/components/ContractTitle.tsx +++ b/packages/app-explorer/src/systems/Contract/components/ContractTitle.tsx @@ -1,18 +1,14 @@ 'use client'; import { Address, useBreakpoints } from '@fuels/ui'; -import { IconChecklist } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; export function ContractTitle({ id }: { id: string }) { const { isLaptop } = useBreakpoints(); return ( } - className="border-b-gray-3" - > - Contract -
- + title="Contract" + subtitle={
} + /> ); } diff --git a/packages/app-explorer/src/systems/Core/components/Footer/Footer.tsx b/packages/app-explorer/src/systems/Core/components/Footer/Footer.tsx index c98e0f903..43d295073 100644 --- a/packages/app-explorer/src/systems/Core/components/Footer/Footer.tsx +++ b/packages/app-explorer/src/systems/Core/components/Footer/Footer.tsx @@ -135,7 +135,7 @@ const styles = tv({ socialIcon: ['text-white hover:text-brand transition-colors duration-200 '], navs: ['flex flex-wrap justify-between gap-y-10 w-full max-w-screen-md'], nav: ['w-full tablet:w-1/2 desktop:w-auto'], - navHeading: ['font-mono justify-start text-gray-400'], + navHeading: ['font-mono justify-start text-white'], navList: ['flex flex-col gap-0'], navLink: [ 'font-mono text-white hover:text-brand hover:no-underline transition-colors duration-200', diff --git a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx index 26fc75006..0041f44ed 100644 --- a/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx +++ b/packages/app-explorer/src/systems/Core/components/Layout/Layout.tsx @@ -1,17 +1,16 @@ 'use client'; + import { Container, VStack } from '@fuels/ui'; -import type { BaseProps } from '@fuels/ui'; import { usePathname } from 'next/navigation'; import { Hero } from '~/systems/Home/components/Hero/Hero'; - import { cx } from '../../utils/cx'; import { Footer } from '../Footer/Footer'; import { TopNav } from '../TopNav/TopNav'; -export type LayoutProps = BaseProps<{ - hero?: boolean; +export type LayoutProps = { + children: React.ReactNode; contentClassName?: string; -}>; +}; export function Layout({ children, contentClassName }: LayoutProps) { const pathname = usePathname(); diff --git a/packages/app-explorer/src/systems/Core/components/PageSubtitle/PageSubtitle.tsx b/packages/app-explorer/src/systems/Core/components/PageSubtitle/PageSubtitle.tsx deleted file mode 100644 index 2970b86d1..000000000 --- a/packages/app-explorer/src/systems/Core/components/PageSubtitle/PageSubtitle.tsx +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; -import { Text } from '@fuels/ui'; - -export type PageSubtitleProps = { - children: React.ReactNode; -}; - -export function PageSubtitle({ children }: PageSubtitleProps) { - return {children}; -} diff --git a/packages/app-explorer/src/systems/Core/components/PointsProgramEyebrow/PointsProgramEyebrow.tsx b/packages/app-explorer/src/systems/Core/components/PointsProgramEyebrow/PointsProgramEyebrow.tsx new file mode 100644 index 000000000..68e20938d --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/PointsProgramEyebrow/PointsProgramEyebrow.tsx @@ -0,0 +1,16 @@ +import { Link } from '@fuels/ui'; + +export function PointsProgramEyebrow() { + return ( +
+ Points Program: Earn rewards and contribute to the Fuel Network. + + Learn more → + +
+ ); +} diff --git a/packages/app-explorer/src/systems/Core/components/Provider/Provider.tsx b/packages/app-explorer/src/systems/Core/components/Provider/Provider.tsx deleted file mode 100644 index a04d1db76..000000000 --- a/packages/app-explorer/src/systems/Core/components/Provider/Provider.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client'; - -import { Toaster } from '@fuels/ui'; -import { ThemeProvider } from '../Theme'; - -export function Provider({ children }: { children: React.ReactNode }) { - return ( - - {children} - - - ); -} diff --git a/packages/app-explorer/src/systems/Core/components/Provider/index.tsx b/packages/app-explorer/src/systems/Core/components/Provider/index.tsx deleted file mode 100644 index 934f15e57..000000000 --- a/packages/app-explorer/src/systems/Core/components/Provider/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { Provider } from './Provider'; diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx index a0db996d3..bb0c7b988 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchForm.tsx @@ -1,10 +1,8 @@ -import { useContext } from 'react'; import { useFormState } from 'react-dom'; import { search } from '~/systems/Core/actions/search'; import type { GQLSearchResult } from '@fuel-explorer/graphql'; import { SearchInput } from './SearchInput'; -import { SearchContext } from './SearchWidget'; import { styles } from './styles'; type SearchFormProps = { @@ -25,7 +23,6 @@ export function SearchForm({ }, null, ); - const { onClear } = useContext(SearchContext); return (
@@ -34,7 +31,6 @@ export function SearchForm({ searchResult={results} autoFocus={autoFocus} expandOnFocus={expandOnFocus} - onClear={onClear} />
); diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchInput.stories.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchInput.stories.tsx index 8f6de8c4c..2418451e2 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchInput.stories.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchInput.stories.tsx @@ -22,7 +22,6 @@ export const Clearable: Story = { className: 'w-sm', value: 'Some value', onSubmit: action('onSubmit'), - onClear: action('onClear'), autoFocus: true, }, }; diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchInput.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchInput.tsx index 9e2165340..e2ff35c33 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchInput.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchInput.tsx @@ -69,7 +69,7 @@ const SearchResultDropdown = forwardRef( ( > {shortAddress(transaction?.id || '', trimL, trimR)} @@ -201,7 +201,6 @@ const SearchResultDropdown = forwardRef( type SearchInputProps = BaseProps & { onSubmit?: (value: string) => void; - onClear?: (value: string) => void; searchResult?: Maybe; alwaysDisplayActionButtons?: boolean; expandOnFocus?: boolean; @@ -210,7 +209,6 @@ type SearchInputProps = BaseProps & { export function SearchInput({ value: initialValue = '', className, - onClear, autoFocus, placeholder = 'Search here...', searchResult, @@ -244,7 +242,6 @@ export function SearchInput({ function handleClear() { setValue(''); setHasSubmitted(false); - onClear?.(value); if (isExpanded) { setIsExpanded(false); } else { diff --git a/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx b/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx index 44552564e..b7aa46a75 100644 --- a/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx +++ b/packages/app-explorer/src/systems/Core/components/Search/SearchWidget.tsx @@ -1,78 +1,30 @@ +'use client'; + import { HStack } from '@fuels/ui'; import type { MutableRefObject } from 'react'; -import { createContext, useEffect, useRef } from 'react'; - +import { createContext, useRef } from 'react'; import { SearchForm } from './SearchForm'; import { styles } from './styles'; export const SearchContext = createContext<{ dropdownRef: null | MutableRefObject; - onClear: (value: string) => void; -}>({ dropdownRef: null, onClear: () => {} }); +}>({ dropdownRef: null }); type SearchWidgetProps = { autoFocus?: boolean; - isSearchOpen: boolean; - setIsSearchOpen: (value: boolean) => void; expandOnFocus?: boolean; }; export const SearchWidget = ({ autoFocus, - setIsSearchOpen, expandOnFocus, }: SearchWidgetProps) => { const classes = styles(); const widgetRef = useRef(null); const dropdownRef = useRef(null); - function onClear(value: string) { - if (!value.length) { - setIsSearchOpen(false); - } - } - - function isClickInBounds( - clickX: number, - clickY: number, - left: number, - right: number, - top: number, - bottom: number, - ) { - return ( - clickX >= left && clickX <= right && clickY >= top && clickY <= bottom - ); - } - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const widgetLeft = widgetRef.current?.offsetLeft ?? 0; - const widgetRight = widgetLeft + (widgetRef.current?.offsetWidth ?? 0); - const widgetTop = widgetRef.current?.offsetTop ?? 0; - const widgetBottom = widgetTop + (widgetRef.current?.offsetHeight ?? 0); - if ( - !isClickInBounds( - event.x, - event.y, - widgetLeft, - widgetRight, - widgetTop, - widgetBottom, - ) && - !dropdownRef?.current?.contains(event.target as Node) - ) { - setIsSearchOpen(false); - } - }; - document.addEventListener('click', handleClickOutside, true); - return () => { - document.removeEventListener('click', handleClickOutside, true); - }; - }, []); - return ( - + {children} + ); } diff --git a/packages/app-explorer/src/systems/Core/components/Theme/index.ts b/packages/app-explorer/src/systems/Core/components/Theme/index.ts new file mode 100644 index 000000000..656c8068c --- /dev/null +++ b/packages/app-explorer/src/systems/Core/components/Theme/index.ts @@ -0,0 +1,4 @@ +'use client'; + +export * from './ThemeProvider'; +export * from './ThemeDefault'; diff --git a/packages/app-explorer/src/systems/Core/components/Theme/index.tsx b/packages/app-explorer/src/systems/Core/components/Theme/index.tsx deleted file mode 100644 index ab55e81a5..000000000 --- a/packages/app-explorer/src/systems/Core/components/Theme/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Theme'; -export * from './ThemeDefault'; diff --git a/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx b/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx index fb7ea6e60..e311a0916 100644 --- a/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx +++ b/packages/app-explorer/src/systems/Core/components/TopNav/TopNav.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Box, Link, Nav, useBreakpoints } from '@fuels/ui'; +import { Nav, useBreakpoints } from '@fuels/ui'; import { isRoute } from 'app-commons'; import { useTheme } from 'next-themes'; import NextLink from 'next/link'; @@ -31,7 +31,7 @@ export function TopNav() { } else if (!isLaptop && isDesktopSearchOpen) { setIsMobileSearchOpen(true); } - }, [isLaptop]); + }, [isLaptop, isDesktopSearchOpen, isMobileSearchOpen]); const logo = ( @@ -76,54 +76,29 @@ export function TopNav() { ); return ( - <> - - Points Program: Earn rewards and contribute to the Fuel Network. - - Learn more → - - - - + ); } diff --git a/packages/app-explorer/src/systems/Core/components/Utxos/Utxos.tsx b/packages/app-explorer/src/systems/Core/components/Utxos/Utxos.tsx index 26be995b0..75289978d 100644 --- a/packages/app-explorer/src/systems/Core/components/Utxos/Utxos.tsx +++ b/packages/app-explorer/src/systems/Core/components/Utxos/Utxos.tsx @@ -100,7 +100,7 @@ function VirtualList({ items, assetId }: UtxosProps) { export function Utxos({ items, assetId, ...props }: UtxosProps) { return ( - + UTXOs ({items?.length ?? 0}) diff --git a/packages/app-explorer/src/systems/Core/utils/asset.ts b/packages/app-explorer/src/systems/Core/utils/asset.ts index c5e789078..af1a6d093 100644 --- a/packages/app-explorer/src/systems/Core/utils/asset.ts +++ b/packages/app-explorer/src/systems/Core/utils/asset.ts @@ -1,4 +1,4 @@ -import { NetworkFuel, assets } from '@fuel-ts/account'; +import { type NetworkFuel, assets } from '@fuel-ts/account'; export const ASSET_LIST = assets; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx deleted file mode 100644 index 51f030b43..000000000 --- a/packages/app-explorer/src/systems/Home/components/Hero/Hero.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Hero } from './Hero'; - -const meta: Meta = { - title: 'Home/Hero', - component: Hero, - parameters: { - layout: 'fullscreen', - }, -}; - -export default meta; -type Story = StoryObj; - -export const Desktop: Story = { - args: {}, -}; - -export const Tablet: Story = { - args: {}, - parameters: { - viewport: { - defaultViewport: 'ipad', - }, - }, -}; - -export const Mobile: Story = { - args: {}, - parameters: { - viewport: { - defaultViewport: 'iphonex', - }, - }, -}; diff --git a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx index b3c68a06e..031314ac5 100644 --- a/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx +++ b/packages/app-explorer/src/systems/Home/components/Hero/Hero.tsx @@ -1,31 +1,23 @@ 'use client'; -import { Box, Container, Heading, Theme } from '@fuels/ui'; -import { useState } from 'react'; +import { Box, Container, Heading } from '@fuels/ui'; import { tv } from 'tailwind-variants'; import { SearchWidget } from '~/systems/Core/components/Search/SearchWidget'; export function Hero() { - const [isSearchOpen, setIsSearchOpen] = useState(false); const classes = styles(); return ( - - - - - Explore Fuel - - - - - - - + + + + Explore Fuel + + + + + + ); } diff --git a/packages/app-explorer/src/systems/Transaction/component/TxHeader/TxHeader.tsx b/packages/app-explorer/src/systems/Transaction/component/TxHeader/TxHeader.tsx index c6914da27..deb0efdd6 100644 --- a/packages/app-explorer/src/systems/Transaction/component/TxHeader/TxHeader.tsx +++ b/packages/app-explorer/src/systems/Transaction/component/TxHeader/TxHeader.tsx @@ -1,6 +1,5 @@ 'use client'; import { Address, useBreakpoints } from '@fuels/ui'; -import { IconChecklist } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; import { useParams } from 'next/navigation'; import { ViewMode } from '~/systems/Core/components/ViewMode/ViewMode'; @@ -18,11 +17,10 @@ export function TxHeader({ return ( } - rightElement={!isLoading && } + title="Transaction" + subtitle={
} > - Transaction -
+ {!isLoading && } ); } diff --git a/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.tsx b/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.tsx index f68dc0c4c..4a9b0ef1c 100644 --- a/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.tsx +++ b/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.tsx @@ -18,7 +18,6 @@ import type { CardProps } from '@fuels/ui'; import { IconArrowUp } from '@tabler/icons-react'; import { bn } from 'fuels'; import NextLink from 'next/link'; -import React from 'react'; import { tv } from 'tailwind-variants'; import { Routes } from '~/routes'; import { AssetItem } from '~/systems/Asset/components/AssetItem/AssetItem'; diff --git a/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.utils.ts b/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.utils.ts index 4256b2f15..f71a1e7a5 100644 --- a/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.utils.ts +++ b/packages/app-explorer/src/systems/Transaction/component/TxOutput/TxOutput.utils.ts @@ -1,4 +1,4 @@ -import { GQLTransactionOutputFragment } from '@fuel-explorer/graphql'; +import type { GQLTransactionOutputFragment } from '@fuel-explorer/graphql'; export const isOutput = ( output: GQLTransactionOutputFragment, diff --git a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx index 055245658..eaefa40c2 100644 --- a/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx +++ b/packages/app-explorer/src/systems/Transactions/components/TxsTitle/TxsTitle.tsx @@ -1,12 +1,6 @@ 'use client'; -import { Icon } from '@fuels/ui'; -import { IconListDetails } from '@tabler/icons-react'; import { PageTitle } from 'app-commons'; export function TxsTitle() { - return ( - }> - Recent Transactions - - ); + return ; } diff --git a/packages/app-portal/src/systems/Bridge/containers/BridgeTabs.tsx b/packages/app-portal/src/systems/Bridge/containers/BridgeTabs.tsx index 7d82c1d57..874615a7f 100644 --- a/packages/app-portal/src/systems/Bridge/containers/BridgeTabs.tsx +++ b/packages/app-portal/src/systems/Bridge/containers/BridgeTabs.tsx @@ -1,4 +1,5 @@ import { ToggleGroup } from '@fuels/ui'; +import { useMemo } from 'react'; import { tv } from 'tailwind-variants'; import { isEthChain, isFuelChain } from '~portal/systems/Chains'; import { useBridge } from '../hooks'; @@ -15,16 +16,18 @@ export const BridgeTabs = () => { handlers.goToWithdraw(); }; - function getDefaultValue() { + const value = useMemo(() => { if (isEthChain(fromNetwork)) return 'deposit'; if (isFuelChain(fromNetwork)) return 'withdraw'; - } + return 'deposit'; + }, [fromNetwork]); return ( { const styles = tv({ slots: { - toggle: [ - 'w-full rounded-md fuel-[ToggleGroupItem]:h-9', - 'fuel-[ToggleGroupItem]:text-md', - ], + toggle: ['w-full rounded-md h-9', 'fuel-[ToggleGroupItem]:text-md'], }, }); diff --git a/packages/app-portal/src/systems/Bridge/pages/Bridge.tsx b/packages/app-portal/src/systems/Bridge/pages/Bridge.tsx index 524728681..7cab48fa3 100644 --- a/packages/app-portal/src/systems/Bridge/pages/Bridge.tsx +++ b/packages/app-portal/src/systems/Bridge/pages/Bridge.tsx @@ -74,29 +74,33 @@ export const Bridge = () => { Asset amount - - handlers.changeAssetAmount({ assetAmount: val || undefined }) - } - placeholder="0.00" - > - - - - - + + + + handlers.changeAssetAmount({ assetAmount: val || undefined }) + } + placeholder="0.00" + > + + + + + + + + {isFuelChain(toNetwork) && balance?.eq(0) && !!ethAssetAddress && ( diff --git a/packages/app-portal/src/systems/Bridge/pages/BridgeHome.tsx b/packages/app-portal/src/systems/Bridge/pages/BridgeHome.tsx index c07319f65..18d3e86bf 100644 --- a/packages/app-portal/src/systems/Bridge/pages/BridgeHome.tsx +++ b/packages/app-portal/src/systems/Bridge/pages/BridgeHome.tsx @@ -1,10 +1,6 @@ import { useNodeInfo } from '@fuels/react'; import { Box, Button } from '@fuels/ui'; -import { - IconArrowBack, - IconArrowsShuffle, - IconHistory, -} from '@tabler/icons-react'; +import { IconArrowBack, IconHistory } from '@tabler/icons-react'; import { FUEL_VERSION, PageTitle } from 'app-commons'; import NextLink from 'next/link'; import { usePathname } from 'next/navigation'; @@ -25,43 +21,36 @@ export const BridgeHome = ({ children }: BridgeHomeProps) => { return ( - } - className="border-b-0 first:mb-0" - rightElement={ - isBridgeHistory ? ( - - ) : ( - - ) - } - > - Fuel Bridge + + {isBridgeHistory ? ( + + ) : ( + + )} {children} diff --git a/packages/app-portal/src/systems/Chains/eth/containers/TxEthToFuelDialog.tsx b/packages/app-portal/src/systems/Chains/eth/containers/TxEthToFuelDialog.tsx index c7fd37e9a..13741747f 100644 --- a/packages/app-portal/src/systems/Chains/eth/containers/TxEthToFuelDialog.tsx +++ b/packages/app-portal/src/systems/Chains/eth/containers/TxEthToFuelDialog.tsx @@ -2,8 +2,7 @@ import { useAsset } from '~portal/systems/Assets/hooks/useAsset'; import { shortAddress } from '~portal/systems/Core'; import { useOverlay } from '~portal/systems/Overlay'; -import { Button, Dialog, HStack, VStack } from '@fuels/ui'; -import { IconTransferIn } from '@tabler/icons-react'; +import { Button, Dialog, VStack } from '@fuels/ui'; import { tv } from 'tailwind-variants'; import { BridgeSteps, @@ -31,12 +30,7 @@ export function TxEthToFuelDialog() { return ( - - - - Deposit - - + Deposit - - - Withdraw - - + Withdraw void; }) { const classes = styles(); + return ( <> - } className="first:mb-0"> - - Explore Fuel DApps - - Here's a list of DApps built on Fuel - - - + + + + { - addComponents({ - '.hero-bg': { - background: - 'url(/logo-faded.svg) no-repeat -40px center, var(--hero-bg)', - backgroundSize: 'auto 100%', - }, - }); - }), - ], + plugins: [], } satisfies Config; diff --git a/packages/e2e-tests/tests/hard/bridge/Bridge.test.ts b/packages/e2e-tests/tests/hard/bridge/Bridge.test.ts index 8f6c79e00..18ef77c92 100644 --- a/packages/e2e-tests/tests/hard/bridge/Bridge.test.ts +++ b/packages/e2e-tests/tests/hard/bridge/Bridge.test.ts @@ -88,6 +88,32 @@ test.describe('Bridge', () => { }); const baseAssetId = fuelWallet.provider.getBaseAssetId(); + await test.step('Fuel wallet should be connected after refresh', async () => { + await goToBridgePage(page); + + const connectedWallet = getByAriaLabel( + page, + 'Fuel Local: Connected Wallet', + ); + const address = await connectedWallet.innerText(); + const balance = getByAriaLabel(page, 'Balance'); + const balanceText = await balance.innerText(); + + // refresh the page + await page.goto('/bridge'); + + const connectedWalletAferRefresh = getByAriaLabel( + page, + 'Fuel Local: Connected Wallet', + ); + const addressAfterRefresh = await connectedWalletAferRefresh.innerText(); + const balanceAfterRefresh = getByAriaLabel(page, 'Balance'); + const balanceTextAfterRefresh = await balanceAfterRefresh.innerText(); + + expect(addressAfterRefresh).toEqual(address); + expect(balanceTextAfterRefresh).toEqual(balanceText); + }); + await test.step('Deposit ETH to Fuel', async () => { const preDepositBalanceFuel = await fuelWallet.getBalance(baseAssetId); const prevDepositBalanceEth = await client.getBalance({ @@ -96,7 +122,7 @@ test.describe('Bridge', () => { await test.step('Fill data and click on deposit', async () => { await hasDropdownSymbol(page, 'ETH'); - const depositInput = page.locator('.fuel-InputAmount input'); + const depositInput = page.locator('.fuel-InputAmountField input'); await depositInput.fill(DEPOSIT_AMOUNT); const depositButton = getByAriaLabel(page, 'Deposit', true); await depositButton.click(); @@ -174,7 +200,7 @@ test.describe('Bridge', () => { await clickWithdrawTab(page); await hasDropdownSymbol(page, 'ETH'); - const withdrawInput = page.locator('.fuel-InputAmount input'); + const withdrawInput = page.locator('.fuel-InputAmountField input'); await withdrawInput.fill(WITHDRAW_AMOUNT); const withdrawButton = getByAriaLabel(page, 'Withdraw', true); await withdrawButton.click(); @@ -334,7 +360,7 @@ test.describe('Bridge', () => { await hasDropdownSymbol(page, 'TKN'); // Deposit asset - const depositInput = page.locator('.fuel-InputAmount input'); + const depositInput = page.locator('.fuel-InputAmountField input'); await depositInput.fill(DEPOSIT_AMOUNT); const depositButton = getByAriaLabel(page, 'Deposit', true); await depositButton.click(); @@ -446,7 +472,7 @@ test.describe('Bridge', () => { await selectToken(page, 'TKN'); await hasDropdownSymbol(page, 'TKN'); - const withdrawInput = page.locator('.fuel-InputAmount input'); + const withdrawInput = page.locator('.fuel-InputAmountField input'); await withdrawInput.fill(WITHDRAW_AMOUNT); const withdrawButton = getByAriaLabel(page, 'Withdraw', true); await withdrawButton.click(); @@ -558,32 +584,6 @@ test.describe('Bridge', () => { await checkTxItemDone(page, withdrawERC20TxId); }); - await test.step('Fuel wallet should be connected after refresh', async () => { - await goToBridgePage(page); - - const connectedWallet = getByAriaLabel( - page, - 'Fuel Local: Connected Wallet', - ); - const address = await connectedWallet.innerText(); - const balance = getByAriaLabel(page, 'Balance: '); - const balanceText = await balance.innerText(); - - // refresh the page - await page.goto('/bridge'); - - const connectedWalletAferRefresh = getByAriaLabel( - page, - 'Fuel Local: Connected Wallet', - ); - const addressAfterRefresh = await connectedWalletAferRefresh.innerText(); - const balanceAfterRefresh = getByAriaLabel(page, 'Balance: '); - const balanceTextAfterRefresh = await balanceAfterRefresh.innerText(); - - expect(addressAfterRefresh).toEqual(address); - expect(balanceTextAfterRefresh).toEqual(balanceText); - }); - await test.step('Check if transaction list reacts correctly to fuel wallet changes', async () => { await goToTransactionsPage(page); @@ -630,7 +630,7 @@ test.describe('Bridge', () => { // Deposit asset const depositAmount = '1.12345'; - const depositInput = page.locator('.fuel-InputAmount input'); + const depositInput = page.locator('.fuel-InputAmountField input'); await depositInput.fill(depositAmount); await test.step('Test deposit alert', async () => { diff --git a/packages/e2e-tests/tests/hard/bridge/utils/bridge.ts b/packages/e2e-tests/tests/hard/bridge/utils/bridge.ts index 072555fb4..cff95e756 100644 --- a/packages/e2e-tests/tests/hard/bridge/utils/bridge.ts +++ b/packages/e2e-tests/tests/hard/bridge/utils/bridge.ts @@ -27,6 +27,9 @@ export const goToBridgePage = async (page: Page) => { }; export const goToTransactionsPage = async (page: Page) => { + const currentUrl = page.url(); + if (currentUrl.includes('/bridge/history')) return; + const transactionList = getByAriaLabel(page, 'Transaction History'); await transactionList.click(); diff --git a/packages/ui/package.json b/packages/ui/package.json index 384a81517..dbb6d59d5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,9 +3,9 @@ "version": "0.0.1", "access": "public", "private": true, - "main": "./src/index.tsx", + "main": "./src/index.ts", "exports": { - ".": "./src/index.tsx", + ".": "./src/index.ts", "./tailwind-preset": "./src/theme/tailwind-preset.ts", "./styles.css": "./src/theme/index.css" }, diff --git a/packages/ui/public/fonts/GeistVariableVF.woff2 b/packages/ui/public/fonts/GeistVariableVF.woff2 deleted file mode 100644 index fa004e702..000000000 Binary files a/packages/ui/public/fonts/GeistVariableVF.woff2 and /dev/null differ diff --git a/packages/ui/public/fonts/InterVariable.woff2 b/packages/ui/public/fonts/InterVariable.woff2 new file mode 100644 index 000000000..22a12b04e Binary files /dev/null and b/packages/ui/public/fonts/InterVariable.woff2 differ diff --git a/packages/ui/src/components/BadgeAsset/BadgeAsset.stories.tsx b/packages/ui/src/components/BadgeAsset/BadgeAsset.stories.tsx new file mode 100644 index 000000000..514e006bb --- /dev/null +++ b/packages/ui/src/components/BadgeAsset/BadgeAsset.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { BadgeAsset } from './BadgeAsset'; + +const meta: Meta = { + title: 'UI/BadgeAsset', + component: BadgeAsset, + argTypes: { + variant: { + options: ['solid', 'transparent'], + control: { type: 'radio' }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Usage: Story = { + args: { + children: 'FUEL V1', + variant: 'solid', + }, +}; diff --git a/packages/ui/src/components/BadgeAsset/BadgeAsset.styles.ts b/packages/ui/src/components/BadgeAsset/BadgeAsset.styles.ts new file mode 100644 index 000000000..4e22365e6 --- /dev/null +++ b/packages/ui/src/components/BadgeAsset/BadgeAsset.styles.ts @@ -0,0 +1,18 @@ +import { tv } from 'tailwind-variants'; + +export const styles = tv({ + base: [ + 'px-2 py-1', + 'text-[var(--gray-12)] text-sm font-semibold', + 'inline-flex items-center self-center shrink-0 grow-0 gap-2', + ], + variants: { + variant: { + solid: 'bg-[var(--gray-3)] rounded-full', + transparent: 'bg-transparent', + }, + }, + defaultVariants: { + variant: 'solid', + }, +}); diff --git a/packages/ui/src/components/BadgeAsset/BadgeAsset.tsx b/packages/ui/src/components/BadgeAsset/BadgeAsset.tsx new file mode 100644 index 000000000..42d2b3727 --- /dev/null +++ b/packages/ui/src/components/BadgeAsset/BadgeAsset.tsx @@ -0,0 +1,36 @@ +import type { WithVariants } from '../../hooks/useVariants'; +import { createComponent } from '../../utils/component'; +import { styles } from './BadgeAsset.styles'; + +type BadgeAssetBaseProps = { + children: React.ReactNode; + className?: string; + icon: string; +}; + +type BadgeAssetVariant = 'solid' | 'transparent'; + +export type BadgeAssetProps = WithVariants< + BadgeAssetBaseProps, + BadgeAssetVariant +>; + +export const BadgeAsset = createComponent({ + id: 'BadgeAsset', + render: (_, { children, icon, className, variant }) => { + const classes = styles({ variant, className }); + return ( + + {`${children} + {children} + + ); + }, + defaultProps: { + variant: 'solid', + }, +}); diff --git a/packages/ui/src/components/BadgeAsset/index.ts b/packages/ui/src/components/BadgeAsset/index.ts new file mode 100644 index 000000000..c51702964 --- /dev/null +++ b/packages/ui/src/components/BadgeAsset/index.ts @@ -0,0 +1,3 @@ +export { BadgeAsset } from './BadgeAsset'; + +export type { BadgeAssetProps } from './BadgeAsset'; diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx index 4524d45c2..62fea2224 100644 --- a/packages/ui/src/components/Card/Card.tsx +++ b/packages/ui/src/components/Card/Card.tsx @@ -4,17 +4,16 @@ import { createPolymorphicComponent, withNamespace, } from '../../utils/component'; +import type { PropsOf, WithAsProps } from '../../utils/types'; import type { BoxProps } from '../Box'; import { Box } from '../Box'; -import type { HeadingProps } from '../Heading'; -import { Heading } from '../Heading'; import { Text } from '../Text'; import type { TextProps } from '../Text'; import { styles } from './styles'; -export type CardProps = BoxProps; +export type CardProps = WithAsProps & PropsOf; export type CardHeaderProps = BoxProps; -export type CardTitleProps = HeadingProps; +export type CardTitleProps = TextProps; export type CardBodyProps = BoxProps; export type CardDescriptionProps = TextProps; export type CardFooterProps = BoxProps; @@ -22,13 +21,20 @@ export type CardFooterProps = BoxProps; export const CardRoot = createPolymorphicComponent({ id: 'Card', className: ({ className }) => styles().root({ className }), + baseElement: Box, + render: (Comp, props) => { + const { className, as, children, ...rest } = props; + return ( + + + {children} + + + ); + }, defaultProps: { as: 'article', }, - render: (_, props) => { - const { className, ...rest } = props; - return ; - }, }); export const CardHeader = createPolymorphicComponent< @@ -43,12 +49,13 @@ export const CardHeader = createPolymorphicComponent< }, }); -export const CardTitle = createComponent({ +export const CardTitle = createComponent({ id: 'CardTitle', - baseElement: Heading, + baseElement: Text, className: ({ className }) => styles().title({ className }), defaultProps: { - size: '6', + size: '1', + weight: 'medium', }, }); diff --git a/packages/ui/src/components/Card/styles.ts b/packages/ui/src/components/Card/styles.ts index 20a4b4e4b..80b5e7a05 100644 --- a/packages/ui/src/components/Card/styles.ts +++ b/packages/ui/src/components/Card/styles.ts @@ -2,10 +2,10 @@ import { tv } from 'tailwind-variants'; export const styles = tv({ slots: { - root: ['flex flex-col py-4 gap-4 bg-panel-solid text-color overflow-clip'], - header: 'flex flex-col gap-1.5 px-4 text-heading', + root: 'flex flex-col py-4 gap-4 bg-panel-solid overflow-clip', + header: 'flex flex-col gap-1.5 px-4', title: - 'm-0 font-semibold leading-none tracking-tight text-xl flex items-center gap-2', + 'm-0 text-gray-11 leading-none tracking-tight flex items-center gap-2', description: 'm-0 text-sm text-secondary', body: 'px-4 text-base', footer: 'px-4 pt-0 self-end flex gap-2', diff --git a/packages/ui/src/components/Collapsible/Collapsible.tsx b/packages/ui/src/components/Collapsible/Collapsible.tsx index 986715ee9..c402ddd8b 100644 --- a/packages/ui/src/components/Collapsible/Collapsible.tsx +++ b/packages/ui/src/components/Collapsible/Collapsible.tsx @@ -183,6 +183,7 @@ const styles = tv({ content: 'p-3 rounded-sm', body: 'pt-2', }, + classic: {}, }, }, defaultVariants: { diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx index 33485cf74..c05a37a23 100644 --- a/packages/ui/src/components/Dialog/Dialog.tsx +++ b/packages/ui/src/components/Dialog/Dialog.tsx @@ -1,13 +1,19 @@ import { Dialog as RD } from '@radix-ui/themes'; +import { IconX } from '@tabler/icons-react'; +import clsx from 'clsx'; import { createComponent, withNamespace } from '../../utils/component'; import type { PropsOf } from '../../utils/types'; +import { IconButton } from '../IconButton'; export type DialogProps = PropsOf; export type DialogTriggerProps = PropsOf; export type DialogTitleProps = PropsOf; export type DialogContentProps = PropsOf; export type DialogCloseProps = PropsOf; +export type DialogCloseButtonProps = Partial< + Omit, 'icon'> +>; export type DialogDescriptionProps = PropsOf; export const DialogRoot = createComponent({ @@ -36,9 +42,33 @@ export const DialogClose = createComponent({ baseElement: RD.Close, }); +export const DialogCloseButton = createComponent< + DialogCloseButtonProps, + typeof IconButton +>({ + id: 'DialogCloseButton', + render: (_, props) => { + return ( + + ); + }, +}); + export const DialogTitle = createComponent({ id: 'DialogTitle', baseElement: RD.Title, + className: () => 'font-mono text-2xl', }); export const DialogDescription = createComponent< @@ -53,6 +83,7 @@ export const Dialog = withNamespace(DialogRoot, { Trigger: DialogTrigger, Content: DialogContent, Close: DialogClose, + CloseButton: DialogCloseButton, Description: DialogDescription, Title: DialogTitle, }); diff --git a/packages/ui/src/components/FuelLogo/FuelLogo.tsx b/packages/ui/src/components/FuelLogo/FuelLogo.tsx index a14c07b0e..5942bdd31 100644 --- a/packages/ui/src/components/FuelLogo/FuelLogo.tsx +++ b/packages/ui/src/components/FuelLogo/FuelLogo.tsx @@ -21,25 +21,38 @@ export const FuelLogo = createComponent({ return ( {showSymbol && ( - + + + )} {showLettering && ( )} diff --git a/packages/ui/src/components/InputAmount/InputAmount.context.ts b/packages/ui/src/components/InputAmount/InputAmount.context.ts new file mode 100644 index 000000000..0c00081b4 --- /dev/null +++ b/packages/ui/src/components/InputAmount/InputAmount.context.ts @@ -0,0 +1,22 @@ +import type { BN, FormatConfig } from '@fuel-ts/math'; +import { createContext } from 'react'; +import type { AmountChangeParameters } from './InputAmount.types'; + +type InputAmountRootContext = { + balance: BN; + formatOpts: FormatConfig; +}; + +type InputAmountFieldContext = { + disabled?: boolean; + assetAmount: string; + handleAmountChange: (props: AmountChangeParameters) => void; +}; + +export const InputAmountRootCtx = createContext( + {} as InputAmountRootContext, +); + +export const InputAmountFieldCtx = createContext( + {} as InputAmountFieldContext, +); diff --git a/packages/ui/src/components/InputAmount/InputAmount.stories.tsx b/packages/ui/src/components/InputAmount/InputAmount.stories.tsx index 894232ed5..90a34310a 100644 --- a/packages/ui/src/components/InputAmount/InputAmount.stories.tsx +++ b/packages/ui/src/components/InputAmount/InputAmount.stories.tsx @@ -1,7 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Box } from '@radix-ui/themes'; -import { Flex } from '../Box'; import { InputAmount } from './InputAmount'; const meta: Meta = { @@ -14,59 +13,14 @@ type Story = StoryObj; export const Usage: Story = { render: () => ( - console.log(balance)} - className="max-w-[400px]" - placeholder="0.00" - > - - { - alert('Max Balance has been clicked'); - }} - /> - alert('Coin selector has been clicked')} - /> - - - - ), -}; - -export const OnlyField: Story = { - render: () => ( - - console.log(balance)} - placeholder="0.00" - /> - - ), -}; - -export const WithoutBalance: Story = { - render: () => ( - - + Choose amount to delegate + console.log(balance)} + className="max-w-[400px]" placeholder="0.00" > @@ -84,6 +38,55 @@ export const WithoutBalance: Story = { onClick={() => alert('Coin selector has been clicked')} /> + + + + ), +}; + +export const OnlyField: Story = { + render: () => ( + + + console.log(balance)} + placeholder="0.00" + /> + + + ), +}; + +export const WithoutBalance: Story = { + render: () => ( + + + console.log(balance)} + placeholder="0.00" + > + + { + alert('Max Balance has been clicked'); + }} + /> + alert('Coin selector has been clicked')} + /> + + ), @@ -92,18 +95,16 @@ export const WithoutBalance: Story = { export const OnlyBalance: Story = { render: () => ( - console.log(balance)} - placeholder="0.00" - > - - - + + console.log(balance)} + placeholder="0.00" + /> + ), diff --git a/packages/ui/src/components/InputAmount/InputAmount.tsx b/packages/ui/src/components/InputAmount/InputAmount.tsx index ae34c6c92..dd3a75d66 100644 --- a/packages/ui/src/components/InputAmount/InputAmount.tsx +++ b/packages/ui/src/components/InputAmount/InputAmount.tsx @@ -1,77 +1,102 @@ -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; -import { type BN, FormatConfig, bn, format } from '@fuel-ts/math'; +import { type BN, type FormatConfig, bn } from '@fuel-ts/math'; import { DECIMAL_UNITS, DEFAULT_MIN_PRECISION } from '@fuel-ts/math/configs'; import { IconChevronDown } from '@tabler/icons-react'; -import { Button, ButtonProps } from '../Button'; +import { Button, type ButtonProps } from '../Button'; import type { InputProps, InputSlotProps } from '../Input/Input'; import { Input } from '../Input/Input'; -import { Text } from '../Text'; +import { Text, type TextProps } from '../Text'; import { Tooltip } from '../Tooltip'; +import clsx from 'clsx'; import { mergeProps } from 'react-aria'; import { tv } from 'tailwind-variants'; import { createComponent, withNamespace } from '../../utils/component'; import { Avatar } from '../Avatar'; +import { Badge, type BadgeProps } from '../Badge'; +import { VStack, type VStackProps } from '../Box'; +import { InputAmountFieldCtx, InputAmountRootCtx } from './InputAmount.context'; +import type { AmountChangeParameters } from './InputAmount.types'; import { createAmount } from './utils'; -export type InputAmountProps = Omit< +export type InputAmountFieldProps = Omit< InputProps, 'size' | 'onChange' | 'value' > & { disabled?: boolean; - balance?: BN; value?: BN | null; onChange?: (val: BN | null) => void; }; -export type InputAmountBalanceProps = InputSlotProps; +export type InputAmountProps = VStackProps & { + balance?: BN; + formatOpts?: FormatConfig; +}; +export type InputAmountBalanceProps = BadgeProps & { + label?: string; + balance?: BN; +}; export type InputAmountButtonMaxBalanceProps = ButtonProps; export type InputAmountCoinSelectorProps = ButtonProps & { asset?: { name?: string; imageUrl?: string; address?: string }; onClick?: () => void; }; -type AmountChangeParameters = - | { text: string; incomingAmount?: never } - | { text?: never; incomingAmount: BN | null | undefined }; -type Context = { - disabled?: boolean; - balance: BN; - assetAmount: string; - handleAmountChange: (props: AmountChangeParameters) => void; -}; - -const ctx = createContext({} as Context); - -const formatOpts: FormatConfig = { +const DEFAULT_FORMAT: FormatConfig = { units: DECIMAL_UNITS, precision: DECIMAL_UNITS, }; -export const InputAmountRoot = createComponent({ - id: 'InputAmount', - render: ( - _, - { - className, - value, - balance: initialBalance, - onChange, - disabled, - children, - ...props +export const InputAmountRoot = createComponent( + { + id: 'InputAmountRoot', + baseElement: VStack, + defaultProps: { + gap: '2', }, - ) => { + render: ( + Root, + { + balance: initialBalance, + formatOpts: initialFormatOpts, + children, + ...props + }, + ) => { + const balance = initialBalance ?? bn(initialBalance); + const formatOpts = initialFormatOpts ?? DEFAULT_FORMAT; + + return ( + + {children} + + ); + }, + }, +); + +export const InputAmountField = createComponent< + InputAmountFieldProps, + typeof Input +>({ + id: 'InputAmountField', + render: (_, { className, value, onChange, disabled, children, ...props }) => { const classes = styles(); - const [assetAmount, setAssetAmount] = useState(() => + + const { formatOpts } = useContext(InputAmountRootCtx); + + const [assetAmount, setAssetAmount] = useState( !value || value.eq(0) ? '' : value.format(formatOpts), ); - const balance = initialBalance ?? bn(initialBalance); - function handleAmountChange({ text, incomingAmount, @@ -81,7 +106,7 @@ export const InputAmountRoot = createComponent({ const shouldRemoveLastCharacter = lastCharacter && lastCharacter !== '.' && - Number.isNaN(parseFloat(lastCharacter)); + Number.isNaN(Number.parseFloat(lastCharacter)); if (shouldRemoveLastCharacter) { amountText = amountText.slice(0, amountText.length - 1); } @@ -111,16 +136,15 @@ export const InputAmountRoot = createComponent({ }, [value?.toString()]); return ( - - + ); }, }); @@ -150,34 +174,58 @@ export const InputAmountSlot = createComponent< }, }); +export const InputAmountLabel = createComponent({ + id: 'InputAmountLabel', + baseElement: Text, + defaultProps: { + size: '2', + weight: 'medium', + as: 'label', + }, +}); + export const InputAmountBalance = createComponent< InputAmountBalanceProps, - typeof Input.Slot + typeof Badge >({ id: 'InputAmountBalance', - render: (_, { className, ...props }) => { - const { balance } = useContext(ctx); - const classes = styles(); + render: ( + _, + { className, label = 'Balance', balance: customBalance, ...props }, + ) => { + const { balance: originalBalance, formatOpts } = + useContext(InputAmountRootCtx); + const balance = customBalance ?? originalBalance; - const formattedBalance = useMemo(() => { + const preview = useMemo(() => { return balance.format({ - ...formatOpts, - precision: balance.eq(0) ? 1 : DEFAULT_MIN_PRECISION, + units: formatOpts.units, + minPrecision: formatOpts.minPrecision, + precision: balance.isZero() ? 1 : DEFAULT_MIN_PRECISION, }); - }, [balance]); + }, [balance, formatOpts]); + + const complete = useMemo(() => { + return balance.format({ + units: formatOpts.units, + precision: formatOpts.precision, + }); + }, [balance, formatOpts]); return ( - - - - Balance: {formattedBalance} - - - + + + {label}: {preview} + + ); }, }); @@ -188,19 +236,22 @@ export const InputAmountButtonMaxBalance = createComponent< >({ id: 'InputAmountButtonMaxBalance', defaultProps: { - variant: 'link', + variant: 'ghost', size: '1', color: 'green', children: 'MAX', }, render: (_, { className, children, ...props }) => { const classes = styles(); - const { disabled, balance, handleAmountChange } = useContext(ctx); + const { balance, formatOpts } = useContext(InputAmountRootCtx); + const { disabled, handleAmountChange } = useContext(InputAmountFieldCtx); const mergedProps = mergeProps(props, { onClick: () => { if (balance) { - handleAmountChange({ text: balance.format(formatOpts) }); + handleAmountChange({ + text: balance.format(formatOpts ?? DEFAULT_FORMAT), + }); } }, }); @@ -208,9 +259,10 @@ export const InputAmountButtonMaxBalance = createComponent< return ( @@ -225,7 +277,7 @@ export const InputAmountCoinSelector = createComponent< id: 'InputAmountCoinSelector', render: (_, { className, asset, onClick, ...props }) => { const classes = styles(); - const { disabled } = useContext(ctx); + const { disabled } = useContext(InputAmountFieldCtx); if (!asset) { return null; @@ -257,6 +309,8 @@ export const InputAmountCoinSelector = createComponent< }); export const InputAmount = withNamespace(InputAmountRoot, { + Label: InputAmountLabel, + Field: InputAmountField, Slot: InputAmountSlot, Balance: InputAmountBalance, ButtonMaxBalance: InputAmountButtonMaxBalance, @@ -265,12 +319,11 @@ export const InputAmount = withNamespace(InputAmountRoot, { const styles = tv({ slots: { - root: [ + field: [ 'flex-row flex-wrap bg-clip-border w-auto h-auto py-3', 'first-child:flex-1 first-child:basis-1/2 first-child:indent-[var(--space-3)]', ], - balance: 'pt-3 mb-auto', - maxBalance: 'font-mono self-center item-center items-center', + maxBalance: ['font-medium text-sm', 'h-6 px-2', 'mr-2', 'rounded-md'], coinSelector: 'gap-1.5 text-xs py-1', }, }); diff --git a/packages/ui/src/components/InputAmount/InputAmount.types.ts b/packages/ui/src/components/InputAmount/InputAmount.types.ts new file mode 100644 index 000000000..ab9cd81eb --- /dev/null +++ b/packages/ui/src/components/InputAmount/InputAmount.types.ts @@ -0,0 +1,5 @@ +import type { BN } from '@fuel-ts/math'; + +export type AmountChangeParameters = + | { text: string; incomingAmount?: never } + | { text?: never; incomingAmount: BN | null | undefined }; diff --git a/packages/ui/src/components/InputAmount/index.ts b/packages/ui/src/components/InputAmount/index.ts index 08d870f39..106fa7f2b 100644 --- a/packages/ui/src/components/InputAmount/index.ts +++ b/packages/ui/src/components/InputAmount/index.ts @@ -2,4 +2,4 @@ export { InputAmount } from './InputAmount'; -export type { InputAmountProps } from './InputAmount'; +export type { InputAmountProps, InputAmountFieldProps } from './InputAmount'; diff --git a/packages/ui/src/components/ListBox/ListBox.stories.tsx b/packages/ui/src/components/ListBox/ListBox.stories.tsx new file mode 100644 index 000000000..bfbe44eae --- /dev/null +++ b/packages/ui/src/components/ListBox/ListBox.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useMemo, useState } from 'react'; +import { HStack } from '../Box'; +import { Text } from '../Text'; +import { ListBox } from './ListBox'; + +const meta: Meta = { + title: 'Form/ListBox', + component: ListBox, + argTypes: { + filter: { + control: 'boolean', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const options = [ + { value: 'allowance', label: 'Allowance', comission: '10%' }, + { value: 'validator', label: 'Validator', comission: '1%' }, + { value: 'convert', label: 'Convert', comission: '5%' }, + { value: 'delegation', label: 'Delegation', comission: '6%' }, + { value: 'redelegation', label: 'Redelegation', comission: '9%' }, + { value: 'undelegation', label: 'Undelegation', comission: '7%' }, + { value: 'withdraw', label: 'Withdraw', comission: '1%' }, +]; + +export const Usage: Story = { + render: ({ filter }) => { + const [selected, setSelected] = useState(''); + const [value, setValue] = useState(''); + + const filtered = useMemo(() => { + return options.filter((option) => { + return option.label.toLowerCase().includes(value.toLowerCase()); + }); + }, [value]); + + return ( + option.value} + render={(option) => ( + + + + {option.label} + + Comission: {option.comission} + + + + )} + onChange={(e) => setValue(e.target.value)} + selected={selected} + onSelect={setSelected} + /> + ); + }, + args: { + filter: true, + }, +}; diff --git a/packages/ui/src/components/ListBox/ListBox.styles.ts b/packages/ui/src/components/ListBox/ListBox.styles.ts new file mode 100644 index 000000000..c6f930ed7 --- /dev/null +++ b/packages/ui/src/components/ListBox/ListBox.styles.ts @@ -0,0 +1,47 @@ +import { tv } from 'tailwind-variants'; + +export const item = tv({ + base: [ + 'flex items-center w-full', + 'p-4 text-base bg-panel-solid', + 'transition-shadow rounded-xl', + 'cursor-pointer', + ], + variants: { + variant: { + idle: [ + 'text-gray-12', + 'shadow-[0_0_0_1px_var(--gray-5)] hover:shadow-[0_0_0_2px_var(--brand-9)]', + ], + focused: ['text-gray-12', 'shadow-[0_0_0_2px_var(--gray-9)]'], + selected: [ + 'text-gray-12', + 'shadow-[0_0_0_2px_var(--brand-9)] hover:shadow-[0_0_0_2px_var(--brand-10)]', + ], + }, + }, + defaultVariants: { + variant: 'idle', + }, +}); + +export const icon = tv({ + base: [ + 'flex items-center justify-center', + 'w-6 h-6 rounded-full me-2', + 'transition-colors', + ], + variants: { + variant: { + idle: 'bg-gray-2', + focused: 'bg-gray-2', + selected: [ + 'bg-green-3 dark:bg-green-9', + 'text-[rgb(32,32,32)]/[.6] dark:text-[rgb(32,32,32)]/[.6]', // text-gray-12 + ], + }, + }, + defaultVariants: { + variant: 'idle', + }, +}); diff --git a/packages/ui/src/components/ListBox/ListBox.tsx b/packages/ui/src/components/ListBox/ListBox.tsx new file mode 100644 index 000000000..689338b80 --- /dev/null +++ b/packages/ui/src/components/ListBox/ListBox.tsx @@ -0,0 +1,247 @@ +import { AnimatePresence, motion } from 'framer-motion'; +import type React from 'react'; +import { createContext, useContext, useMemo, useState } from 'react'; +import { createComponent, withNamespace } from '../../utils/component'; +import { Input, type InputProps } from '../Input'; +import { Text } from '../Text'; +import { icon, item } from './ListBox.styles'; + +type ListBoxBaseProps = { + children: React.ReactNode; + className?: string; +}; + +type ListBoxItemVariant = 'idle' | 'selected' | 'focused'; + +export interface ListBoxProps { + filter?: boolean; + options: T[]; + selected: string; + getValue: (option: T, index: number) => string; + render: (option: T) => React.ReactNode; + onChange: InputProps['onChange']; + onSelect: (value: string) => void; +} + +interface ListBoxInputProps { + onChange: ListBoxProps['onChange']; + options: ListBoxProps['options']; + getValue: ListBoxProps['getValue']; +} + +type ListBoxBaseContextType = { + total: number; + focused: number; + setFocused: React.Dispatch>; + onSelect: (value: string) => void; +}; + +type ListBoxItemContextType = { + selected: string; + value: string; + index: number; +}; + +type ListBoxItemIconContextType = { + variant: ListBoxItemVariant; + index: number; +}; + +const ListBoxBaseContext = createContext( + undefined, +); + +const ListBoxItemContext = createContext( + undefined, +); + +const ListBoxItemIconContext = createContext< + ListBoxItemIconContextType | undefined +>(undefined); + +const ListBoxRoot = ({ + filter, + options, + selected, + getValue, + render, + onChange, + onSelect, +}: ListBoxProps) => { + const [focused, setFocused] = useState(-1); + const total = options.length; + + return ( + +
+ {filter && ( + + )} + +
    + + {options.map((option, index) => { + const key = getValue(option, index); + + return ( + + + {render(option)} + + + ); + })} + {total === 0 && ( +
    + + No results found + +
    + )} +
    +
+
+ + ); +}; + +const ListBoxInput = ({ + options, + getValue, + onChange, +}: ListBoxInputProps) => { + const ctx = useContext(ListBoxBaseContext); + + if (!ctx) { + throw new Error('ListBoxInput must be used within a ListBox'); + } + + const { total, setFocused, focused, onSelect } = ctx; + + return ( +
+ { + if (e.key === 'Escape') { + setFocused(-1); + e.preventDefault(); + } + if (e.key === 'Enter') { + onSelect(getValue(options[focused], focused)); + setFocused(-1); + } + if (e.key === 'ArrowUp') { + setFocused((prev) => (prev - 1 <= -1 ? -1 : prev - 1)); + e.preventDefault(); + } + if (e.key === 'ArrowDown') { + setFocused((prev) => (prev >= total - 1 ? prev : prev + 1)); + e.preventDefault(); + } + }} + onChange={(e) => { + setFocused(-1); + onChange?.(e); + }} + /> +
+ ); +}; + +const ListBoxItem = createComponent({ + id: 'ListBoxItem', + render: (_, { children, className }) => { + const baseCtx = useContext(ListBoxBaseContext); + const ctx = useContext(ListBoxItemContext); + + const variant = useMemo(() => { + if (ctx?.selected === ctx?.value) { + return 'selected'; + } + + if (baseCtx?.focused === ctx?.index) { + return 'focused'; + } + + return 'idle'; + }, [ctx?.selected, ctx?.value, ctx?.index, baseCtx?.focused]); + + if (!ctx || !baseCtx) { + throw new Error('ListBoxItem must be used within a ListBoxItem'); + } + + const classes = item({ + className, + variant, + }); + + return ( +
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
  • + role="option" + tabIndex={-1} + aria-selected={variant === 'selected'} + aria-disabled="false" + onClick={() => baseCtx.onSelect(ctx.value)} + className={classes} + > + + {children} + +
  • +
    + ); + }, +}); + +const ListBoxItemIcon = createComponent<{}, 'div'>({ + id: 'ListBoxItemIcon', + render: () => { + const ctx = useContext(ListBoxItemIconContext); + + if (!ctx) { + throw new Error('ListBoxItemIcon must be used within a ListBoxItem'); + } + + const classes = icon({ variant: ctx.variant }); + + return ( + + {ctx.index + 1} + + ); + }, +}); + +export const ListBox = withNamespace(ListBoxRoot, { + Item: ListBoxItem, + ItemIcon: ListBoxItemIcon, +}); diff --git a/packages/ui/src/components/ListBox/index.ts b/packages/ui/src/components/ListBox/index.ts new file mode 100644 index 000000000..edad825f3 --- /dev/null +++ b/packages/ui/src/components/ListBox/index.ts @@ -0,0 +1,3 @@ +export { ListBox } from './ListBox'; + +export type { ListBoxProps } from './ListBox'; diff --git a/packages/ui/src/components/Nav/styles.ts b/packages/ui/src/components/Nav/styles.ts index 93627b88b..991bd037d 100644 --- a/packages/ui/src/components/Nav/styles.ts +++ b/packages/ui/src/components/Nav/styles.ts @@ -28,12 +28,16 @@ export const styles = tv({ 'absolute opacity-100 transition-all duration-200 text-icon transform', 'aria-[label=Sun]:right-2', 'aria-[label=Moon]:left-2', - 'dark-theme:aria-[label=Sun]:transform', - 'dark-theme:aria-[label=Sun]:opacity-0', - 'dark-theme:aria-[label=Sun]:-translate-x-full', - 'light-theme:aria-[label=Moon]:transform', - 'light-theme:aria-[label=Moon]:opacity-0', - 'light-theme:aria-[label=Moon]:translate-x-full', + 'aria-[label=Moon]:transform', + 'aria-[label=Moon]:opacity-0', + 'aria-[label=Sun]:opacity-100', + 'aria-[label=Sun]:-translate-x-0', + 'aria-[label=Moon]:translate-x-full', + 'dark:aria-[label=Sun]:transform', + 'dark:aria-[label=Sun]:opacity-0', + 'dark:aria-[label=Moon]:opacity-100', + 'dark:aria-[label=Moon]:translate-x-0', + 'dark:aria-[label=Sun]:-translate-x-full', ], desktop: [ 'mobile:max-desktop:hidden gap-8 flex-row items-center', diff --git a/packages/ui/src/components/Nav/useNavContext.tsx b/packages/ui/src/components/Nav/useNavContext.ts similarity index 88% rename from packages/ui/src/components/Nav/useNavContext.tsx rename to packages/ui/src/components/Nav/useNavContext.ts index fd864a3c8..23c8beb67 100644 --- a/packages/ui/src/components/Nav/useNavContext.tsx +++ b/packages/ui/src/components/Nav/useNavContext.ts @@ -6,7 +6,6 @@ import type { NavProps } from './Nav'; type ContextProps = NavProps; -// TODO: put this inside the component const ctx = createContext({} as ContextProps); export function useNavContext() { return useContext(ctx); diff --git a/packages/ui/src/components/Nav/useNavMobileContext.tsx b/packages/ui/src/components/Nav/useNavMobileContext.ts similarity index 100% rename from packages/ui/src/components/Nav/useNavMobileContext.tsx rename to packages/ui/src/components/Nav/useNavMobileContext.ts diff --git a/packages/ui/src/components/Separator/Separator.tsx b/packages/ui/src/components/Separator/Separator.tsx index 5334d773b..0d07570d3 100644 --- a/packages/ui/src/components/Separator/Separator.tsx +++ b/packages/ui/src/components/Separator/Separator.tsx @@ -9,5 +9,8 @@ export const Separator = createComponent( { id: 'Separator', baseElement: RadixSeparator, + defaultProps: { + orientation: 'horizontal', + }, }, ); diff --git a/packages/ui/src/components/Skeleton/Skeleton.stories.tsx b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx new file mode 100644 index 000000000..67b1456f5 --- /dev/null +++ b/packages/ui/src/components/Skeleton/Skeleton.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Skeleton } from './Skeleton'; + +const meta: Meta = { + title: 'UI/Skeleton', + component: Skeleton, + argTypes: { + width: { + control: { type: 'text' }, + }, + height: { + control: { type: 'text' }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Usage: Story = { + args: { + width: '100%', + height: '26px', + }, +}; diff --git a/packages/ui/src/components/Skeleton/Skeleton.tsx b/packages/ui/src/components/Skeleton/Skeleton.tsx new file mode 100644 index 000000000..dddeba3f7 --- /dev/null +++ b/packages/ui/src/components/Skeleton/Skeleton.tsx @@ -0,0 +1,11 @@ +import { + Skeleton as RadixSkeleton, + type SkeletonProps, +} from '@radix-ui/themes'; +import { createComponent } from '../../utils/component'; + +export type { SkeletonProps }; +export const Skeleton = createComponent({ + id: 'Skeleton', + baseElement: RadixSkeleton, +}); diff --git a/packages/ui/src/components/Skeleton/index.ts b/packages/ui/src/components/Skeleton/index.ts new file mode 100644 index 000000000..48c1108bb --- /dev/null +++ b/packages/ui/src/components/Skeleton/index.ts @@ -0,0 +1,3 @@ +export { Skeleton } from './Skeleton'; + +export type { SkeletonProps } from './Skeleton'; diff --git a/packages/ui/src/components/Spinner/Spinner.css b/packages/ui/src/components/Spinner/Spinner.css index ff1198efb..2a501542a 100644 --- a/packages/ui/src/components/Spinner/Spinner.css +++ b/packages/ui/src/components/Spinner/Spinner.css @@ -38,7 +38,6 @@ stroke-linecap: round; stroke-dasharray: var(--circumference); } -.dark .fuel-Spinner__circle-bg, -.dark-theme .fuel-Spinner__circle-bg { +.dark .fuel-Spinner__circle-bg { stroke: var(--black-a8); } diff --git a/packages/ui/src/components/Stepper/Stepper.stories.tsx b/packages/ui/src/components/Stepper/Stepper.stories.tsx new file mode 100644 index 000000000..ad00e0efa --- /dev/null +++ b/packages/ui/src/components/Stepper/Stepper.stories.tsx @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Stepper } from './Stepper'; + +const meta: Meta = { + title: 'UI/Stepper', + component: Stepper, + argTypes: { + step: { + control: { + type: 'range', + min: 1, + max: 3, + step: 1, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Usage: Story = { + render: ({ step }) => { + return ( + + + + Allowance + + + + Validator + + + + Convert + + + ); + }, + args: { + step: 2, + }, +}; diff --git a/packages/ui/src/components/Stepper/Stepper.styles.ts b/packages/ui/src/components/Stepper/Stepper.styles.ts new file mode 100644 index 000000000..2a6e10487 --- /dev/null +++ b/packages/ui/src/components/Stepper/Stepper.styles.ts @@ -0,0 +1,59 @@ +import { tv } from 'tailwind-variants'; + +export const root = tv({ + base: ['flex items-center w-full', 'font-medium text-sm text-center'], +}); + +export const item = tv({ + base: 'flex items-center', + variants: { + variant: { + idle: 'text-[rgb(32,32,32)]/[.6] dark:text-[rgba(238,238,238)]/[.6]', // text-gray-12 + active: 'text-gray-12', + completed: 'text-[rgb(32,32,32)]/[.6] dark:text-[rgba(238,238,238)]/[.6]', // text-gray-12 + }, + separator: { + true: [ + 'w-full', + 'after:mobile:max-tablet:hidden', + 'after:inline-block', + "after:w-full after:h-0.5 after:content-['']", + 'after:bg-gradient-to-r after:from-transparent after:to-[var(--gray-a6)]', + 'after:mx-6', + ], + false: '', + }, + }, + defaultVariants: { + variant: 'idle', + separator: false, + }, +}); + +export const icon = tv({ + base: [ + 'flex items-center justify-center', + 'w-8 h-8 rounded-full me-2', + 'border transition', + ], + variants: { + variant: { + idle: [ + 'bg-[rgb(32,32,32)]/[.06] border-[rgb(32,32,32)]/[.08]', + 'dark:bg-[rgb(238,238,238)]/[.06] dark:border-[rgb(238,238,238)]/[.08]', + ], + active: [ + 'bg-transparent', + 'border-[rgb(32,32,32)]/[.24] shadow-[0_1px_5px_0_rgba(0,0,0,0.07)]', + 'dark:border-[rgb(238,238,238)]/[.24] dark:shadow-none', + ], + completed: [ + 'bg-[rgb(32,32,32)]/[.06] border-[rgb(32,32,32)]/[.08]', + 'dark:bg-[rgb(238,238,238)]/[.06] dark:border-[rgb(238,238,238)]/[.08]', + ], + }, + }, + defaultVariants: { + variant: 'idle', + }, +}); diff --git a/packages/ui/src/components/Stepper/Stepper.tsx b/packages/ui/src/components/Stepper/Stepper.tsx new file mode 100644 index 000000000..dd59ae7a9 --- /dev/null +++ b/packages/ui/src/components/Stepper/Stepper.tsx @@ -0,0 +1,127 @@ +import { IconCircleCheckFilled } from '@tabler/icons-react'; +import React, { createContext, useContext, useMemo } from 'react'; +import { createComponent, withNamespace } from '../../utils/component'; +import { icon, item, root } from './Stepper.styles'; + +type StepperBaseProps = { + children: React.ReactNode; + className?: string; +}; + +type StepperItemVariant = 'idle' | 'completed' | 'active'; + +export interface StepperProps extends StepperBaseProps { + step: number; +} +export type StepperItemProps = StepperBaseProps; + +type StepperItemContextType = { + total: number; + step: number; + index: number; +}; + +type StepperItemIconContextType = { + index: number; + variant: StepperItemVariant; +}; + +const StepperItemContext = createContext( + undefined, +); + +const StepperItemIconContext = createContext< + StepperItemIconContextType | undefined +>(undefined); + +export const StepperRoot = createComponent({ + id: 'Stepper', + render: (_, { children, className, step }) => { + const classes = root({ className }); + const total = React.Children.count(children); + + return ( +
      + {React.Children.map(children, (child, index) => { + return ( + + {child} + + ); + })} +
    + ); + }, +}); + +export const StepperItem = createComponent({ + id: 'StepperItem', + render: (_, { children, className }) => { + const ctx = useContext(StepperItemContext); + + const variant = useMemo(() => { + if (ctx?.index === ctx?.step) { + return 'active'; + } + if (ctx && ctx.step > ctx.index) { + return 'completed'; + } + + return 'idle'; + }, [ctx]); + + if (!ctx) { + throw new Error('StepperItem must be used within a StepperItem'); + } + + const classes = item({ + className, + variant, + separator: Boolean(ctx.index < ctx.total), + }); + + return ( +
  • + + + {children} + + +
  • + ); + }, +}); + +export const StepperItemIcon = createComponent<{}, 'div'>({ + id: 'StepperItemIcon', + render: () => { + const ctx = useContext(StepperItemIconContext); + + if (!ctx) { + throw new Error('StepperItemIcon must be used within a StepperItem'); + } + + const classes = icon({ variant: ctx.variant }); + + if (ctx.variant === 'completed') { + return ( + + + + ); + } + + return ( + {ctx.index.toString().padStart(2, '0')} + ); + }, +}); + +export const Stepper = withNamespace(StepperRoot, { + Item: StepperItem, + ItemIcon: StepperItemIcon, +}); diff --git a/packages/ui/src/components/Stepper/index.ts b/packages/ui/src/components/Stepper/index.ts new file mode 100644 index 000000000..c36180924 --- /dev/null +++ b/packages/ui/src/components/Stepper/index.ts @@ -0,0 +1,3 @@ +export { Stepper } from './Stepper'; + +export type { StepperProps } from './Stepper'; diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index 6899b9e62..b420a1eeb 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -130,42 +130,42 @@ const styles = tv({ variant: 'surface', size: '1', class: { - trigger: ['h-8 px-3 text-sm'], + trigger: ['text-sm'], }, }, { variant: 'surface', size: '2', class: { - trigger: ['h-10 px-4 text-md'], + trigger: ['text-md'], }, }, { variant: 'surface', size: '3', class: { - trigger: ['h-12 px-5 text-lg'], + trigger: ['text-lg'], }, }, { variant: 'line', size: '1', class: { - trigger: ['py-2 px-3 text-sm'], + trigger: ['text-sm'], }, }, { variant: 'line', size: '2', class: { - trigger: ['py-3 px-4 text-md'], + trigger: ['text-md'], }, }, { variant: 'line', size: '3', class: { - trigger: ['py-4 px-5 text-lg'], + trigger: ['text-lg'], }, }, ], diff --git a/packages/ui/src/components/Theme/Theme.tsx b/packages/ui/src/components/Theme/Theme.tsx index e0121573a..085845a5d 100644 --- a/packages/ui/src/components/Theme/Theme.tsx +++ b/packages/ui/src/components/Theme/Theme.tsx @@ -41,6 +41,6 @@ export const Theme = createComponent({ accentColor: 'grass', radius: 'medium', panelBackground: 'translucent', - scaling: '105%', + scaling: '100%', }, }); diff --git a/packages/ui/src/components/Toast/Toast.stories.tsx b/packages/ui/src/components/Toast/Toast.stories.tsx index af962408b..c8f8526a6 100644 --- a/packages/ui/src/components/Toast/Toast.stories.tsx +++ b/packages/ui/src/components/Toast/Toast.stories.tsx @@ -1,9 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { IconHelpCircle } from '@tabler/icons-react'; import { HStack } from '../Box'; import { Button } from '../Button/Button'; -import { Icon } from '../Icon/Icon'; import { Toast } from './Toast'; import { toast, useToast } from './useToast'; @@ -40,7 +38,6 @@ export const Usage: Story = { const toastProps = { title: 'Some title', description: 'Some description', - icon: , action: Action, }; diff --git a/packages/ui/src/components/Toast/styles.ts b/packages/ui/src/components/Toast/styles.ts index cbfd3b85d..c61de0993 100644 --- a/packages/ui/src/components/Toast/styles.ts +++ b/packages/ui/src/components/Toast/styles.ts @@ -4,37 +4,47 @@ const vars = { base: [ '[--tw-ring-color:var(--gray-4)]', '[--radix-toast-color:currentColor]', - '[--radix-toast-bg:var(--gray-2)]', + '[--radix-toast-bg:var(--gray-3)]', '[--radix-toast-border-color:var(--gray-6)]', - '[--radix-toast-action-color:var(--gray-11)]', + '[--radix-toast-title-color:var(--gray-12)]', + '[--radix-toast-description-color:var(--gray-11)]', + '[--radix-toast-shadow:0_2px_16px_0_rgba(0,0,0,0.05),0_1px_1px_0_rgba(0,0,0,0.10)]', ], info: [ '[--tw-ring-color:var(--blue-4)]', '[--radix-toast-color:currentColor]', - '[--radix-toast-bg:var(--blue-2)]', + '[--radix-toast-bg:var(--blue-3)]', '[--radix-toast-border-color:var(--blue-6)]', - '[--radix-toast-action-color:var(--blue-11)]', + '[--radix-toast-title-color:var(--blue-12)]', + '[--radix-toast-description-color:var(--blue-11)]', + '[--radix-toast-shadow:0_2px_16px_0_rgba(16,88,213,0.05),0_1px_1px_0_rgba(16,88,213,0.10)]', ], warning: [ '[--tw-ring-color:var(--yellow-4)]', '[--radix-toast-color:currentColor]', - '[--radix-toast-bg:var(--yellow-2)]', + '[--radix-toast-bg:var(--yellow-3)]', '[--radix-toast-border-color:var(--yellow-6)]', - '[--radix-toast-action-color:var(--yellow-11)]', + '[--radix-toast-title-color:var(--yellow-12)]', + '[--radix-toast-description-color:var(--yellow-11)]', + '[--radix-toast-shadow:0_2px_16px_0_rgba(187,148,7,0.05),0_1px_1px_0_rgba(187,148,7,0.10)]', ], success: [ '[--tw-ring-color:var(--green-4)]', '[--radix-toast-color:currentColor]', - '[--radix-toast-bg:var(--green-2)]', + '[--radix-toast-bg:var(--green-3)]', '[--radix-toast-border-color:var(--green-6)]', - '[--radix-toast-action-color:var(--green-11)]', + '[--radix-toast-title-color:var(--green-12)]', + '[--radix-toast-description-color:var(--green-11)]', + '[--radix-toast-shadow:0_2px_16px_0_rgba(0,0,0,0.05),0_1px_1px_0_rgba(0,0,0,0.10)]', ], error: [ '[--tw-ring-color:var(--red-4)]', '[--radix-toast-color:currentColor]', - '[--radix-toast-bg:var(--red-2)]', + '[--radix-toast-bg:var(--red-3)]', '[--radix-toast-border-color:var(--red-6)]', - '[--radix-toast-action-color:var(--red-11)]', + '[--radix-toast-title-color:var(--red-12)]', + '[--radix-toast-description-color:var(--red-11)]', + '[--radix-toast-shadow:0_2px_16px_0_rgba(160,21,8,0.05),0_1px_1px_0_rgba(160,21,8,0.10)]', ], }; @@ -45,8 +55,9 @@ export const styles = tv({ 'tablet:top-auto tablet:flex-col', ], toast: [ - 'group pointer-events-auto relative flex w-full items-center justify-between overflow-hidden rounded-md', - 'p-4 shadow-lg transition-all outline-none focus-visible:ring-2 not-first:mt-4', + 'group pointer-events-auto relative w-full overflow-hidden rounded-lg', + 'flex flex-row items-center justify-between gap-6', + 'p-4 transition-all outline-none focus-visible:ring-2 not-first:mt-4', 'tablet:min-w-[var(--radix-toast-width)]', 'data-[swipe=end]:transition-none', 'data-[swipe=end]:translate-x-[var(--radix-toast-swipe-move-x)]', @@ -57,27 +68,28 @@ export const styles = tv({ 'data-[state=closed]:fade-out-80', 'tablet:data-[state=closed]:slide-out-from-bottom-full', 'bg-[var(--radix-toast-bg)]', - 'border-[var(--radix-toast-border-color)]', + 'border border-[var(--radix-toast-border-color)]', + 'shadow-[var(--radix-toast-shadow)]', 'text-[var(--radix-toast-color)]', 'fuel-[Icon]:text-gray-1', 'overflow-visible', ], action: [ - 'inline-flex h-7 shrink-0 items-center justify-center rounded-md border border-border', - 'bg-transparent ml-4 px-3 text-sm font-medium transition-colors focus:outline-none', + 'inline-flex h-9 shrink-0 items-center justify-center rounded-lg', + 'px-4 text-sm font-medium transition-colors focus:outline-none', 'disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-2', - 'text-[var(--radix-toast-action-color)]', - 'border-[var(--radix-toast-border-color)]', - 'hover:text-[var(--radix-toast-color)]', + 'bg-gray-12 text-gray-1', + 'hover:opacity-90', ], close: [ - 'absolute focus-visible:ring-2 top-3 right-3 opacity-0 group-hover:opacity-100', - 'fuel-[Icon]:text-[var(--radix-toast-action-color)]', - 'fuel-[Icon]:opacity-50', - 'hover:fuel-[Icon]:opacity-100', + 'absolute focus-visible:ring-2 top-0 -right-3', + 'h-6 w-6 transition-colors transition-opacity', + 'opacity-0 group-hover:opacity-100', + 'bg-gray-8 fuel-[Icon]:text-gray-1', + 'hover:fuel-[Icon]:text-gray-4', ], - title: ['text-sm font-semibold [&+div]:text-sm'], - description: 'text-sm opacity-90 text-[var(--radix-toast-action-color)]', + title: ['text-md font-medium', 'text-[var(--radix-toast-title-color)]'], + description: 'text-sm text-[var(--radix-toast-description-color)]', }, variants: { variant: { diff --git a/packages/ui/src/components/Toast/toaster.tsx b/packages/ui/src/components/Toast/toaster.tsx index cd588c8e6..87de0177d 100644 --- a/packages/ui/src/components/Toast/toaster.tsx +++ b/packages/ui/src/components/Toast/toaster.tsx @@ -14,7 +14,13 @@ export function Toaster() { const { toasts } = useToast(); return ( - + {toasts.map( ({ id, title, description, action, icon, width = 350, ...props }) => ( diff --git a/packages/ui/src/components/Toast/useToast.tsx b/packages/ui/src/components/Toast/useToast.tsx index 43c1a3c53..a91ee8dba 100644 --- a/packages/ui/src/components/Toast/useToast.tsx +++ b/packages/ui/src/components/Toast/useToast.tsx @@ -1,15 +1,8 @@ 'use client'; -import { - IconAlertCircle, - IconAlertTriangle, - IconCheck, - IconHelpCircle, -} from '@tabler/icons-react'; +import {} from '@tabler/icons-react'; import { useEffect, useState } from 'react'; -import { Icon } from '../Icon/Icon'; - import type { ToastActionElement, ToastIconElement, ToastProps } from './Toast'; // Constants @@ -212,7 +205,6 @@ toast = Object.assign(toast, { ...props, title: msg, variant: 'success', - icon: props?.icon || , }); }, base: (msg: string, props?: Omit) => { @@ -220,7 +212,6 @@ toast = Object.assign(toast, { ...props, title: msg, variant: 'base', - icon: props?.icon || , }); }, info: (msg: string, props?: Omit) => { @@ -228,7 +219,6 @@ toast = Object.assign(toast, { ...props, title: msg, variant: 'info', - icon: props?.icon || , }); }, warning: (msg: string, props?: Omit) => { @@ -236,7 +226,6 @@ toast = Object.assign(toast, { ...props, title: msg, variant: 'warning', - icon: props?.icon || , }); }, error: (msg: string, props?: Omit) => { @@ -244,7 +233,6 @@ toast = Object.assign(toast, { ...props, title: msg, variant: 'error', - icon: props?.icon || , }); }, }); diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.ts similarity index 94% rename from packages/ui/src/index.tsx rename to packages/ui/src/index.ts index 75235b6bb..85be5c025 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.ts @@ -14,6 +14,7 @@ export * from './components/Asset'; export * from './components/ComboBox'; export * from './components/Avatar'; export * from './components/Badge'; +export * from './components/BadgeAsset'; export * from './components/Box'; export * from './components/Breadcrumb'; export * from './components/Button'; @@ -43,6 +44,7 @@ export * from './components/InputPassword'; export * from './components/Inset'; export * from './components/Link'; export * from './components/List'; +export * from './components/ListBox'; export * from './components/LoadingBox'; export * from './components/LoadingWrapper'; export * from './components/Nav'; @@ -51,8 +53,10 @@ export * from './components/RadioGroup'; export * from './components/ScrollArea'; export * from './components/Select'; export * from './components/Separator'; +export * from './components/Skeleton'; export * from './components/Slider'; export * from './components/Spinner'; +export * from './components/Stepper'; export * from './components/Switch'; export * from './components/Table'; export * from './components/Tabs'; diff --git a/packages/ui/src/theme/breakpoints.ts b/packages/ui/src/theme/breakpoints.ts index 3bac37fd9..b7daa1119 100644 --- a/packages/ui/src/theme/breakpoints.ts +++ b/packages/ui/src/theme/breakpoints.ts @@ -1,6 +1,15 @@ export const breakpoints = { + // @deprecated + // Since this is conflicting with the breakpoints from the radix theme. + // I haven't removed it yet because we don't want to break the explorer and bridge, but please don't use it anymore. mobile: 300, tablet: 640, laptop: 960, desktop: 1280, + // https://www.radix-ui.com/themes/docs/theme/breakpoints + xs: 520, + sm: 768, + md: 1024, + lg: 1280, + xl: 1640, }; diff --git a/packages/ui/src/theme/components/badge.css b/packages/ui/src/theme/components/badge.css new file mode 100644 index 000000000..6fa2f571a --- /dev/null +++ b/packages/ui/src/theme/components/badge.css @@ -0,0 +1,12 @@ +[data-accent-color="gray"].rt-Badge:where(.rt-variant-solid) { + background-color: var(--gray-4); + color: var(--gray-12); +} + +@media all { + .rt-Badge:where(.rt-r-size-3) { + font-size: var(--font-size-2); + padding: var(--space-2) var(--space-4); + border-radius: max(var(--radius-4), var(--radius-full)); + } +} \ No newline at end of file diff --git a/packages/ui/src/theme/components/button.css b/packages/ui/src/theme/components/button.css new file mode 100644 index 000000000..8ef7a3ba1 --- /dev/null +++ b/packages/ui/src/theme/components/button.css @@ -0,0 +1,139 @@ +.rt-BaseButton { + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, opacity 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; +} + +.rt-BaseButton:where(.rt-variant-solid) { + background-color: var(--brand-9); + color: #000000; + box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.05), 0px 0.5px 1px 0px rgba(0, 0, 0, 0.10); +} + +[data-accent-color="gray"].rt-BaseButton:where(.rt-variant-solid) { + background-color: var(--gray-contrast); + color: #202020; + box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.05), 0px 0.5px 1px 0px rgba(0, 0, 0, 0.10); +} + +[data-accent-color="gray"].rt-BaseButton:where(.rt-variant-outline) { + background-color: var(--color-surface); + color: var(--gray-12); + box-shadow: inset 0 0 0 1px var(--gray-5); +} + +.rt-BaseButton:where([data-disabled]) { + cursor: var(--cursor-disabled); + color: var(--gray-a8); + background-color: var(--gray-a3); +} + +@media (hover: hover) { + .rt-BaseButton:where(.rt-variant-solid):where(:hover):where(:not([data-disabled])) { + background-color: var(--brand-10); + } + [data-accent-color="gray"].rt-BaseButton:where(.rt-variant-solid):where(:hover):where(:not([data-disabled])) { + background-color: #E8E8E8; + } + [data-accent-color="gray"].rt-BaseButton:where(.rt-variant-outline):where(:hover):where(:not([data-disabled])) { + background-color: var(--gray-2); + } +} + +/** + * Breakpoints reference: + * https://www.radix-ui.com/themes/docs/theme/breakpoints + * + * Size: initial + * Description: Phones (portrait) + */ +@media all { + .rt-BaseButton:where(.rt-r-size-1) { + --base-button-height: 28px; + border-radius: max(var(--radius-3), var(--radius-full)); + } + .rt-BaseButton:where(.rt-r-size-2) { + --base-button-height: 36px; + border-radius: max(var(--radius-4), var(--radius-full)); + } + .rt-BaseButton:where(.rt-r-size-3) { + --base-button-height: 44px; + border-radius: max(var(--radius-5), var(--radius-full)); + } + .rt-BaseButton:where(.rt-r-size-4) { + --base-button-height: 44px; + border-radius: max(var(--radius-5), var(--radius-full)); + } + + .rt-Button:where(.rt-r-size-1) { + font-size: var(--font-size-2); + font-weight: var(--font-weight-medium); + } + .rt-Button:where(.rt-r-size-2) { + font-size: var(--font-size-2); + font-weight: var(--font-weight-medium); + } + .rt-Button:where(.rt-r-size-3) { + font-size: var(--font-size-3); + font-weight: var(--font-weight-medium); + } + .rt-Button:where(.rt-r-size-4) { + font-size: var(--font-size-4); + font-weight: var(--font-weight-medium); + } + + .rt-Button:where(.rt-r-size-1):where(:not(.rt-variant-ghost)) { + padding-left: 10px; + padding-right: 10px; + } + .rt-Button:where(.rt-r-size-2):where(:not(.rt-variant-ghost)) { + padding-left: var(--space-4); + padding-right: var(--space-4); + } + .rt-Button:where(.rt-r-size-3):where(:not(.rt-variant-ghost)) { + padding-left: 20px; + padding-right: 20px; + } + .rt-Button:where(.rt-r-size-4):where(:not(.rt-variant-ghost)) { + padding-left: 20px; + padding-right: 20px; + } +} + +/** + * Size: xs + * Description: Phones (landscape) + */ +@media (min-width: 520px) { + /* ... */ +} + +/** +* Size: sm +* Description: Tablets (portrait) +*/ +@media (min-width: 768px) { + /* ... */ +} + +/** +* Size: md +* Description: Tablets (landscape) +*/ +@media (min-width: 1024px) { + /* ... */ +} + +/** +* Size: lg +* Description: Laptops +*/ +@media (min-width: 1280px) { + /* ... */ +} + +/** +* Size: xl +* Description: Desktops +*/ +@media (min-width: 1640px) { + /* ... */ +} \ No newline at end of file diff --git a/packages/ui/src/theme/components/card.css b/packages/ui/src/theme/components/card.css new file mode 100644 index 000000000..7375db2fc --- /dev/null +++ b/packages/ui/src/theme/components/card.css @@ -0,0 +1,30 @@ +:root { + --single-notch-polygon: polygon(1.5px 19.1818px, 1.5px 19.1818px, 1.70443871px 16.29866355px, 2.29578208px 13.5691904px, 3.24107097px 11.02866985px, 4.50734624px 8.7123912px, 6.06164875px 6.65564375px, 7.87101936px 4.8937168px, 9.90249893px 3.46189965px, 12.12312832px 2.3954816px, 14.49994839px 1.72975195px, 17px 1.5px, calc(100% - 13px) 1.5px, calc(100% - 13px) 1.5px, calc(100% - 11.148801px) 1.67025966px, calc(100% - 9.3874879999998px) 2.16375168px, calc(100% - 7.7407870000002px) 2.95454562px, calc(100% - 6.233424px) 4.01671104px, calc(100% - 4.890125px) 5.3243175px, calc(100% - 3.7356159999999px) 6.85143456px, calc(100% - 2.794623px) 8.57213178px, calc(100% - 2.091872px) 10.46047872px, calc(100% - 1.6520889999999px) 12.49054494px, calc(100% - 1.5000000000001px) 14.6364px, calc(100% - 1.5px) calc(100% - 54.9857px), calc(100% - 1.5px) calc(100% - 54.9857px), calc(100% - 1.5490309999998px) calc(100% - 53.5801903px), calc(100% - 1.6946079999998px) calc(100% - 52.1928384px), calc(100% - 1.934457px) calc(100% - 50.8295801px), calc(100% - 2.266304px) calc(100% - 49.4963512px), calc(100% - 2.6878750000001px) calc(100% - 48.1990875px), calc(100% - 3.1968960000002px) calc(100% - 46.9437248px), calc(100% - 3.791093px) calc(100% - 45.7361989px), calc(100% - 4.468192px) calc(100% - 44.5824456px), calc(100% - 5.225919px) calc(100% - 43.4884007px), calc(100% - 6.0620000000001px) calc(100% - 42.46px), calc(100% - 36.689px) calc(100% - 7.656px), calc(100% - 36.689px) calc(100% - 7.656px), calc(100% - 37.592747px) calc(100% - 6.707059px), calc(100% - 38.552696px) calc(100% - 5.8483519999999px), calc(100% - 39.563609px) calc(100% - 5.082153px), calc(100% - 40.620248px) calc(100% - 4.410736px), calc(100% - 41.717375px) calc(100% - 3.836375px), calc(100% - 42.849752px) calc(100% - 3.361344px), calc(100% - 44.012141px) calc(100% - 2.987917px), calc(100% - 45.199304px) calc(100% - 2.718368px), calc(100% - 46.406003px) calc(100% - 2.554971px), calc(100% - 47.627px) calc(100% - 2.5px), 13px calc(100% - 2.5px), 13px calc(100% - 2.5px), 11.14876212px calc(100% - 2.670249px), 9.38742656px calc(100% - 3.163712px), 7.74071644px calc(100% - 3.954463px), 6.23335488px calc(100% - 5.016576px), 4.890065px calc(100% - 6.324125px), 3.73556992px calc(100% - 7.851184px), 2.79459276px calc(100% - 9.571827px), 2.09185664px calc(100% - 11.460128px), 1.65208468px calc(100% - 13.490161px), 1.5px calc(100% - 15.636px), 1.5px 19.1818px) +} + +@media all { + .rt-Card:where(.rt-r-size-1) { + --card-padding: var(--space-0); + --card-border-radius: 14px; + } + .rt-Card:where(.rt-r-size-2) { + --card-padding: var(--space-0); + --card-border-radius: 14px; + } + .rt-Card:where(.rt-r-size-3) { + --card-padding: var(--space-0); + --card-border-radius: 14px; + } + .rt-Card:where(.rt-r-size-4) { + --card-padding: var(--space-0); + --card-border-radius: 14px; + } +} + +.border-single-clip-polygon { + clip-path: var(--single-notch-polygon); +} +.filter-single-clip-polygon { + filter: drop-shadow(1px 0 0 var(--gray-6)) drop-shadow(-1px 0 0 var(--gray-6)) + drop-shadow(0 1px 0 var(--gray-6)) drop-shadow(0 -1px 0 var(--gray-6)); +} \ No newline at end of file diff --git a/packages/ui/src/theme/components/dialog.css b/packages/ui/src/theme/components/dialog.css new file mode 100644 index 000000000..26a1a53f9 --- /dev/null +++ b/packages/ui/src/theme/components/dialog.css @@ -0,0 +1,56 @@ +.light .rt-BaseDialogContent { + background-color: #FFFFFF; +} + +.dark .rt-BaseDialogContent { + background-color: var(--gray-2); +} + +/** + * Breakpoints reference: + * https://www.radix-ui.com/themes/docs/theme/breakpoints + * + * Size: initial + * Description: Phones (portrait) + */ +@media all { + .rt-BaseDialogContent:where(.rt-r-size-1) { + --dialog-content-padding: var(--space-5); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-2) { + --dialog-content-padding: var(--space-5); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-3) { + --dialog-content-padding: var(--space-5); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-4) { + --dialog-content-padding: var(--space-5); + border-radius: 28px; + } +} + +/** +* Size: md +* Description: Tablets (landscape) +*/ +@media (min-width: 1024px) { + .rt-BaseDialogContent:where(.rt-r-size-1) { + --dialog-content-padding: var(--space-6); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-2) { + --dialog-content-padding: var(--space-6); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-3) { + --dialog-content-padding: var(--space-6); + border-radius: 28px; + } + .rt-BaseDialogContent:where(.rt-r-size-4) { + --dialog-content-padding: var(--space-6); + border-radius: 28px; + } +} diff --git a/packages/ui/src/theme/components/separator.css b/packages/ui/src/theme/components/separator.css new file mode 100644 index 000000000..d1cf769c3 --- /dev/null +++ b/packages/ui/src/theme/components/separator.css @@ -0,0 +1,37 @@ +.rt-Separator:where(.rt-r-orientation-vertical) { + width: 0px; + background-color: unset; + border-style: dashed; + border-color: var(--accent-a6); + border-right-width: 1px; +} + +.rt-Separator:where(.rt-r-orientation-horizontal) { + height: 0px; + background-color: unset; + border-style: dashed; + border-color: var(--accent-a6); + border-bottom-width: 1px; +} + + +/** +* Size: md +* Description: Tablets (landscape) +*/ +@media (min-width: 1024px) { + .rt-Separator:where(.md\:rt-r-orientation-vertical) { + width: 0px; + background-color: unset; + border-style: dashed; + border-color: var(--accent-a6); + border-right-width: 1px; + } + .rt-Separator:where(.md\:rt-r-orientation-horizontal) { + height: 0px; + background-color: unset; + border-style: dashed; + border-color: var(--accent-a6); + border-bottom-width: 1px; + } +} \ No newline at end of file diff --git a/packages/ui/src/theme/components/tabs.css b/packages/ui/src/theme/components/tabs.css new file mode 100644 index 000000000..928d596d2 --- /dev/null +++ b/packages/ui/src/theme/components/tabs.css @@ -0,0 +1,27 @@ +.rt-TabsTrigger { + padding: 0px; +} + +/** + * Breakpoints reference: + * https://www.radix-ui.com/themes/docs/theme/breakpoints + * + * Size: initial + * Description: Phones (portrait) + */ +@media all { + .rt-BaseTabList:where(.rt-r-size-1) { + --tab-padding-x: var(--space-0); + --tab-inner-padding-x: var(--space-3); + --tab-inner-padding-y: var(--space-1); + --tab-inner-border-radius: var(--radius-3) var(--radius-3) 0 0; + --tab-height: auto; + } + .rt-BaseTabList:where(.rt-r-size-2) { + --tab-padding-x: var(--space-0); + --tab-inner-padding-x: var(--space-4); + --tab-inner-padding-y: var(--space-2); + --tab-inner-border-radius: var(--radius-4) var(--radius-4) 0 0; + --tab-height: auto; + } +} diff --git a/packages/ui/src/theme/components/textfield.css b/packages/ui/src/theme/components/textfield.css new file mode 100644 index 000000000..038314e56 --- /dev/null +++ b/packages/ui/src/theme/components/textfield.css @@ -0,0 +1,25 @@ +.rt-TextFieldRoot:where(.rt-variant-surface) { + box-shadow: inset 0 0 0 var(--text-field-border-width) var(--gray-a7), 0px 1px 1px 0px rgba(0, 0, 0, 0.05), 0px 0.5px 1px 0px rgba(0, 0, 0, 0.10); +} + +/** + * Breakpoints reference: + * https://www.radix-ui.com/themes/docs/theme/breakpoints + * + * Size: initial + * Description: Phones (portrait) + */ +@media all { + .rt-TextFieldRoot:where(.rt-r-size-1) { + --text-field-border-radius: 8px; + } + .rt-TextFieldRoot:where(.rt-r-size-2) { + --text-field-border-radius: 8px; + } + .rt-TextFieldRoot:where(.rt-r-size-3) { + --text-field-border-radius: 8px; + } + .rt-TextFieldRoot:where(.rt-r-size-4) { + --text-field-border-radius: 8px; + } +} \ No newline at end of file diff --git a/packages/ui/src/theme/fonts.css b/packages/ui/src/theme/fonts.css index a2d52187b..22b545bcd 100644 --- a/packages/ui/src/theme/fonts.css +++ b/packages/ui/src/theme/fonts.css @@ -1,6 +1,6 @@ @font-face { - font-family: 'GeistSans'; - src: url('/fonts/GeistVariableVF.woff2') format('woff2'); + font-family: 'Inter'; + src: url('/fonts/InterVariable.woff2') format('woff2'); font-weight: normal; font-style: normal; } @@ -13,7 +13,7 @@ } :root { - --font-geist-sans: 'GeistSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', + --font-inter: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; --font-geist-mono: 'GeistMono', 'SF Mono', 'Segoe UI Mono', 'Roboto Mono', Menlo, Courier, monospace; diff --git a/packages/ui/src/theme/index.css b/packages/ui/src/theme/index.css index 39ac7e86d..cc0b40289 100644 --- a/packages/ui/src/theme/index.css +++ b/packages/ui/src/theme/index.css @@ -1,25 +1,41 @@ +/* Base */ @import "tailwindcss/base"; @import "@radix-ui/themes/styles.css"; + +/* Components */ @import "../components/Spinner/Spinner.css"; + +/* Radix overrides */ @import "./theme.css"; +@import "./components/badge.css"; +@import "./components/button.css"; +@import "./components/card.css"; +@import "./components/dialog.css"; +@import "./components/separator.css"; +@import "./components/tabs.css"; +@import "./components/textfield.css"; + +/* Tailwind */ @tailwind components; @tailwind utilities; :root, .radix-themes { + /* Font */ + --font-size-1: calc(12px * var(--scaling)); + --font-size-2: calc(13px * var(--scaling)); + --font-size-3: calc(15px * var(--scaling)); + --font-size-4: calc(17px * var(--scaling)); + --font-size-5: calc(20px * var(--scaling)); + + /* Default */ --default-font-size: var(--font-size-3); - --default-font-family: var(--font-geist-sans); + --default-font-family: var(--font-inter); + /* Radix Adjustments */ --green-contrast: #111111; -} - -:root { - - --screen-mobile: 300px; - --screen-tablet: 640px; - --screen-laptop: 960px; - --screen-desktop: 1280px; + /* Space */ --space-0: 0px; --space-1: calc(4px * var(--scaling)); --space-2: calc(8px * var(--scaling)); @@ -50,7 +66,9 @@ --space-72: calc(288px * var(--scaling)); --space-80: calc(320px * var(--scaling)); --space-96: calc(384px * var(--scaling)); +} +:root { /* general colors */ --color-border: var(--gray-6); --color-border-hover: var(--gray-8); @@ -91,5 +109,8 @@ --color-error-muted: var(--red-8); --color-error-contrast: var(--red-9-contrast); --color-error-border: var(--red-8); +} +.rt-DialogContent { + box-shadow: none; } \ No newline at end of file diff --git a/packages/ui/src/theme/tailwind-preset.ts b/packages/ui/src/theme/tailwind-preset.ts index 24dbbf647..289933aeb 100644 --- a/packages/ui/src/theme/tailwind-preset.ts +++ b/packages/ui/src/theme/tailwind-preset.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; import { globbySync } from 'globby'; import _ from 'lodash'; import type { Config } from 'tailwindcss'; @@ -74,7 +74,7 @@ const preset: Config = { spacing: tailwindDefaultTheme.spacing, extend: { fontFamily: { - sans: ['var(--font-geist-sans)'], + sans: ['var(--font-inter)'], mono: ['var(--font-geist-mono)'], }, keyframes, @@ -96,6 +96,11 @@ const preset: Config = { tablet: `${breakpoints.tablet}px`, laptop: `${breakpoints.laptop}px`, desktop: `${breakpoints.desktop}px`, + xs: `${breakpoints.xs}px`, + sm: `${breakpoints.sm}px`, + md: `${breakpoints.md}px`, + lg: `${breakpoints.lg}px`, + xl: `${breakpoints.xl}px`, }, }, }, @@ -117,8 +122,6 @@ const preset: Config = { addVariant('first-child', '& > :first-child'); addVariant('first-type', '&:first-of-type'); addVariant('last-type', '&:last-of-type'); - addVariant('dark-theme', ['.dark &', '.dark-theme &']); - addVariant('light-theme', ['.light &', '.light-theme &']); const components = getComponents(); const componentsMap = _.fromPairs(components.map((c) => [c, c])); diff --git a/packages/ui/src/theme/theme.css b/packages/ui/src/theme/theme.css index 2945d918f..bfd4d716a 100644 --- a/packages/ui/src/theme/theme.css +++ b/packages/ui/src/theme/theme.css @@ -1,5 +1,5 @@ -/* Brand (just a link for the green vars) */ :root { + /* Brand (just a link for the green vars) */ --brand-1: var(--green-1); --brand-2: var(--green-2); --brand-3: var(--green-3); @@ -14,6 +14,16 @@ --brand-12: var(--green-12); } +:root:is(.light) { + --color-panel-solid: #FFF; + --color-surface: #FFF; +} + +:root:is(.dark) { + --color-panel-solid: #191919; + --color-surface: #222; +} + [data-accent-color='brand'] { --accent-contrast: #111111; } @@ -826,7 +836,6 @@ --gray-contrast: #FFFFFF; --gray-surface: rgba(0, 0, 0, 0.05); --gray-indicator: var(--gray-9); - --color-surface: #222; } /* Gray Light Mode L1 */ @@ -864,7 +873,6 @@ --gray-contrast: #FFFFFF; --gray-surface: #ffffffcc; --gray-indicator: var(--gray-9); - --color-surface: #FFF; } @supports (color: color(display-p3 1 1 1)) { @@ -948,4 +956,4 @@ } } -} \ No newline at end of file +} diff --git a/packages/ui/tsup.config.mjs b/packages/ui/tsup.config.mjs index 5b82b1239..e91dca4d8 100644 --- a/packages/ui/tsup.config.mjs +++ b/packages/ui/tsup.config.mjs @@ -23,7 +23,7 @@ export default [ { ...defConfig, entry: { - index: 'src/index.tsx', + index: 'src/index.ts', }, publicDir: 'public', },