Merge branch 'batch-1' into topic-implement-card

This commit is contained in:
Hossein Mehrabi 2023-02-23 20:19:43 +03:30
commit 74729ea72e
57 changed files with 1508 additions and 203 deletions

View File

@ -0,0 +1,17 @@
export const autocompleteClasses = {
root: `lsd-autocomplete`,
input: `lsd-autocomplete__input`,
icon: `lsd-autocomplete__icon`,
listBox: `lsd-autocomplete-list-box`,
dropdownItem: `lsd-autocomplete-dropdown-item`,
dropdownItemPlaceholder: `lsd-autocomplete-dropdown-item__placeholder`,
disabled: `lsd-autocomplete--disabled`,
error: 'lsd-autocomplete--error',
large: `lsd-autocomplete--large`,
medium: `lsd-autocomplete--medium`,
withIcon: `lsd-autocomplete--with-icon`,
}

View File

@ -0,0 +1,33 @@
import { Meta, Story } from '@storybook/react'
import { Autocomplete, AutocompleteProps } from './Autocomplete'
const list = JSON.parse(
'["BitTorrent","VANIG","BenjiRolls","Status Network Token","GigTricks","Vechain","Insureum","Instant Sponsor Token","IslaCoin","Kcash","Litecoin","BoutsPro","Valorem","Artcoin","Insureum","Zen Protocol","CryptoPennies","MediBond","WishFinance","BlockNet","Big Data Block","Loom Network","Gamedex","Universal Recognition Token","Soarcoin","IZX","aelf","Bitcoin Private","TerraCoin","Coinnec","Revolution VR","SecureCoin","Swarm Fund","Fan360","LakeBanker","16BitCoin","CyberTrust","Actinium","ZestCoin","Monero 0","DACash","Bitswift","Bethereum","Hedge Token","Megastake","HeelCoin","RealChain","LiteBitcoin","SexCoin","Suretly","Internet of People","Rate3","Invictus","WorldPay","iOlite","Cashaa","NovaCoin","U Network","Gimli","Chynge.net","BMChain","Ethereum Premium","BlackShadowCoin","Javvy","Befund","Bancor Network Token","BOONSCoin","Pantos","IDEX Membership","BitStation","Lynx","Encrybit","HighVibe.Network","Credo","HiCoin","RiptideCoin","BitBoss","NXTTY","Presale Ventures","Urbit Data","Xeonbit","Newbium","Mint","Crypto Wine Exchange","HARA","Pioneer Coin","Ethereum Dark","FazzCoin","Fitrova","The EFFECT Network","CargoX","PolicyPal Network","Vechain","President Clinton","GenesysCoin","Trivver","Bitdeal","ShareMeAll","Primas","RoboAdvisorCoin","Liquid","Napoleon X","NOKU Master token","Liqnet","ZeroState","0chain","DarkCash","Sudan Gold Coin","PokerSports","Bankera","Think And Get Rich Coin","ELTCOIN","USOAMIC","XDNA","Autoria","Cosmo","Bigbom","EagsCoin","Stakinglab","SaffronCoin","BnrtxCoin","FoodCoin","PutinCoin","SID Token","Quartz","IOU1","Spend","NEO Gold","High Voltage Coin","Unobtanium","Sandcoin","FrazCoin","Sudan Gold Coin","VeriCoin","Aurora","NANJCOIN","Muse","DuckDuckCoin","Saifu","CryCash","Rustbits","BitFlip","Cpollo","Monkey Project","Coin Analyst","Scanetchain Token","Aditus","LendConnect","FOREXCOIN","Crypto Improvement Fund","Electra","Psilocybin","GameLeagueCoin","Switcheo","BitQuark","BinaryCoin","CyberVein","KATZcoin","BtcEX","ByteCoin","Shping Coin","Liquid","Stacktical","Tezos","ParkByte","Imbrex","Pinmo","Sigil","LePenCoin","OmiseGO Classic","Digix DAO","ShareRing","CryptoCarbon","CDX Network","SONM","PhoenixCoin","Incent","Tokyo Coin","Premium","Unattanium","Biotron","WETH","Rublebit","KRCoin","Aegis","Legends Cryptocurrency","VARcrypt","Witcoin","GlowShares","HOQU","VARcrypt","Linx","BlackholeCoin","NumbersCoin","ZayedCoin","CarVertical","Securosys","ElliotCoin","Zelcash","AcesCoin","EtherInc"]',
)
export default {
title: 'Autocomplete',
component: Autocomplete,
argTypes: {
size: {
type: {
name: 'enum',
value: ['medium', 'large'],
},
defaultValue: 'large',
},
},
} as Meta
export const Root: Story<AutocompleteProps> = (args) => (
<Autocomplete {...args}>Autocomplete</Autocomplete>
)
Root.args = {
size: 'large',
disabled: false,
withIcon: false,
error: false,
placeholder: 'Placeholder',
onChange: undefined,
options: list,
}

View File

@ -0,0 +1,83 @@
import { css } from '@emotion/react'
import { autocompleteClasses } from './Autocomplete.classes'
export const AutocompleteStyles = css`
.${autocompleteClasses.root} {
width: auto;
border: 1px solid rgb(var(--lsd-border-primary));
box-sizing: border-box;
align-items: center;
}
.${autocompleteClasses.root} > div {
display: flex;
justify-content: space-between;
}
.${autocompleteClasses.disabled} {
opacity: 0.34;
}
.${autocompleteClasses.input} {
border: none;
outline: none;
font-size: 14px;
color: rgb(var(--lsd-text-primary));
background: none;
width: 100%;
}
.${autocompleteClasses.input}:hover {
outline: none;
}
.${autocompleteClasses.input}::placeholder {
color: rgb(var(--lsd-text-primary));
opacity: 0.3;
}
.${autocompleteClasses.error} {
text-decoration: line-through;
}
.${autocompleteClasses.large} {
width: 208px;
height: 40px;
padding: 10px 14px;
}
.${autocompleteClasses.medium} {
width: 188px;
height: 32px;
padding: 6px 12px;
}
.${autocompleteClasses.withIcon} {
}
.${autocompleteClasses.icon} {
cursor: pointer;
display: flex;
align-items: center;
}
.${autocompleteClasses.listBox} {
max-height: 400px;
overflow: auto;
border: 1px solid rgb(var(--lsd-border-primary));
border-top: 0;
}
.${autocompleteClasses.dropdownItem} {
border: 0;
&:not(:last-child) {
border-bottom: 1px solid rgb(var(--lsd-border-primary));
}
}
.${autocompleteClasses.dropdownItemPlaceholder} {
opacity: 0.5;
white-space: pre;
}
`

