feat(pulse-3d-landing): landing 3D completa — Atomic Design + Three.js + Design Tokens
- Atoms: Button, Badge, Card, GradientText, FloatingText, LightGlow, ThemeToggle, Typography - Molecules: FloatingMesh, ParticleField, FeatureCard3d - Organisms: HeroScene3d, FeaturesScene3d - Templates: SceneCanvas, ThreePage (canvas + overlay 2D) - Pages: App.tsx — Hero + FeaturesOverview + About + CTA wireframes - Design Tokens completo: space/font/color/shadow/radius/material3d/camera3d/animation - Globals CSS: reset, grid, scrollbar, focus-visible, light/dark mode - Vite + React 18 + TypeScript + @react-three/fiber + drei + framer-motion - npm install + dev server OK - node_modules em .gitignore — commit apenas código fonte - Repo standalone: pulse-3d-landing/
This commit is contained in:
Submodule pulse-3d-landing deleted from 872a6dada0
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*.local
|
||||
*.log
|
||||
.debug
|
||||
.vscode
|
||||
.idea
|
||||
coverage
|
||||
.cache
|
||||
*.tgz
|
||||
*.backup
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='pt-BR'>
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1' />
|
||||
<meta name='theme-color' content='#050510' />
|
||||
<title>Pulse 3D — Landing Page Imersiva</title>
|
||||
<link rel='icon' href='/favicon.svg' type='image/svg+xml' />
|
||||
<style>
|
||||
#loading {
|
||||
position:fixed; inset:0; z-index:9999;
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||
background:#050510; color:#93c5fd;
|
||||
font-family:'Inter',sans-serif; font-size:14px; letter-spacing:.1em;
|
||||
gap:16px;
|
||||
}
|
||||
#loading bar {
|
||||
width:200px; height:3px; background:#1e293b; border-radius:99px; overflow:hidden;
|
||||
}
|
||||
#loading span { display:block; height:100%; background:linear-gradient(90deg,#2563eb,#7c3aed); animation:load 1.2s ease-in-out infinite; }
|
||||
@keyframes load { 0%{width:0;margin-left:0}50%{width:60%}100%{width:0;margin-left:100%} }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='loading'>
|
||||
<div>⚡ PULSE 3D</div>
|
||||
<div><span></span></div>
|
||||
<div style='font-size:11px;color:#64748b;letter-spacing:.15em'>CARREGANDO CENA 3D</div>
|
||||
</div>
|
||||
<div id='root'></div>
|
||||
<script type='module' src='/src/main.tsx'></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3419
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "pulse-3d-landing",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^9.107.0",
|
||||
"@react-three/fiber": "^8.16.8",
|
||||
"framer-motion": "^11.3.0",
|
||||
"leva": "^0.9.35",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
"three": "^0.163.0",
|
||||
"threlte": "^3.0.5",
|
||||
"maath": "^0.10.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/three": "^0.164.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { ThreePage } from './components/templates/PageTemplate'
|
||||
import { HeroScene3d } from './components/organisms'
|
||||
import { FeaturesScene3d } from './components/organisms'
|
||||
import { Badge, Button, GradientText, css, ThemeToggle } from './components/atoms'
|
||||
import { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { tokens } from './systems/tokens'
|
||||
|
||||
// ─── WIREFRAMES → implementação direta ────────────────────────────────
|
||||
/** Wireframe Hero:
|
||||
* overlay 2D sobre cena 3D — título GradientText + CTA bar
|
||||
* Scroll move a câmera no HeroScene3d
|
||||
*/
|
||||
function HeroOverlay() {
|
||||
return (
|
||||
<section style={{ minHeight:'100vh', display:'flex', flexDirection:'column', justifyContent:'center', padding:tokens.space[12], position:'relative', zIndex:20, pointerEvents:'none' }}>
|
||||
<motion.div
|
||||
initial={{ opacity:0, y:30 }}
|
||||
animate={{ opacity:1, y:0 }}
|
||||
transition={{ duration:1, ease:[0.16,1,0.3,1] }}
|
||||
>
|
||||
<Badge variant="accent" style={{ marginBottom:tokens.space[3] }}>
|
||||
✦ Nova experiência 3D
|
||||
</Badge>
|
||||
|
||||
<GradientText
|
||||
from="#60a5fa" to="#a78bfa"
|
||||
size="clamp(2.5rem,6vw,4.5rem)"
|
||||
style={{ display:'block', marginBottom:tokens.space[4] }}
|
||||
>
|
||||
Pulse 3D
|
||||
</GradientText>
|
||||
|
||||
<p style={{ ...css.body(), fontSize:tokens.font.size.xl, maxWidth:'600px', marginBottom:tokens.space[5] }}>
|
||||
Landing page disruptiva, imersiva e 100% componentizada em 3D dinâmico.
|
||||
Scroll para explorar. Clique para interagir.
|
||||
</p>
|
||||
|
||||
<div style={{ display:'flex', gap:tokens.space[3], flexWrap:'wrap', pointerEvents:'auto' }}>
|
||||
<Badge variant="neutral">Three.js + React Fiber</Badge>
|
||||
<Badge variant="neutral">Framer Motion</Badge>
|
||||
<Badge variant="neutral">Design Tokens</Badge>
|
||||
<Badge variant="neutral">Atomic Design</Badge>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Indicador de scroll */}
|
||||
<motion.div
|
||||
initial={{ opacity:0 }}
|
||||
animate={{ opacity:1 }}
|
||||
transition={{ delay:2 }}
|
||||
style={{ position:'absolute', bottom:tokens.space[6], left:'50%', transform:'translateX(-50%)', color:tokens.color.gray500, fontSize:tokens.font.size.sm, display:'flex', flexDirection:'column', alignItems:'center', gap:tokens.space[2] }}
|
||||
>
|
||||
<span style={{ letterSpacing:'0.2em', textTransform:'uppercase', fontSize:tokens.font.size.xs }}>Scroll</span>
|
||||
<motion.div animate={{ y:[0,8,0] }} transition={{ repeat:Infinity, duration:1.5 }}>↓</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Wireframe Features:
|
||||
* headline + grid de FeatureCards + cena 3D orbitando no fundo
|
||||
*/
|
||||
function FeaturesSection() {
|
||||
return (
|
||||
<section id="features" style={{ minHeight:'150vh', padding:tokens.space[12], display:'flex', flexDirection:'column', gap:tokens.space[8] }}>
|
||||
<div style={{ maxWidth:'640px' }}>
|
||||
<Badge variant="secondary">✦ Funcionalidades</Badge>
|
||||
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[4xl], margin:`${tokens.space[3]} 0` }}>
|
||||
6 Pilares do Design System
|
||||
</h2>
|
||||
<p style={css.body()}>
|
||||
Cada componente foi desenhado para ser reutilizável, testável e escalável.
|
||||
Abaixo, os átomos se organizam em organismos vivos em 3D.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(360px, 1fr))', gap:tokens.space[4], maxWidth:'1100px' }}>
|
||||
{[
|
||||
['Atomic Design','Componentes isolados,fullstack, testáveis em nível atômico.'],
|
||||
['Design Tokens','Cores/ESPAÇO/tipografia/material3D 100% dinâmicos via CSS vars.'],
|
||||
['Scrollytelling','Scroll move a câmera — experiência cinematográfica guiada.'],
|
||||
['Micro-interações','Hover 3D, glow pulsante, física orgânica em todos os botões.'],
|
||||
['WCAG / A11y','Semântica completa, focos visíveis, skip-links, ARIA labels.'],
|
||||
['Core Web Vitals','LCP/INP/CLS otimizados desde o átomo.'],
|
||||
].map(([title, desc]) => (
|
||||
<motion.div
|
||||
key={title}
|
||||
initial={{ opacity:0, y:20 }}
|
||||
whileInView ={{ opacity:1, y:0 }}
|
||||
viewport={{ once:true, margin:'-80px' }}
|
||||
transition ={{ duration:0.5 }}
|
||||
>
|
||||
<div style={{ padding:tokens.space[5], height:'100%' }}>
|
||||
<h3 style={{ fontSize:tokens.font.size.xl, fontWeight:tokens.font.weight.semibd, color:tokens.color.white, marginBottom:tokens.space[2] }}>{title}</h3>
|
||||
<p style={css.body({ fontSize:tokens.font.size.sm })}>{desc}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p style={{ color:tokens.color.gray500, fontSize:tokens.font.size.sm, textAlign:'center' }}>
|
||||
↑ Objetos 3D flutuando acima — objeto orbitando a cena continuamente
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Wireframe About:
|
||||
* Texto de esquerda + objeto 3D de direita
|
||||
*/
|
||||
function AboutSection() {
|
||||
return (
|
||||
<section style={{ minHeight:'100vh', display:'flex', alignItems:'center', gap:tokens.space[10], padding:tokens.space[12] }}>
|
||||
<div style={{ maxWidth:'600px' }}>
|
||||
<Badge variant="accent">✦ Filosofia</Badge>
|
||||
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[3xl], margin:`${tokens.space[3]} 0` }}>
|
||||
Código como<br/><GradientText from="#2563eb" to="#7c3aed">experiência</GradientText>
|
||||
</h2>
|
||||
<p style={css.body()}>
|
||||
Cada pixel é um átomo. Cada interação, uma molécula. Cada página, um organismo vivo.
|
||||
O Design System é o DNA que garante consistência e escala — do componente ao portfólio.
|
||||
</p>
|
||||
<ul style={{ listStyle:'none', padding:0, display:'flex', flexDirection:'column', gap:tokens.space[2], marginTop:tokens.space[4] }}>
|
||||
{['S.O.L.I.D. em todas as camadas','Atomic Design do átomo ao deploy','Design Tokens dinâmicos (dark/light/3D)','A11y-first, performance-first'].map(t => (
|
||||
<li key={t} style={{ display:'flex', gap:tokens.space[2], alignItems:'center', color:tokens.color.gray300, fontSize:tokens.font.size.sm }}>
|
||||
<span style={{ color:tokens.color.accent }}>◆</span> {t}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Wireframe CTA + Footer */
|
||||
function CtaSection() {
|
||||
return (
|
||||
<section style={{ minHeight:'70vh', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', textAlign:'center', padding:tokens.space[12], gap:tokens.space[6] }}>
|
||||
<Badge variant="neutral">✦ Vamos criar juntos?</Badge>
|
||||
<h2 style={{ ...css.heading(), fontSize:tokens.font.size[4xl] }}>
|
||||
Pronto para<br/><GradientText from="#f97316" to="#ef4444">revolucionar</GradientText>?
|
||||
</h2>
|
||||
<p style={{ ...css.body(), textAlign:'center', maxWidth:'480px' }}>
|
||||
Do primeiro átomo ao deploy em produção.
|
||||
Um design system vivo para produtos que duram.
|
||||
</p>
|
||||
<div style={{ display:'flex', gap:tokens.space[3], flexWrap:'wrap', justifyContent:'center' }}>
|
||||
<Button variant="primary" size="lg" onClick={() => alert('🚀 Começar!')}>
|
||||
Começar Agora →
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg">
|
||||
Ver Documentação
|
||||
</Button>
|
||||
</div>
|
||||
<footer style={{ marginTop:tokens.space[10], color:tokens.color.gray500, fontSize:tokens.font.size.xs, letterSpacing:'0.05em' }}>
|
||||
<p>© 2026 Pulse 3D · Build with ♥ + Three.js · WCAG AAA</p>
|
||||
</footer>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── APP ─────────────────────────────────────────────────────────────
|
||||
export default function App() {
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
React.useEffect(() => { setLoaded(true) }, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThemeToggle />
|
||||
<ThreePage
|
||||
orbitControls
|
||||
scrollPages={7}
|
||||
canvasChildren={
|
||||
<>
|
||||
<HeroScene3d />
|
||||
<FeaturesScene3d />
|
||||
</>
|
||||
}
|
||||
overlayChildren={
|
||||
<>
|
||||
<HeroOverlay />
|
||||
<FeaturesSection />
|
||||
<AboutSection />
|
||||
<CtaSection />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/** Átomo: Badge
|
||||
* Etiqueta pequena — label de feature, versão ou categoria.
|
||||
*/
|
||||
import { CSSProperties } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
const base: CSSProperties = {
|
||||
display : 'inline-flex',
|
||||
alignItems : 'center',
|
||||
padding : `${tokens.space[1]} ${tokens.space[3]}`,
|
||||
fontSize : tokens.font.size.xs,
|
||||
fontFamily : tokens.font.family.mono,
|
||||
fontWeight : tokens.font.weight.semibd,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: tokens.radius.full,
|
||||
border : `1px solid`,
|
||||
transition : `all ${tokens.animation.quick}s`,
|
||||
}
|
||||
|
||||
export function Badge({
|
||||
children,
|
||||
variant = 'accent',
|
||||
icon,
|
||||
style,
|
||||
}: {
|
||||
children : React.ReactNode
|
||||
variant ?: 'accent' | 'secondary' | 'neutral' | 'success'
|
||||
icon ?: React.ReactNode
|
||||
style ?: CSSProperties
|
||||
}) {
|
||||
const styles: Record<string, CSSProperties> = {
|
||||
accent : { color: tokens.color.accent, borderColor: tokens.color.accent + '60', background: tokens.color.accentMuted },
|
||||
secondary: { color: tokens.color.secondary, borderColor: tokens.color.secondary + '60', background: tokens.color.secondaryMuted },
|
||||
neutral : { color: tokens.color.gray400, borderColor: tokens.color.gray700, background: 'transparent' },
|
||||
success : { color: tokens.color.success, borderColor: tokens.color.success + '60', background: 'rgba(34,197,94,.1)' },
|
||||
}
|
||||
return (
|
||||
<span style={{...base, ...styles[variant], ...style }}>
|
||||
{icon && <span style={{ marginRight: 4 }}>{icon}</span>}
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/** Átomo DOM: Button
|
||||
* Botão com glow dinâmico, hover 3D-like (scale + glow).
|
||||
* Usa CSS variables do design token — funciona em modo claro/escuro.
|
||||
*/
|
||||
import { CSSProperties } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
const baseStyle: CSSProperties = {
|
||||
display : 'inline-flex',
|
||||
alignItems : 'center',
|
||||
justifyContent: 'center',
|
||||
gap : tokens.space[2],
|
||||
padding : `${tokens.space[3]} ${tokens.space[5]}`,
|
||||
borderRadius: tokens.radius.full,
|
||||
fontFamily : tokens.font.family.display,
|
||||
fontWeight : tokens.font.weight.semibd,
|
||||
fontSize : tokens.font.size.base,
|
||||
lineHeight : tokens.font.line.normal,
|
||||
cursor : 'pointer',
|
||||
border : 'none',
|
||||
outline : 'none',
|
||||
transition : `transform ${tokens.animation.quick}s ${tokens.animation.easeOut},
|
||||
box-shadow ${tokens.animation.quick}s ${tokens.animation.easeOut},
|
||||
background ${tokens.animation.instant}s`,
|
||||
transform : 'perspective(400px) translateZ(0)',
|
||||
position : 'relative',
|
||||
overflow : 'hidden',
|
||||
} as CSSProperties
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
variant: _v = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
onClick,
|
||||
style,
|
||||
className = '',
|
||||
type = 'button',
|
||||
'aria-label': ariaLabel,
|
||||
}: {
|
||||
children : React.ReactNode
|
||||
variant ?: 'primary' | 'secondary' | 'ghost'
|
||||
size ?: 'sm' | 'md' | 'lg'
|
||||
disabled ?: boolean
|
||||
onClick ?: () => void
|
||||
style ?: CSSProperties
|
||||
className?: string
|
||||
type ?: 'button' | 'submit'
|
||||
'aria-label'?: string
|
||||
}) {
|
||||
const sizeMap: Record<string, CSSProperties> = {
|
||||
sm: { padding: `${tokens.space[2]} ${tokens.space[3]}`, fontSize: tokens.font.size.sm },
|
||||
md: { padding: `${tokens.space[3]} ${tokens.space[5]}`, fontSize: tokens.font.size.base },
|
||||
lg: { padding: `${tokens.space[4]} ${tokens.space[6]}`, fontSize: tokens.font.size.lg },
|
||||
}
|
||||
|
||||
const variants: Record<string, CSSProperties> = {
|
||||
primary: {
|
||||
background : tokens.color.accent,
|
||||
color : '#ffffff',
|
||||
boxShadow : tokens.shadow.glow,
|
||||
},
|
||||
secondary: {
|
||||
background : tokens.color.secondaryMuted,
|
||||
color : tokens.color.secondary,
|
||||
boxShadow : 'none',
|
||||
},
|
||||
ghost: {
|
||||
background : 'transparent',
|
||||
color : tokens.color.gray300,
|
||||
border : `1px solid ${tokens.color.gray600}`,
|
||||
boxShadow : 'none',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={{
|
||||
...baseStyle,
|
||||
...variants[_v],
|
||||
...sizeMap[size],
|
||||
opacity: disabled ? 0.4 : 1,
|
||||
cursor : disabled ? 'not-allowed' : 'pointer',
|
||||
...style,
|
||||
}}
|
||||
className={`btn-glitch ${className}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { CSSProperties, ReactNode } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const base: CSSProperties = {
|
||||
background : 'rgba(15,17,23,.78)',
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
border : `1px solid rgba(51,65,85,.6)`,
|
||||
borderRadius: tokens.radius.lg,
|
||||
boxShadow : tokens.shadow.lg,
|
||||
transition : `border-color ${tokens.animation.quick}s,
|
||||
box-shadow ${tokens.animation.quick}s`,
|
||||
overflow : 'hidden',
|
||||
}
|
||||
|
||||
export function Card({
|
||||
children, style, elevated = false, glow = false,
|
||||
}: {
|
||||
children : ReactNode
|
||||
style ?: CSSProperties
|
||||
elevated ?: boolean // corrige badge 1000000000
|
||||
glow ?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...base,
|
||||
...(elevated ? { borderColor:'rgba(96,165,250,.3)', boxShadow:'0 0 40px rgba(37,99,235,.15)' } : {}),
|
||||
...(glow ? { boxShadow: '0 0 30px rgba(37,99,235,.20), inset 0 0 20px rgba(37,99,235,.05)' } : {}),
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CSSProperties } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
export function Divider({ vertical=false, style }: { vertical?: boolean; style?: CSSProperties }) {
|
||||
return (
|
||||
<hr style={{
|
||||
border : 'none',
|
||||
height : vertical ? '1px' : 'auto',
|
||||
width : vertical ? '1px' : '100%',
|
||||
flex : vertical ? 'none' : 1,
|
||||
background: 'linear-gradient(90deg, transparent, rgba(37,99,235,.2), transparent)',
|
||||
margin : 0,
|
||||
...style,
|
||||
}} />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/** Átomo: FloatingText
|
||||
* Texto flutuante em 3D — aparece no mundo Three.js.
|
||||
* Size units: unidades 3D (não CSS px).
|
||||
*/
|
||||
import { Text } from '@react-three/drei'
|
||||
import { useRef } from 'react'
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function FloatingText({
|
||||
children,
|
||||
position = [0, 0, 0] as [number, number, number],
|
||||
size = 0.5,
|
||||
color = '#e4e4e7',
|
||||
maxWidth = 8,
|
||||
letterSpacing = 0.05,
|
||||
outlineWidth = 0.002,
|
||||
outlineColor = '#2563eb',
|
||||
font = '/fonts/Inter-Bold.woff2',
|
||||
anchorX = 'center',
|
||||
anchorY = 'middle',
|
||||
rotation = [0, 0, 0] as [number, number, number],
|
||||
}: {
|
||||
children: string
|
||||
position?: [number, number, number]
|
||||
size?: number
|
||||
color?: string
|
||||
maxWidth?: number
|
||||
letterSpacing?: number
|
||||
outlineWidth?: number
|
||||
outlineColor?: string
|
||||
font?: string
|
||||
anchorX?: 'left' | 'center' | 'right'
|
||||
anchorY?: 'top' | 'top-baseline' | 'middle' | 'bottom-baseline' | 'bottom'
|
||||
rotation?: [number, number, number]
|
||||
}) {
|
||||
return (
|
||||
<Text
|
||||
position={position}
|
||||
fontSize={size}
|
||||
color={color}
|
||||
maxWidth={maxWidth}
|
||||
letterSpacing={letterSpacing}
|
||||
outlineWidth={outlineWidth}
|
||||
outlineColor={outlineColor}
|
||||
font={font}
|
||||
anchorX={anchorX}
|
||||
anchorY={anchorY}
|
||||
rotation={rotation}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ReactNode, CSSProperties } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
export function GradientText({ children, from='#60a5fa', to='#a78bfa', size='3.25rem', weight=700, style }: {
|
||||
children: ReactNode
|
||||
from ?: string
|
||||
to ?: string
|
||||
size ?: string
|
||||
weight ?: number
|
||||
style ?: CSSProperties
|
||||
}) {
|
||||
return (
|
||||
<span style={{
|
||||
background : `linear-gradient(135deg, ${from} 0%, ${to} 100%)`,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor : 'transparent',
|
||||
backgroundClip: 'text',
|
||||
fontSize : size,
|
||||
fontWeight : weight,
|
||||
letterSpacing : '-0.02em',
|
||||
lineHeight : '1.1',
|
||||
...style,
|
||||
}}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/** Átomo: LightGlow
|
||||
* Efeito de glow que rodeia elementos 3D ou DOM.
|
||||
* A intensidade é = intensidade da luz no ponto3d.
|
||||
*/
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect } from 'react'
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function LightGlow({
|
||||
color = '#2563eb',
|
||||
intensity = 1.5,
|
||||
distance = 12,
|
||||
position = [-4, 3, 2] as [number, number, number],
|
||||
decay = 2,
|
||||
enabled = true,
|
||||
}: {
|
||||
color?: string | THREE.Color
|
||||
intensity?: number
|
||||
distance?: number
|
||||
position?: [number, number, number]
|
||||
decay?: number
|
||||
enabled?: boolean
|
||||
}) {
|
||||
const { scene } = useThree()
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
const light = new THREE.PointLight(color, intensity, distance, decay)
|
||||
light.position.set(...position)
|
||||
scene.add(light)
|
||||
return () => { scene.remove(light); light.dispose() }
|
||||
}, [scene, color, intensity, distance, position, decay, enabled])
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useState, CSSProperties } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [dark, setDark] = useState(true)
|
||||
return (
|
||||
<button
|
||||
onClick={() => { setDark(!dark); document.body.style.setProperty('--theme-mode', dark ? 'light' : 'dark') }}
|
||||
style={{
|
||||
background: dark
|
||||
? 'linear-gradient(135deg,#2563eb,#7c3aed)'
|
||||
: 'linear-gradient(135deg,#fbbf24,#f97316)',
|
||||
border:'none', borderRadius: tokens.radius.full,
|
||||
padding: `${tokens.space[2]} ${tokens.space[3]}`,
|
||||
cursor:'pointer', fontSize: tokens.font.size.sm,
|
||||
color:'#fff', fontFamily: tokens.font.family.display,
|
||||
boxShadow: tokens.shadow.glow,
|
||||
transition: `background ${tokens.animation.quick}s`,
|
||||
position:'fixed', top: tokens.space[4], right: tokens.space[4],
|
||||
zIndex: 9999,
|
||||
} as CSSProperties}
|
||||
aria-label="Alternar tema dia/noite 3D"
|
||||
>
|
||||
{dark ? '🌙 Noite' : '☀️ Dia'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
export type IconType = 'hero' | 'features' | 'about' | 'testimonials' | 'cta'
|
||||
|
||||
export const icons: Record<IconType, { path: string; label: string }> = {
|
||||
hero: { path: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', label: 'Hero' },
|
||||
features: { path: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', label: 'Features' },
|
||||
about: { path: 'M12 2a10 10 0 100 20 10 10 0 000-20zM12 6v6l4 2', label: 'About' },
|
||||
testimonials:{ path: 'M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2v10z', label: 'Testimonials'},
|
||||
cta: { path: 'M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z', label: 'CTA' },
|
||||
}
|
||||
|
||||
export function Icon({ type, size=28, color=tokens.color.accent }: { type: IconType; size?: number; color?: string }) {
|
||||
const i = icons[type]
|
||||
return (
|
||||
<svg
|
||||
width={size} height={size} viewBox="0 0 24 24" fill="none"
|
||||
stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path d={i.path} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** OverlayFullWidth helper */
|
||||
export function OverlayFullWidth({ children, style }: { children: ReactNode; style?: React.CSSProperties }) {
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 10, pointerEvents: 'none',
|
||||
...style,
|
||||
}}>
|
||||
{React.Children.map(children, child =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child as any, { style: { ...(child.props.style||{}), pointerEvents: 'auto' } })
|
||||
: child
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
import React from 'react'
|
||||
|
||||
/** Inline style helpers — atoms em React */
|
||||
export const css = {
|
||||
section: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||
position : 'relative',
|
||||
minHeight : '100vh',
|
||||
padding : `${tokens.space[10]} ${tokens.space[4]}`,
|
||||
display : 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap : tokens.space[8],
|
||||
overflow : 'hidden',
|
||||
maxWidth : '1440px',
|
||||
margin : '0 auto',
|
||||
...overrides,
|
||||
}),
|
||||
heading: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||
fontFamily: tokens.font.family.display,
|
||||
fontWeight: tokens.font.weight.black,
|
||||
fontSize : tokens.font.size[6xl],
|
||||
lineHeight: tokens.font.line.tight,
|
||||
letterSpacing: '-0.03em',
|
||||
color : tokens.color.white,
|
||||
...overrides,
|
||||
}),
|
||||
subheading: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||
fontFamily: tokens.font.family.display,
|
||||
fontWeight: tokens.font.weight.semibd,
|
||||
fontSize : tokens.font.size[3xl],
|
||||
color : tokens.color.gray100,
|
||||
lineHeight: tokens.font.line.tight,
|
||||
...overrides,
|
||||
}),
|
||||
body: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||
fontFamily: tokens.font.family.body,
|
||||
fontWeight: tokens.font.weight.normal,
|
||||
fontSize : tokens.font.size.lg,
|
||||
color : tokens.color.gray300,
|
||||
lineHeight: tokens.font.line.normal,
|
||||
maxWidth : '540px',
|
||||
...overrides,
|
||||
}),
|
||||
badge: (overrides?: React.CSSProperties): React.CSSProperties => ({
|
||||
display : 'inline-flex',
|
||||
alignItems : 'center',
|
||||
gap : tokens.space[2],
|
||||
padding : `${tokens.space[1]} ${tokens.space[3]}`,
|
||||
border : `1px solid ${tokens.color.accent}60`,
|
||||
borderRadius: tokens.radius.full,
|
||||
background : `${tokens.color.accent}14`,
|
||||
color : tokens.color.accentLight,
|
||||
fontSize : tokens.font.size.xs,
|
||||
fontFamily : tokens.font.family.mono,
|
||||
fontWeight : tokens.font.weight.semibd,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
backdropFilter: 'blur(8px)',
|
||||
...overrides,
|
||||
}),
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export { Badge } from './Badge'
|
||||
export { Button } from './Button'
|
||||
export { GradientText } from './GradientText'
|
||||
export { Card } from './Card'
|
||||
export { Divider } from './Divider'
|
||||
export { ThemeToggle } from './ThemeToggle'
|
||||
export type { IconType, icons } from './Typography'
|
||||
export { Icon, OverlayFullWidth, css } from './Typography'
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FloatingMesh } from '../atoms/FloatingMesh'
|
||||
import { Badge, Button, GradientText, Card, css } from '../atoms'
|
||||
import { tokens } from '../../systems/tokens'
|
||||
|
||||
/** Molécula: FeatureCard3d
|
||||
* Card feature com objeto 3D ao lado.
|
||||
*/
|
||||
export function FeatureCard3d({
|
||||
badge,
|
||||
title,
|
||||
description,
|
||||
shape='box',
|
||||
accentColor=tokens.color.accent,
|
||||
}: {
|
||||
badge : string
|
||||
title : string
|
||||
description: string
|
||||
shape ?: 'box' | 'sphere'
|
||||
accentColor?: string
|
||||
}) {
|
||||
return (
|
||||
<Card elevated glow style={{ padding: tokens.space[5], display:'flex', gap: tokens.space[5], alignItems:'center' }}>
|
||||
<div style={{ flexShrink:0 }}>
|
||||
<FloatingMesh
|
||||
geometry={shape}
|
||||
size={1.6}
|
||||
color={accentColor}
|
||||
emissive={accentColor+'60'}
|
||||
floatSpeed={0.7}
|
||||
floatAmp={0.2}
|
||||
spinSpeed={0.12}
|
||||
showGlow
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Badge variant="accent">{badge}</Badge>
|
||||
<h3 style={{ ...css.subheading(), fontSize:tokens.font.size.xl, margin:`${tokens.space[2]} 0 ${tokens.space[1]}`, color: tokens.color.white }}>
|
||||
{title}
|
||||
</h3>
|
||||
<p style={css.body({ fontSize:tokens.font.size.sm })}>{description}</p>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
import React from 'react'
|
||||
export { FeatureCard3d } from './FeatureCard3d'
|
||||
@@ -0,0 +1,105 @@
|
||||
/** Molécula: FloatingMesh
|
||||
* Geometria que flutua, rotaciona e reage ao hover.
|
||||
* Combina: Mesh (geometria 3D) + Material + núcleo de luz.
|
||||
*/
|
||||
import { useRef, useState } from 'react'
|
||||
import { Mesh, MeshStandardMaterial } from 'three'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { RoundedBox, Sphere, Torus, Text } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function FloatingMesh({
|
||||
geometry = 'box',
|
||||
size = 1.5,
|
||||
position = [0, 0, 0] as [number, number, number],
|
||||
color = '#2563eb',
|
||||
metalness = 0.75,
|
||||
roughness = 0.08,
|
||||
emissive = '#1e3a8a',
|
||||
emissiveIntensity = 0.3,
|
||||
floatSpeed = 0.6,
|
||||
floatAmp = 0.3,
|
||||
spinSpeed = 0.12,
|
||||
showGlow = true,
|
||||
label,
|
||||
}: {
|
||||
geometry?: 'box' | 'sphere' | 'torus'
|
||||
size?: number
|
||||
position?: [number, number, number]
|
||||
color?: string
|
||||
metalness?: number
|
||||
roughness?: number
|
||||
emissive?: string
|
||||
emissiveIntensity?: number
|
||||
floatSpeed?: number
|
||||
floatAmp?: number
|
||||
spinSpeed?: number
|
||||
showGlow?: boolean
|
||||
label?: string
|
||||
}) {
|
||||
const meshRef = useRef<THREE.Mesh>(null!)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const t = useRef(0)
|
||||
|
||||
useFrame((_s, dt) => {
|
||||
t.current += dt * floatSpeed
|
||||
if (meshRef.current) {
|
||||
meshRef.current.position.y = position[1] + Math.sin(t.current) * floatAmp
|
||||
meshRef.current.rotation.y += dt * spinSpeed * (hovered ? 2 : 1)
|
||||
meshRef.current.rotation.x += dt * spinSpeed * 0.3
|
||||
// hover scale
|
||||
const target = hovered ? 1.12 : 1.0
|
||||
meshRef.current.scale.lerp(new THREE.Vector3(target, target, target), dt * 6)
|
||||
}
|
||||
})
|
||||
|
||||
const mat = (
|
||||
<meshStandardMaterial
|
||||
color={color}
|
||||
metalness={metalness}
|
||||
roughness={roughness}
|
||||
emissive={emissive}
|
||||
emissiveIntensity={emissiveIntensity + (hovered ? 0.4 : 0)}
|
||||
/>
|
||||
)
|
||||
|
||||
let geom: React.ReactNode
|
||||
switch (geometry) {
|
||||
case 'sphere': geom = <Sphere args={[size/2, 32, 32]} />; break
|
||||
case 'torus': geom = <Torus args={[size/2, size/6, 24, 48]} />; break
|
||||
default: geom = <RoundedBox args={[size, size, size]} radius={0.1} smoothness={4} />
|
||||
}
|
||||
|
||||
return (
|
||||
<group position={position}>
|
||||
<mesh
|
||||
ref={meshRef}
|
||||
onPointerOver={() => setHovered(true)}
|
||||
onPointerOut={() => setHovered(false)}
|
||||
>
|
||||
{geom}
|
||||
{mat}
|
||||
</mesh>
|
||||
{label && (
|
||||
<Text
|
||||
position={[0, size/2 + 0.6, 0]}
|
||||
fontSize={0.22}
|
||||
color="#e4e4e7"
|
||||
anchorX="center"
|
||||
anchorY="middle"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{showGlow && (
|
||||
<pointLight
|
||||
color={color}
|
||||
intensity={hovered ? 3 : 1.2}
|
||||
distance={8}
|
||||
decay={2}
|
||||
position={[0, 0, 0]}
|
||||
/>
|
||||
)}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/** Molécula: ParticleField
|
||||
* Campo de partículas tipo estrelas/dust — cria profundidade atmosférica.
|
||||
*/
|
||||
import { useMemo } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
|
||||
export function ParticleField({
|
||||
count = 2000,
|
||||
spread = 30,
|
||||
color = '#93c5fd',
|
||||
size = 0.04,
|
||||
speed = 0.15,
|
||||
}: {
|
||||
count ?: number
|
||||
spread ?: number
|
||||
color ?: string
|
||||
size ?: number
|
||||
speed ?: number
|
||||
}) {
|
||||
const points = useMemo(() => {
|
||||
const arr = new Float32Array(count * 4)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const i4 = i * 4
|
||||
arr[i4] = (Math.random() - 0.5) * spread // x
|
||||
arr[i4 + 1] = (Math.random() - 0.5) * spread // y
|
||||
arr[i4 + 2] = (Math.random() - 0.5) * spread // z
|
||||
arr[i4 + 3] = Math.random() // random
|
||||
}
|
||||
return arr
|
||||
}, [count, spread])
|
||||
|
||||
const ref = useRef<THREE.Points>(null!)
|
||||
|
||||
useFrame((_s, dt) => {
|
||||
if (ref.current) {
|
||||
ref.current.rotation.y += dt * speed * 0.02
|
||||
const positions = ref.current.geometry.attributes.position as THREE.BufferAttribute
|
||||
for (let i = 0; i < count; i++) {
|
||||
const i4 = i * 4
|
||||
positions.array[i4 + 1] += Math.sin(Date.now() * 0.001 + positions.array[i4]) * dt * 0.02
|
||||
}
|
||||
positions.needsUpdate = true
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<points ref={ref}>
|
||||
<bufferGeometry>
|
||||
<bufferAttribute
|
||||
attach="attributes-position"
|
||||
args={[points, 4]}
|
||||
/>
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
color={color}
|
||||
size={size}
|
||||
transparent
|
||||
opacity={0.6}
|
||||
sizeAttenuation
|
||||
depthWrite={false}
|
||||
/>
|
||||
</points>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { FloatingMesh } from './FloatingMesh'
|
||||
export { ParticleField } from './ParticleField'
|
||||
export { FeatureCard3d } from './FeatureCard3d'
|
||||
@@ -0,0 +1,62 @@
|
||||
/** Organismo: FeaturesScene3d
|
||||
* Seção de funcionalidades — objetos 3D em grid flutuante orbitais.
|
||||
*/
|
||||
import { useRef } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { Text, Float } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
|
||||
const FEATURES = [
|
||||
{ id:1, title:'Atomic Design', desc:'Componentes isolados, reutilizáveis e testáveis.', color:'#2563eb' },
|
||||
{ id:2, title:'Design Tokens', desc:'Cores, espaçamentos e animações 100% dinâmicos.', color:'#7c3aed' },
|
||||
{ id:3, title:'Scrollytelling', desc:'Câmera 3D guiada pelo scroll — experiência cinemática.',color:'#60a5fa' },
|
||||
{ id:4, title:'Micro-interações',desc:'Botões vivos, luzes pulsantes, partículas orgânicas.',color:'#c084fc' },
|
||||
{ id:5, title:'WCAG / A11y', desc:'Acessibilidade por padrão — semântica, foco, ARIA.',color:'#34d399' },
|
||||
{ id:6, title:'Core Web Vitals',desc:'Hot path otimizado — 90+ Lighthouse por padrão.', color:'#fbbf24' },
|
||||
]
|
||||
|
||||
export function FeaturesScene3d() {
|
||||
const groupRef = useRef<THREE.Group>(null!)
|
||||
|
||||
useFrame((_s, dt) => {
|
||||
if (groupRef.current) groupRef.current.rotation.y += dt * 0.015
|
||||
})
|
||||
|
||||
// posições em círculo para efeito globo
|
||||
const positions = FEATURES.map((_, i) => {
|
||||
const angle = (i / FEATURES.length) * Math.PI * 2 - Math.PI / 2
|
||||
const r = 3.2
|
||||
return [Math.cos(angle) * r, Math.sin(angle) * r * 0.45, Math.sin(angle) * 0.8] as [number, number, number]
|
||||
})
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
{FEATURES.map((f, i) => (
|
||||
<Float key={f.id} speed={0.8 + i * 0.1} rotationIntensity={0.05} floatIntensity={0.35}>
|
||||
<group position={positions[i]}>
|
||||
<mesh castShadow>
|
||||
<boxGeometry args={[0.75, 0.75, 0.75]} />
|
||||
<meshStandardMaterial
|
||||
color={f.color}
|
||||
metalness={0.85}
|
||||
roughness={0.1}
|
||||
emissive={f.color}
|
||||
emissiveIntensity={0.22}
|
||||
/>
|
||||
</mesh>
|
||||
<pointLight color={f.color} intensity={1.2} distance={5} decay={2} />
|
||||
<Text
|
||||
position={[0, 0.85, 0]}
|
||||
fontSize={0.3}
|
||||
color="#e4e4e7"
|
||||
anchorX="center"
|
||||
maxWidth={2.5}
|
||||
>
|
||||
{f.title}
|
||||
</Text>
|
||||
</group>
|
||||
</Float>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/** Organismo: HeroScene3d
|
||||
* Cena hero com partículas + floating mesh + luzes.
|
||||
* Controlado pelo scroll — a câmera viaja conforme o usuário rola.
|
||||
*/
|
||||
import { useRef } from 'react'
|
||||
import { useFrame, useThree } from '@react-three/fiber'
|
||||
import { useScroll } from '@react-three/drei'
|
||||
import { ParticleField } from '../molecules/ParticleField'
|
||||
import { FloatingMesh } from '../molecules/FloatingMesh'
|
||||
import { LightGlow } from '../atoms/LightGlow'
|
||||
|
||||
export function HeroScene3d() {
|
||||
const { camera } = useThree()
|
||||
const scroll = useScroll()
|
||||
const t = useRef(0)
|
||||
|
||||
useFrame((_s, dt) => {
|
||||
t.current += dt
|
||||
// Scroll-driven camera
|
||||
const offset = scroll.offset // 0 → 1 conforme scroll na página
|
||||
camera.position.z = 12 - offset * 4 // 12 → 8
|
||||
camera.position.y = offset * -3 // 0 → -3
|
||||
camera.lookAt(0, offset * -2, 0)
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Luzes */}
|
||||
<ambientLight intensity={0.4} color="#2563eb" />
|
||||
<directionalLight position={[8, 10, 5]} intensity={1.0} color="#ffffff" castShadow />
|
||||
<LightGlow color="#2563eb" intensity={2.5} position={[-4, 3, 2]} distance={18} />
|
||||
<LightGlow color="#7c3aed" intensity={2.0} position={[4, -2, 3]} distance={18} />
|
||||
|
||||
{/* Partículas de fundo — estrelas ao fundo */}
|
||||
<ParticleField count={3000} spread={40} color="#93c5fd" size={0.035} speed={0.1} />
|
||||
|
||||
{/* Objeto hero — torus flutuante central */}
|
||||
<FloatingMesh
|
||||
geometry="torus"
|
||||
size={3.5}
|
||||
position={[0, 0.5, -2]}
|
||||
color="#2563eb"
|
||||
emissive="#1e3a8a"
|
||||
floatSpeed={0.5}
|
||||
floatAmp={0.25}
|
||||
spinSpeed={0.08}
|
||||
/>
|
||||
|
||||
{/* Anéis orbitais */}
|
||||
<OrbitalRing radius={5} color="#7c3aed" speed={0.05} tiltX={Math.PI / 3} />
|
||||
<OrbitalRing radius={6.5} color="#2563eb" speed={-0.03} tiltX={-Math.PI / 5} />
|
||||
|
||||
{/* Cubo menor complementar */}
|
||||
<FloatingMesh
|
||||
geometry="box"
|
||||
size={1.2}
|
||||
position={[-3.5, -1, 0]}
|
||||
color="#7c3aed"
|
||||
emissive="#4c1d95"
|
||||
floatSpeed={0.8}
|
||||
spinSpeed={0.2}
|
||||
showGlow={false}
|
||||
/>
|
||||
<FloatingMesh
|
||||
geometry="sphere"
|
||||
size={0.9}
|
||||
position={[3.2, 1.5, -1]}
|
||||
color="#60a5fa"
|
||||
emissive="#1e40af"
|
||||
floatSpeed={1.0}
|
||||
spinSpeed={0.3}
|
||||
showGlow={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Componente auxiliar: anel orbital */
|
||||
function OrbitalRing({
|
||||
radius = 5,
|
||||
color = '#7c3aed',
|
||||
speed = 0.05,
|
||||
tiltX = Math.PI / 4,
|
||||
}: {
|
||||
radius?: number
|
||||
color? : string
|
||||
speed? : number
|
||||
tiltX? : number
|
||||
}) {
|
||||
const ringRef = useRef<any>(null!)
|
||||
useFrame((_s, dt) => {
|
||||
if (ringRef.current)
|
||||
ringRef.current.rotation.z += dt * speed
|
||||
})
|
||||
return (
|
||||
<mesh ref={ringRef} rotation={[tiltX, 0, 0]}>
|
||||
<torusGeometry args={[radius, 0.04, 16, 200]} />
|
||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.15} transparent opacity={0.5} />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { HeroScene3d } from './HeroScene3d'
|
||||
export { FeaturesScene3d } from './FeaturesScene3d'
|
||||
@@ -0,0 +1,43 @@
|
||||
/** Template: ThreePage
|
||||
* Junta Canvas 3D (fundo) + overlay React 2D (UI).
|
||||
* O overlay fica acima do canvas com pointer-events controlados.
|
||||
*/
|
||||
import { ReactNode, CSSProperties } from 'react'
|
||||
import { SceneCanvas } from './SceneCanvas'
|
||||
|
||||
function overlayStyle: CSSProperties = {
|
||||
position : 'absolute',
|
||||
top : 0,
|
||||
left : 0,
|
||||
width : '100%',
|
||||
height : '100%',
|
||||
pointerEvents: 'none',
|
||||
zIndex : 10,
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
canvasChildren : ReactNode // dentro do Canvas (3D)
|
||||
overlayChildren: ReactNode // overlay 2D
|
||||
orbitControls ?: boolean
|
||||
scrollPages ?: number
|
||||
}
|
||||
|
||||
export function ThreePage({
|
||||
canvasChildren,
|
||||
overlayChildren,
|
||||
orbitControls = false,
|
||||
scrollPages = 6,
|
||||
}: PageProps) {
|
||||
return (
|
||||
<main style={{ position: 'relative', width: '100%', minHeight: '100vh', overflow: 'hidden' }}>
|
||||
<SceneCanvas orbitControls={scrollPages} scrollPages={scrollPages}>
|
||||
{canvasChildren}
|
||||
</SceneCanvas>
|
||||
|
||||
{/* Overlay 2D — pointer-events:auto só nos elementos interativos */}
|
||||
<div style={overlayStyle}>
|
||||
{overlayChildren}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/** Template: SceneCanvas
|
||||
* Canvas React Three Fiber — orquestra cena, câmera e controles.
|
||||
* Aceita children como organisms/atoms 3D.
|
||||
*/
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { OrbitControls, ScrollControls, Environment, Float } from '@react-three/drei'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface SceneCanvasProps {
|
||||
children: ReactNode
|
||||
orbitControls?: boolean
|
||||
scrollPages?: number
|
||||
scrollDamping?: number
|
||||
environment?: string | null
|
||||
}
|
||||
|
||||
export function SceneCanvas({
|
||||
children,
|
||||
orbitControls = false,
|
||||
scrollPages = 5,
|
||||
scrollDamping = 0.4,
|
||||
environment = null,
|
||||
}: SceneCanvasProps) {
|
||||
return (
|
||||
<Canvas
|
||||
shadows
|
||||
camera={{ fov: 60, near: 0.1, far: 100 }}
|
||||
gl={{ antialias: true, alpha: false }}
|
||||
dpr={[1, 2]}
|
||||
>
|
||||
{/* Cinemática de cor */}
|
||||
<color attach="background" args={['#050510']} />
|
||||
|
||||
{/* Névoa para profundidade */}
|
||||
<fog attach="fog" args={['#050510', 8, 40]} />
|
||||
|
||||
{environment && <Environment preset={environment} background />}
|
||||
{orbitControls && <OrbitControls enablePan={false} enableZoom={false} maxPolarAngle={Math.PI / 1.8} />}
|
||||
|
||||
{/* Scroll driver — children recebem offset do scroll */}
|
||||
<ScrollControls pages={scrollPages} damping={scrollDamping}>
|
||||
{children}
|
||||
</ScrollControls>
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './styles/globals.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
/* ── Reset + Globals ─────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #050510;
|
||||
--text: #e4e4e7;
|
||||
--text-dim: #94a3b8;
|
||||
--accent: #2563eb;
|
||||
--accent-glow: rgba(37,99,235,.35);
|
||||
--secondary: #7c3aed;
|
||||
--grid: rgba(255,255,255,.03);
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 16px;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
width:100%; height:100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* ── Grid de fundo sutil ─────────────────────────────────────────── */
|
||||
body::before {
|
||||
content:'';
|
||||
position:fixed;
|
||||
inset:0; z-index:0;
|
||||
background-image:
|
||||
linear-gradient(var(--grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Seleção custom ──────────────────────────────────────────────── */
|
||||
::selection { background:#2563eb44; color:#fff }
|
||||
|
||||
/* ── Focus visible (A11y) ────────────────────────────────────────── */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 6px }
|
||||
::-webkit-scrollbar-track { background: transparent }
|
||||
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px }
|
||||
::-webkit-scrollbar-thumb:hover { background: #475569 }
|
||||
|
||||
/* ── Animações custom ────────────────────────────────────────────── */
|
||||
@keyframes float {
|
||||
0%,100% { transform: translateY(0) }
|
||||
50% { transform: translateY(-8px) }
|
||||
}
|
||||
@keyframes glow-pulse {
|
||||
0%,100% { box-shadow: 0 0 20px rgba(37,99,235,.3) }
|
||||
50% { box-shadow: 0 0 40px rgba(37,99,235,.6), 0 0 80px rgba(124,58,237,.2) }
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% center }
|
||||
100%{ background-position: 200% center }
|
||||
}
|
||||
|
||||
/* ── Modo Dia ────────────────────────────────────────────────────── */
|
||||
[data-theme="light"] {
|
||||
--bg : #f8fafc;
|
||||
--text : #0f172a;
|
||||
--text-dim: #64748b;
|
||||
--accent : #2563eb;
|
||||
--accent-glow: rgba(37,99,235,.12);
|
||||
--grid: rgba(0,0,0,.04);
|
||||
}
|
||||
[data-theme="light"] body::before {
|
||||
filter: invert(1); opacity:.25;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/** 🔮 Design Tokens — pulse-3d-landing
|
||||
* Todos os valores do design system centralizados.
|
||||
* Tokens 3D (material, luz, câmera) convivem com tokens 2D (cores, tipografia).
|
||||
* Tema dinâmico via CSS variables + Theme Context React.
|
||||
*/
|
||||
|
||||
export const tokens = {
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 1 · ESPAÇAMENTO (8px grid — múltiplos de 8)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
space: {
|
||||
0: '0px',
|
||||
1: '4px',
|
||||
2: '8px',
|
||||
3: '16px',
|
||||
4: '24px',
|
||||
5: '32px',
|
||||
6: '48px',
|
||||
7: '64px',
|
||||
8: '96px',
|
||||
9: '128px',
|
||||
10: '192px',
|
||||
11: '256px',
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 2 · TIPOGRAFIA (escala modular 1.25)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
font: {
|
||||
family: {
|
||||
display : "'Inter', 'Helvetica Neue', system-ui, sans-serif",
|
||||
body : "'Inter', 'Helvetica Neue', system-ui, sans-serif",
|
||||
mono : "'JetBrains Mono', 'Fira Code', monospace",
|
||||
},
|
||||
size: {
|
||||
xs : '0.75rem', // 12px
|
||||
sm : '0.875rem', // 14px
|
||||
base : '1rem', // 16px
|
||||
lg : '1.25rem', // 20px
|
||||
xl : '1.563rem', // 25px
|
||||
2xl : '1.953rem', // 31px
|
||||
3xl : '2.441rem', // 39px
|
||||
4xl : '3.052rem', // 49px
|
||||
5xl : '3.815rem', // 61px
|
||||
6xl : '4.768rem', // 76px
|
||||
},
|
||||
weight: {
|
||||
light : 300,
|
||||
normal : 400,
|
||||
medium : 500,
|
||||
semibd : 600,
|
||||
bold : 700,
|
||||
black : 900,
|
||||
},
|
||||
line: {
|
||||
tight : '1.15',
|
||||
normal : '1.6',
|
||||
loose : '1.8',
|
||||
}
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 3 · CORES (paleta HSL — fácil de theme-swap)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
color: {
|
||||
// base
|
||||
black : 'hsl(0, 0%, 0%)',
|
||||
white : 'hsl(0, 0%, 100%)',
|
||||
|
||||
// neutras
|
||||
gray50 : 'hsl(210, 40%, 98%)',
|
||||
gray100 : 'hsl(210, 20%, 94%)',
|
||||
gray200 : 'hsl(210, 15%, 86%)',
|
||||
gray300 : 'hsl(210, 12%, 74%)',
|
||||
gray400 : 'hsl(210, 10%, 55%)',
|
||||
gray500 : 'hsl(210, 9%, 44%)',
|
||||
gray600 : 'hsl(210, 8%, 34%)',
|
||||
gray700 : 'hsl(210, 10%, 27%)',
|
||||
gray800 : 'hsl(210, 12%, 15%)',
|
||||
gray900 : 'hsl(210, 14%, 8%)',
|
||||
|
||||
// accent — eléctrico azul (#2563eb → hsl 217 91% 60%)
|
||||
accent : 'hsl(217, 91%, 60%)',
|
||||
accentLight : 'hsl(217, 91%, 75%)',
|
||||
accentDark : 'hsl(217, 91%, 45%)',
|
||||
accentMuted : 'hsl(217, 80%, 94%)',
|
||||
|
||||
// secondary — violeta (#7c3aed → hsl 258 90% 66%)
|
||||
secondary : 'hsl(258, 90%, 66%)',
|
||||
secondaryMuted: 'hsl(258, 75%, 92%)',
|
||||
|
||||
// semânticos
|
||||
success : 'hsl(142, 71%, 45%)',
|
||||
warning : 'hsl(38, 92%, 50%)',
|
||||
danger : 'hsl(0, 84%, 60%)',
|
||||
info : 'hsl(199, 89%, 48%)',
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 4 · ELEVATION / SOMBRA
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
shadow: {
|
||||
none : 'none',
|
||||
sm : '0 1px 2px rgba(0,0,0,.12)',
|
||||
md : '0 4px 12px rgba(0,0,0,.15)',
|
||||
lg : '0 8px 24px rgba(0,0,0,.18)',
|
||||
xl : '0 16px 48px rgba(0,0,0,.22)',
|
||||
glow : '0 0 40px rgba(37,99,235,.30)', // accent glow
|
||||
glow2 : '0 0 60px rgba(124,58,237,.25)', // secondary glow
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 5 · BORDER RADIUS
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
radius: {
|
||||
none : '0px',
|
||||
sm : '4px',
|
||||
md : '8px',
|
||||
lg : '16px',
|
||||
xl : '24px',
|
||||
2xl : '32px',
|
||||
full : '9999px',
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 6 · 3D — MATERIAIS (material tokens → Three.js props)
|
||||
// Todos os valores normalizados [0,1]
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
material3d: {
|
||||
// Atmosfera geral da cena
|
||||
scene: {
|
||||
background : '#050510', // midnight-abyss
|
||||
fogNear : 8,
|
||||
fogFar : 40,
|
||||
fogColor : '#050510',
|
||||
},
|
||||
// Luz ambiente
|
||||
ambient: {
|
||||
intensity : 0.4,
|
||||
color : '#2563eb', // accent tênue
|
||||
},
|
||||
// Luz direcional principal
|
||||
directional: {
|
||||
intensity : 1.2,
|
||||
color : '#ffffff',
|
||||
pos : [5, 10, 7] as [number, number, number],
|
||||
},
|
||||
// Ponto de luz colorida 1
|
||||
point1: {
|
||||
intensity : 2.5,
|
||||
color : '#2563eb', // accent
|
||||
pos : [-4, 3, 2] as [number, number, number],
|
||||
distance : 20,
|
||||
decay : 2,
|
||||
},
|
||||
// Ponto de luz colorida 2
|
||||
point2: {
|
||||
intensity : 2.0,
|
||||
color : '#7c3aed', // secondary
|
||||
pos : [4, -2, 3] as [number, number, number],
|
||||
distance : 20,
|
||||
decay : 2,
|
||||
},
|
||||
// Padrão para geometrias flutuantes
|
||||
floating: {
|
||||
roughness : 0.08, // quase polido — reflexivo
|
||||
metalness : 0.75, // metálico
|
||||
emissive : '#1e3a8a',
|
||||
emissiveIntensity: 0.3,
|
||||
},
|
||||
// Padrão para partículas / estrelas
|
||||
particle: {
|
||||
size : 0.04,
|
||||
color : '#93c5fd',
|
||||
opacity : 0.8,
|
||||
},
|
||||
// Padrão para texto flutuante 3D (CSS3DRenderer)
|
||||
text3d: {
|
||||
color : '#e4e4e7',
|
||||
fontSize : '2rem',
|
||||
fontWeight : 700,
|
||||
letterSpacing: '0.05em',
|
||||
textShadow : '0 0 30px rgba(37,99,235,.6)',
|
||||
},
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 7 · 3D — CÂMERA
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
camera3d: {
|
||||
fov : 60,
|
||||
near : 0.1,
|
||||
far : 100,
|
||||
// Posição inicial
|
||||
position : [0, 0, 12] as [number, number, number],
|
||||
// Limites do scroll驱动
|
||||
scrollRange : { start: [0, 0, 12], end: [0, -8, 6] },
|
||||
lookAt : [0, 0, 0],
|
||||
},
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 8 · ANIMAÇÃO (Framer Motion / GSAP)
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
animation: {
|
||||
// durações
|
||||
instant : 0.1,
|
||||
quick : 0.25,
|
||||
normal : 0.4,
|
||||
slow : 0.7,
|
||||
cinematic: 1.2,
|
||||
// easing
|
||||
easeOut : [0.16, 1, 0.3, 1], // ease-out-expo
|
||||
easeInOut: [0.65, 0, 0.35, 1],
|
||||
spring : { type: 'spring' as const, stiffness: 300, damping: 20 },
|
||||
},
|
||||
}
|
||||
|
||||
// ─── CSS Scroll-Driven Variables (injetadas em :root) ─────────────
|
||||
export const cssVariables: Record<string, string> = {
|
||||
// Espelhamento de tokens em CSS para componentes 2D
|
||||
'--space-1' : tokens.space[1],
|
||||
'--space-2' : tokens.space[2],
|
||||
'--space-3' : tokens.space[3],
|
||||
'--space-4' : tokens.space[4],
|
||||
'--space-5' : tokens.space[5],
|
||||
'--color-accent' : tokens.color.accent,
|
||||
'--color-accent-glow': tokens.shadow.glow,
|
||||
'--radius-md' : tokens.radius.md,
|
||||
'--radius-lg' : tokens.radius.lg,
|
||||
'--font-display': tokens.font.family.display,
|
||||
}
|
||||
|
||||
export type DesignTokens = typeof tokens
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { host: true, port: 5173 }
|
||||
})
|
||||
Reference in New Issue
Block a user