website poc
This commit is contained in:
53
apps/website/components/ui/Button.tsx
Normal file
53
apps/website/components/ui/Button.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ButtonHTMLAttributes, AnchorHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
type ButtonAsButton = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
as?: 'button';
|
||||
href?: never;
|
||||
};
|
||||
|
||||
type ButtonAsLink = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
as: 'a';
|
||||
href: string;
|
||||
};
|
||||
|
||||
type ButtonProps = (ButtonAsButton | ButtonAsLink) & {
|
||||
variant?: 'primary' | 'secondary';
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
variant = 'primary',
|
||||
children,
|
||||
className = '',
|
||||
as = 'button',
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const baseStyles = 'rounded-full px-6 py-3 text-sm font-semibold transition-all duration-75 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 hover:scale-[1.03] active:scale-[0.98]';
|
||||
|
||||
const variantStyles = {
|
||||
primary: 'bg-primary-blue text-white hover:shadow-glow active:ring-2 active:ring-primary-blue focus-visible:outline-primary-blue',
|
||||
secondary: 'bg-iron-gray text-white border border-charcoal-outline hover:shadow-glow-strong hover:border-primary-blue focus-visible:outline-primary-blue'
|
||||
};
|
||||
|
||||
const classes = `${baseStyles} ${variantStyles[variant]} ${className}`;
|
||||
|
||||
if (as === 'a') {
|
||||
return (
|
||||
<a
|
||||
className={classes}
|
||||
{...(props as AnchorHTMLAttributes<HTMLAnchorElement>)}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classes}
|
||||
{...(props as ButtonHTMLAttributes<HTMLButtonElement>)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
14
apps/website/components/ui/Card.tsx
Normal file
14
apps/website/components/ui/Card.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Card({ children, className = '' }: CardProps) {
|
||||
return (
|
||||
<div className={`rounded-lg bg-iron-gray p-6 shadow-card border border-charcoal-outline hover:shadow-glow transition-shadow duration-200 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
apps/website/components/ui/Container.tsx
Normal file
30
apps/website/components/ui/Container.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
center?: boolean;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Container({
|
||||
size = 'lg',
|
||||
center = false,
|
||||
children,
|
||||
className = ''
|
||||
}: ContainerProps) {
|
||||
const sizeStyles = {
|
||||
sm: 'max-w-2xl',
|
||||
md: 'max-w-4xl',
|
||||
lg: 'max-w-7xl',
|
||||
xl: 'max-w-[1400px]'
|
||||
};
|
||||
|
||||
const centerStyles = center ? 'text-center' : '';
|
||||
|
||||
return (
|
||||
<div className={`mx-auto ${sizeStyles[size]} ${centerStyles} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
apps/website/components/ui/Heading.tsx
Normal file
25
apps/website/components/ui/Heading.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface HeadingProps {
|
||||
level: 1 | 2 | 3;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Heading({ level, children, className = '' }: HeadingProps) {
|
||||
const baseStyles = 'font-bold tracking-tight';
|
||||
|
||||
const levelStyles = {
|
||||
1: 'text-4xl sm:text-6xl',
|
||||
2: 'text-3xl sm:text-4xl',
|
||||
3: 'text-xl sm:text-2xl'
|
||||
};
|
||||
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
|
||||
return (
|
||||
<Tag className={`${baseStyles} ${levelStyles[level]} ${className}`}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
31
apps/website/components/ui/Input.tsx
Normal file
31
apps/website/components/ui/Input.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { InputHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
error?: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export default function Input({
|
||||
error = false,
|
||||
errorMessage,
|
||||
className = '',
|
||||
...props
|
||||
}: InputProps) {
|
||||
const baseStyles = 'block w-full rounded-md border-0 px-4 py-3 bg-iron-gray text-white shadow-sm ring-1 ring-inset placeholder:text-gray-500 focus:ring-2 focus:ring-inset focus:ring-primary-blue transition-all duration-150 sm:text-sm sm:leading-6';
|
||||
const errorStyles = error ? 'ring-warning-amber' : 'ring-charcoal-outline';
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<input
|
||||
className={`${baseStyles} ${errorStyles} ${className}`}
|
||||
aria-invalid={error}
|
||||
{...props}
|
||||
/>
|
||||
{error && errorMessage && (
|
||||
<p className="mt-2 text-sm text-warning-amber">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
apps/website/components/ui/MockupStack.tsx
Normal file
91
apps/website/components/ui/MockupStack.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client';
|
||||
|
||||
import { motion, useReducedMotion } from 'framer-motion';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface MockupStackProps {
|
||||
children: ReactNode;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export default function MockupStack({ children, index = 0 }: MockupStackProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const seed = index * 1337;
|
||||
const rotation1 = ((seed * 17) % 80 - 40) / 20;
|
||||
const rotation2 = ((seed * 23) % 80 - 40) / 20;
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full" style={{ perspective: '1200px' }}>
|
||||
<motion.div
|
||||
className="absolute rounded-lg bg-iron-gray/80 border border-charcoal-outline"
|
||||
style={{
|
||||
rotate: rotation1,
|
||||
zIndex: 1,
|
||||
top: '-8px',
|
||||
left: '-8px',
|
||||
right: '-8px',
|
||||
bottom: '-8px',
|
||||
boxShadow: '0 12px 40px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.92 }}
|
||||
animate={{ opacity: 0.5, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.1 }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="absolute rounded-lg bg-iron-gray/90 border border-charcoal-outline"
|
||||
style={{
|
||||
rotate: rotation2,
|
||||
zIndex: 2,
|
||||
top: '-4px',
|
||||
left: '-4px',
|
||||
right: '-4px',
|
||||
bottom: '-4px',
|
||||
boxShadow: '0 16px 48px rgba(0,0,0,0.35)',
|
||||
}}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 0.7, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.15 }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="relative z-10 w-full h-full rounded-lg overflow-hidden"
|
||||
style={{
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.45)',
|
||||
}}
|
||||
whileHover={
|
||||
shouldReduceMotion
|
||||
? {}
|
||||
: {
|
||||
scale: 1.02,
|
||||
rotateY: 3,
|
||||
rotateX: -2,
|
||||
y: -12,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="absolute inset-0 pointer-events-none rounded-lg"
|
||||
whileHover={
|
||||
shouldReduceMotion
|
||||
? {}
|
||||
: {
|
||||
boxShadow: '0 0 40px rgba(25, 140, 255, 0.4)',
|
||||
transition: { duration: 0.2 },
|
||||
}
|
||||
}
|
||||
/>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
apps/website/components/ui/Section.tsx
Normal file
30
apps/website/components/ui/Section.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface SectionProps {
|
||||
variant?: 'default' | 'dark' | 'light';
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export default function Section({
|
||||
variant = 'default',
|
||||
children,
|
||||
className = '',
|
||||
id
|
||||
}: SectionProps) {
|
||||
const variantStyles = {
|
||||
default: 'bg-deep-graphite',
|
||||
dark: 'bg-iron-gray',
|
||||
light: 'bg-charcoal-outline'
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
className={`${variantStyles[variant]} px-6 py-32 sm:py-40 lg:px-8 ${className}`}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user