homework 1: components

This commit is contained in:
2023-07-29 12:47:02 +02:00
parent e0792c6b6f
commit 06afed2826
35 changed files with 1343 additions and 14 deletions

View File

@@ -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;
}

View 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%);
}

View 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',
},
},
}

View 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>
})

View File

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

View 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;
}

View 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)}
/>
)
},
}

View 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>
)
}

View File

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

View File

@@ -1 +1,4 @@
export * from './button'
export * from './card'
export * from './typography'
export * from './checkbox'

View File

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

View 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);
}
}

View 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',
},
}

View 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
}

View File

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

View 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);
}

View 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',
},
}

View 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} />
}