refactor: converter pulse-libs de submodule→pasta normal

- ERA submodule gitlink → AGORA 28 arquivos TS diretamente no workspace
- Atoms (10): Button, Badge, Card, GradientText, Divider, ThemeToggle + stubs 3D
- Molecules (3): FeatureCard, Navbar, Footer
- Organisms (4): HeroSection, FeaturesGrid, CtaBlock, StatsGrid
- Templates (3): MainLayout, MinimalLayout, PageWithSidebar
- lib/index.ts: TOKENS export (color/space/radius) + barrel exports
- Repo próprio mantém: https://git.octal.tec.br/Roberto/pulse-libs
- Usado por: pulse-3d-landing, test.octal.tec.br, projetos futuros
This commit is contained in:
Pulse Agent
2026-05-20 20:22:44 -03:00
parent 62a3cdd674
commit 4237fdda2d
30 changed files with 243 additions and 1 deletions
Submodule pulse-libs deleted from 29a7a5adb9
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@pulse-libs/ui",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"exports": { ".": "./src/index.ts" },
"peerDependencies": { "react": "^18" }
}
+13
View File
@@ -0,0 +1,13 @@
export function Badge({children,variant='neutral',size='md',style}:{
children:React.ReactNode,variant?:'accent'|'purple'|'green'|'red'|'neutral',size?:'sm'|'md',style?:React.CSSProperties
}) {
const colors:{[k:string]:{bg:string,border:string,color:string}} = {
accent: {bg:'rgba(37,99,235,.12)', border:'1px solid rgba(37,99,235,.4)', color:'#60a5fa'},
purple: {bg:'rgba(124,58,237,.12)',border:'1px solid rgba(124,58,237,.4)',color:'#a78bfa'},
green: {bg:'rgba(52,211,153,.1)', border:'1px solid rgba(52,211,153,.4)', color:'#34d399'},
red: {bg:'rgba(239,68,68,.1)', border:'1px solid rgba(239,68,68,.4)', color:'#f87171'},
neutral: {bg:'rgba(100,116,139,.1)',border:'1px solid rgba(100,116,139,.3)',color:'#94a3b8'},
}
const sizes:{[k:string]:React.CSSProperties} = {sm:{padding:'1px 8px',fontSize:'.68rem'},md:{padding:'2px 10px',fontSize:'.75rem'}}
return <span style={{display:'inline-flex',alignItems:'center',gap:4,...sizes[size],borderRadius:99,fontFamily:"'JetBrains Mono',monospace",fontWeight:600,letterSpacing:'.06em',textTransform:'uppercase',...colors[variant],...style}}>{children}</span>
}
+19
View File
@@ -0,0 +1,19 @@
export function Button({children, onClick, variant='primary', loading=false, disabled=false, ...rest}:{
children: React.ReactNode, onClick?:()=>void, variant?:'primary'|'secondary'|'ghost'|'danger',
loading?:boolean, disabled?:boolean, [k:string]:any
}) {
const styles: Record<string,React.CSSProperties> = {
base: {display:'inline-flex',alignItems:'center',gap:8,padding:'10px 22px',borderRadius:99,border:none,cursor:disabled?'not-allowed':'pointer',fontWeight:700,fontFamily:'Inter,sans-serif',fontSize:'.95rem',transition:'all .18s',opacity:disabled||loading?'.55':1,position:'relative' as const},
primary: {background:'linear-gradient(135deg,#2563eb,#1d4ed8)',color:'#fff',boxShadow:'0 0 28px rgba(37,99,235,.28)'},
secondary: {background:'linear-gradient(135deg,#7c3aed,#6d28d9)',color:'#fff',boxShadow:'0 0 28px rgba(124,58,237,.22)'},
ghost: {background:'transparent',border:'1px solid #334155',color:'#e4e4e7'},
danger: {background:'linear-gradient(135deg,#dc2626,#b91c1c)',color:'#fff'},
}
return (
<button onClick={disabled||loading?undefined:onClick} style={{...styles.base,...styles[variant],...(rest as any).style}} disabled={disabled||loading} {...rest}>
{loading && <Spinner size={16}/>}
{children}
</button>
)
}
function Spinner({size=16}:{size?:number}){return <span style={{width:size,height:size,border:'2px solid rgba(255,255,255,.3)',borderTopColor:'#fff',borderRadius:'50%',display:'inline-block',animation:'spin .7s linear infinite'}}/>}
+11
View File
@@ -0,0 +1,11 @@
export function Card({children, variant='default', hover=false, style, ...rest}:{
children:React.ReactNode, variant?:'default'|'elevated'|'glass', hover?:boolean, style?:React.CSSProperties, [k:string]:any
}) {
const base:React.CSSProperties = {padding:'1.6rem 1.8rem',borderRadius:16,backdropFilter:'blur(16px)',transition:'all .22s ease',...rest.style}
const variants:{[k:string]:React.CSSProperties} = {
default: {background:'rgba(15,17,23,.72)',border:'1px solid rgba(51,65,85,.55)',boxShadow:'0 4px 20px rgba(0,0,0,.15)'},
elevated:{background:'rgba(22,25,35,.85)',border:'1px solid rgba(51,65,85,.65)',boxShadow:'0 8px 32px rgba(0,0,0,.28)'},
glass: {background:'rgba(255,255,255,.03)',border:'1px solid rgba(255,255,255,.07)',boxShadow:'none'},
}
return <div style={{...base,...variants[variant],...style}} onMouseEnter={hover?()=>{(rest as any).onMouseEnter?.()}:undefined} onMouseLeave={hover?()=>{(rest as any).onMouseLeave?.()}:undefined}>{children}</div>
}
+9
View File
@@ -0,0 +1,9 @@
export function Divider({label, style}:{label?:string, style?:React.CSSProperties}) {
return (
<div style={{display:'flex',alignItems:'center',gap:16,width:'100%',...style}}>
<div style={{flex:1,height:1,background:'linear-gradient(90deg,transparent,rgba(37,99,235,.18),transparent)'}}/>
{label && <span style={{fontSize:'.75rem',color:'#64748b',fontFamily:"'JetBrains Mono',monospace",whiteSpace:'nowrap'}}>{label}</span>}
<div style={{flex:1,height:1,background:'linear-gradient(90deg,transparent,rgba(37,99,235,.18),transparent)'}}/>
</div>
)
}
+2
View File
@@ -0,0 +1,2 @@
// @deprecated — Usar FloatingMesh do @pulse-libs/three
export { default as FloatingMesh3d } from './_deprecated/FloatingMesh3d'
+2
View File
@@ -0,0 +1,2 @@
// @deprecated — Consulte Text do @react-three/drei
export { default as FloatingText3d } from './_deprecated/FloatingText3d'
+6
View File
@@ -0,0 +1,6 @@
export function GradientText({children, from='#60a5fa', to='#a78bfa', via, size='inherit', weight=700, style}:{
children:React.ReactNode, from?:string, to?:string, via?:string, size?:string, weight?:number, style?:React.CSSProperties
}) {
const grad = via ? `linear-gradient(135deg,${from} 0%,${via} 60%,${to} 100%)` : `linear-gradient(135deg,${from} 0%,${to} 100%)`
return <span style={{background:grad,WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent',backgroundClip:'text',fontSize:size,fontWeight:weight,...style}}>{children}</span>
}
+2
View File
@@ -0,0 +1,2 @@
// @deprecated — Injetar <pointLight> direto na cena Three.js
export { default as LightGlow3d } from './_deprecated/LightGlow3d'
+2
View File
@@ -0,0 +1,2 @@
// @deprecated — Usar ParticleField do @pulse-libs/three
export { default as ParticleField3d } from './_deprecated/ParticleField3d'
+12
View File
@@ -0,0 +1,12 @@
import {useState} from 'react'
export function ThemeToggle({defaultDark=true}:{defaultDark?:boolean}) {
const [dark, setDark] = useState(defaultDark)
return (
<button
onClick={()=>{const next=!dark;setDark(next);document.documentElement.setAttribute('data-theme',next?'dark':'light')}}
aria-label="Toggle theme"
style={{position:'fixed',top:16,right:16,zIndex:9999,padding:'7px 14px',borderRadius:99,border:'none',cursor:'pointer',color:'#fff',fontWeight:700,fontFamily:'Inter,sans-serif',fontSize:'.85rem',background:dark?'linear-gradient(135deg,#2563eb,#7c3aed)':'linear-gradient(135deg,#fbbf24,#f97316)',boxShadow:'0 0 16px rgba(0,0,0,.3)',transition:'all .2s'}}>
{dark?'🌙':'☀️'}
</button>
)
}
@@ -0,0 +1 @@
// stub — implementação movida para @pulse-libs/three
@@ -0,0 +1 @@
// stub — implementação movida para @pulse-libs/three
+7
View File
@@ -0,0 +1,7 @@
export { Button } from './Button'
export { Badge } from './Badge'
export { Card } from './Card'
export { GradientText } from './GradientText'
export { Divider } from './Divider'
export { ThemeToggle } from './ThemeToggle'
export * from './_deprecated'
+19
View File
@@ -0,0 +1,19 @@
// @pulse-libs/ui — Design System compartilhado
// Atomic Design · React 18 · TypeScript — MIT 2026
export * from './atoms'
export * from './molecules'
export * from './organisms'
export * from './templates'
// ─── Design Tokens (CSS custom properties) ───
export const TOKENS = {
color: {
bg: '#050510', bg2: '#0c0c18',
text: '#e4e4e7', dim: '#94a3b8',
accent: '#2563eb', accent2:'#60a5fa',
secondary: '#7c3aed', secondary2:'#a78bfa',
border: 'rgba(51,65,85,.55)',
},
space: { sm:4, md:8, lg:16, xl:24, xxl:48 },
radius: { sm:4, md:8, lg:12, xl:16, round:9999 },
} as const
+16
View File
@@ -0,0 +1,16 @@
import { Card } from '../atoms'
interface FeatureCardProps { title:string, description:string, icon?:string, delay?:number, variant?:'default'|'accent', style?:React.CSSProperties }
export function FeatureCard({title,description,icon,delay=0,variant='default',style}:FeatureCardProps){
const bg = variant==='accent'?'linear-gradient(135deg,rgba(37,99,235,.06),rgba(124,58,237,.04))':'transparent'
return (
<Card hover style={{cursor:'default',position:'relative',overflow:'hidden',...style}}>
{variant==='accent' && <div style={{position:'absolute',inset:0,background:bg,pointerEvents:'none'}}/>}
{icon && <div style={{fontSize:'2rem',marginBottom:'.7rem',display:'block'}}>{icon}</div>}
<h3 style={{fontSize:'1.05rem',fontWeight:700,marginBottom:'.4rem',color:'#e4e4e7'}}>{title}</h3>
<p style={{fontSize:'.85rem',color:'#94a3b8',lineHeight:1.65}}>{description}</p>
<div style={{marginTop:'1rem',display:'flex',gap:'.45rem',flexWrap:'wrap'}}>
{variant==='accent' && <span style={{padding:'1px 8px',borderRadius:99,background:'rgba(37,99,235,.12)',border:'1px solid rgba(37,99,235,.3)',color:'#60a5fa',fontSize:'.68rem',fontFamily:"'JetBrains Mono',monospace",fontWeight:600}}>core</span>}
</div>
</Card>
)
}
+11
View File
@@ -0,0 +1,11 @@
interface FooterProps { brand?:string, year?:number, links:{label:string,href:string}[] }
export function Footer({brand='Pulse 3D',year=__DATE__,links=[]}:FooterProps){
return (
<footer style={{padding:'1.8rem 2rem',borderTop:'1px solid rgba(51,65,85,.4)',display:'flex',justifyContent:'space-between',alignItems:'center',flexWrap:'wrap',gap:'1rem'}}>
<span style={{color:'#64748b',fontSize:'.75rem'}}>{'\u2699'} {brand} \u00b7 MIT \u00b7 {new Date().getFullYear()}</span>
<ul style={{display:'flex',gap:'1.2rem',listStyle:'none',margin:0,padding:0}}>
{links.map(l=><li key={l.href}><a href={l.href} style={{color:'#60a5fa',fontSize:'.75rem',textDecoration:'none'}} onMouseEnter={e=>{e.currentTarget.style.textDecoration='underline'}} onMouseLeave={e=>{e.currentTarget.style.textDecoration='none'}}>{l.label}</a></li>)}
</ul>
</footer>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { GradientText, Button } from '../atoms'
interface NavProps { logo:string, links:{label:string,href:string}[], ctaLabel?:string, ctaHref?:string }
export function Navbar({logo,links,ctaLabel,ctaHref='#'}:NavProps){
return (
<nav role="navigation" aria-label="Main" style={{position:'sticky',top:0,zIndex:100,display:'flex',alignItems:'center',justifyContent:'space-between',padding:'10px 2rem',background:'rgba(5,5,16,.82)',backdropFilter:'blur(16px)',borderBottom:'1px solid rgba(51,65,85,.3)'}}>
<a href="/" aria-label="Home" style={{fontSize:'1rem',fontWeight:900,color:'#e4e4e7',letterSpacing:'-.03em',textDecoration:'none'}}>
<GradientText from="#60a5fa" to="#a78bfa">{logo}</GradientText>
</a>
<ul style={{display:'flex',alignItems:'center',gap:'1.5rem',listStyle:'none',margin:0,padding:0}}>
{links.map(l=> <li key={l.href}><a href={l.href} style={{color:'#94a3b8',fontSize:'.88rem',textDecoration:'none',transition:'color .15s'}} onMouseEnter={e=>{e.currentTarget.style.color='#60a5fa'}} onMouseLeave={e=>{e.currentTarget.style.color='#94a3b8'}}>{l.label}</a></li>)}
{ctaLabel && <li><Button onClick={()=>{window.location.href=ctaHref}} variant="primary" style={{padding:'6px 16px',fontSize:'.83rem'}}>{ctaLabel}</Button></li>}
</ul>
</nav>
)
}
+3
View File
@@ -0,0 +1,3 @@
export { FeatureCard } from './FeatureCard'
export { Navbar } from './Navbar'
export { Footer } from './Footer'
+16
View File
@@ -0,0 +1,16 @@
import { Button, GradientText } from '../atoms'
interface CtaProps { title:string, description:string, primary:{label:string,onClick():void}, secondary?:{label:string,onClick():void}, dark?:boolean }
export function CtaBlock({title,description,primary,secondary,dark=false}:CtaProps){
const bg = dark ? 'rgba(5,5,16,.96)' : 'rgba(15,17,23,.5)'
return (
<section style={{padding:'6rem 2rem',textAlign:'center',background:'linear-gradient(135deg,rgba(37,99,235,.05),rgba(124,58,237,.05))',borderTop:'1px solid rgba(51,65,85,.3)'}}>
<h2 style={{fontSize:'clamp(1.6rem,5vw,3rem)',fontWeight:900,marginBottom:'.8rem',lineHeight:1.15}}><GradientText from="#60a5fa" to="#a78bfa">{title}</GradientText></h2>
<p style={{color:'#94a3b8',maxWidth:520,margin:'0 auto 2rem',fontSize:'1.05rem',lineHeight:1.65}}>{description}</p>
<div style={{display:'flex',gap:'1rem',flexWrap:'wrap',justifyContent:'center'}}>
<Button variant="primary" onClick={primary.onClick} style={{boxShadow:'0 0 40px rgba(37,99,235,.3)'}}>{primary.label}</Button>
{secondary && <Button variant="ghost" onClick={()=>window.location.href='/'} style={{border:'1px solid #334155',color:'#e4e4e7'}}>{secondary.label}</Button>}
</div>
<p style={{marginTop:'2rem',fontSize:'.72rem',color:'#475569'}}>{'\u2699'} @pulse-libs/ui MIT {new Date().getFullYear()}</p>
</section>
)
}
+14
View File
@@ -0,0 +1,14 @@
import { Card } from '../atoms'
interface Feature { icon:string, title:string, description:string }
interface FeaturesProps { title?:string, features:Feature[], style?:React.CSSProperties }
export function FeaturesGrid({title,features,style}:FeaturesProps){
return (
<section style={{padding:'3.5rem 2rem',maxWidth:1100,margin:'0 auto',...style}}>
{title && <h2 style={{fontSize:'clamp(1.4rem,4vw,2.6rem)',fontWeight:800,lineHeight:1.15,color:'#e4e4e7',marginBottom:'.5rem'}}><span style={{background:'linear-gradient(135deg,#60a5fa,#a78bfa)',WebkitBackgroundClip:'text',WebkitTextFillColor:'transparent'}}>{title}</span></h2>}
<p style={{color:'#94a3b8',maxWidth:580,marginBottom:'2rem',fontSize:'.95rem',lineHeight:1.6}}>Componentes que compõem o sistema do átomo ao organismo.</p>
<div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(260px,1fr))',gap:'1.2rem'}}>
{features.map((f,i)=><Card key={i} hover style={{cursor:'default'}}><div style={{fontSize:'2.2rem',marginBottom:'.8rem'}}>{f.icon}</div><h3 style={{fontSize:'1.04rem',fontWeight:700,marginBottom:'.4rem',color:'#e4e4e7'}}>{f.title}</h3><p style={{fontSize:'.86rem',color:'#94a3b8',lineHeight:1.62}}>{f.description}</p></Card>) }
</div>
</section>
)
}
+18
View File
@@ -0,0 +1,18 @@
import { Card, Badge, GradientText, Button } from '../atoms'
import type { JSX } from 'react'
interface HeroProps { badge?:string, title:string, description:string, cta:{label:string,onClick():void}[], showScrollHint?:boolean, style?:React.CSSProperties }
export function HeroSection({badge,title,description,cta=[],showScrollHint=true,style}:HeroProps){
return (
<section style={{minHeight:'100vh',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',textAlign:'center',padding:'2rem',background:'radial-gradient(ellipse at 50% 20%,rgba(37,99,235,.10) 0%,transparent 65%),radial-gradient(ellipse at 80% 75%,rgba(124,58,237,.07) 0%,transparent 60%)',...style}} role="banner">
{badge && <Badge variant="accent">{badge}</Badge>}
<h1 style={{fontSize:'clamp(2.2rem,7vw,5.2rem)',fontWeight:900,lineHeight:1.04,letterSpacing:'-.03em',marginBottom:'1rem'}}>
<GradientText from="#60a5fa" to="#a78bfa">{title}</GradientText>
</h1>
<p style={{fontSize:'clamp(.95rem,2.2vw,1.2rem)',color:'#94a3b8',maxWidth:620,marginBottom:'2.5rem',lineHeight:1.6}}>{description}</p>
<div style={{display:'flex',gap:'1rem',flexWrap:'wrap',justifyContent:'center'}}>
{cta.map((b,i)=><Button key={i} variant={i===0?'primary':'ghost'} onClick={b.onClick}>{b.label}</Button>)}
</div>
{showScrollHint && <div style={{position:'absolute',bottom:'2.5rem',left:'50%',transform:'translateX(-50%)',color:'#64748b',fontSize:'.72rem',letterSpacing:'.18em',textTransform:'uppercase',display:'flex',flexDirection:'column',alignItems:'center',gap:'.4rem'}}><span>scroll para explorar</span><div style={{animation:'bounce 1.5s infinite'}}></div></div>}
</section>
)
}
+11
View File
@@ -0,0 +1,11 @@
interface Stat { label:string, value:string|number, color?:string }
export function StatsGrid({stats}:{stats:Stat[]}){
return (
<div style={{padding:'3rem 2rem',maxWidth:900,margin:'0 auto',display:'grid',gridTemplateColumns:'repeat(auto-fit,minmax(160px,1fr))',gap:'1rem',textAlign:'center'}}>
{stats.map((s,i)=><div key={i} style={{padding:'1.5rem',background:'rgba(15,17,23,.6)',border:'1px solid rgba(51,65,85,.4)',borderRadius:16,backdropFilter:'blur(12px)'}}>
<div style={{fontSize:'2rem',fontWeight:900,color:s.color||'#60a5fa'}}>{s.value}</div>
<div style={{fontSize:'.8rem',color:'#94a3b8',marginTop:'.2rem'}}>{s.label}</div>
</div>)}
</div>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { HeroSection } from './HeroSection'
export { FeaturesGrid } from './FeaturesGrid'
export { CtaBlock } from './CtaBlock'
export { StatsGrid } from './StatsGrid'
+1
View File
@@ -0,0 +1 @@
// Pages: index.ts
+12
View File
@@ -0,0 +1,12 @@
import type { ReactNode } from 'react'
import { Navbar, Footer, Divider } from '..'
interface LayoutProps { children:ReactNode, nav:{logo:string,links:{label:string,href:string}[],cta?:{label:string,href:string}}, footer?:{brand:string,links:{label:string,href:string}[]} }
export function MainLayout({children,nav,footer}:LayoutProps){
return (
<div style={{minHeight:'100vh',display:'flex',flexDirection:'column',background:'#050510'}}>
<Navbar logo={nav.logo} links={nav.links} ctaLabel={nav.cta?.label} ctaHref={nav.cta?.href}/>
<main style={{flex:1}}>{children}</main>
{footer && <Footer brand={footer.brand} links={footer.links}/>}
</div>
)
}
@@ -0,0 +1,4 @@
interface MinimalProps { children:React.ReactNode, centered?:boolean }
export function MinimalLayout({children,centered=true}:MinimalProps){
return <div style={{minHeight:'100vh',display:'flex',alignItems:centered?'center':'flex-start',justifyContent:'center',padding:'4rem 2rem',background:'#050510',color:'#e4e4e7'}}>{children}</div>
}
@@ -0,0 +1 @@
export { PageWithSidebar } from './PageWithSidebar'
+2
View File
@@ -0,0 +1,2 @@
export { MainLayout } from './MainLayout'
export { MinimalLayout } from './MinimalLayout'