diff --git a/packages/block/block.module.css b/packages/block/block.module.css new file mode 100644 index 00000000..54e79573 --- /dev/null +++ b/packages/block/block.module.css @@ -0,0 +1,38 @@ +.foreground { + background: var(--lido-color-foreground); + color: var(--lido-color-text-secondary); +} + +.background { + background: var(--lido-color-background); + color: var(--lido-color-text-secondary); +} + +.accent { + background: var(--lido-color-accent); + color: var(--lido-color-accent-contrast); +} + +.flat { + box-shadow: none; +} + +.shadow { + box-shadow: var(--lido-shadows-lg) var(--lido-color-shadow-light); +} + +.padding { + padding: var(--lido-space-lg); + + @media (--lido-media-breakpoint-up-md) { + padding: var(--lido-space-xxl); + } +} + +.block { + font-weight: 400; + font-size: var(--lido-font-size-xxs); + line-height: 1.6em; + border-radius: var(--lido-border-radius-xl); + margin: 0; +} \ No newline at end of file diff --git a/packages/block/block.stories.tsx b/packages/block/block.stories.tsx new file mode 100644 index 00000000..62ba51ee --- /dev/null +++ b/packages/block/block.stories.tsx @@ -0,0 +1,28 @@ +import { StoryFn, Meta } from '@storybook/react' +import { Block, BlockProps, BlockColor, BlockVariant } from '.' + +const getOptions = (enumObject: Record) => + Object.values(enumObject).filter((value) => typeof value === 'string') + +export default { + component: Block, + title: 'Layout/Block', + args: { + children: 'Example content', + variant: 'flat', + color: 'foreground', + paddingLess: false, + }, + argTypes: { + variant: { + options: getOptions(BlockVariant), + control: 'inline-radio', + }, + color: { + options: getOptions(BlockColor), + control: 'inline-radio', + }, + }, +} satisfies Meta + +export const Basic: StoryFn = (props) => \ No newline at end of file diff --git a/packages/block/block.tsx b/packages/block/block.tsx new file mode 100644 index 00000000..9a4bcd0a --- /dev/null +++ b/packages/block/block.tsx @@ -0,0 +1,51 @@ +import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from 'react' +import cn from 'classnames' +import styles from './block.module.css' + +export enum BlockVariant { + flat, + shadow, +} +export type BlockVariants = keyof typeof BlockVariant + +export enum BlockColor { + foreground, + background, + accent, +} +export type BlockColors = keyof typeof BlockColor + +export type BlockProps = ComponentPropsWithoutRef<'div'> & { + color?: BlockColors + variant?: BlockVariants + paddingLess?: boolean +} + +export const Block = forwardRef( + ( + { + color = 'foreground', + variant = 'flat', + paddingLess = false, + className, + ...rest + }: BlockProps, + ref?: ForwardedRef, + ) => { + return ( +
+ ) + }, +) +Block.displayName = 'Block' \ No newline at end of file diff --git a/packages/block/index.ts b/packages/block/index.ts new file mode 100644 index 00000000..b27c9553 --- /dev/null +++ b/packages/block/index.ts @@ -0,0 +1 @@ +export * from './block' \ No newline at end of file diff --git a/packages/icons/Icon.stories.tsx b/packages/icons/Icon.stories.tsx index 86d655a2..d68471cf 100644 --- a/packages/icons/Icon.stories.tsx +++ b/packages/icons/Icon.stories.tsx @@ -1,5 +1,4 @@ import { StoryFn, Meta } from '@storybook/react' -import styled from '../utils/styled-components-wrapper.js' import * as components from './index.js' type IconVariants = keyof typeof components @@ -30,204 +29,204 @@ Base.args = { type: 'History', } -const IconList = styled.div` - display: flex; - flex-wrap: wrap; -` - -const IconListItem = styled.div` - text-align: center; - font-size: 13px; - line-height: 15px; - width: 120px; - margin: 2px; - padding: 10px; - border-radius: 4px; - color: var(--lido-color-text); -` - -const IconListTitle = styled.div` - opacity: 0.5; -` - -export const List: StoryFn = () => ( - - {iconKeys.map((componentName) => { - const Icon = components[componentName] - - return ( - - - {componentName} - - ) - })} - -) - -const SocialList = styled.div` - display: flex; -` - -const SocialListItem = styled.div<{ $color: string }>` - background: ${({ $color }) => $color}; - line-height: 0; - border-radius: 6px; - margin: 4px; - padding: 4px; - - svg { - fill: #fff; - } -` - -export const Social: StoryFn = () => { - const { Facebook, Twitter, Linkedin, Email, Telegram } = components - - return ( - - - - - - - - - - - - - - - - - - ) -} - -export const CryptoCurrencies: StoryFn = () => { - const { Eth, Weth, Steth, Wsteth, Beth, Ldo, Ldopl, Solana, Stsol, Terra } = - components - const iconKeys = Object.keys({ - Eth, - Weth, - Steth, - Wsteth, - Beth, - Ldo, - Ldopl, - Solana, - Stsol, - Terra, - }) as IconVariants[] - - return ( - - {iconKeys.map((componentName) => { - const Icon = components[componentName] - - return ( - - - {componentName} - - ) - })} - - ) -} - -export const CryptoWallets: StoryFn = () => { - const { - MetaMask, - MetaMaskCircle, - MetaMaskCircleInversion, - WalletConnect, - WalletConnectCircle, - Coinbase, - Ledger, - LedgerCircle, - LedgerCircleInversion, - Trust, - TrustCircle, - Imtoken, - ImtokenCircle, - MathWalletCircle, - MathWalletCircleInversion, - Coin98Circle, - Ambire, - Blochainwallet, - BlochainwalletInversion, - Exodus, - OperaWallet, - Unstoppabledomains, - Zengo, - Gamestop, - XdefiWallet, - } = components - const iconKeys = Object.keys({ - MetaMask, - MetaMaskCircle, - MetaMaskCircleInversion, - WalletConnect, - WalletConnectCircle, - Coinbase, - Ledger, - LedgerCircle, - LedgerCircleInversion, - Trust, - TrustCircle, - Imtoken, - ImtokenCircle, - MathWalletCircle, - MathWalletCircleInversion, - Coin98Circle, - Ambire, - Zengo, - Blochainwallet, - BlochainwalletInversion, - Exodus, - OperaWallet, - Unstoppabledomains, - Gamestop, - XdefiWallet, - }) as IconVariants[] - - return ( - - {iconKeys.map((componentName) => { - const Icon = components[componentName] - - return ( - - - {componentName} - - ) - })} - - ) -} - -export const CryptoExchanges: StoryFn = () => { - const { Uniswap, OneInch } = components - const iconKeys = Object.keys({ - Uniswap, - OneInch, - }) as IconVariants[] - - return ( - - {iconKeys.map((componentName) => { - const Icon = components[componentName] - - return ( - - - {componentName} - - ) - })} - - ) -} +// const IconList = styled.div` +// display: flex; +// flex-wrap: wrap; +// ` + +// const IconListItem = styled.div` +// text-align: center; +// font-size: 13px; +// line-height: 15px; +// width: 120px; +// margin: 2px; +// padding: 10px; +// border-radius: 4px; +// color: var(--lido-color-text); +// ` + +// const IconListTitle = styled.div` +// opacity: 0.5; +// ` + +// export const List: StoryFn = () => ( +// +// {iconKeys.map((componentName) => { +// const Icon = components[componentName] + +// return ( +// +// +// {componentName} +// +// ) +// })} +// +// ) + +// const SocialList = styled.div` +// display: flex; +// ` + +// const SocialListItem = styled.div<{ $color: string }>` +// background: ${({ $color }) => $color}; +// line-height: 0; +// border-radius: 6px; +// margin: 4px; +// padding: 4px; + +// svg { +// fill: #fff; +// } +// ` + +// export const Social: StoryFn = () => { +// const { Facebook, Twitter, Linkedin, Email, Telegram } = components + +// return ( +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// ) +// } + +// export const CryptoCurrencies: StoryFn = () => { +// const { Eth, Weth, Steth, Wsteth, Beth, Ldo, Ldopl, Solana, Stsol, Terra } = +// components +// const iconKeys = Object.keys({ +// Eth, +// Weth, +// Steth, +// Wsteth, +// Beth, +// Ldo, +// Ldopl, +// Solana, +// Stsol, +// Terra, +// }) as IconVariants[] + +// return ( +// +// {iconKeys.map((componentName) => { +// const Icon = components[componentName] + +// return ( +// +// +// {componentName} +// +// ) +// })} +// +// ) +// } + +// export const CryptoWallets: StoryFn = () => { +// const { +// MetaMask, +// MetaMaskCircle, +// MetaMaskCircleInversion, +// WalletConnect, +// WalletConnectCircle, +// Coinbase, +// Ledger, +// LedgerCircle, +// LedgerCircleInversion, +// Trust, +// TrustCircle, +// Imtoken, +// ImtokenCircle, +// MathWalletCircle, +// MathWalletCircleInversion, +// Coin98Circle, +// Ambire, +// Blochainwallet, +// BlochainwalletInversion, +// Exodus, +// OperaWallet, +// Unstoppabledomains, +// Zengo, +// Gamestop, +// XdefiWallet, +// } = components +// const iconKeys = Object.keys({ +// MetaMask, +// MetaMaskCircle, +// MetaMaskCircleInversion, +// WalletConnect, +// WalletConnectCircle, +// Coinbase, +// Ledger, +// LedgerCircle, +// LedgerCircleInversion, +// Trust, +// TrustCircle, +// Imtoken, +// ImtokenCircle, +// MathWalletCircle, +// MathWalletCircleInversion, +// Coin98Circle, +// Ambire, +// Zengo, +// Blochainwallet, +// BlochainwalletInversion, +// Exodus, +// OperaWallet, +// Unstoppabledomains, +// Gamestop, +// XdefiWallet, +// }) as IconVariants[] + +// return ( +// +// {iconKeys.map((componentName) => { +// const Icon = components[componentName] + +// return ( +// +// +// {componentName} +// +// ) +// })} +// +// ) +// } + +// export const CryptoExchanges: StoryFn = () => { +// const { Uniswap, OneInch } = components +// const iconKeys = Object.keys({ +// Uniswap, +// OneInch, +// }) as IconVariants[] + +// return ( +// +// {iconKeys.map((componentName) => { +// const Icon = components[componentName] + +// return ( +// +// +// {componentName} +// +// ) +// })} +// +// ) +// } diff --git a/packages/icons/converter/index.cjs b/packages/icons/converter/index.cjs index 7c2e77ce..a71b599f 100644 --- a/packages/icons/converter/index.cjs +++ b/packages/icons/converter/index.cjs @@ -2,7 +2,7 @@ const svgr = require('@svgr/core').default const svgrSvgo = require('@svgr/plugin-svgo').default const svgrJsx = require('@svgr/plugin-jsx').default const svgrPrettier = require('@svgr/plugin-prettier').default -const indexTemplate = require('./template.index.js') +const indexTemplate = require('./template.index.cjs') const fs = require('fs/promises') const { extname, resolve } = require('path') @@ -80,7 +80,7 @@ const convertFiles = async () => { fill: 'currentColor', }, plugins: [svgrSvgo, svgrJsx, svgrPrettier], - template: require('./template.component.js'), + template: require('./template.component.cjs'), }, { componentName }, ) diff --git a/packages/icons/index.tsx b/packages/icons/index.tsx index cfd8e0f1..8ea38715 100644 --- a/packages/icons/index.tsx +++ b/packages/icons/index.tsx @@ -4,7 +4,7 @@ import React from 'react' export const Ambire = React.forwardRef(function Ambire( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , + svgRef?: React.Ref +) { + return ( + + + + ) +}) + export const ArrowLeft = React.forwardRef(function ArrowLeft( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( ) - }, + } ) export const Blochainwallet = React.forwardRef(function Blochainwallet( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( ) - }, + } ) export const LedgerCircle = React.forwardRef(function LedgerCircle( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( ) - }, + } ) export const MathWalletCircle = React.forwardRef(function MathWalletCircle( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( ) - }, + } ) export const MetaMaskCircle = React.forwardRef(function MetaMaskCircle( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( ) - }, + } ) export const WalletConnect = React.forwardRef(function WalletConnect( props: React.SVGProps, - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( , - svgRef?: React.Ref, + svgRef?: React.Ref ) { return ( + + diff --git a/packages/index.ts b/packages/index.ts index 50fba4d9..94a28565 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -3,9 +3,4 @@ export * from './hooks/index.js' export * from './icons/index.js' export * from './transition/index.js' export * from './utils/index.js' -export * from './tabs/index.js' -export * from './tooltip/index.js' -export * from './tooltip/index.js' -export * from './theme-css' -export * from './block' -export * from './text' \ No newline at end of file +export * from './theme-css' \ No newline at end of file diff --git a/packages/tabs/icons/tab-icon-arbitrum.svg b/packages/tabs/icons/tab-icon-arbitrum.svg new file mode 100644 index 00000000..cabe7579 --- /dev/null +++ b/packages/tabs/icons/tab-icon-arbitrum.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/tabs/icons/tab-icon-optimizm.svg b/packages/tabs/icons/tab-icon-optimizm.svg new file mode 100644 index 00000000..6123fef4 --- /dev/null +++ b/packages/tabs/icons/tab-icon-optimizm.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/tabs/index.ts b/packages/tabs/index.ts new file mode 100644 index 00000000..811d3d4a --- /dev/null +++ b/packages/tabs/index.ts @@ -0,0 +1 @@ +export * from "./tabs"; diff --git a/packages/tabs/tabs.module.css b/packages/tabs/tabs.module.css new file mode 100644 index 00000000..46edd8cf --- /dev/null +++ b/packages/tabs/tabs.module.css @@ -0,0 +1,141 @@ +.tabs { + --local-tab-icon-size: var(--undefined); + + display: flex; + gap: 10px; + padding: 6px; +} + +.tab { + flex: 1 1 0; + position: relative; + border: 1px solid var(--lido-color-borders-fog); + background-color: transparent; + cursor: pointer; + + &::before { + content: ""; + position: absolute; + top: -7px; + right: -7px; + bottom: -7px; + left: -7px; + border: 3px solid transparent; + border-radius: var(--lido-border-radius-100); + transition: border-color 0.35s ease; + } + + &:hover { + color: var(--lido-color-accent-ocean); + border-color: var(--lido-color-borders-water); + } + + &.active { + color: var(--lido-color-secondary); + border-radius: var(--lido-border-radius-100); + background-color: var(--lido-color-primary); + transition-property: color, background-color, outline; + transition-duration: 0.35s; + transition-timing-function: ease-in-out; + } + + &.active::before { + border-color: var(--lido-color-accent-ocean); + } + + &:disabled { + color: var(--lido-color-primary-20); + border-color: var(--lido-color-borders-fog); + } +} + +.button { + font-weight: var(--lido-font-weight-bold); + border-radius: var(--lido-border-radius-100); + + .rightDecorator { + color: var(--lido-color-primary-50); + font-weight: var(--lido-font-weight-regular); + font-size: var(--lido-font-size-subheader); + line-height: var(--lido-line-height-subheader); + margin-left: 6px; + } + + &:hover { + .rightDecorator { + color: var(--lido-color-borders-water); + } + } + + &.active { + .rightDecorator { + color: var(--lido-color-secondary-72); + } + } + + &:disabled { + .rightDecorator { + color: var(--lido-color-primary-20); + } + } + + &.sizeM { + min-height: 52px; + padding: 10px 20px; + font-size: 17px; + line-height: 31px; + } + + &.sizeL { + min-height: 70px; + padding: 15px 30px; + font-size: var(--lido-font-size-subheader); + line-height: var(--lido-line-height-subheader); + } + + &.sizeXL { + min-height: 120px; + + /* there are no designs provided for size XL */ + } +} + +.icon { + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + width: var(--local-tab-icon-size); + height: var(--local-tab-icon-size); + + > * { + width: 100%; + height: 100%; + } + + &.sizeM { + padding: 4px; + width: var(--local-tab-icon-size, 52px); + height: var(--local-tab-icon-size, 52px); + } + + &.sizeL { + padding: 10px; + width: var(--local-tab-icon-size, 64px); + height: var(--local-tab-icon-size, 64px); + } + + &.sizeXL { + padding: 10px; + width: var(--local-tab-icon-size, 92px); + height: var(--local-tab-icon-size, 92px); + } + + &:disabled { + svg { + path { + fill: var(--lido-color-primary-20) !important; + } + } + } +} diff --git a/packages/tabs/tabs.stories.tsx b/packages/tabs/tabs.stories.tsx new file mode 100644 index 00000000..6b16ac01 --- /dev/null +++ b/packages/tabs/tabs.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Tabs } from "."; +// import TabIconOrbitrum from "./icons/tab-icon-arbitrum.svg"; +// import TabIconOptimizm from "./icons/tab-icon-optimizm.svg"; + +const meta = { + title: "Layout/Tabs", + component: Tabs, + parameters: { + layout: "centered", + }, + args: { + type: "buttons", + size: "m", + }, + argTypes: { + activeKey: { + control: { + type: "number", + }, + }, + type: { + table: { + disable: true, + }, + }, + items: { + table: { + disable: true, + }, + }, + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const TabButtons: Story = { + args: { + items: [ + { + key: "1", + children: "Tab", + }, + { + key: "2", + children: "Taaaaaaaaaaaaaaaaaaaaaaaaab", + rightDecorator: 3, + }, + { + key: "3", + children: "Yet Another Tab", + }, + { + key: "4", + disabled: true, + children: "Disabled tab", + }, + ], + }, +}; + +export const TabIcons: Story = { + args: { + type: "icons", + items: [ + { + key: "1", + children: 'text', + }, + { + key: "2", + children: 'text', + }, + { + key: "3", + disabled: true, + children: 'text', + }, + ], + }, +}; diff --git a/packages/tabs/tabs.tsx b/packages/tabs/tabs.tsx new file mode 100644 index 00000000..3104896c --- /dev/null +++ b/packages/tabs/tabs.tsx @@ -0,0 +1,99 @@ +import { ComponentPropsWithoutRef, MouseEvent, ReactNode, useEffect, useState } from "react"; +import styles from "./tabs.module.css"; +import cn from "classnames"; + +export type TabBaseItem = { + key: string; + disabled?: boolean; + children?: ReactNode; +}; + +export type TabIconItem = TabBaseItem & { + rightDecorator?: never; +}; + +export type TabButtonItem = TabBaseItem & { + rightDecorator?: ReactNode; +}; + +export type TabsBaseProps = Omit, "onChange"> & { + size?: "m" | "l" | "xl"; + defaultKey?: string; + activeKey?: string; + onKeyChange?: (key: string) => unknown; +}; + +export type TabsButtonProps = TabsBaseProps & { + type?: "buttons"; + items?: TabButtonItem[]; +}; + +export type TabsIconProps = TabsBaseProps & { + type?: "icons"; + items?: TabIconItem[]; +}; + +export type TabsProps = TabsButtonProps | TabsIconProps; + +export const Tabs = ({ + type = "buttons", + size = "m", + defaultKey, + activeKey: _activeKey, + items, + onKeyChange, + className, + ...rest +}: TabsProps) => { + const [activeKey, setActiveKey] = useState(undefined); + + useEffect(() => { + setActiveKey(defaultKey); + }, [defaultKey]); + + useEffect(() => { + if (_activeKey != null) { + setActiveKey(_activeKey); + } else { + const firstItem = items?.[0]; + if (firstItem == null) { + return; + } + setActiveKey(firstItem.key); + } + }, [_activeKey, items]); + + const handleClick = (key: string) => (event: MouseEvent) => { + event.preventDefault(); + onKeyChange?.(key); + if (_activeKey != null) { + return; + } + setActiveKey(key); + }; + + return ( +
+ {items?.map((item) => ( + + ))} +
+ ); +}; diff --git a/packages/text/index.ts b/packages/text/index.ts new file mode 100644 index 00000000..42b5475c --- /dev/null +++ b/packages/text/index.ts @@ -0,0 +1 @@ +export * from './text' \ No newline at end of file diff --git a/packages/text/text.module.css b/packages/text/text.module.css new file mode 100644 index 00000000..553c892a --- /dev/null +++ b/packages/text/text.module.css @@ -0,0 +1,70 @@ +.text { + margin: 0; + padding: 0; +} + +.xxs { + font-size: var(--lido-font-size-xxs); + line-height: 1.5em; +} + +.xs { + font-size: var(--lido-font-size-xs); + line-height: 1.5em; +} + +.sm { + font-size: var(--lido-font-size-sm); + line-height: 1.5em; +} + +.md { + font-size: var(--lido-font-size-md); + line-height: 1.5em; +} + +.lg { + font-size: var(--lido-font-size-lg); + line-height: 1.4em; +} + +.xl { + font-size: var(--lido-font-size-xl); + line-height: 1.4em; +} + +.italic { + font-style: italic; +} + +.underline { + text-decoration: underline; +} + +.strikeThrough { + text-decoration: line-through; +} + +.default { + color: var(--lido-color-text); +} + +.secondary { + color: var(--lido-color-text-secondary); +} + +.primary { + color: var(--lido-color-primary); +} + +.warning { + color: var(--lido-color-warning); +} + +.error { + color: var(--lido-color-error); +} + +.success { + color: var(--lido-color-success); +} \ No newline at end of file diff --git a/packages/text/text.stories.tsx b/packages/text/text.stories.tsx new file mode 100644 index 00000000..0232def3 --- /dev/null +++ b/packages/text/text.stories.tsx @@ -0,0 +1,34 @@ +import { StoryFn, Meta } from '@storybook/react' +import { Text, TextProps, TextColor, TextSize } from './' + +const getOptions = (enumObject: Record) => + Object.values(enumObject).filter((value) => typeof value === 'string') + +export default { + component: Text, + title: 'Typography/Text', + args: { + children: 'Example Text', + color: 'default', + size: 'md', + underline: false, + strikeThrough: false, + strong: false, + italic: false, + }, + argTypes: { + children: { + control: 'text', + }, + color: { + options: getOptions(TextColor), + control: 'inline-radio', + }, + size: { + options: getOptions(TextSize), + control: 'inline-radio', + }, + }, +} satisfies Meta + +export const Basic: StoryFn = (props) => \ No newline at end of file diff --git a/packages/text/text.tsx b/packages/text/text.tsx new file mode 100644 index 00000000..84c100c7 --- /dev/null +++ b/packages/text/text.tsx @@ -0,0 +1,82 @@ +import { ComponentPropsWithoutRef, ForwardedRef, forwardRef } from 'react' +import cn from 'classnames' +import styles from './text.module.css' + +export enum TextColor { + default, + secondary, + primary, + warning, + error, + success, +} +export type TextColors = keyof typeof TextColor + +export enum TextSize { + xxs, + xs, + sm, + md, + lg, + xl, +} +export type TextSizes = keyof typeof TextSize + +export type TextWeight = 400 | 500 | 700 | 800 | string | number + +export type TextProps = ComponentPropsWithoutRef<'p'> & { + color?: TextColors + size?: TextSizes + weight?: TextWeight + underline?: boolean + strikeThrough?: boolean + strong?: boolean + italic?: boolean +} + +export const Text = forwardRef( + ( + { + size = 'md', + weight = 400, + color = 'default', + underline, + italic, + strikeThrough, + strong, + className, + ...rest + }: TextProps, + ref?: ForwardedRef, + ) => { + return ( +

+ ) + }, +) +Text.displayName = 'Text' \ No newline at end of file diff --git a/packages/theme-css/theme-toggler/theme-toggler.module.css b/packages/theme-css/theme-toggler/theme-toggler.module.css new file mode 100644 index 00000000..46f876db --- /dev/null +++ b/packages/theme-css/theme-toggler/theme-toggler.module.css @@ -0,0 +1,36 @@ +.icon { + grid-area: a; + align-self: center; + justify-self: center; +} + +.light { + composes: icon; + visibility: var(--lido-dark-mode-visibility); +} + +.dark { + composes: icon; + visibility: var(--lido-light-mode-visibility); +} + +.themeToggler { + display: inline-grid; + grid-template-areas: 'a'; + min-width: 0; + margin-left: var(--lido-space-sm); + line-height: 0; + font-size: 0; + padding: 0; + width: 44px; + height: 44px; + + /* + button element contains span as children container, we want to bypass it. + by aiming explicit "span:first-child" we're verifying that this is what we are aiming for + witout breaking our SVGs + */ + &>span:first-child { + display: contents; + } +} \ No newline at end of file diff --git a/packages/theme-css/theme-toggler/theme-toggler.stories.tsx b/packages/theme-css/theme-toggler/theme-toggler.stories.tsx new file mode 100644 index 00000000..8f59073a --- /dev/null +++ b/packages/theme-css/theme-toggler/theme-toggler.stories.tsx @@ -0,0 +1,33 @@ +import { StoryFn, Meta } from '@storybook/react' +import { Block } from '../../block' +import { ContentTheme } from '../content-theme' +import { Text } from '../../text' +import { ThemeToggler } from './' + +export default { + title: 'Theme/Toggler', + args: { + themeOverride: 'follow cookie and system', + }, +} satisfies Meta + +export const Basic: StoryFn = () => ( + <> + Use button to toggle theme{' '} + + 👉 + {' '} + and reload page! +
+
+ The block depended by theme cookie + Lorem ipsum dolor sit amet... +
+
+ Example of using ContentTheme component + You see only dark content!} + lightContent={You see only light content!} + /> + +) diff --git a/packages/theme-css/theme-toggler/theme-toggler.tsx b/packages/theme-css/theme-toggler/theme-toggler.tsx new file mode 100644 index 00000000..916c4745 --- /dev/null +++ b/packages/theme-css/theme-toggler/theme-toggler.tsx @@ -0,0 +1,32 @@ +import { ForwardedRef, forwardRef } from 'react' +import { useThemeToggle } from '../use-theme-toggle' +import { Dark, Light } from '../../icons' +import cn from 'classnames' +import styles from './theme-toggler.module.css' + +export type ThemeTogglerProps = any + +export const ThemeToggler = forwardRef( + ( + { className, ...rest }: any, + ref: ForwardedRef, + ) => { + const { toggleTheme, themeName } = useThemeToggle() + + return ( + + ) + }, +) +ThemeToggler.displayName = 'ThemeToggler' \ No newline at end of file diff --git a/packages/theme-css/use-theme-toggle.ts b/packages/theme-css/use-theme-toggle.ts new file mode 100644 index 00000000..0d9e9272 --- /dev/null +++ b/packages/theme-css/use-theme-toggle.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react' +import { ThemeToggleContext, ThemeContext } from './cookie-theme-provider' + +export const useThemeToggle = (): ThemeContext => { + return useContext(ThemeToggleContext) +} \ No newline at end of file diff --git a/packages/theme-css/utils/cookies.ts b/packages/theme-css/utils/cookies.ts new file mode 100644 index 00000000..32a069bb --- /dev/null +++ b/packages/theme-css/utils/cookies.ts @@ -0,0 +1,15 @@ +// TODO: use /packages/utils/cookies-client-side.ts + +import { themeCookieKey, ThemeName } from '../constants.js' + +// we're using all-same regex in element-theme-script.tsx. +// Sadly, we cannot re-use it as this script is supposed to be inlined in document head +const cookieThemeMatcher = new RegExp(`(^| )${themeCookieKey}=([^;]+)`) + +export const getThemeNameFromCookies = (): ThemeName | null => { + if (typeof window === 'undefined') { + return null + } + + return (document.cookie.match(cookieThemeMatcher)?.[2] as ThemeName) ?? null +} \ No newline at end of file diff --git a/packages/theme-css/utils/set-theme-cookie.ts b/packages/theme-css/utils/set-theme-cookie.ts new file mode 100644 index 00000000..649a5f1d --- /dev/null +++ b/packages/theme-css/utils/set-theme-cookie.ts @@ -0,0 +1,31 @@ +import { UAParser } from 'ua-parser-js' +import { getTopLevelDomain } from '../../utils/index.js' +import { themeCookieMaxAge, themeCookieKey, ThemeName } from '../constants.js' + +const parser = new UAParser() + +const setSecureCookie = (cookie: string) => { + // 1. we want this cookie to be available on HTTP websites too. + // 2. there is a bug on localhost which causes Chrome to ignore cookies set without Secure, + // and Safari when cookies are set with Secure, so we're forcing cookie into both + if (parser.getBrowser()?.name?.toLowerCase() === 'safari') { + if (window.location.protocol !== 'https:') { + document.cookie = cookie + } else { + document.cookie = `${cookie}Secure;` + } + } else { + document.cookie = `${cookie}Secure;` + } +} + +export const setThemeCookie = (theme: ThemeName) => { + const cookie = `${themeCookieKey}=${theme};max-age=${themeCookieMaxAge};path=/;samesite=None;` + // For top level domain - *.some-domain.fi + setSecureCookie(`${cookie}domain=${getTopLevelDomain()};`) + + if (!document.cookie.includes(`${themeCookieKey}=${theme}`)) { + // For specific.domain.fi, if cookie can't be set to top level domain + setSecureCookie(cookie) + } +} \ No newline at end of file diff --git a/packages/tooltip/Tooltip.stories.tsx b/packages/tooltip/Tooltip.stories.tsx index 97012b0f..63786e42 100644 --- a/packages/tooltip/Tooltip.stories.tsx +++ b/packages/tooltip/Tooltip.stories.tsx @@ -1,46 +1,33 @@ -import { StoryFn, Meta } from '@storybook/react' -import { Question } from '../icons/index.js' -import { PopoverOffset, PopoverPlacement } from '../popover/index.js' -import { Tooltip, TooltipProps } from './index.js' +import type { Meta, StoryObj } from "@storybook/react"; +import { Tooltip } from "."; -const getOptions = (enumObject: Record) => - Object.values(enumObject).filter((value) => typeof value === 'string') - -export default { +const meta = { + title: "Dialogs/Tooltip", component: Tooltip, - title: 'Dialogs/Tooltip', parameters: { - layout: 'centered', + layout: "centered", }, args: { - title: - 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum voluptates pariatur culpa consectetur velit iste rem, aspernatur voluptatem aperiam itaque obcaecati vero non quis id iure vitae, quae quibusdam quidem.', - offset: 'xs', - placement: 'bottom', - }, - argTypes: { - title: { - control: 'text', - }, - offset: { - options: getOptions(PopoverOffset), - control: 'inline-radio', - }, - placement: { - options: getOptions(PopoverPlacement), - control: 'radio', + icon: { + size: 'big', + color: 'black' }, + position: "right", + content: "Under normal circumstances. The withdrawal time may take longer under special circumstances", }, -} as Meta + tags: ["autodocs"], +} satisfies Meta; -export const Basic: StoryFn = (props) => ( - - Hover me - -) +export default meta; -export const Icon: StoryFn = (props) => ( - - - -) +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Content: Story = { + args: { + children: "Hover me", + position: "right", + content: "Under normal circumstances. The withdrawal time may take longer under special circumstances", + }, +}; diff --git a/packages/tooltip/Tooltip.tsx b/packages/tooltip/Tooltip.tsx deleted file mode 100644 index 5aec0fa3..00000000 --- a/packages/tooltip/Tooltip.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - ForwardedRef, - forwardRef, - Children, - useRef, - cloneElement, - useState, - MouseEvent, -} from 'react' -import { isElement } from 'react-is' -import { TooltipPopoverStyle } from './TooltipStyles.js' -import { useMergeRefs } from '../hooks/index.js' -import { TooltipProps } from './types.js' - -const BODY_PERSISTENT_TIMEOUT = 150 - -export const Tooltip = forwardRef( - ( - { title, children, ...rest }: TooltipProps, - ref?: ForwardedRef, - ) => { - const [state, setState] = useState(false) - const keepTimeoutRef = useRef(null) - - const child = Children.only(children) - if (!isElement(child)) throw new Error('Child must be a React element') - - const anchorRef = useRef(null) - const mergedRef = useMergeRefs([child.ref, anchorRef]) - - const handleMouseEnter = () => { - if (keepTimeoutRef.current) { - clearTimeout(keepTimeoutRef.current) - keepTimeoutRef.current = null - } - setState(true) - } - - const handleMouseLeave = () => { - keepTimeoutRef.current = setTimeout(() => { - setState(false) - keepTimeoutRef.current = null - }, BODY_PERSISTENT_TIMEOUT) - } - - return ( - <> - {cloneElement(child, { - ref: mergedRef, - onMouseEnter(event: MouseEvent) { - handleMouseEnter() - child.props.onMouseEnter?.(event) - }, - onMouseLeave(event: MouseEvent) { - handleMouseLeave() - child.props.onMouseLeave?.(event) - }, - })} - - {title} - - - ) - }, -) -Tooltip.displayName = 'Tooltip' diff --git a/packages/tooltip/index.ts b/packages/tooltip/index.ts index 714481ac..3c61782a 100644 --- a/packages/tooltip/index.ts +++ b/packages/tooltip/index.ts @@ -1,2 +1 @@ -export * from './Tooltip.js' -export * from './types.js' +export * from "./tooltip"; diff --git a/packages/tooltip/infoIcon/icon-info.stories.tsx b/packages/tooltip/infoIcon/icon-info.stories.tsx new file mode 100644 index 00000000..53a4ca17 --- /dev/null +++ b/packages/tooltip/infoIcon/icon-info.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { InfoIcon } from "."; + +const meta = { + title: "Layout/InfoIcon", + component: InfoIcon, + parameters: { + layout: "centered", + }, + args: { + size: 'big', + color: 'black', + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/packages/tooltip/infoIcon/index.ts b/packages/tooltip/infoIcon/index.ts new file mode 100644 index 00000000..56496156 --- /dev/null +++ b/packages/tooltip/infoIcon/index.ts @@ -0,0 +1 @@ +export * from './infoIcon' \ No newline at end of file diff --git a/packages/tooltip/infoIcon/info-icon.module.css b/packages/tooltip/infoIcon/info-icon.module.css new file mode 100644 index 00000000..aa90f3e1 --- /dev/null +++ b/packages/tooltip/infoIcon/info-icon.module.css @@ -0,0 +1,42 @@ +.infoIcon { + display: block; + transition: color .5s; + + &:hover { + transition: color .3s; + } +} + +.big { + width: 24px; + height: 24px; +} + +.small { + width: 15px; + height: 15px; +} + +.black { + color: var(--lido-color-primary-72); + + &:hover { + color: var(--lido-color-primary-50); + } +} + +.ocean { + color: var(--lido-color-accent-ocean); + + &:hover { + color: var(--lido-color-accent-sky); + } +} + +.white { + color: var(--lido-color-secondary-72); + + &:hover { + color: var(--lido-color-secondary); + } +} \ No newline at end of file diff --git a/packages/tooltip/infoIcon/infoIcon.tsx b/packages/tooltip/infoIcon/infoIcon.tsx new file mode 100644 index 00000000..dba11c5e --- /dev/null +++ b/packages/tooltip/infoIcon/infoIcon.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import s from './info-icon.module.css' +import cn from 'classnames' + +export type InfoIconProps = React.SVGProps & { + size?: 'small' | 'big' + color?: 'black' | 'ocean' | 'white' +} + +export const InfoIcon = React.forwardRef(function InfoIcon( + { className, color = 'black', size = 'big', ...rest }: InfoIconProps, + svgRef?: React.Ref, +) { + return ( + + + + + ) +}) diff --git a/packages/tooltip/tooltip.module.css b/packages/tooltip/tooltip.module.css new file mode 100644 index 00000000..5eec6845 --- /dev/null +++ b/packages/tooltip/tooltip.module.css @@ -0,0 +1,61 @@ +.tooltip { + position: relative; + + &:hover { + .content { + display: block; + } + } +} + +.content { + z-index: 9; + display: none; + position: absolute; + width: max-content; + max-width: 224px; + box-sizing: border-box; + padding: 24px; + background-color: var(--lido-color-primary-72); + backdrop-filter: blur(20px); + color: var(--lido-color-secondary); + border-radius: var(--lido-border-radius-24); + + &.right { + top: 0; + transform: translateY(-45%); + left: calc(100% + 10px); + } + + &.bottomRight { + top: 0; + left: calc(100% + 10px); + } + + &.topRight { + bottom: calc(100% - 2px); + left: calc(100% - 3px); + } + + &.bottomCenter { + top: calc(100% + 3px); + left: 50%; + transform: translateX(-50%); + } + + &.cardBottom { + top: calc(100% + 5px); + left: 0; + transform: translateX(-60%); + } +} + +.icon-black { + color: var(--lido-color-primary-72); + transition: color .5s; + + &:hover { + color: var(--lido-color-primary-50); + transition: color .3s; + } +} \ No newline at end of file diff --git a/packages/tooltip/tooltip.tsx b/packages/tooltip/tooltip.tsx new file mode 100644 index 00000000..4825b0df --- /dev/null +++ b/packages/tooltip/tooltip.tsx @@ -0,0 +1,49 @@ +import { ReactNode } from 'react' +import cn from 'classnames' +import styles from './tooltip.module.css' +import { InfoIcon, InfoIconProps } from './infoIcon' +import { LidoComponentProps } from 'packages/utils' + +export type TooltipProps = LidoComponentProps< + 'div', + { + content?: ReactNode + position?: + | 'right' + | 'bottom-right' + | 'top-right' + | 'bottom-center' + | 'card-bottom' + icon?: { + size?: InfoIconProps['size'] + color?: InfoIconProps['color'] + } + } +> + +export const Tooltip = ({ + content, + position = 'right', + icon, + className, + children, + ...rest +}: TooltipProps) => { + return ( +

+ {children || } + +
+ {content} +
+
+ ) +}