first commit

This commit is contained in:
2026-05-22 18:44:46 -03:00
parent eaea57b7e9
commit 0fb351a3e2
40 changed files with 4746 additions and 71 deletions
+43
View File
@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Adaptivework } from './components/Adaptivework';
import { CTA } from './components/CTA';
import { Footer } from './components/Footer';
import { Hero } from './components/Hero';
import { Navbar } from './components/Navbar';
import { Process } from './components/Process';
import { Services } from './components/Services';
import { WhyUs } from './components/WhyUs';
import { useTheme } from './hooks/useTheme';
export default function App() {
const { i18n } = useTranslation();
const { isDark, toggle } = useTheme();
useEffect(() => {
const lang = (i18n.resolvedLanguage ?? i18n.language ?? 'pt').slice(0, 2);
document.documentElement.lang = lang;
}, [i18n.resolvedLanguage, i18n.language]);
return (
<>
<a
href="#main"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:rounded-md focus:bg-secondary focus:px-4 focus:py-2 focus:text-secondary-fg"
>
Skip to content
</a>
<Navbar isDark={isDark} onToggleTheme={toggle} />
<main id="main">
<Hero />
<Services />
<Adaptivework />
<WhyUs />
<Process />
<CTA />
</main>
<Footer isDark={isDark} />
</>
);
}
+85
View File
@@ -0,0 +1,85 @@
import { motion } from 'framer-motion';
import {
ArrowRight,
GraduationCap,
Plug,
Settings2,
Sparkles,
Wand2,
type LucideIcon,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
type FeatureKey = 'implementation' | 'customization' | 'integration' | 'training';
const FEATURES: { key: FeatureKey; Icon: LucideIcon }[] = [
{ key: 'implementation', Icon: Settings2 },
{ key: 'customization', Icon: Wand2 },
{ key: 'integration', Icon: Plug },
{ key: 'training', Icon: GraduationCap },
];
export function Adaptivework() {
const { t } = useTranslation();
return (
<section id="adaptivework" className="section relative overflow-hidden">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-radial-fade opacity-60"
/>
<div className="container-rl relative grid gap-12 lg:grid-cols-[0.95fr_1.05fr] lg:items-center">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}
>
<span className="badge">
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
{t('adaptivework.badge')}
</span>
<h2 className="heading text-balance mt-5 text-3xl md:text-4xl">
{t('adaptivework.title')}
</h2>
<p className="mt-4 text-fg-muted text-balance md:text-lg">
{t('adaptivework.subtitle')}
</p>
<a href="#contact" className="btn-primary mt-8">
{t('adaptivework.cta')}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</a>
</motion.div>
<div className="grid gap-4 sm:grid-cols-2">
{FEATURES.map(({ key, Icon }, index) => (
<motion.div
key={key}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{
duration: 0.5,
delay: 0.08 + index * 0.07,
ease: [0.4, 0, 0.2, 1],
}}
className="card card-hover"
>
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-secondary text-secondary-fg">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<h3 className="heading mt-4 text-lg">
{t(`adaptivework.features.${key}.title`)}
</h3>
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
{t(`adaptivework.features.${key}.description`)}
</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
+100
View File
@@ -0,0 +1,100 @@
import { motion } from 'framer-motion';
import { ArrowRight, Mail, Phone } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function CTA() {
const { t } = useTranslation();
const email = t('cta.email');
const whatsapp = t('cta.whatsapp');
return (
<section id="contact" className="section">
<div className="container-rl">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
className="relative overflow-hidden rounded-2xl bg-secondary text-secondary-fg shadow-lg"
>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-32 -right-32 h-[420px] w-[420px] rounded-full bg-primary/30 blur-3xl"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -bottom-40 -left-32 h-[380px] w-[380px] rounded-full bg-primary/20 blur-3xl"
/>
<div className="relative grid gap-10 p-10 lg:grid-cols-[1.1fr_0.9fr] lg:p-16">
<div>
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/20 px-3 py-1 text-xs font-semibold text-primary">
{t('cta.eyebrow')}
</span>
<h2 className="heading text-balance mt-5 text-3xl text-secondary-fg md:text-4xl">
{t('cta.title')}
</h2>
<p className="mt-4 text-secondary-fg/80 md:text-lg">
{t('cta.subtitle')}
</p>
<a
href={`mailto:${email}`}
className="btn-primary mt-8"
>
{t('cta.ctaPrimary')}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
<div className="grid gap-4 self-center">
<ContactCard
Icon={Mail}
label={t('cta.emailLabel')}
value={email}
href={`mailto:${email}`}
/>
<ContactCard
Icon={Phone}
label={t('cta.whatsappLabel')}
value={whatsapp}
href={`https://wa.me/${whatsapp.replace(/\D/g, '')}`}
/>
</div>
</div>
</motion.div>
</div>
</section>
);
}
function ContactCard({
Icon,
label,
value,
href,
}: {
Icon: typeof Mail;
label: string;
value: string;
href: string;
}) {
return (
<a
href={href}
className="group flex items-center gap-4 rounded-xl border border-white/10 bg-white/5 p-5 transition-colors hover:bg-white/10"
>
<div className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary text-primary-fg">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<div className="min-w-0">
<div className="text-xs uppercase tracking-wider text-secondary-fg/60">
{label}
</div>
<div className="truncate font-semibold text-secondary-fg group-hover:underline">
{value}
</div>
</div>
</a>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { Github, Linkedin, Mail } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { rivusLogoSrc } from '../lib/logos';
import { NAV_SECTIONS } from '../lib/sections';
const SOCIALS = [
{ Icon: Linkedin, href: 'https://www.linkedin.com', label: 'LinkedIn' },
{ Icon: Github, href: 'https://github.com', label: 'GitHub' },
{ Icon: Mail, href: 'mailto:contato@rivuslab.com', label: 'Email' },
] as const;
export function Footer({ isDark }: { isDark: boolean }) {
const { t } = useTranslation();
const year = new Date().getFullYear();
return (
<footer className="border-t border-border bg-bg-muted">
<div className="container-rl py-14">
<div className="grid gap-10 md:grid-cols-[1.4fr_1fr_1fr]">
<div>
<img
src={rivusLogoSrc(isDark)}
alt="RivusLab"
className="h-9 w-auto"
width={160}
height={36}
/>
<p className="mt-4 max-w-sm text-sm text-fg-muted leading-relaxed">
{t('footer.tagline')}
</p>
<div className="mt-6 flex items-center gap-2">
{SOCIALS.map(({ Icon, href, label }) => (
<a
key={label}
href={href}
target="_blank"
rel="noreferrer"
aria-label={label}
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border bg-bg text-fg-muted transition-colors hover:border-primary hover:text-primary"
>
<Icon className="h-4 w-4" />
</a>
))}
</div>
</div>
<FooterColumn title={t('footer.navigation')}>
{NAV_SECTIONS.map((section) => (
<li key={section.id}>
<a
href={`#${section.id}`}
className="text-sm text-fg-muted transition-colors hover:text-fg"
>
{t(section.labelKey)}
</a>
</li>
))}
</FooterColumn>
<FooterColumn title={t('footer.contact')}>
<li>
<a
href="mailto:contato@rivuslab.com"
className="text-sm text-fg-muted transition-colors hover:text-fg"
>
contato@rivuslab.com
</a>
</li>
<li className="text-sm text-fg-muted">+55 (00) 00000-0000</li>
</FooterColumn>
</div>
<div className="mt-12 flex flex-col items-start justify-between gap-3 border-t border-border pt-6 text-xs text-fg-muted md:flex-row md:items-center">
<span>© {year} RivusLab. {t('footer.rights')}</span>
<span className="font-mono">v0.1.0</span>
</div>
</div>
</footer>
);
}
function FooterColumn({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div>
<h4 className="text-xs font-bold uppercase tracking-[0.18em] text-fg">
{title}
</h4>
<ul className="mt-4 space-y-2.5">{children}</ul>
</div>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { motion } from 'framer-motion';
import { ArrowRight, Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function Hero() {
const { t } = useTranslation();
return (
<section
id="top"
className="relative overflow-hidden pt-32 pb-20 md:pt-40 md:pb-28"
>
<div
aria-hidden="true"
className="bg-radial-fade pointer-events-none absolute inset-0"
/>
<div
aria-hidden="true"
className="bg-grid pointer-events-none absolute inset-0 opacity-[0.35] [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_70%)]"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -top-32 -right-32 h-[480px] w-[480px] rounded-full bg-primary/30 blur-3xl"
/>
<div
aria-hidden="true"
className="pointer-events-none absolute -bottom-40 -left-24 h-[420px] w-[420px] rounded-full bg-secondary/20 blur-3xl"
/>
<div className="container-rl relative grid items-center gap-14 lg:grid-cols-[1.1fr_0.9fr]">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}
className="max-w-2xl"
>
<span className="badge">
<Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
{t('hero.eyebrow')}
</span>
<h1 className="heading text-balance mt-6 text-4xl leading-[1.05] sm:text-5xl md:text-6xl">
{t('hero.title')}
</h1>
<p className="mt-6 max-w-xl text-lg text-fg-muted text-balance">
{t('hero.subtitle')}
</p>
<div className="mt-9 flex flex-wrap items-center gap-3">
<a href="#contact" className="btn-primary">
{t('hero.ctaPrimary')}
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</a>
<a href="#services" className="btn-ghost">
{t('hero.ctaSecondary')}
</a>
</div>
{/* <dl className="mt-12 grid max-w-lg grid-cols-3 gap-6 border-t border-border pt-8">
{STATS.map((stat) => (
<div key={stat.key}>
<dt className="text-xs uppercase tracking-wider text-fg-muted">
{t(`hero.stats.${stat.key}`)}
</dt>
<dd className="mt-1 font-display text-2xl font-bold text-fg md:text-3xl">
{stat.value}
</dd>
</div>
))}
</dl> */}
</motion.div>
{/* <motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.7, delay: 0.15, ease: [0.4, 0, 0.2, 1] }}
className="relative hidden lg:block"
>
<HeroVisual />
</motion.div> */}
</div>
</section>
);
}
+52
View File
@@ -0,0 +1,52 @@
import { Globe } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '../i18n';
const LABELS: Record<SupportedLanguage, string> = {
pt: 'PT',
en: 'EN',
es: 'ES',
};
export function LanguageSwitcher({ className = '' }: { className?: string }) {
const { i18n, t } = useTranslation();
const current = (i18n.resolvedLanguage ?? i18n.language ?? 'pt').slice(0, 2) as SupportedLanguage;
const handleChange = (lang: SupportedLanguage) => {
if (lang === current) return;
void i18n.changeLanguage(lang);
};
return (
<div
className={`inline-flex items-center gap-1 rounded-full border border-border bg-bg p-1 text-xs font-semibold ${className}`}
role="group"
aria-label={t('lang.switchTo')}
>
<Globe
className="ml-1.5 mr-0.5 h-3.5 w-3.5 text-fg-muted"
aria-hidden="true"
/>
{SUPPORTED_LANGUAGES.map((lang) => {
const active = lang === current;
return (
<button
key={lang}
type="button"
onClick={() => handleChange(lang)}
aria-pressed={active}
className={[
'rounded-full px-2.5 py-1 transition-colors',
active
? 'bg-secondary text-secondary-fg'
: 'text-fg-muted hover:text-fg',
].join(' ')}
>
{LABELS[lang]}
</button>
);
})}
</div>
);
}
+127
View File
@@ -0,0 +1,127 @@
import { Menu, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { rivusLogoSrc } from '../lib/logos';
import { NAV_SECTIONS } from '../lib/sections';
import { LanguageSwitcher } from './LanguageSwitcher';
import { ThemeToggle } from './ThemeToggle';
export function Navbar({
isDark,
onToggleTheme,
}: {
isDark: boolean;
onToggleTheme: () => void;
}) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
useEffect(() => {
document.body.style.overflow = open ? 'hidden' : '';
return () => {
document.body.style.overflow = '';
};
}, [open]);
const closeMenu = () => setOpen(false);
return (
<header
className={[
'fixed inset-x-0 top-0 z-50 transition-all duration-300 ease-smooth',
scrolled
? 'border-b border-border/80 bg-bg/85 backdrop-blur-md shadow-sm'
: 'border-b border-transparent bg-transparent',
].join(' ')}
>
<div className="container-rl flex h-16 items-center justify-between gap-4 md:h-20">
<a href="#top" className="flex items-center gap-2" aria-label="RivusLab">
<img
src={rivusLogoSrc(isDark)}
alt="RivusLab"
className="h-8 w-auto md:h-9"
width={160}
height={36}
/>
</a>
<nav
className="hidden items-center gap-7 lg:flex"
aria-label="Primary"
>
{NAV_SECTIONS.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="text-sm font-medium text-fg-muted transition-colors hover:text-fg"
>
{t(section.labelKey)}
</a>
))}
</nav>
<div className="hidden items-center gap-3 lg:flex">
<ThemeToggle isDark={isDark} onToggle={onToggleTheme} />
<LanguageSwitcher />
<a href="#contact" className="btn-primary">
{t('nav.cta')}
</a>
</div>
<button
type="button"
className="inline-flex h-10 w-10 items-center justify-center rounded-md border border-border bg-bg text-fg lg:hidden"
aria-label={open ? t('nav.closeMenu') : t('nav.openMenu')}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
<div
className={[
'lg:hidden overflow-hidden transition-[max-height,opacity] duration-300 ease-smooth',
open
? 'max-h-[80vh] opacity-100 border-t border-border bg-bg'
: 'max-h-0 opacity-0',
].join(' ')}
>
<div className="container-rl flex flex-col gap-1 py-4">
{NAV_SECTIONS.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
onClick={closeMenu}
className="rounded-md px-3 py-3 text-base font-medium text-fg hover:bg-bg-muted"
>
{t(section.labelKey)}
</a>
))}
<div className="mt-2 flex items-center justify-between gap-3 px-3 pt-3 border-t border-border">
<div className="flex items-center gap-2">
<ThemeToggle isDark={isDark} onToggle={onToggleTheme} />
<LanguageSwitcher />
</div>
<a
href="#contact"
onClick={closeMenu}
className="btn-primary flex-1 text-center"
>
{t('nav.cta')}
</a>
</div>
</div>
</div>
</header>
);
}
+58
View File
@@ -0,0 +1,58 @@
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { SectionHeader } from './Services';
const STEPS = ['diagnose', 'propose', 'execute', 'evolve'] as const;
export function Process() {
const { t } = useTranslation();
return (
<section id="process" className="section bg-bg-muted">
<div className="container-rl">
<SectionHeader
eyebrow={t('process.eyebrow')}
title={t('process.title')}
/>
<ol className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{STEPS.map((step, index) => (
<motion.li
key={step}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{
duration: 0.5,
delay: index * 0.08,
ease: [0.4, 0, 0.2, 1],
}}
className="relative"
>
<div className="card h-full">
<div className="flex items-center justify-between">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-secondary font-display text-sm font-bold text-secondary-fg">
{String(index + 1).padStart(2, '0')}
</span>
{index < STEPS.length - 1 && (
<span
aria-hidden="true"
className="hidden h-px flex-1 ml-4 bg-gradient-to-r from-primary/60 to-transparent lg:block"
/>
)}
</div>
<h3 className="heading mt-5 text-lg">
{t(`process.steps.${step}.title`)}
</h3>
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
{t(`process.steps.${step}.description`)}
</p>
</div>
</motion.li>
))}
</ol>
</div>
</section>
);
}
+82
View File
@@ -0,0 +1,82 @@
import { motion } from 'framer-motion';
import { Code2, Compass, type LucideIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
type ServiceKey = 'outsourcing' | 'squads' | 'consulting';
const SERVICES: { key: ServiceKey; Icon: LucideIcon }[] = [
{ key: 'outsourcing', Icon: Code2 },
// { key: 'squads', Icon: Users },
{ key: 'consulting', Icon: Compass },
];
export function Services() {
const { t } = useTranslation();
return (
<section id="services" className="section bg-bg-muted">
<div className="container-rl">
<SectionHeader
eyebrow={t('services.eyebrow')}
title={t('services.title')}
subtitle={t('services.subtitle')}
/>
<div className="mt-14 grid gap-6 md:grid-cols-3">
{SERVICES.map(({ key, Icon }, index) => (
<motion.article
key={key}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{
duration: 0.5,
delay: index * 0.08,
ease: [0.4, 0, 0.2, 1],
}}
className="card card-hover group"
>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-secondary text-secondary-fg transition-transform group-hover:scale-110">
<Icon className="h-6 w-6" aria-hidden="true" />
</div>
<h3 className="heading mt-5 text-xl">
{t(`services.items.${key}.title`)}
</h3>
<p className="mt-3 text-fg-muted leading-relaxed">
{t(`services.items.${key}.description`)}
</p>
</motion.article>
))}
</div>
</div>
</section>
);
}
export function SectionHeader({
eyebrow,
title,
subtitle,
align = 'center',
}: {
eyebrow?: string;
title: string;
subtitle?: string;
align?: 'center' | 'left';
}) {
const alignClass =
align === 'center' ? 'mx-auto text-center' : 'text-left';
return (
<div className={`max-w-2xl ${alignClass}`}>
{eyebrow && (
<span className="text-xs font-bold uppercase tracking-[0.18em] text-primary">
{eyebrow}
</span>
)}
<h2 className="heading text-balance mt-3 text-3xl md:text-4xl">{title}</h2>
{subtitle && (
<p className="mt-4 text-fg-muted text-balance md:text-lg">{subtitle}</p>
)}
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
import { Moon, Sun } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function ThemeToggle({
isDark,
onToggle,
className = '',
}: {
isDark: boolean;
onToggle: () => void;
className?: string;
}) {
const { t } = useTranslation();
return (
<button
type="button"
onClick={onToggle}
className={[
'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-border bg-bg text-fg transition-colors hover:bg-bg-muted',
className,
].join(' ')}
aria-label={isDark ? t('theme.switchToLight') : t('theme.switchToDark')}
>
{isDark ? (
<Sun className="h-5 w-5" aria-hidden="true" />
) : (
<Moon className="h-5 w-5" aria-hidden="true" />
)}
</button>
);
}
+62
View File
@@ -0,0 +1,62 @@
import { motion } from 'framer-motion';
import {
MessageCircle,
ShieldCheck,
Sparkles,
Target,
type LucideIcon,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SectionHeader } from './Services';
type Key = 'senior' | 'delivery' | 'communication' | 'quality';
const PILLARS: { key: Key; Icon: LucideIcon }[] = [
{ key: 'senior', Icon: Sparkles },
{ key: 'delivery', Icon: Target },
{ key: 'communication', Icon: MessageCircle },
{ key: 'quality', Icon: ShieldCheck },
];
export function WhyUs() {
const { t } = useTranslation();
return (
<section id="whyus" className="section">
<div className="container-rl">
<SectionHeader
eyebrow={t('whyus.eyebrow')}
title={t('whyus.title')}
/>
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{PILLARS.map(({ key, Icon }, index) => (
<motion.div
key={key}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-80px' }}
transition={{
duration: 0.5,
delay: index * 0.07,
ease: [0.4, 0, 0.2, 1],
}}
className="group relative rounded-xl border border-border bg-bg p-6 transition-all duration-300 hover:-translate-y-1 hover:border-primary/50 hover:shadow-md"
>
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary text-primary-fg transition-transform group-hover:scale-110">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<h3 className="heading mt-5 text-lg">
{t(`whyus.items.${key}.title`)}
</h3>
<p className="mt-2 text-sm text-fg-muted leading-relaxed">
{t(`whyus.items.${key}.description`)}
</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
+43
View File
@@ -0,0 +1,43 @@
import { useCallback, useEffect, useState } from 'react';
import {
applyTheme,
getStoredTheme,
readEffectiveDark,
resolveDark,
toggleStoredTheme,
} from '../lib/theme';
export function useTheme(): { isDark: boolean; toggle: () => void } {
const [isDark, setIsDark] = useState(() => readEffectiveDark());
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const sync = () => {
const stored = getStoredTheme();
const dark = resolveDark(stored, mq.matches);
applyTheme(dark);
setIsDark(dark);
};
sync();
const onPreferenceChange = () => {
if (getStoredTheme() !== null) return;
const dark = resolveDark(null, mq.matches);
applyTheme(dark);
setIsDark(dark);
};
mq.addEventListener('change', onPreferenceChange);
return () => mq.removeEventListener('change', onPreferenceChange);
}, []);
const toggle = useCallback(() => {
const nextDark = toggleStoredTheme();
setIsDark(nextDark);
}, []);
return { isDark, toggle };
}
+31
View File
@@ -0,0 +1,31 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
import es from './locales/es.json';
import pt from './locales/pt.json';
export const SUPPORTED_LANGUAGES = ['pt', 'en', 'es'] as const;
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
void i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
pt: { translation: pt },
en: { translation: en },
es: { translation: es },
},
fallbackLng: 'pt',
supportedLngs: SUPPORTED_LANGUAGES,
interpolation: { escapeValue: false },
detection: {
order: ['localStorage', 'navigator', 'htmlTag'],
caches: ['localStorage'],
lookupLocalStorage: 'rivuslab-lang',
},
});
export default i18n;
+135
View File
@@ -0,0 +1,135 @@
{
"nav": {
"services": "Services",
"adaptivework": "AdaptiveWork",
"process": "Process",
"whyus": "Why us",
"contact": "Contact",
"cta": "Get in touch",
"openMenu": "Open menu",
"closeMenu": "Close menu"
},
"theme": {
"switchToLight": "Switch to light theme",
"switchToDark": "Switch to dark theme"
},
"hero": {
"eyebrow": "Software engineering & Planview AdaptiveWork",
"title": "Custom software engineering for your business",
"subtitle": "We are a boutique software studio. We deliver senior teams, maintainable code and specialized support on Planview AdaptiveWork to accelerate your results.",
"ctaPrimary": "Get in touch",
"ctaSecondary": "Explore our services",
"stats": {
"experience": "years of experience",
"projects": "delivered projects",
"uptime": "customer satisfaction"
}
},
"services": {
"eyebrow": "Services",
"title": "End-to-end solutions for your software lifecycle",
"subtitle": "From concept to continuous evolution, we operate as an extension of your team.",
"items": {
"outsourcing": {
"title": "Development Outsourcing",
"description": "On-demand developers and full teams with technical leadership and a delivery mindset."
},
"squads": {
"title": "Dedicated Squads",
"description": "Cross-functional teams (dev, QA, design, PM) embedded with your business."
},
"consulting": {
"title": "Technical Consulting",
"description": "Architecture diagnostics, legacy modernization and technology roadmap definition."
}
}
},
"adaptivework": {
"badge": "Planview Specialists",
"title": "Planview AdaptiveWork support and evolution",
"subtitle": "We help you get the most out of your portfolio management platform with implementation, customization, integrations and training.",
"features": {
"implementation": {
"title": "Implementation & Setup",
"description": "We configure workflows, hierarchies and templates tailored to your process."
},
"customization": {
"title": "Advanced Customization",
"description": "Calculated fields, automations, business rules and custom reports."
},
"integration": {
"title": "Integrations",
"description": "We connect AdaptiveWork with Jira, Azure DevOps, ERPs and internal tools."
},
"training": {
"title": "Training & Support",
"description": "Enablement for PMOs and end users plus continuous support from our team."
}
},
"cta": "Tell me more"
},
"whyus": {
"eyebrow": "Why RivusLab",
"title": "A tech partner — not just a vendor",
"items": {
"senior": {
"title": "Senior teams",
"description": "Experienced engineers that drive technical decisions, not just lines of code."
},
"delivery": {
"title": "Predictable delivery",
"description": "Lean process, clear metrics and short validation cycles."
},
"communication": {
"title": "Direct communication",
"description": "No unnecessary layers: you talk to the people who actually build."
},
"quality": {
"title": "Real quality",
"description": "Testing, code review and observability as part of our daily routine."
}
}
},
"process": {
"eyebrow": "How we work",
"title": "A simple, transparent and iterative process",
"steps": {
"diagnose": {
"title": "Diagnose",
"description": "We understand context, pain points and goals before any proposal."
},
"propose": {
"title": "Propose",
"description": "Scope, team, timeline and budget in a clear and objective proposal."
},
"execute": {
"title": "Execute",
"description": "Incremental delivery with agile rituals and full progress visibility."
},
"evolve": {
"title": "Evolve",
"description": "Post go-live support with maintenance and new features."
}
}
},
"cta": {
"eyebrow": "Let's talk?",
"title": "Share your challenge and we'll design the solution together",
"subtitle": "We reply within 1 business day.",
"emailLabel": "Email",
"email": "contact@rivuslab.com",
"whatsappLabel": "WhatsApp",
"whatsapp": "+55 (00) 00000-0000",
"ctaPrimary": "Send message"
},
"footer": {
"tagline": "Software engineering & Planview AdaptiveWork specialists.",
"navigation": "Navigation",
"company": "Company",
"contact": "Contact",
"rights": "All rights reserved."
},
"lang": {
"switchTo": "Change language"
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"nav": {
"services": "Servicios",
"adaptivework": "AdaptiveWork",
"process": "Proceso",
"whyus": "Por qué nosotros",
"contact": "Contacto",
"cta": "Hablemos",
"openMenu": "Abrir menú",
"closeMenu": "Cerrar menú"
},
"theme": {
"switchToLight": "Activar tema claro",
"switchToDark": "Activar tema oscuro"
},
"hero": {
"eyebrow": "Ingeniería de software & Planview AdaptiveWork",
"title": "Ingeniería de software a la medida de tu negocio",
"subtitle": "Somos una fábrica de software boutique. Entregamos equipos senior, código mantenible y soporte especializado en Planview AdaptiveWork para acelerar tus resultados.",
"ctaPrimary": "Hablemos",
"ctaSecondary": "Conoce nuestros servicios",
"stats": {
"experience": "años de experiencia",
"projects": "proyectos entregados",
"uptime": "satisfacción de clientes"
}
},
"services": {
"eyebrow": "Servicios",
"title": "Soluciones completas para el ciclo de vida de tu software",
"subtitle": "Desde la concepción hasta la evolución continua, somos una extensión de tu equipo.",
"items": {
"outsourcing": {
"title": "Outsourcing de Desarrollo",
"description": "Desarrolladores y equipos completos bajo demanda, con gestión técnica y foco en la entrega."
},
"squads": {
"title": "Squads Dedicados",
"description": "Equipos multidisciplinarios (dev, QA, diseño, PM) trabajando junto a tu negocio."
},
"consulting": {
"title": "Consultoría Técnica",
"description": "Diagnóstico de arquitectura, modernización de legado y definición de roadmap tecnológico."
}
}
},
"adaptivework": {
"badge": "Especialistas Planview",
"title": "Soporte y evolución en Planview AdaptiveWork",
"subtitle": "Sacamos el máximo provecho de tu plataforma de gestión de portafolio con implementación, personalización, integraciones y capacitación.",
"features": {
"implementation": {
"title": "Implementación & Configuración",
"description": "Configuramos flujos, jerarquías y plantillas adaptadas a tu proceso."
},
"customization": {
"title": "Personalización Avanzada",
"description": "Campos calculados, automatizaciones, reglas de negocio e informes a medida."
},
"integration": {
"title": "Integraciones",
"description": "Conectamos AdaptiveWork con Jira, Azure DevOps, ERPs y herramientas internas."
},
"training": {
"title": "Capacitación & Soporte",
"description": "Formación para PMOs y usuarios finales, además de soporte continuo de nuestro equipo."
}
},
"cta": "Quiero saber más"
},
"whyus": {
"eyebrow": "Por qué elegir RivusLab",
"title": "Un socio tecnológico — no un proveedor más",
"items": {
"senior": {
"title": "Equipos senior",
"description": "Profesionales experimentados que aportan decisiones técnicas, no solo líneas de código."
},
"delivery": {
"title": "Entrega predecible",
"description": "Proceso ágil, métricas claras y ciclos cortos de validación."
},
"communication": {
"title": "Comunicación directa",
"description": "Sin capas innecesarias: hablas con quienes realmente construyen."
},
"quality": {
"title": "Calidad real",
"description": "Pruebas, code review y observabilidad como parte del día a día."
}
}
},
"process": {
"eyebrow": "Cómo trabajamos",
"title": "Un proceso simple, transparente e iterativo",
"steps": {
"diagnose": {
"title": "Diagnóstico",
"description": "Entendemos el contexto, los dolores y los objetivos antes de cualquier propuesta."
},
"propose": {
"title": "Propuesta",
"description": "Alcance, equipo, plazo y presupuesto en una propuesta clara y objetiva."
},
"execute": {
"title": "Ejecución",
"description": "Entregas incrementales, con rituales ágiles y total visibilidad del progreso."
},
"evolve": {
"title": "Evolución",
"description": "Acompañamos el producto post go-live con mantenimiento y nuevas funcionalidades."
}
}
},
"cta": {
"eyebrow": "¿Hablamos?",
"title": "Cuéntanos tu desafío y diseñamos la solución contigo",
"subtitle": "Respondemos en hasta 1 día hábil.",
"emailLabel": "Correo",
"email": "contacto@rivuslab.com",
"whatsappLabel": "WhatsApp",
"whatsapp": "+55 (00) 00000-0000",
"ctaPrimary": "Enviar mensaje"
},
"footer": {
"tagline": "Ingeniería de software & especialistas en Planview AdaptiveWork.",
"navigation": "Navegación",
"company": "Empresa",
"contact": "Contacto",
"rights": "Todos los derechos reservados."
},
"lang": {
"switchTo": "Cambiar idioma"
}
}
+135
View File
@@ -0,0 +1,135 @@
{
"nav": {
"services": "Serviços",
"adaptivework": "AdaptiveWork",
"process": "Processo",
"whyus": "Por que nós",
"contact": "Contato",
"cta": "Fale com a gente",
"openMenu": "Abrir menu",
"closeMenu": "Fechar menu"
},
"theme": {
"switchToLight": "Ativar tema claro",
"switchToDark": "Ativar tema escuro"
},
"hero": {
"eyebrow": "Engenharia de software & Planview AdaptiveWork",
"title": "Engenharia de software sob medida para o seu negócio",
"subtitle": "Somos uma fábrica de software boutique. Entregamos times sêniores, código sustentável e suporte especializado em Planview AdaptiveWork para acelerar seus resultados.",
"ctaPrimary": "Fale com a gente",
"ctaSecondary": "Conheça nossos serviços",
"stats": {
"experience": "anos de experiência",
"projects": "projetos entregues",
"uptime": "satisfação dos clientes"
}
},
"services": {
"eyebrow": "Serviços",
"title": "Soluções completas para o ciclo de vida do seu software",
"subtitle": "Da concepção à evolução contínua, atuamos como extensão do seu time.",
"items": {
"outsourcing": {
"title": "Outsourcing de Desenvolvimento",
"description": "Desenvolvedores e times completos alocados sob demanda, com gestão técnica e foco em entrega."
},
"squads": {
"title": "Squads Dedicadas",
"description": "Times multidisciplinares (dev, QA, design, PM) trabalhando lado a lado com o seu negócio."
},
"consulting": {
"title": "Consultoria Técnica",
"description": "Diagnóstico de arquitetura, modernização de legado e definição de roadmap tecnológico."
}
}
},
"adaptivework": {
"badge": "Especialistas Planview",
"title": "Suporte e evolução em Planview AdaptiveWork",
"subtitle": "Tiramos o máximo da sua plataforma de gestão de portfólio com implementação, customização, integrações e treinamento.",
"features": {
"implementation": {
"title": "Implementação & Configuração",
"description": "Configuramos workflows, hierarquias e templates aderentes ao seu processo."
},
"customization": {
"title": "Customização Avançada",
"description": "Campos calculados, automações, regras de negócio e relatórios sob medida."
},
"integration": {
"title": "Integrações",
"description": "Conectamos o AdaptiveWork com Jira, Azure DevOps, ERPs e ferramentas internas."
},
"training": {
"title": "Treinamento & Suporte",
"description": "Capacitação para PMOs e usuários finais e suporte contínuo da nossa equipe."
}
},
"cta": "Quero saber mais"
},
"whyus": {
"eyebrow": "Por que escolher a RivusLab",
"title": "Parceria de tecnologia, não fornecedor",
"items": {
"senior": {
"title": "Times sêniores",
"description": "Profissionais experientes que entregam decisões técnicas, não só linhas de código."
},
"delivery": {
"title": "Entrega previsível",
"description": "Processo enxuto, métricas claras e ciclos curtos de validação."
},
"communication": {
"title": "Comunicação direta",
"description": "Sem camadas desnecessárias: você fala com quem realmente constrói."
},
"quality": {
"title": "Qualidade de verdade",
"description": "Testes, code review e observabilidade como parte do dia a dia."
}
}
},
"process": {
"eyebrow": "Como trabalhamos",
"title": "Um processo simples, transparente e iterativo",
"steps": {
"diagnose": {
"title": "Diagnóstico",
"description": "Entendemos o contexto, dores e objetivos antes de qualquer proposta."
},
"propose": {
"title": "Proposta",
"description": "Escopo, time, prazo e orçamento em uma proposta clara e objetiva."
},
"execute": {
"title": "Execução",
"description": "Entregas incrementais, com rituais ágeis e visibilidade total do progresso."
},
"evolve": {
"title": "Evolução",
"description": "Acompanhamos o produto pós go-live com manutenção e novas features."
}
}
},
"cta": {
"eyebrow": "Vamos conversar?",
"title": "Conte seu desafio e desenhamos a solução com você",
"subtitle": "Respondemos em até 1 dia útil.",
"emailLabel": "E-mail",
"email": "contato@rivuslab.com",
"whatsappLabel": "WhatsApp",
"whatsapp": "+55 (00) 00000-0000",
"ctaPrimary": "Enviar mensagem"
},
"footer": {
"tagline": "Engenharia de software & especialistas Planview AdaptiveWork.",
"navigation": "Navegação",
"company": "Empresa",
"contact": "Contato",
"rights": "Todos os direitos reservados."
},
"lang": {
"switchTo": "Mudar idioma"
}
}
+108
View File
@@ -0,0 +1,108 @@
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
@import '@fontsource/inter/600.css';
@import '@fontsource/inter/700.css';
@import '@fontsource/inter/800.css';
@import './styles/theme.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@apply bg-bg text-fg font-sans;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
::selection {
background: rgb(var(--color-primary));
color: rgb(var(--color-primary-fg));
}
*:focus-visible {
outline: 2px solid rgb(var(--color-primary));
outline-offset: 3px;
border-radius: var(--radius-sm);
}
}
@layer components {
.container-rl {
@apply mx-auto w-full max-w-container px-gutter;
}
.section {
@apply py-section;
}
.btn {
@apply inline-flex items-center justify-center gap-2 rounded-md px-5 py-3
font-semibold text-sm transition-all duration-200 ease-smooth
focus-visible:outline-none disabled:opacity-50 disabled:pointer-events-none;
}
.btn-primary {
@apply btn bg-primary text-primary-fg hover:brightness-95 hover:-translate-y-0.5
shadow-sm hover:shadow-md;
}
.btn-secondary {
@apply btn bg-secondary text-secondary-fg hover:brightness-110;
}
.btn-ghost {
@apply btn bg-transparent text-fg hover:bg-bg-muted border border-border;
}
.badge {
@apply inline-flex items-center gap-1.5 rounded-full bg-primary-soft
px-3 py-1 text-xs font-semibold;
color: rgb(var(--color-badge-fg) / 1);
}
.card {
@apply rounded-xl border border-border bg-bg p-6 shadow-sm
transition-all duration-300 ease-smooth;
}
.card-hover {
@apply hover:-translate-y-1 hover:shadow-lg;
}
.card-hover:hover {
border-color: rgb(var(--color-primary) / 0.4);
}
.heading {
@apply font-display font-bold tracking-tight text-fg;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.bg-grid {
background-image:
linear-gradient(to right, rgb(var(--color-border)) 1px, transparent 1px),
linear-gradient(to bottom, rgb(var(--color-border)) 1px, transparent 1px);
background-size: 32px 32px;
}
.bg-radial-fade {
background: radial-gradient(
ellipse at top,
rgb(var(--color-primary-soft)) 0%,
transparent 60%
);
}
}
+7
View File
@@ -0,0 +1,7 @@
/** Public assets in `public/` — filenames must match deployed files. */
const LOGO_LIGHT = '/logo-light.png';
const LOGO_DARK = '/logo-dark.png';
export function rivusLogoSrc(isDark: boolean): string {
return isDark ? LOGO_DARK : LOGO_LIGHT;
}
+14
View File
@@ -0,0 +1,14 @@
export type SectionId =
| 'services'
| 'adaptivework'
| 'process'
| 'whyus'
| 'contact';
export const NAV_SECTIONS: { id: SectionId; labelKey: `nav.${string}` }[] = [
{ id: 'services', labelKey: 'nav.services' },
{ id: 'adaptivework', labelKey: 'nav.adaptivework' },
{ id: 'process', labelKey: 'nav.process' },
{ id: 'whyus', labelKey: 'nav.whyus' },
{ id: 'contact', labelKey: 'nav.contact' },
];
+30
View File
@@ -0,0 +1,30 @@
export const THEME_STORAGE_KEY = 'rivus-theme';
export type StoredTheme = 'light' | 'dark';
export function getStoredTheme(): StoredTheme | null {
const raw = localStorage.getItem(THEME_STORAGE_KEY);
if (raw === 'light' || raw === 'dark') return raw;
return null;
}
export function resolveDark(stored: string | null, prefersDark: boolean): boolean {
if (stored === 'dark') return true;
if (stored === 'light') return false;
return prefersDark;
}
export function applyTheme(isDark: boolean): void {
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
}
export function readEffectiveDark(): boolean {
return document.documentElement.dataset.theme === 'dark';
}
export function toggleStoredTheme(): boolean {
const nextDark = !readEffectiveDark();
localStorage.setItem(THEME_STORAGE_KEY, nextDark ? 'dark' : 'light');
applyTheme(nextDark);
return nextDark;
}
+15
View File
@@ -0,0 +1,15 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './i18n';
import './index.css';
const root = document.getElementById('root');
if (!root) throw new Error('Root element not found');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);
+82
View File
@@ -0,0 +1,82 @@
/* =============================================================================
THEME TOKENS — RivusLab
-----------------------------------------------------------------------------
This is the ONLY file you need to edit to change the brand identity.
All tokens are exposed via CSS variables and consumed by Tailwind (see
tailwind.config.ts) so changes here propagate to every component.
Colors are stored as "R G B" triplets so Tailwind can apply opacity
modifiers (e.g. `bg-primary/40`).
============================================================================= */
:root {
/* ─── BRAND COLORS (R G B) ───────────────────────────────────────────────── */
--color-primary: 35 196 192; /* #23C4C0 — teal */
--color-primary-fg: 15 58 108; /* #0F3A6C — navy on primary */
--color-primary-soft: 230 249 248; /* #E6F9F8 — tint of primary */
--color-secondary: 15 58 108; /* #0F3A6C — navy */
--color-secondary-fg: 255 255 255;
--color-accent: 35 196 192;
/* ─── SURFACES ────────────────────────────────────────────────────────────── */
--color-bg: 255 255 255;
--color-bg-muted: 245 248 251; /* #F5F8FB */
--color-bg-inverse: 15 58 108;
/* ─── TEXT ───────────────────────────────────────────────────────────────── */
--color-fg: 15 27 45; /* #0F1B2D */
--color-fg-muted: 90 107 130; /* #5A6B82 */
--color-fg-inverse: 255 255 255;
/* ─── BORDERS ────────────────────────────────────────────────────────────── */
--color-border: 227 233 241; /* #E3E9F1 */
--color-border-strong: 201 212 226; /* #C9D4E2 */
/* ─── BADGE (pill labels on primary-soft) ─────────────────────────────────── */
--color-badge-fg: 15 58 108; /* navy — readable on light primary-soft */
/* ─── TYPOGRAPHY ─────────────────────────────────────────────────────────── */
--font-sans: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
--font-display: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
/* ─── RADIUS ─────────────────────────────────────────────────────────────── */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-2xl: 32px;
--radius-full: 9999px;
/* ─── SPACING / LAYOUT ───────────────────────────────────────────────────── */
--space-section-y: 6rem; /* vertical padding for full sections */
--space-container: 1.25rem; /* horizontal padding on small screens */
--container-max: 1200px; /* max content width */
/* ─── SHADOWS ────────────────────────────────────────────────────────────── */
--shadow-sm: 0 1px 2px rgb(15 27 45 / 0.06);
--shadow-md: 0 6px 24px rgb(15 27 45 / 0.08);
--shadow-lg: 0 20px 60px rgb(15 27 45 / 0.12);
--shadow-glow: 0 0 0 6px rgb(35 196 192 / 0.18);
}
/* ─── DARK THEME (optional) ──────────────────────────────────────────────────
Activate by adding `data-theme="dark"` on <html>.
─────────────────────────────────────────────────────────────────────────── */
[data-theme='dark'] {
--color-bg: 11 22 38; /* #0B1626 */
--color-bg-muted: 16 32 54; /* #102036 */
--color-bg-inverse: 35 196 192;
--color-fg: 242 245 250; /* #F2F5FA */
--color-fg-muted: 147 162 184; /* #93A2B8 */
--color-fg-inverse: 15 27 45;
--color-border: 27 46 72; /* #1B2E48 */
--color-border-strong: 42 67 102; /* #2A4366 */
--color-primary-soft: 18 50 78; /* tinted dark surface */
--color-badge-fg: 147 220 218; /* light teal — readable on dark primary-soft */
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />