chore: input component (#84)
This commit is contained in:
parent
e81132a9b8
commit
4e9023ce3f
|
|
@ -1,13 +1,17 @@
|
|||
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
|
||||
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
|
||||
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
|
||||
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
|
||||
import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck';
|
||||
import { MetamaskIcon, EthereumIcon } from './custom';
|
||||
|
||||
export const IconLibrary = Object.freeze({
|
||||
back: IoArrowBackCircleSharp,
|
||||
check: AiOutlineCheck,
|
||||
ethereum: EthereumIcon,
|
||||
github: IoLogoGithub,
|
||||
info: IoInformationCircleSharp,
|
||||
upload: IoCloudUploadSharp,
|
||||
metamask: MetamaskIcon, //remove if not used
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './input';
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { Flex } from '../../layout';
|
||||
import { dripStitches } from '../../../theme';
|
||||
import { forwardRef, useRef, useState } from 'react';
|
||||
import { Icon } from '../icon';
|
||||
import { Form } from '../../../components/form/form';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
const BorderInput = styled('div', {
|
||||
borderStyle: 'solid',
|
||||
borderColor: '$gray7',
|
||||
width: '$22',
|
||||
height: '$22',
|
||||
transition: 'border-color 0.2s ease-in-out',
|
||||
borderWidth: '$default',
|
||||
borderRadius: '$lg',
|
||||
zIndex: '$docked',
|
||||
my: '$1h',
|
||||
|
||||
'&:hover': {
|
||||
borderColor: '$gray8',
|
||||
},
|
||||
});
|
||||
|
||||
const DEFAULT_MAX_FILE_SIZE = 10; // in KB
|
||||
|
||||
// The file size must be capped to a size that the contract can handle
|
||||
const validateFileSize = (
|
||||
file: File,
|
||||
maxSize = DEFAULT_MAX_FILE_SIZE
|
||||
): boolean => {
|
||||
return file.size <= 1024 * maxSize;
|
||||
};
|
||||
|
||||
type InputFileProps = {
|
||||
value: File | null;
|
||||
onChange: (file: File | null) => void;
|
||||
} & React.ComponentProps<typeof Flex>;
|
||||
|
||||
export const StyledInputFile = forwardRef<HTMLDivElement, InputFileProps>(
|
||||
({ value: file, onChange, css, ...props }, ref) => {
|
||||
const inputFileRef = useRef<HTMLInputElement>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
setErrorMessage(null);
|
||||
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
if (validateFileSize(e.target.files[0])) onChange(e.target.files[0]);
|
||||
else {
|
||||
onChange(null);
|
||||
setErrorMessage('File size is too big');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
css={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
...(css || {}),
|
||||
}}
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={() => inputFileRef.current?.click()}
|
||||
>
|
||||
{file ? (
|
||||
<img
|
||||
className="absolute w-14 h-14"
|
||||
src={URL.createObjectURL(file)}
|
||||
alt="logo"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="upload" size="md" css={{ position: 'absolute' }} />
|
||||
)}
|
||||
<BorderInput />
|
||||
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept={'.svg'}
|
||||
ref={inputFileRef}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</Flex>
|
||||
{errorMessage && <Form.Error>{errorMessage}</Form.Error>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { Input } from './';
|
||||
|
||||
export default {
|
||||
title: 'Components/Input',
|
||||
component: Input,
|
||||
};
|
||||
|
||||
export const Variants = () => {
|
||||
return (
|
||||
<>
|
||||
<Input size="sm" placeholder="Small" />
|
||||
<Input size="md" placeholder="Medium" required />
|
||||
<Input size="md" placeholder="Medium Invaild" aria-invalid />
|
||||
<Input size="lg" placeholder="Large" />
|
||||
<Input size="sm" placeholder="Small" disabled />
|
||||
<Input size="md" placeholder="Medium" disabled />
|
||||
<Input size="lg" placeholder="Large" disabled />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { dripStitches } from '../../../theme';
|
||||
import { StyledInputFile } from './input-file';
|
||||
const { styled } = dripStitches;
|
||||
|
||||
const styles = {
|
||||
all: 'unset',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
borderStyle: 'solid',
|
||||
minWidth: '$0',
|
||||
color: '$slate12',
|
||||
my: '$1h',
|
||||
|
||||
transition: 'border-color 0.2s ease-in-out',
|
||||
borderWidth: '$default',
|
||||
borderColor: '$slate7',
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': {
|
||||
borderColor: '$gray8',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
borderColor: '$blue9',
|
||||
},
|
||||
'&[aria-invalid=true], &[data-invalid]': {
|
||||
borderColor: '$red9',
|
||||
},
|
||||
'&:disabled': {
|
||||
color: '$slate8',
|
||||
borderColor: '$slate6',
|
||||
backgroundColor: '$slate2',
|
||||
'&::placeholder': {
|
||||
color: '$slate8',
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
borderRadius: '$md',
|
||||
fontSize: '$xs',
|
||||
lineHeight: '$4',
|
||||
p: '$1h',
|
||||
},
|
||||
md: {
|
||||
borderRadius: '$lg',
|
||||
fontSize: '$sm',
|
||||
p: '$3 $3h',
|
||||
},
|
||||
lg: {
|
||||
borderRadius: '$xl',
|
||||
fontSize: '$md',
|
||||
p: '$4 $5',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
},
|
||||
};
|
||||
|
||||
export const Input = styled('input', styles);
|
||||
|
||||
export const Textarea = styled('textarea', styles);
|
||||
|
||||
export const LogoFileInput = StyledInputFile;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { useState } from 'react';
|
||||
import { Form } from './form';
|
||||
|
||||
export default {
|
||||
title: 'Components/Form',
|
||||
component: Form,
|
||||
};
|
||||
|
||||
export const Fields = () => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
return (
|
||||
<>
|
||||
<Form.Field>
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Input placeholder="Input" />
|
||||
<Form.Error>Input error</Form.Error>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.Textarea placeholder="Textarea" />
|
||||
<Form.Error>Textarea error</Form.Error>
|
||||
</Form.Field>
|
||||
<Form.Field css={{ width: '$24' }}>
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.LogoFileInput value={file} onChange={(file) => setFile(file)} />
|
||||
</Form.Field>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { dripStitches } from '../../theme';
|
||||
import { Flex } from '../layout';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
export abstract class FormStyles {
|
||||
static readonly Field = styled(Flex, {
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
static readonly Label = styled('label', {
|
||||
color: '$slate11',
|
||||
|
||||
'&:disabled': {
|
||||
color: '$slate8',
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
sm: {
|
||||
fontSize: '$xs', //TODO check with royce font size
|
||||
},
|
||||
md: {
|
||||
fontSize: '$xs',
|
||||
},
|
||||
lg: {
|
||||
fontSize: '$md',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
},
|
||||
});
|
||||
|
||||
static readonly ErrorMessage = styled('span', {
|
||||
color: '$red11',
|
||||
fontSize: '0.625rem',
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
lg: {
|
||||
fontSize: '$sm',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { Input, LogoFileInput, Textarea } from '../core/input';
|
||||
import { FormStyles } from './form.styles';
|
||||
|
||||
export abstract class Form {
|
||||
static readonly Field = forwardRef<HTMLDivElement, Form.FieldProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
return (
|
||||
<FormStyles.Field ref={ref} {...props}>
|
||||
{children}
|
||||
</FormStyles.Field>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
static readonly Label = forwardRef<HTMLLabelElement, Form.LabelProps>(
|
||||
({ children, ...props }, ref) => (
|
||||
<FormStyles.Label ref={ref} {...props}>
|
||||
{children}
|
||||
</FormStyles.Label>
|
||||
)
|
||||
);
|
||||
|
||||
static readonly Error = forwardRef<HTMLDivElement, Form.ErrorProps>(
|
||||
({ children, ...props }, ref) => (
|
||||
<FormStyles.ErrorMessage ref={ref} {...props}>
|
||||
{children}
|
||||
</FormStyles.ErrorMessage>
|
||||
)
|
||||
);
|
||||
|
||||
static readonly Input = forwardRef<HTMLInputElement, Form.InputProps>(
|
||||
(props, ref) => {
|
||||
return <Input ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
|
||||
static readonly Textarea = forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
Form.TextareaProps
|
||||
>((props, ref) => {
|
||||
return <Textarea ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
static readonly LogoFileInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
Form.LogoFileInputProps
|
||||
>((props, ref) => {
|
||||
return <LogoFileInput ref={ref} {...props} />;
|
||||
});
|
||||
}
|
||||
|
||||
export namespace Form {
|
||||
export type FieldProps = {
|
||||
children: React.ReactNode;
|
||||
} & React.ComponentProps<typeof FormStyles.Field>;
|
||||
|
||||
export type LabelProps = React.ComponentProps<typeof FormStyles.Label>;
|
||||
|
||||
export type ErrorProps = React.ComponentProps<typeof FormStyles.ErrorMessage>;
|
||||
|
||||
export type InputProps = React.ComponentProps<typeof Input>;
|
||||
|
||||
export type TextareaProps = React.ComponentProps<typeof Textarea>;
|
||||
|
||||
export type LogoFileInputProps = React.ComponentProps<typeof LogoFileInput>;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './form';
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './core';
|
||||
export * from './layout';
|
||||
export * from './form';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { dripStitches } from '../../theme/stitches'; //TODO replace with absolute path
|
||||
import React from 'react';
|
||||
|
||||
const { styled } = dripStitches;
|
||||
|
||||
export const Flex = styled('div', {
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
export type FlexProps = React.ComponentProps<typeof Flex>;
|
||||
|
|
@ -14,10 +14,12 @@ export const spacing = {
|
|||
8: '2rem',
|
||||
9: '2.25rem',
|
||||
10: '2.5rem',
|
||||
11: '2.75rem',
|
||||
12: '3rem',
|
||||
14: '3.5rem',
|
||||
16: '4rem',
|
||||
20: '5rem',
|
||||
20: '5rem', // 80px
|
||||
22: '5.5rem', // 88px
|
||||
24: '6rem',
|
||||
28: '7rem',
|
||||
32: '8rem',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { globalCss } from '@stitches/react';
|
||||
|
||||
export const themeGlobals = globalCss({
|
||||
'html, body': {
|
||||
'html, body, #root': {
|
||||
height: '100%',
|
||||
padding: 0,
|
||||
margin: '25px 50px',
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ export const createDripStitches = <
|
|||
...darkColors, // TODO: replace with light colors once it's done the light mode
|
||||
...(theme?.colors || {}),
|
||||
},
|
||||
borderWidths: {
|
||||
default: '1px',
|
||||
},
|
||||
space: {
|
||||
..._spacing,
|
||||
...(theme?.space || {}),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
import { Flex } from '@/components';
|
||||
import React from 'react';
|
||||
import { Form } from '@/components';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const Home = () => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
return (
|
||||
<Flex css={{ justifyContent: 'center' }}>
|
||||
<Flex
|
||||
css={{
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<h1>Home</h1>
|
||||
<Form.Field css={{ width: '$24' }}>
|
||||
<Form.Label>Label</Form.Label>
|
||||
<Form.LogoFileInput value={file} onChange={(file) => setFile(file)} />
|
||||
</Form.Field>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue