mirror of
https://github.com/ershisan99/flashcards-example-project.git
synced 2025-12-18 05:09:23 +00:00
homework 1: components
This commit is contained in:
@@ -1,19 +1,102 @@
|
||||
@mixin button {
|
||||
all: unset;
|
||||
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 0.625rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
padding: 0.375rem 1.75rem;
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
transition:
|
||||
var(--transition-duration-basic) background-color,
|
||||
var(--transition-duration-basic) color;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-info-700);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.fullWidth {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: red;
|
||||
@include button;
|
||||
|
||||
color: var(--color-light-100);
|
||||
background-color: var(--color-accent-500);
|
||||
box-shadow: 0 4px 18px rgb(140 97 255 / 35%);
|
||||
|
||||
&:hover:enabled {
|
||||
background-color: var(--color-accent-300);
|
||||
}
|
||||
|
||||
&:active:enabled {
|
||||
background-color: var(--color-accent-700);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: green;
|
||||
@include button;
|
||||
|
||||
color: var(--color-light-100);
|
||||
background-color: var(--color-dark-300);
|
||||
box-shadow: 0 2px 10px 0 #6d6d6d40;
|
||||
|
||||
&:hover:enabled {
|
||||
background-color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
&:active:enabled {
|
||||
background-color: var(--color-dark-500);
|
||||
}
|
||||
}
|
||||
|
||||
.tertiary {
|
||||
background-color: blue;
|
||||
@include button;
|
||||
|
||||
color: var(--color-accent-500);
|
||||
background-color: var(--color-dark-900);
|
||||
border: 1px solid var(--color-accent-700);
|
||||
|
||||
&:hover:enabled {
|
||||
background-color: var(--color-dark-500);
|
||||
}
|
||||
|
||||
&:active:enabled {
|
||||
background-color: var(--color-accent-900);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
background-color: yellow;
|
||||
}
|
||||
@include button;
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0;
|
||||
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-m);
|
||||
color: var(--color-accent-500);
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
7
src/components/ui/card/card.module.scss
Normal file
7
src/components/ui/card/card.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.root {
|
||||
background-color: var(--color-dark-700);
|
||||
border-radius: 2px;
|
||||
box-shadow:
|
||||
1px 1px 2px rgb(0 0 0 / 10%),
|
||||
-1px -1px 2px rgb(0 0 0 / 10%);
|
||||
}
|
||||
25
src/components/ui/card/card.stories.tsx
Normal file
25
src/components/ui/card/card.stories.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Card } from './'
|
||||
|
||||
import { Typography } from '@/components'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Card',
|
||||
component: Card,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Card>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: <Typography variant={'large'}>Card</Typography>,
|
||||
style: {
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
padding: '24px',
|
||||
},
|
||||
},
|
||||
}
|
||||
15
src/components/ui/card/card.tsx
Normal file
15
src/components/ui/card/card.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ComponentPropsWithoutRef, forwardRef } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './card.module.scss'
|
||||
|
||||
export type CardProps = {} & ComponentPropsWithoutRef<'div'>
|
||||
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...restProps }, ref) => {
|
||||
const classNames = {
|
||||
root: clsx(s.root, className),
|
||||
}
|
||||
|
||||
return <div ref={ref} className={classNames.root} {...restProps}></div>
|
||||
})
|
||||
1
src/components/ui/card/index.ts
Normal file
1
src/components/ui/card/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './card'
|
||||
80
src/components/ui/checkbox/checkbox.module.scss
Normal file
80
src/components/ui/checkbox/checkbox.module.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
color: var(--color-dark-100);
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
background-color: var(--color-dark-900);
|
||||
border: 2px solid var(--color-light-900);
|
||||
border-radius: 2px;
|
||||
|
||||
&:focus {
|
||||
background-color: var(--color-bg-focus);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: initial;
|
||||
border-color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
&[data-state='checked']:disabled {
|
||||
background-color: var(--color-checkbox-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
background-color: var(--color-dark-900);
|
||||
border-radius: 50%;
|
||||
|
||||
&.disabled {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover:not(.disabled),
|
||||
&:hover .root:not([data-state='checked']) {
|
||||
background-color: var(--color-dark-500);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
&.left {
|
||||
margin-left: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
35
src/components/ui/checkbox/checkbox.stories.tsx
Normal file
35
src/components/ui/checkbox/checkbox.stories.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Checkbox } from './checkbox'
|
||||
const meta = {
|
||||
title: 'Components/Checkbox',
|
||||
component: Checkbox,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Checkbox>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
export const Uncontrolled: Story = {
|
||||
args: {
|
||||
label: 'Click here',
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: args => {
|
||||
const [checked, setChecked] = useState(false)
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
{...args}
|
||||
label="Click here"
|
||||
checked={checked}
|
||||
onChange={() => setChecked(!checked)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}
|
||||
66
src/components/ui/checkbox/checkbox.tsx
Normal file
66
src/components/ui/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
import * as CheckboxRadix from '@radix-ui/react-checkbox'
|
||||
import * as LabelRadix from '@radix-ui/react-label'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './checkbox.module.scss'
|
||||
|
||||
import { Check } from '@/assets/icons'
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export type CheckboxProps = {
|
||||
className?: string
|
||||
checked?: boolean
|
||||
onChange?: (checked: boolean) => void
|
||||
disabled?: boolean
|
||||
required?: boolean
|
||||
label?: string
|
||||
id?: string
|
||||
position?: 'left'
|
||||
}
|
||||
|
||||
export const Checkbox: FC<CheckboxProps> = ({
|
||||
checked,
|
||||
onChange,
|
||||
position,
|
||||
disabled,
|
||||
required,
|
||||
label,
|
||||
id,
|
||||
className,
|
||||
}) => {
|
||||
const classNames = {
|
||||
container: clsx(s.container, className),
|
||||
buttonWrapper: clsx(s.buttonWrapper, disabled && s.disabled, position === 'left' && s.left),
|
||||
root: s.root,
|
||||
indicator: s.indicator,
|
||||
label: clsx(s.label, disabled && s.disabled),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.container}>
|
||||
<LabelRadix.Root asChild>
|
||||
<Typography variant="body2" className={classNames.label} as={'label'}>
|
||||
<div className={classNames.buttonWrapper}>
|
||||
<CheckboxRadix.Root
|
||||
className={classNames.root}
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
id={id}
|
||||
>
|
||||
{checked && (
|
||||
<CheckboxRadix.Indicator className={classNames.indicator} forceMount>
|
||||
<Check />
|
||||
</CheckboxRadix.Indicator>
|
||||
)}
|
||||
</CheckboxRadix.Root>
|
||||
</div>
|
||||
{label}
|
||||
</Typography>
|
||||
</LabelRadix.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1
src/components/ui/checkbox/index.ts
Normal file
1
src/components/ui/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './checkbox'
|
||||
@@ -1 +1,4 @@
|
||||
export * from './button'
|
||||
export * from './card'
|
||||
export * from './typography'
|
||||
export * from './checkbox'
|
||||
|
||||
1
src/components/ui/text-field/index.ts
Normal file
1
src/components/ui/text-field/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './text-field'
|
||||
71
src/components/ui/text-field/text-field.module.scss
Normal file
71
src/components/ui/text-field/text-field.module.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
.root {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fieldContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: var(--color-light-100);
|
||||
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-dark-300);
|
||||
outline: 0;
|
||||
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: var(--color-info-700);
|
||||
outline: 1px solid var(--color-info-700);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-dark-700);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--color-danger-300);
|
||||
border-color: var(--color-danger-300);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 1px;
|
||||
color: var(--color-dark-100);
|
||||
}
|
||||
|
||||
.showPassword {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
bottom: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
padding: 0;
|
||||
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
&:focus-visible {
|
||||
outline: var(--outline-focus);
|
||||
}
|
||||
}
|
||||
35
src/components/ui/text-field/text-field.stories.ts
Normal file
35
src/components/ui/text-field/text-field.stories.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { TextField } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/TextField',
|
||||
component: TextField,
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof TextField>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Label',
|
||||
placeholder: 'Placeholder',
|
||||
},
|
||||
}
|
||||
|
||||
export const Password: Story = {
|
||||
args: {
|
||||
label: 'Label',
|
||||
placeholder: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
label: 'Input with error',
|
||||
value: 'Wrong value',
|
||||
errorMessage: 'Error message',
|
||||
},
|
||||
}
|
||||
94
src/components/ui/text-field/text-field.tsx
Normal file
94
src/components/ui/text-field/text-field.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { ChangeEvent, ComponentProps, ComponentPropsWithoutRef, forwardRef, useState } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './text-field.module.scss'
|
||||
|
||||
import { VisibilityOff, Eye } from '@/assets'
|
||||
import { Typography } from '@/components'
|
||||
|
||||
export type TextFieldProps = {
|
||||
onValueChange?: (value: string) => void
|
||||
containerProps?: ComponentProps<'div'>
|
||||
labelProps?: ComponentProps<'label'>
|
||||
errorMessage?: string
|
||||
label?: string
|
||||
} & ComponentPropsWithoutRef<'input'>
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
errorMessage,
|
||||
placeholder,
|
||||
type,
|
||||
containerProps,
|
||||
labelProps,
|
||||
label,
|
||||
onChange,
|
||||
onValueChange,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const isShowPasswordButtonShown = type === 'password'
|
||||
|
||||
const finalType = getFinalType(type, showPassword)
|
||||
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
onChange?.(e)
|
||||
onValueChange?.(e.target.value)
|
||||
}
|
||||
|
||||
const classNames = {
|
||||
root: clsx(s.root, containerProps?.className),
|
||||
fieldContainer: clsx(s.fieldContainer),
|
||||
field: clsx(s.field, !!errorMessage && s.error, className),
|
||||
label: clsx(s.label, labelProps?.className),
|
||||
error: clsx(s.error),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.root}>
|
||||
{label && (
|
||||
<Typography variant="body2" as="label" className={classNames.label}>
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
<div className={classNames.fieldContainer}>
|
||||
<input
|
||||
className={classNames.field}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
type={finalType}
|
||||
onChange={handleChange}
|
||||
{...restProps}
|
||||
/>
|
||||
{isShowPasswordButtonShown && (
|
||||
<button
|
||||
className={s.showPassword}
|
||||
type={'button'}
|
||||
onClick={() => setShowPassword(prev => !prev)}
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Eye />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Typography variant="error" className={classNames.error}>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
function getFinalType(type: ComponentProps<'input'>['type'], showPassword: boolean) {
|
||||
if (type === 'password' && showPassword) {
|
||||
return 'text'
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
1
src/components/ui/typography/index.ts
Normal file
1
src/components/ui/typography/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './typography'
|
||||
72
src/components/ui/typography/typography.module.scss
Normal file
72
src/components/ui/typography/typography.module.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
@mixin typography($fontSize, $lineHeight, $fontWeight) {
|
||||
margin: 0;
|
||||
|
||||
font-size: $fontSize;
|
||||
font-weight: $fontWeight;
|
||||
line-height: $lineHeight;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.large {
|
||||
@include typography(var(--font-size-xxl), var(--line-height-l), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.h1 {
|
||||
@include typography(var(--font-size-xl), var(--line-height-l), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.h2 {
|
||||
@include typography(var(--font-size-l), var(--line-height-m), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.h3 {
|
||||
@include typography(var(--font-size-m), var(--line-height-m), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.body1 {
|
||||
@include typography(var(--font-size-m), var(--line-height-m), var(--font-weight-regular));
|
||||
}
|
||||
|
||||
.subtitle1 {
|
||||
@include typography(var(--font-size-m), var(--line-height-m), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.body2 {
|
||||
@include typography(var(--font-size-s), var(--line-height-m), var(--font-weight-regular));
|
||||
}
|
||||
|
||||
.subtitle2 {
|
||||
@include typography(var(--font-size-s), var(--line-height-m), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.overline {
|
||||
@include typography(var(--font-size-xs), var(--line-height-s), var(--font-weight-bold));
|
||||
}
|
||||
|
||||
.caption {
|
||||
@include typography(var(--font-size-xs), var(--line-height-s), var(--font-weight-regular));
|
||||
}
|
||||
|
||||
.link1,
|
||||
.link1:visited {
|
||||
@include typography(var(--font-size-s), var(--line-height-m), var(--font-weight-regular));
|
||||
|
||||
cursor: pointer;
|
||||
color: var(--color-text-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link2,
|
||||
.link2:visited {
|
||||
@include typography(var(--font-size-xs), var(--line-height-s), var(--font-weight-regular));
|
||||
|
||||
cursor: pointer;
|
||||
color: var(--color-text-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.error {
|
||||
@include typography(var(--font-size-xs), var(--line-height-m), var(--font-weight-regular));
|
||||
|
||||
color: var(--color-danger-300);
|
||||
}
|
||||
123
src/components/ui/typography/typography.stories.tsx
Normal file
123
src/components/ui/typography/typography.stories.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { Typography } from './'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Typography',
|
||||
component: Typography,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
options: [
|
||||
'large',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'body1',
|
||||
'body2',
|
||||
'subtitle1',
|
||||
'subtitle2',
|
||||
'caption',
|
||||
'overline',
|
||||
'link1',
|
||||
'link2',
|
||||
'error',
|
||||
],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Typography>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'large',
|
||||
},
|
||||
}
|
||||
|
||||
export const H1: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'h1',
|
||||
},
|
||||
}
|
||||
|
||||
export const H2: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'h2',
|
||||
},
|
||||
}
|
||||
|
||||
export const H3: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'h3',
|
||||
},
|
||||
}
|
||||
|
||||
export const Body1: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'body1',
|
||||
},
|
||||
}
|
||||
|
||||
export const Body2: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'body2',
|
||||
},
|
||||
}
|
||||
|
||||
export const Subtitle1: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'subtitle1',
|
||||
},
|
||||
}
|
||||
|
||||
export const Subtitle2: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'subtitle2',
|
||||
},
|
||||
}
|
||||
|
||||
export const Caption: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'caption',
|
||||
},
|
||||
}
|
||||
|
||||
export const Overline: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'overline',
|
||||
},
|
||||
}
|
||||
|
||||
export const Link1: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'link1',
|
||||
},
|
||||
}
|
||||
|
||||
export const Link2: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'link2',
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
children: 'Card content',
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
37
src/components/ui/typography/typography.tsx
Normal file
37
src/components/ui/typography/typography.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import s from './typography.module.scss'
|
||||
|
||||
export interface TextProps<T extends ElementType> {
|
||||
as?: T
|
||||
variant?:
|
||||
| 'large'
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'body1'
|
||||
| 'body2'
|
||||
| 'subtitle1'
|
||||
| 'subtitle2'
|
||||
| 'caption'
|
||||
| 'overline'
|
||||
| 'link1'
|
||||
| 'link2'
|
||||
| 'error'
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Typography<T extends ElementType = 'p'>({
|
||||
as,
|
||||
className,
|
||||
variant = 'body1',
|
||||
...restProps
|
||||
}: TextProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof TextProps<T>>) {
|
||||
const classNames = clsx(s.text, s[variant], className)
|
||||
const Component = as || 'p'
|
||||
|
||||
return <Component className={classNames} {...restProps} />
|
||||
}
|
||||
Reference in New Issue
Block a user