View File

@ -0,0 +1,150 @@
import clsx from 'clsx'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useInput } from '../../utils/useInput'
import { DropdownItem } from '../DropdownItem'
import { CloseIcon, SearchIcon } from '../Icons'
import { ListBox } from '../ListBox'
import { Portal } from '../PortalProvider/Portal'
import { autocompleteClasses } from './Autocomplete.classes'
export type AutocompleteProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'onChange' | 'value'
> &
Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
size?: 'large' | 'medium'
withIcon?: boolean
error?: boolean
disabled?: boolean
placeholder?: string
value?: string
defaultValue?: string
options?: string[]
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}
export const Autocomplete: React.FC<AutocompleteProps> & {
classes: typeof autocompleteClasses
} = ({
size = 'large',
withIcon = false,
error = false,
disabled = false,
children,
value,
defaultValue,
placeholder,
onChange,
options = [],
inputProps = {},
...props
}) => {
const ref = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const input = useInput({ defaultValue, value, onChange, ref })
const inputValue = input.value as string
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<string>()
const onCancel = () => input.setValue('')
const handleDropdownClick = (value: string) => {
setOpen(false)
setSelected(value)
input.setValue(value)
}
const suggestions = useMemo(
() =>
input.filled
? options
.filter((option) =>
new RegExp(`^${input.value}.+`, 'i').test(option),
)
.map((option) => [
option,
option.slice(0, inputValue.length),
option.slice(inputValue.length),
])
: options,
[input.value, options],
)
useEffect(() => {
!selected && input.filled && !open && setOpen(true)
}, [input.value, selected, open])
const isOpen = !disabled && open && suggestions.length > 0 && input.filled
return (
<div
ref={containerRef}
className={clsx(
props.className,
autocompleteClasses.root,
autocompleteClasses[size],
disabled && autocompleteClasses.disabled,
withIcon && autocompleteClasses.withIcon,
)}
{...props}
>
<div>
<input
{...inputProps}
ref={ref}
value={input.value}
placeholder={placeholder}
onChange={input.onChange}
disabled={disabled}
onFocus={() => setOpen(true)}
className={clsx(
inputProps.className,
autocompleteClasses.input,
error && autocompleteClasses.error,
)}
/>
{withIcon && input.value ? (
<span className={autocompleteClasses.icon} onClick={onCancel}>
<CloseIcon color="primary" />
</span>
) : withIcon && !input.value ? (
<span className={autocompleteClasses.icon}>
<SearchIcon color="primary" />
</span>
) : null}
</div>
<Portal id="autocomplete">
<ListBox
handleRef={containerRef}
open={isOpen}
onClose={() => setOpen(false)}
className={autocompleteClasses.listBox}
>
{suggestions.map((opt, idx: number) => (
<DropdownItem
key={idx}
size={size}
tabIndex={0}
label={
<>
{opt[1]}
<span className={autocompleteClasses.dropdownItemPlaceholder}>
{opt[2]}
</span>
</>
}
className={autocompleteClasses.dropdownItem}
onClick={() => handleDropdownClick(opt[0])}
onKeyDown={(e) =>
e.key === 'Enter' && handleDropdownClick(opt[0])
}
/>
))}
</ListBox>
</Portal>
</div>
)
}
Autocomplete.classes = autocompleteClasses

View File

@ -0,0 +1 @@
export * from './Autocomplete'

View File

@ -6,6 +6,8 @@ export const buttonClasses = {
large: `${LSD_NAMESPACE}--large`, large: `${LSD_NAMESPACE}--large`,
medium: `${LSD_NAMESPACE}--medium`, medium: `${LSD_NAMESPACE}--medium`,
small: `${LSD_NAMESPACE}--small`, small: `${LSD_NAMESPACE}--small`,
withIcon: `${LSD_NAMESPACE}--with-icon`,
text: `${LSD_NAMESPACE}-button__text`, text: `${LSD_NAMESPACE}-button__text`,
icon: `${LSD_NAMESPACE}-button__icon`,
} }

View File

@ -1,14 +1,42 @@
import { Meta, Story } from '@storybook/react' import { Meta, Story } from '@storybook/react'
import { useStorybookIconComponent } from '../../utils/storybook.utils'
import { Button, ButtonProps } from './Button' import { Button, ButtonProps } from './Button'
export default { export default {
title: 'Button', title: 'Button',
component: Button, component: Button,
argTypes: {
size: {
type: {
name: 'enum',
value: ['small', 'medium', 'large'],
},
defaultValue: 'large',
},
icon: {
type: {
name: 'enum',
value: useStorybookIconComponent.options,
},
},
},
} as Meta } as Meta
export const Root: Story<ButtonProps> = (args) => ( export const Root: Story<ButtonProps & { icon: string }> = ({
<Button {...args}>Button</Button> icon,
) ...args
}) => {
const IconComponent = useStorybookIconComponent(icon)
return (
<Button
{...args}
icon={IconComponent && <IconComponent color="primary"></IconComponent>}
>
Button
</Button>
)
}
Root.args = { Root.args = {
disabled: false, disabled: false,
} }

View File

@ -22,6 +22,7 @@ export const ButtonStyles = css`
} }
.${buttonClasses.medium} { .${buttonClasses.medium} {
padding: 6px, 24px;
} }
.${buttonClasses.small} { .${buttonClasses.small} {
@ -35,4 +36,33 @@ export const ButtonStyles = css`
} }
} }
} }
.${buttonClasses.withIcon} {
display: flex;
}
.${buttonClasses.icon} {
display: flex;
}
.${buttonClasses.large}.${buttonClasses.withIcon} {
padding: 10px 14px 10px 18px;
.${buttonClasses.icon} {
margin-left: 18px;
}
}
.${buttonClasses.medium}.${buttonClasses.withIcon} {
padding: 6px 12px 6px 14px;
.${buttonClasses.icon} {
margin-left: 14px;
}
}
.${buttonClasses.small}.${buttonClasses.withIcon} {
padding: 6px 10px 6px 12px;
.${buttonClasses.icon} {
margin-left: 10px;
}
}
` `

View File

