feat: initialize reactjs project using vite
This commit is contained in:
82
src/components/ui/Button.module.css
Normal file
82
src/components/ui/Button.module.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-sm);
|
||||
font-family: var(--md-sys-typescale-body-font);
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.sm {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.md {
|
||||
padding: var(--space-md) var(--space-xl);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lg {
|
||||
padding: var(--space-lg) var(--space-2xl);
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.primary {
|
||||
background-color: var(--md-sys-color-primary);
|
||||
color: var(--md-sys-color-on-primary);
|
||||
}
|
||||
|
||||
.primary:hover:not(:disabled) {
|
||||
background-color: var(--md-sys-color-on-primary-container);
|
||||
box-shadow: 0 4px 20px rgba(127, 217, 152, 0.3);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.secondary:hover:not(:disabled) {
|
||||
background-color: var(--md-sys-color-surface-container-highest);
|
||||
}
|
||||
|
||||
.outline {
|
||||
background-color: transparent;
|
||||
color: var(--md-sys-color-primary);
|
||||
border: 2px solid var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.outline:hover:not(:disabled) {
|
||||
background-color: rgba(127, 217, 152, 0.1);
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.loader {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
42
src/components/ui/Button.tsx
Normal file
42
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
children: ReactNode;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
children,
|
||||
isLoading,
|
||||
disabled,
|
||||
className,
|
||||
type = 'button',
|
||||
onClick,
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<motion.button
|
||||
type={type}
|
||||
className={`${styles.button} ${styles[variant]} ${styles[size]} ${className || ''}`}
|
||||
disabled={disabled || isLoading}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className={styles.loader} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</motion.button>
|
||||
);
|
||||
}
|
||||
51
src/components/ui/Card.module.css
Normal file
51
src/components/ui/Card.module.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.card {
|
||||
position: relative;
|
||||
padding: var(--space-xl);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--radius-lg);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.card.hoverable:hover {
|
||||
border-color: var(--md-sys-color-primary);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(127, 217, 152, 0.1),
|
||||
inset 0 1px 0 rgba(127, 217, 152, 0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: var(--space-lg);
|
||||
background-color: var(--md-sys-color-primary-container);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
opacity: 0.75;
|
||||
}
|
||||
49
src/components/ui/Card.tsx
Normal file
49
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import styles from './Card.module.css';
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
export function Card({ children, className, hover = true }: CardProps) {
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.card} ${hover ? styles.hoverable : ''} ${className || ''}`}
|
||||
whileHover={hover ? { y: -4 } : undefined}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CardIconProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function CardIcon({ children }: CardIconProps) {
|
||||
return <div className={styles.icon}>{children}</div>;
|
||||
}
|
||||
|
||||
interface CardTitleProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function CardTitle({ children }: CardTitleProps) {
|
||||
return <h3 className={styles.title}>{children}</h3>;
|
||||
}
|
||||
|
||||
interface CardDescriptionProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function CardDescription({ children }: CardDescriptionProps) {
|
||||
return <p className={styles.description}>{children}</p>;
|
||||
}
|
||||
|
||||
Card.Icon = CardIcon;
|
||||
Card.Title = CardTitle;
|
||||
Card.Description = CardDescription;
|
||||
58
src/components/ui/Input.module.css
Normal file
58
src/components/ui/Input.module.css
Normal file
@@ -0,0 +1,58 @@
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: var(--space-md);
|
||||
font-family: var(--md-sys-typescale-body-font);
|
||||
font-size: 1rem;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
background-color: var(--md-sys-color-surface-container);
|
||||
border: 1px solid var(--md-sys-color-outline-variant);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
box-shadow var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: var(--md-sys-color-outline);
|
||||
}
|
||||
|
||||
.input:hover {
|
||||
border-color: var(--md-sys-color-outline);
|
||||
background-color: var(--md-sys-color-surface-container-high);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--md-sys-color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(127, 217, 152, 0.15);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
min-height: 150px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.hasError .input {
|
||||
border-color: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
.hasError .input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(255, 180, 171, 0.15);
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--md-sys-color-error);
|
||||
}
|
||||
58
src/components/ui/Input.tsx
Normal file
58
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { type InputHTMLAttributes, type TextareaHTMLAttributes, forwardRef } from 'react';
|
||||
import styles from './Input.module.css';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ label, error, id, className, ...props }, ref) => {
|
||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
return (
|
||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||
<label htmlFor={inputId} className={styles.label}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={styles.input}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className={styles.error}>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ label, error, id, className, ...props }, ref) => {
|
||||
const inputId = id || label.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
return (
|
||||
<div className={`${styles.field} ${error ? styles.hasError : ''} ${className || ''}`}>
|
||||
<label htmlFor={inputId} className={styles.label}>
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
className={`${styles.input} ${styles.textarea}`}
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className={styles.error}>{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
3
src/components/ui/index.ts
Normal file
3
src/components/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { Button } from './Button';
|
||||
export { Card } from './Card';
|
||||
export { Input, Textarea } from './Input';
|
||||
Reference in New Issue
Block a user