@ -4,11 +4,12 @@ import { buttonClasses } from './Button.classes'
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & { export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
size?: 'large' | 'medium' | 'small' size?: 'large' | 'medium' | 'small'
icon: React.ReactNode
} }
export const Button: React.FC<ButtonProps> & { export const Button: React.FC<ButtonProps> & {
classes: typeof buttonClasses classes: typeof buttonClasses
} = ({ size = 'medium', children, ...props }) => { } = ({ size = 'medium', icon, children, ...props }) => {
return ( return (
<> <>
<button <button
@ -18,9 +19,11 @@ export const Button: React.FC<ButtonProps> & {
buttonClasses.root, buttonClasses.root,
buttonClasses[size], buttonClasses[size],
props.disabled && buttonClasses.disabled, props.disabled && buttonClasses.disabled,
icon && buttonClasses.withIcon,
)} )}
> >
<span className={buttonClasses.text}>{children}</span> <span className={buttonClasses.text}>{children}</span>
{icon && <span className={buttonClasses.icon}>{icon}</span>}
</button> </button>
</> </>
) )

View File

@ -1,24 +1,31 @@
import { Global, SerializedStyles } from '@emotion/react' import { Global, SerializedStyles } from '@emotion/react'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { ButtonStyles } from '../Button/Button.styles' import { AutocompleteStyles } from '../Autocomplete/Autocomplete.styles'
import { DropdownStyles } from '../Dropdown/Dropdown.styles'
import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles'
import { LsdIconStyles } from '../Icons/LsdIcon/LsdIcon.styles'
import { IconTagStyles } from '../IconTag/IconTag.styles'
import { ListBoxStyles } from '../ListBox/ListBox.styles'
import { TabItemStyles } from '../TabItem/TabItem.styles'
import { TabsStyles } from '../Tabs/Tabs.styles'
import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles' import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles'
import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles' import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles'
import { ButtonStyles } from '../Button/Button.styles'
import { CardStyles } from '../Card/Card.styles'
import { CardBodyStyles } from '../CardBody/CardBody.styles'
import { CardHeaderStyles } from '../CardHeader/CardHeader.styles'
import { CollapseStyles } from '../Collapse/Collapse.styles'
import { CollapseHeaderStyles } from '../CollapseHeader/CollapseHeader.styles'
import { DropdownStyles } from '../Dropdown/Dropdown.styles'
import { DropdownItemStyles } from '../DropdownItem/DropdownItem.styles'
import { IconButtonStyles } from '../IconButton/IconButton.styles'
import { LsdIconStyles } from '../Icons/LsdIcon/LsdIcon.styles'
import { ListBoxStyles } from '../ListBox/ListBox.styles'
import { QuoteStyles } from '../Quote/Quote.styles'
import { TabItemStyles } from '../TabItem/TabItem.styles'
import { TabsStyles } from '../Tabs/Tabs.styles'
import { TagStyles } from '../Tag/Tag.styles'
import { TextFieldStyles } from '../TextField/TextField.styles'
import { defaultThemes, Theme, withTheme } from '../Theme' import { defaultThemes, Theme, withTheme } from '../Theme'
import { TypographyStyles } from '../Typography/Typography.styles' import { TypographyStyles } from '../Typography/Typography.styles'
import { CardStyles } from '../Card/Card.styles'
import { CardHeaderStyles } from '../CardHeader/CardHeader.styles'
import { CardBodyStyles } from '../CardBody/CardBody.styles'
const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> = const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
[ [
ButtonStyles, ButtonStyles,
IconButtonStyles,
TypographyStyles, TypographyStyles,
LsdIconStyles, LsdIconStyles,
TabItemStyles, TabItemStyles,
@ -26,12 +33,17 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
ListBoxStyles, ListBoxStyles,
DropdownStyles, DropdownStyles,
DropdownItemStyles, DropdownItemStyles,
IconTagStyles,
BreadcrumbStyles, BreadcrumbStyles,
BreadcrumbItemStyles, BreadcrumbItemStyles,
CardStyles, CardStyles,
CardHeaderStyles, CardHeaderStyles,
CardBodyStyles, CardBodyStyles,
TagStyles,
TextFieldStyles,
AutocompleteStyles,
QuoteStyles,
CollapseStyles,
CollapseHeaderStyles,
] ]
export const CSSBaseline: React.FC<{ theme?: Theme }> = ({ export const CSSBaseline: React.FC<{ theme?: Theme }> = ({

View File

@ -0,0 +1,8 @@
export const collapseClasses = {
root: `lsd-collapse`,
content: `lsd-collapse-content`,
open: 'lsd-collapse--open-item',
disabled: 'lsd-collapse--disabled',
}

View File

@ -0,0 +1,34 @@
import { Meta, Story } from '@storybook/react'
import { Typography } from '../Typography'
import { Collapse, CollapseProps } from './Collapse'
export default {
title: 'Collapse',
component: Collapse,
argTypes: {
size: {
type: {
name: 'enum',
value: ['small', 'medium', 'large'],
},
},
},
} as Meta
export const Root: Story<CollapseProps> = (args) => (
<div style={{ width: 'fit-content' }}>
<Collapse {...args}>
<div style={{ padding: '10px 18px' }}>
<Typography color="primary" component="label">
Slot component
</Typography>
</div>
</Collapse>
</div>
)
Root.args = {
size: 'large',
label: 'Title',
disabled: false,
}

View File

@ -0,0 +1,20 @@
import { css } from '@emotion/react'
import { collapseClasses } from './Collapse.classes'
export const CollapseStyles = css`
.${collapseClasses.root} {
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.${collapseClasses.open} {
.${collapseClasses.content} {
border-top: 1px solid transparent;
}
}
.${collapseClasses.content} {
border: 1px solid rgb(var(--lsd-border-primary));
}
`

View File

@ -0,0 +1,58 @@
import clsx from 'clsx'
import React, { useEffect, useRef, useState } from 'react'
import { CollapseHeader } from '../CollapseHeader'
import { collapseClasses } from './Collapse.classes'
export type CollapseProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'label'
> & {
label: string
disabled?: boolean
size?: 'small' | 'medium' | 'large'
open?: boolean
onChange?: (open: boolean) => void
}
export const Collapse: React.FC<CollapseProps> & {
classes: typeof collapseClasses
} = ({ label, disabled = false, size = 'large', children, ...props }) => {
const ref = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(props.open ?? false)
const handleChange = (value: boolean) => {
if (typeof props.open === 'undefined') return setOpen(value)
props.onChange && props.onChange(value)
}
const onTrigger = () => !disabled && handleChange(!open)
useEffect(() => {
disabled && open && handleChange(false)
}, [disabled, open, handleChange])
return (
<div
{...props}
ref={ref}
className={clsx(
props.className,
collapseClasses.root,
disabled && collapseClasses.disabled,
open && collapseClasses.open,
)}
>
<CollapseHeader
label={label}
open={open}
setOpen={setOpen}
size={size}
onTrigger={onTrigger}
disabled={disabled}
/>
{open && <div className={collapseClasses.content}>{children}</div>}
</div>
)
}
Collapse.classes = collapseClasses

View File

@ -0,0 +1 @@
export * from './Collapse'

View File

@ -0,0 +1,16 @@
export const collapseHeaderClasses = {
root: `lsd-collapse-header`,
trigger: `lsd-collapse-header-trigger`,
triggerLabel: `lsd-collapse-header-trigger__label`,
triggerIcons: `lsd-collapse-header-trigger-icons`,
triggerIcon: `lsd-collapse-header-trigger-icons__icon`,
triggerMenuIcon: `lsd-collapse-header-trigger-icons__menu-icon`,
open: 'lsd-collapse-header--open',
disabled: 'lsd-collapse-header--disabled',
small: `lsd-collapse-header--small`,
medium: `lsd-collapse-header--medium`,
large: `lsd-collapse-header--large`,
}

View File

@ -0,0 +1,27 @@
import { Meta, Story } from '@storybook/react'
import { CollapseHeader, CollapseHeaderProps } from './CollapseHeader'
export default {
title: 'CollapseHeader',
component: CollapseHeader,
argTypes: {
size: {
type: {
name: 'enum',
value: ['small', 'medium', 'large'],
},
},
},
} as Meta
export const Root: Story<CollapseHeaderProps> = (args) => (
<div style={{ width: 'fit-content' }}>
<CollapseHeader {...args}></CollapseHeader>
</div>
)
Root.args = {
size: 'large',
label: 'title',
disabled: false,
}

View File

@ -0,0 +1,88 @@
import { css } from '@emotion/react'
import { collapseHeaderClasses } from './CollapseHeader.classes'
export const CollapseHeaderStyles = css`
.${collapseHeaderClasses.root} {
box-sizing: border-box;
}
.${collapseHeaderClasses.root}:not(.${collapseHeaderClasses.disabled}) {
.${collapseHeaderClasses.trigger} {
&:hover {
.${collapseHeaderClasses.triggerLabel} {
text-decoration: underline;
}
}
}
}
.${collapseHeaderClasses.trigger} {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 10px 14px 10px 18px;
cursor: pointer;
background: none;
border: 1px solid rgb(var(--lsd-border-primary));
&:focus {
outline: none;
}
}
.${collapseHeaderClasses.triggerLabel} {
cursor: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: auto;
}
.${collapseHeaderClasses.triggerIcons} {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.${collapseHeaderClasses.triggerIcon} {
margin-right: 8px;
}
.${collapseHeaderClasses.triggerMenuIcon} {
}
.${collapseHeaderClasses.disabled} {
.${collapseHeaderClasses.trigger} {
opacity: 0.34;
cursor: initial;
}
}
.${collapseHeaderClasses.large} {
.${collapseHeaderClasses.trigger} {
width: 299px;
height: 40px;
padding: 10px 18px;
}
}
.${collapseHeaderClasses.medium} {
.${collapseHeaderClasses.trigger} {
width: 270px;
height: 32px;
padding: 6px 14px;
}
}
.${collapseHeaderClasses.small} {
.${collapseHeaderClasses.trigger} {
width: 235px;
height: 28px;
padding: 6px 12px;
}
}
`

View File

@ -0,0 +1,71 @@
import clsx from 'clsx'
import React, { useEffect, useState } from 'react'
import { ArrowDownIcon, ArrowUpIcon } from '../Icons'
import { Typography } from '../Typography'
import { collapseHeaderClasses } from './CollapseHeader.classes'
export type CollapseHeaderProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'label' | 'disabled'
> & {
label: string
open: boolean
setOpen: React.Dispatch<React.SetStateAction<boolean>>
disabled?: boolean
onTrigger: () => void
size?: 'small' | 'medium' | 'large'
}
export const CollapseHeader: React.FC<CollapseHeaderProps> & {
classes: typeof collapseHeaderClasses
} = ({
label,
disabled = false,
open,
setOpen,
size = 'large',
onTrigger,
...props
}) => {
return (
<div
{...props}
className={clsx(
props.className,
collapseHeaderClasses.root,
collapseHeaderClasses[size],
disabled && collapseHeaderClasses.disabled,
open && collapseHeaderClasses.open,
)}
>
<button
className={clsx(collapseHeaderClasses.trigger)}
onClick={onTrigger}
>
<Typography
color="primary"
component="label"
variant={size === 'small' ? 'label2' : 'label1'}
className={collapseHeaderClasses.triggerLabel}
>
{label}
</Typography>
<div className={collapseHeaderClasses.triggerIcons}>
{open ? (
<ArrowUpIcon
color="primary"
className={collapseHeaderClasses.triggerMenuIcon}
/>
) : (
<ArrowDownIcon
color="primary"
className={collapseHeaderClasses.triggerMenuIcon}
/>
)}
</div>
</button>
</div>
)
}
CollapseHeader.classes = collapseHeaderClasses

View File

@ -0,0 +1 @@
export * from './CollapseHeader'

View File

@ -4,8 +4,11 @@ import { CheckboxFilledIcon, CheckboxIcon, LsdIconProps } from '../Icons'
import { Typography } from '../Typography' import { Typography } from '../Typography'
import { dropdownItemClasses } from './DropdownItem.classes' import { dropdownItemClasses } from './DropdownItem.classes'
export type DropdownItemProps = React.HTMLAttributes<HTMLDivElement> & { export type DropdownItemProps = Omit<
label: string React.HTMLAttributes<HTMLDivElement>,
'label'
> & {
label: React.ReactNode
selected?: boolean selected?: boolean
withIcon?: boolean withIcon?: boolean
disabled?: boolean disabled?: boolean
@ -32,7 +35,6 @@ export const DropdownItem: React.FC<DropdownItemProps> & {
<div <div
role="option" role="option"
aria-selected={selected ? 'true' : 'false'} aria-selected={selected ? 'true' : 'false'}
aria-label={label}
className={clsx( className={clsx(
className, className,
dropdownItemClasses.root, dropdownItemClasses.root,

View File

@ -0,0 +1,12 @@
export const iconButtonClasses = {
root: `lsd-icon-button`,
outlined: `lsd-icon-button--outlined`,
filled: `lsd-icon-button--filled`,
disabled: `lsd-icon-button--disabled`,
small: `lsd-icon-button--small`,
medium: `lsd-icon-button--medium`,
large: `lsd-icon-button--large`,
}

View File

@ -0,0 +1,40 @@
import { Meta, Story } from '@storybook/react'
import { useStorybookIconComponent } from '../../utils/storybook.utils'
import { IconButton, IconButtonProps } from './IconButton'
export default {
title: 'IconButton',
component: IconButton,
argTypes: {
size: {
type: {
name: 'enum',
value: ['small', 'medium', 'large'],
},
defaultValue: 'large',
},
icon: {
type: {
name: 'enum',
value: useStorybookIconComponent.options,
},
defaultValue: 'FolderIcon',
},
},
} as Meta
export const Root: Story<IconButtonProps & { icon: string }> = ({
icon,
...args
}) => {
const Icon = useStorybookIconComponent(icon)
return <IconButton {...args}>{Icon && <Icon color="primary" />}</IconButton>
}
Root.args = {
variant: 'outlined',
icon: 'FolderIcon',
size: 'large',
disabled: false,
}

View File

@ -0,0 +1,47 @@
import { css } from '@emotion/react'
import { iconButtonClasses } from './IconButton.classes'
export const IconButtonStyles = css`
.${iconButtonClasses.root} {
display: flex;
width: 32px;
height: 28px;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
background: none;
border: 1px solid rgb(var(--lsd-border-primary));
}
.${iconButtonClasses.filled} {
background-color: rgb(var(--lsd-icon-primary));
svg {
--lsd-icon-primary: var(--lsd-icon-secondary);
}
}
.${iconButtonClasses.outlined} {
}
.${iconButtonClasses.disabled} {
opacity: 0.34;
cursor: default;
}
.${iconButtonClasses.large} {
width: 40px;
height: 40px;
}
.${iconButtonClasses.medium} {
width: 32px;
height: 32px;
}
.${iconButtonClasses.small} {
width: 28px;
height: 28px;
}
`

View File

@ -0,0 +1,35 @@
import clsx from 'clsx'
import React from 'react'
import { iconButtonClasses } from './IconButton.classes'
export type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'outlined' | 'filled'
size?: 'small' | 'medium' | 'large'
}
export const IconButton: React.FC<IconButtonProps> & {
classes: typeof iconButtonClasses
} = ({
size = 'large',
disabled,
variant = 'outlined',
children,
...props
}) => {
return (
<button
{...props}
className={clsx(
props.className,
iconButtonClasses.root,
iconButtonClasses[size],
iconButtonClasses[variant],
disabled && iconButtonClasses.disabled,
)}
>
{children}
</button>
)
}
IconButton.classes = iconButtonClasses

View File

@ -0,0 +1 @@
export * from './IconButton'

View File

@ -1,6 +0,0 @@
export const iconTagClasses = {
root: `lsd-icon-tag`,
outlined: `lsd-icon-tag--outlined`,
filled: `lsd-icon-tag--filled`,
}

View File

@ -1,18 +0,0 @@
import { Meta, Story } from '@storybook/react'
import { AddIcon, FolderIcon, KeyboardReturnIcon, MenuIcon } from '../Icons'
import { IconTag, IconTagProps } from './IconTag'
export default {
title: 'IconTag',
component: IconTag,
} as Meta
export const Root: Story<IconTagProps> = (args) => (
<IconTag {...args}>
<FolderIcon color="primary" />
</IconTag>
)
Root.args = {
variant: 'outlined',
}

View File

@ -1,25 +0,0 @@
import { css } from '@emotion/react'
import { iconTagClasses } from './IconTag.classes'
export const IconTagStyles = css`
.${iconTagClasses.root} {
display: flex;
width: 32px;
height: 28px;
flex-direction: row;
align-items: center;
justify-content: center;
border: 1px solid rgb(var(--lsd-icon-primary));
}
.${iconTagClasses.filled} {
background-color: rgb(var(--lsd-icon-primary));
svg {
--lsd-icon-primary: var(--lsd-icon-secondary);
}
}
.${iconTagClasses.outlined} {
}
`

View File

@ -1,28 +0,0 @@
import clsx from 'clsx'
import React from 'react'
import { iconTagClasses } from './IconTag.classes'
export type IconTagProps = React.HTMLAttributes<HTMLDivElement> & {
label?: string
variant?: 'outlined' | 'filled'
}
export const IconTag: React.FC<IconTagProps> & {
classes: typeof iconTagClasses
} = ({ label, variant = 'outlined', children, ...props }) => {
return (
<div
{...props}
aria-label={label}
className={clsx(
props.className,
iconTagClasses.root,
iconTagClasses[variant],
)}
>
{children}
</div>
)
}
IconTag.classes = iconTagClasses

View File

@ -1 +0,0 @@
export * from './IconTag'

View File

@ -1,17 +1,22 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const ArrowForwardIcon = LsdIcon((props) => ( export const ArrowForwardIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
d="M6.99992 2.33334L6.17742 3.15584L9.43242 6.41668H2.33325V7.58334H9.43242L6.17742 10.8442L6.99992 11.6667L11.6666 7.00001L6.99992 2.33334Z" <path
fill="black" d="M6.99992 2.33334L6.17742 3.15584L9.43242 6.41668H2.33325V7.58334H9.43242L6.17742 10.8442L6.99992 11.6667L11.6666 7.00001L6.99992 2.33334Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -1,17 +1,22 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const CheckIcon = LsdIcon((props) => ( export const CheckIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
d="M11.0833 2.91667V11.0833H2.91667V2.91667H11.0833ZM11.0833 1.75H2.91667C2.275 1.75 1.75 2.275 1.75 2.91667V11.0833C1.75 11.725 2.275 12.25 2.91667 12.25H11.0833C11.725 12.25 12.25 11.725 12.25 11.0833V2.91667C12.25 2.275 11.725 1.75 11.0833 1.75Z" <path
fill="black" d="M5.25009 9.43247L2.81759 6.99997L1.98926 7.82247L5.25009 11.0833L12.2501 4.0833L11.4276 3.2608L5.25009 9.43247Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -1,17 +1,22 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const CloseIcon = LsdIcon((props) => ( export const CloseIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
d="M11.0834 3.73916L10.2609 2.91666L7.00008 6.17749L3.73925 2.91666L2.91675 3.73916L6.17758 6.99999L2.91675 10.2608L3.73925 11.0833L7.00008 7.82249L10.2609 11.0833L11.0834 10.2608L7.82258 6.99999L11.0834 3.73916Z" <path
fill="black" d="M11.0834 3.73916L10.2609 2.91666L7.00008 6.17749L3.73925 2.91666L2.91675 3.73916L6.17758 6.99999L2.91675 10.2608L3.73925 11.0833L7.00008 7.82249L10.2609 11.0833L11.0834 10.2608L7.82258 6.99999L11.0834 3.73916Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -1,19 +1,24 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const MoreIcon = LsdIcon((props) => ( export const MoreIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
fillRule="evenodd" <path
clipRule="evenodd" fillRule="evenodd"
d="M3.49992 5.83334C2.85825 5.83334 2.33325 6.35834 2.33325 7.00001C2.33325 7.64168 2.85825 8.16668 3.49992 8.16668C4.14159 8.16668 4.66659 7.64168 4.66659 7.00001C4.66659 6.35834 4.14159 5.83334 3.49992 5.83334ZM10.4999 5.83334C9.85825 5.83334 9.33325 6.35834 9.33325 7.00001C9.33325 7.64168 9.85825 8.16668 10.4999 8.16668C11.1416 8.16668 11.6666 7.64168 11.6666 7.00001C11.6666 6.35834 11.1416 5.83334 10.4999 5.83334ZM5.83325 7.00001C5.83325 6.35834 6.35825 5.83334 6.99992 5.83334C7.64159 5.83334 8.16659 6.35834 8.16659 7.00001C8.16659 7.64168 7.64159 8.16668 6.99992 8.16668C6.35825 8.16668 5.83325 7.64168 5.83325 7.00001Z" clipRule="evenodd"
fill="black" d="M3.49992 5.83334C2.85825 5.83334 2.33325 6.35834 2.33325 7.00001C2.33325 7.64168 2.85825 8.16668 3.49992 8.16668C4.14159 8.16668 4.66659 7.64168 4.66659 7.00001C4.66659 6.35834 4.14159 5.83334 3.49992 5.83334ZM10.4999 5.83334C9.85825 5.83334 9.33325 6.35834 9.33325 7.00001C9.33325 7.64168 9.85825 8.16668 10.4999 8.16668C11.1416 8.16668 11.6666 7.64168 11.6666 7.00001C11.6666 6.35834 11.1416 5.83334 10.4999 5.83334ZM5.83325 7.00001C5.83325 6.35834 6.35825 5.83334 6.99992 5.83334C7.64159 5.83334 8.16659 6.35834 8.16659 7.00001C8.16659 7.64168 7.64159 8.16668 6.99992 8.16668C6.35825 8.16668 5.83325 7.64168 5.83325 7.00001Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -1,19 +1,24 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const NewPageIcon = LsdIcon((props) => ( export const NewPageIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
fillRule="evenodd" <path
clipRule="evenodd" fillRule="evenodd"
d="M9.79293 3.5H3.00004V2.5H11.5V11H10.5V4.20711L3.35359 11.3536L2.64648 10.6464L9.79293 3.5Z" clipRule="evenodd"
fill="black" d="M9.79293 3.5H3.00004V2.5H11.5V11H10.5V4.20711L3.35359 11.3536L2.64648 10.6464L9.79293 3.5Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -0,0 +1,22 @@
import { LsdIcon } from '../LsdIcon'
export const PickIcon = LsdIcon(
(props) => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.79287 3.5H2.99998V2.5H11.5V11H10.5V4.20711L3.35353 11.3536L2.64642 10.6464L9.79287 3.5Z"
fill="black"
/>
</svg>
),
{ filled: true },
)

View File

@ -0,0 +1 @@
export * from './PickIcon'

View File

@ -1,19 +1,24 @@
import { LsdIcon } from '../LsdIcon' import { LsdIcon } from '../LsdIcon'
export const SearchIcon = LsdIcon((props) => ( export const SearchIcon = LsdIcon(
<svg (props) => (
width="14" <svg
height="14" width="14"
viewBox="0 0 14 14" height="14"
fill="none" viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg" fill="none"
{...props} xmlns="http://www.w3.org/2000/svg"
> {...props}
<path >
fillRule="evenodd" <path
clipRule="evenodd" fillRule="evenodd"
d="M8.61 7.74083L11.9525 11.0833L11.0833 11.9525L7.74083 8.61C7.11667 9.05917 6.36417 9.33333 5.54167 9.33333C3.4475 9.33333 1.75 7.63583 1.75 5.54167C1.75 3.4475 3.4475 1.75 5.54167 1.75C7.63583 1.75 9.33333 3.4475 9.33333 5.54167C9.33333 6.36417 9.05917 7.11667 8.61 7.74083ZM5.54167 2.91667C4.08917 2.91667 2.91667 4.08917 2.91667 5.54167C2.91667 6.99417 4.08917 8.16667 5.54167 8.16667C6.99417 8.16667 8.16667 6.99417 8.16667 5.54167C8.16667 4.08917 6.99417 2.91667 5.54167 2.91667Z" clipRule="evenodd"
fill="black" d="M8.61 7.74083L11.9525 11.0833L11.0833 11.9525L7.74083 8.61C7.11667 9.05917 6.36417 9.33333 5.54167 9.33333C3.4475 9.33333 1.75 7.63583 1.75 5.54167C1.75 3.4475 3.4475 1.75 5.54167 1.75C7.63583 1.75 9.33333 3.4475 9.33333 5.54167C9.33333 6.36417 9.05917 7.11667 8.61 7.74083ZM5.54167 2.91667C4.08917 2.91667 2.91667 4.08917 2.91667 5.54167C2.91667 6.99417 4.08917 8.16667 5.54167 8.16667C6.99417 8.16667 8.16667 6.99417 8.16667 5.54167C8.16667 4.08917 6.99417 2.91667 5.54167 2.91667Z"
/> fill="black"
</svg> />
)) </svg>
),
{
filled: true,
},
)

View File

@ -16,3 +16,4 @@ export * from './NavigateBeforeIcon'
export * from './NavigateNextIcon' export * from './NavigateNextIcon'
export * from './NewPageIcon' export * from './NewPageIcon'
export * from './SearchIcon' export * from './SearchIcon'
export * from './PickIcon'

View File

@ -0,0 +1,8 @@
export const quoteClasses = {
root: `lsd-quote`,
indentedInline: 'lsd-quote--indented-inline',
parentheses: 'lsd-quote--parentheses',
text: 'lsd-quote__text',
}

View File

@ -0,0 +1,29 @@
import { Meta, Story } from '@storybook/react'
import { Quote, QuoteProps } from './Quote'
export default {
title: 'Quote',
component: Quote,
argTypes: {
mode: {
type: {
name: 'enum',
value: ['indented-line', 'parentheses'],
},
},
},
} as Meta
export const Root: Story<QuoteProps> = (args) => (
<div style={{ maxWidth: '600px' }}>
<Quote {...args}>
<div>
A wise man can learn more from a foolish question than a fool can learn
from a wise answer.
</div>
</Quote>
</div>
)
Root.args = {
mode: 'indented-line',
}

View File

@ -0,0 +1,27 @@
import { css } from '@emotion/react'
import { quoteClasses } from './Quote.classes'
export const QuoteStyles = css`
.${quoteClasses.root} {
color: rgb(var(--lsd-text-primary));
white-space: pre-wrap;
}
.${quoteClasses.indentedInline} {
border-left: 1px solid rgb(var(--lsd-border-primary));
padding: 4px 8px 4px 28px;
}
.${quoteClasses.parentheses} {
padding: 0px;
text-align: center;
}
.${quoteClasses.parentheses}::before {
content: '***';
}
.${quoteClasses.parentheses}::after {
content: '***';
}
`

View File

@ -0,0 +1,33 @@
import clsx from 'clsx'
import React from 'react'
import { Typography } from '../Typography'
import { quoteClasses } from './Quote.classes'
export type QuoteProps = React.HTMLAttributes<HTMLDivElement> & {
mode?: 'indented-line' | 'parentheses'
}
export const Quote: React.FC<QuoteProps> & {
classes: typeof quoteClasses
} = ({ mode = 'indented-line', children, ...props }) => {
return (
<>
<div
{...props}
className={clsx(
props.className,
quoteClasses.root,
mode && mode === 'parentheses'
? quoteClasses.parentheses
: quoteClasses.indentedInline,
)}
>
<Typography color="primary" component="label" variant={'label1'}>
{children}
</Typography>
</div>
</>
)
}
Quote.classes = quoteClasses

View File

@ -0,0 +1 @@
export * from './Quote'

View File

@ -0,0 +1,7 @@
export const tagClasses = {
root: `lsd-tag`,
outlined: `lsd-tag--outlined`,
filled: `lsd-tag--filled`,
disabled: 'lsd-tag--disabled',
}

View File

@ -0,0 +1,39 @@
import { Meta, Story } from '@storybook/react'
import { FolderIcon } from '../Icons'
import { Tag, TagProps } from './Tag'
export default {
title: 'Tag',
component: Tag,
argTypes: {
variant: {
type: {
name: 'enum',
value: ['outlined', 'filled'],
},
},
iconDirection: {
type: {
name: 'enum',
value: ['left', 'right', 'none'],
},
},
disabled: {
type: {
name: 'boolean',
value: [true, false],
},
},
},
} as Meta
export const Root: Story<TagProps> = (args) => (
<Tag {...args} icon={<FolderIcon color="primary" />} />
)
Root.args = {
variant: 'outlined',
label: 'Tag',
iconDirection: 'left',
disabled: false,
}

View File

@ -0,0 +1,42 @@
import { css } from '@emotion/react'
import { tagClasses } from './Tag.classes'
export const TagStyles = css`
.${tagClasses.root} {
width: fit-content;
height: 28px;
padding: 4px 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 12px;
border: 1px solid rgb(var(--lsd-icon-primary));
&:hover,
&:focus {
text-decoration: underline;
cursor: pointer;
}
}
.${tagClasses.filled} {
background-color: rgb(var(--lsd-icon-primary));
color: rgb(var(--lsd-text-secondary));
svg {
--lsd-icon-primary: var(--lsd-icon-secondary);
}
}
.${tagClasses.outlined} {
color: rgb(var(--lsd-text-primary));
}
.${tagClasses.disabled} {
opacity: 0.3;
cursor: initial;
pointer-events: none;
}
`

View File

@ -0,0 +1,49 @@
import clsx from 'clsx'
import React from 'react'
import { Typography } from '../Typography'
import { tagClasses } from './Tag.classes'
export type TagProps = React.HTMLAttributes<HTMLDivElement> & {
variant?: 'outlined' | 'filled'
label: string
icon?: React.ReactNode
iconDirection?: 'left' | 'right'
disabled?: boolean
}
export const Tag: React.FC<TagProps> & {
classes: typeof tagClasses
} = ({
label,
variant = 'outlined',
disabled = 'false',
icon,
iconDirection = 'left',
children,
...props
}) => {
const renderItems = () => (
<>
{iconDirection === 'left' && icon}
<Typography className={tagClasses[variant]}>{label}</Typography>
{iconDirection === 'right' && icon}
</>
)
return (
<div
aria-label={label}
{...props}
className={clsx(
props.className,
tagClasses.root,
tagClasses[variant],
disabled && tagClasses.disabled,
)}
>
{renderItems()}
</div>
)
}
Tag.classes = tagClasses

View File

@ -0,0 +1 @@
export * from './Tag'

View File

@ -0,0 +1,15 @@
export const textFieldClasses = {
root: `lsd-textField`,
input: `lsd-textField__input`,
icon: `lsd-textField__icon`,
supportingText: 'lsd-textField__supporting-text',
disabled: `lsd-textField--disabled`,
error: 'lsd-textField--error',
large: `lsd-textField--large`,
medium: `lsd-textField--medium`,
withIcon: `lsd-textField--with-icon`,
}

View File

@ -0,0 +1,30 @@
import { Meta, Story } from '@storybook/react'
import { TextField, TextFieldProps } from './TextField'
export default {
title: 'TextField',
component: TextField,
argTypes: {
size: {
type: {
name: 'enum',
value: ['medium', 'large'],
},
defaultValue: 'large',
},
},
} as Meta
export const Root: Story<TextFieldProps> = (args) => (
<TextField {...args}>TextField</TextField>
)
Root.args = {
size: 'large',
supportingText: 'Supporting text',
disabled: false,
withIcon: false,
error: false,
placeholder: 'Placeholder',
defaultValue: 'default value',
onChange: undefined,
}

View File

@ -0,0 +1,68 @@
import { css } from '@emotion/react'
import { textFieldClasses } from './TextField.classes'
export const TextFieldStyles = css`
.${textFieldClasses.root} {
width: auto;
border-bottom: 1px solid rgb(var(--lsd-border-primary));
box-sizing: border-box;
align-items: center;
}
.${textFieldClasses.root} > div {
display: flex;
justify-content: space-between;
}
.${textFieldClasses.disabled} {
opacity: 0.34;
}
.${textFieldClasses.input} {
border: none;
outline: none;
font-size: 14px;
color: rgb(var(--lsd-text-primary));
background: none;
width: 100%;
}
.${textFieldClasses.input}:hover {
outline: none;
}
.${textFieldClasses.input}::placeholder {
color: rgb(var(--lsd-text-primary));
opacity: 0.3;
}
.${textFieldClasses.error} {
text-decoration: line-through;
}
.${textFieldClasses.supportingText} {
width: fit-content;
margin-top: 20px;
}
.${textFieldClasses.large} {
width: 208px;
height: 40px;
padding: 10px 14px;
}
.${textFieldClasses.medium} {
width: 188px;
height: 32px;
padding: 6px 12px;
}
.${textFieldClasses.withIcon} {
}
.${textFieldClasses.icon} {
cursor: pointer;
display: flex;
align-items: center;
}
`

View File

@ -0,0 +1,96 @@
import clsx from 'clsx'
import React, { useRef } from 'react'
import { useInput } from '../../utils/useInput'
import { CheckIcon, CloseIcon, ErrorIcon } from '../Icons'
import { Typography } from '../Typography'
import { textFieldClasses } from './TextField.classes'
export type TextFieldProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
'onChange' | 'value'
> &
Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> & {
size?: 'large' | 'medium'
withIcon?: boolean
error?: boolean
disabled?: boolean
supportingText?: string
value?: string
defaultValue?: string
placeholder?: string
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}
export const TextField: React.FC<TextFieldProps> & {
classes: typeof textFieldClasses
} = ({
size = 'large',
withIcon = false,
supportingText,
error = false,
children,
value,
placeholder,
defaultValue,
onChange,
inputProps = {},
...props
}) => {
const ref = useRef<HTMLInputElement>(null)
const input = useInput({ defaultValue, value, onChange, ref })
const onCancel = () => input.setValue('')
return (
<div
className={clsx(
props.className,
textFieldClasses.root,
textFieldClasses[size],
props.disabled && textFieldClasses.disabled,
withIcon && textFieldClasses.withIcon,
)}
{...props}
>
<div>
<input
placeholder={placeholder}
{...inputProps}
ref={ref}
value={input.value}
onChange={input.onChange}
className={clsx(
inputProps.className,
textFieldClasses.input,
error && textFieldClasses.error,
)}
/>
{withIcon && error ? (
<span className={textFieldClasses.icon} onClick={onCancel}>
<ErrorIcon color="primary" className={textFieldClasses.icon} />
</span>
) : withIcon && !input.filled ? (
<span className={textFieldClasses.icon}>
<CheckIcon color="primary" />
</span>
) : withIcon && input.filled ? (
<span className={textFieldClasses.icon} onClick={onCancel}>
<CloseIcon color="primary" />
</span>
) : null}
</div>
{supportingText && (
<div className={clsx(textFieldClasses.supportingText)}>
<Typography
variant={size === 'large' ? 'label1' : 'label2'}
component="p"
>
{supportingText}
</Typography>
</div>
)}
</div>
)
}
TextField.classes = textFieldClasses

View File

@ -0,0 +1 @@
export * from './TextField'

View File

@ -1,14 +1,19 @@
export * from './components/Button' export * from './components/Autocomplete'
export * from './components/Dropdown'
export * from './components/DropdownItem'
export * from './components/Icons'
export * from './components/IconTag'
export * from './components/ListBox'
export * from './components/TabItem'
export * from './components/Tabs'
export * from './components/Theme'
export * from './components/Breadcrumb' export * from './components/Breadcrumb'
export * from './components/BreadcrumbItem' export * from './components/BreadcrumbItem'
export * from './components/Button'
export * from './components/Card' export * from './components/Card'
export * from './components/CardHeader'
export * from './components/CardBody' export * from './components/CardBody'
export * from './components/CardHeader'
export * from './components/Collapse'
export * from './components/CollapseHeader'
export * from './components/Dropdown'
export * from './components/DropdownItem'
export * from './components/IconButton'
export * from './components/Icons'
export * from './components/ListBox'
export * from './components/Quote'
export * from './components/TabItem'
export * from './components/Tabs'
export * from './components/Tag'
export * from './components/Theme'

View File

@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react'
export type InputValueType =
React.InputHTMLAttributes<HTMLInputElement>['value']
export type InputOnChangeType =
React.InputHTMLAttributes<HTMLInputElement>['onChange']
export type InputProps = {
value?: InputValueType
defaultValue?: InputValueType
onChange?: InputOnChangeType
ref?: React.RefObject<HTMLInputElement>
}
export const useInput = (props: InputProps) => {
const [value, setValue] = useState<InputValueType>(
props.value ?? props.defaultValue ?? '',
)
const uncontrolled = typeof props.value === 'undefined'
const filled =
typeof value === 'undefined'
? false
: typeof value === 'string'
? value.length > 0
: value.toString().length > 0
const onChange: InputOnChangeType = (event) => {
if (uncontrolled) return setValue(event.target.value)
props.onChange && props.onChange(event)
}
const setter = (value: InputValueType) => {
if (!props.ref?.current) return
const element = props.ref.current
const event = new Event('input', { bubbles: true })
Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set?.call?.(element, value)
element.dispatchEvent(event)
}
useEffect(() => {
!uncontrolled && setValue(props.value)
}, [uncontrolled, props.value])
return {
value,
filled,
onChange,
setValue: setter,
}
}