first commit
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.vite
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -1,96 +1,112 @@
|
|||||||
# RivusLab — Landing Page
|
# RivusLab — Landing Page
|
||||||
|
|
||||||
Landing page institucional da RivusLab.
|
Landing page institucional da **RivusLab**: engenharia de software sob medida e especialização em Planview AdaptiveWork.
|
||||||
Stack: **Vite + React + TypeScript + Tailwind CSS** com i18n (PT/EN/ES) e tokens de design centralizados em um único arquivo CSS.
|
|
||||||
|
**Stack:** Vite · React 18 · TypeScript · Tailwind CSS · i18next (PT / EN / ES) · Framer Motion
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Rodando o projeto
|
## Início rápido
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run dev # ambiente de desenvolvimento (http://localhost:5173)
|
npm run dev # http://localhost:5173
|
||||||
npm run build # build de produção em ./dist
|
npm run build # saída em ./dist
|
||||||
npm run preview # serve o build de produção localmente
|
npm run preview # preview do build de produção
|
||||||
npm run lint # type-check com TypeScript
|
npm run lint # type-check (tsc --noEmit)
|
||||||
```
|
```
|
||||||
|
|
||||||
Requer **Node 18+** (testado em Node 24).
|
Requer **Node 18+**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Personalizando a identidade visual
|
## Funcionalidades
|
||||||
|
|
||||||
> Tudo o que define a marca (cores, tipografia, raios, espaçamentos, sombras) está em **um único arquivo**:
|
| Recurso | Comportamento |
|
||||||
> [`src/styles/theme.css`](src/styles/theme.css)
|
| -------- | ------------- |
|
||||||
|
| **Idiomas** | PT, EN e ES; detecção pelo navegador; persistência em `localStorage` (`rivuslab-lang`) |
|
||||||
|
| **Tema claro/escuro** | Segue o sistema por padrão; botão na navbar alterna e grava `light` ou `dark` em `rivus-theme` |
|
||||||
|
| **Logos** | `logo-light.png` no tema claro, `logo-dark.png` no escuro (navbar e footer) |
|
||||||
|
| **Design tokens** | Cores, tipografia, raios e sombras centralizados em `src/styles/theme.css` |
|
||||||
|
| **Acessibilidade** | Skip link, `aria-label`, foco visível, contraste pensado para AA |
|
||||||
|
|
||||||
Basta editar os valores das CSS variables e a mudança propaga para o site inteiro.
|
---
|
||||||
|
|
||||||
|
## Personalizar a marca
|
||||||
|
|
||||||
|
Edite **[`src/styles/theme.css`](src/styles/theme.css)** — é o único arquivo de tokens. As variáveis são consumidas pelo Tailwind em [`tailwind.config.ts`](tailwind.config.ts).
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
:root {
|
||||||
--color-primary: 35 196 192; /* #23C4C0 — teal */
|
--color-primary: 35 196 192; /* #23C4C0 */
|
||||||
--color-secondary: 15 58 108; /* #0F3A6C — navy */
|
--color-secondary: 15 58 108; /* #0F3A6C */
|
||||||
--radius-md: 10px;
|
--color-badge-fg: 15 58 108; /* texto dos badges (tema claro) */
|
||||||
--space-section-y: 6rem;
|
/* ... */
|
||||||
/* ...etc */
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] {
|
||||||
|
--color-bg: 11 22 38;
|
||||||
|
--color-badge-fg: 147 220 218; /* texto dos badges (tema escuro) */
|
||||||
|
/* ... */
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ Cores são armazenadas como triplets **R G B** (sem vírgulas, sem `rgb()`).
|
> Cores usam triplets **R G B** (sem `rgb()`), para o Tailwind aplicar opacidade (`bg-primary/40`, etc.).
|
||||||
> Esse formato permite que o Tailwind aplique opacidade automaticamente, ex.: `bg-primary/40`.
|
> Ex.: `#23C4C0` → `35 196 192`.
|
||||||
|
|
||||||
Para encontrar o triplet de um hex, basta converter (ex.: `#23C4C0` → `35 196 192`).
|
### Tema claro / escuro
|
||||||
|
|
||||||
### Tema escuro (opcional)
|
- Ativação: `data-theme="light"` ou `data-theme="dark"` no `<html>`.
|
||||||
|
- Script inline em [`index.html`](index.html) evita flash na primeira carga.
|
||||||
|
- Lógica React: [`src/lib/theme.ts`](src/lib/theme.ts), [`src/hooks/useTheme.ts`](src/hooks/useTheme.ts), botão [`src/components/ThemeToggle.tsx`](src/components/ThemeToggle.tsx).
|
||||||
|
|
||||||
Já vem incluído como `[data-theme="dark"]`.
|
| Situação | Resultado |
|
||||||
Para ativar, defina o atributo no `<html>`:
|
| -------- | --------- |
|
||||||
|
| Sem valor em `rivus-theme` | Segue `prefers-color-scheme` do SO |
|
||||||
```html
|
| Usuário clicou no toggle | Grava `light` ou `dark` e mantém a escolha |
|
||||||
<html lang="pt-BR" data-theme="dark">
|
| SO muda o tema | Só atualiza o site se não houver preferência salva |
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Editando os textos
|
## Textos (i18n)
|
||||||
|
|
||||||
Os textos vivem em arquivos JSON simples, um por idioma:
|
Um JSON por idioma:
|
||||||
|
|
||||||
- [`src/i18n/locales/pt.json`](src/i18n/locales/pt.json) — Português (padrão)
|
- [`src/i18n/locales/pt.json`](src/i18n/locales/pt.json) — padrão / fallback
|
||||||
- [`src/i18n/locales/en.json`](src/i18n/locales/en.json) — Inglês
|
- [`src/i18n/locales/en.json`](src/i18n/locales/en.json)
|
||||||
- [`src/i18n/locales/es.json`](src/i18n/locales/es.json) — Espanhol
|
- [`src/i18n/locales/es.json`](src/i18n/locales/es.json)
|
||||||
|
|
||||||
Para adicionar uma nova chave de texto, edite os três arquivos com a mesma estrutura e referencie via `t('caminho.da.chave')` no componente.
|
Use a mesma chave nos três arquivos e no componente: `t('secao.chave')`.
|
||||||
|
|
||||||
O idioma padrão é detectado a partir do navegador e é persistido em `localStorage` (chave `rivuslab-lang`).
|
Chaves de tema (exemplo): `theme.switchToLight`, `theme.switchToDark`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Trocando os logos
|
## Logos e favicon
|
||||||
|
|
||||||
Os logos estão em `public/`:
|
Coloque os arquivos em **`public/`**:
|
||||||
|
|
||||||
| Arquivo | Onde aparece |
|
| Arquivo | Uso |
|
||||||
| ----------------------- | ---------------------------------- |
|
| ------- | --- |
|
||||||
| `public/logo-light.png` | Navbar e footer (fundo claro) |
|
| `logo-light.png` | Navbar e footer no **tema claro** |
|
||||||
| `public/logo-dark.png` | Reservado para variações em escuro |
|
| `logo-dark.png` | Navbar e footer no **tema escuro** |
|
||||||
| `public/favicon.svg` | Aba do navegador |
|
| `favicon.svg` | Ícone da aba |
|
||||||
|
|
||||||
Para trocar, basta substituir os arquivos mantendo os nomes (ou ajustar os `src` nos componentes `Navbar.tsx` e `Footer.tsx`).
|
Os caminhos estão centralizados em [`src/lib/logos.ts`](src/lib/logos.ts). Para outros nomes de arquivo, altere só esse módulo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Estrutura
|
## Estrutura do projeto
|
||||||
|
|
||||||
```
|
```
|
||||||
HomePage/
|
HomePage/
|
||||||
├── public/
|
├── public/
|
||||||
│ ├── logo-light.png ← logo principal (fundo claro)
|
│ ├── logo-light.png
|
||||||
│ ├── logo-dark.png ← logo para fundos escuros
|
│ ├── logo-dark.png
|
||||||
│ └── favicon.svg
|
│ └── favicon.svg
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── Navbar.tsx
|
│ │ ├── Navbar.tsx # menu + idioma + tema
|
||||||
│ │ ├── Hero.tsx
|
│ │ ├── Hero.tsx
|
||||||
│ │ ├── Services.tsx
|
│ │ ├── Services.tsx
|
||||||
│ │ ├── Adaptivework.tsx
|
│ │ ├── Adaptivework.tsx
|
||||||
@@ -98,50 +114,79 @@ HomePage/
|
|||||||
│ │ ├── Process.tsx
|
│ │ ├── Process.tsx
|
||||||
│ │ ├── CTA.tsx
|
│ │ ├── CTA.tsx
|
||||||
│ │ ├── Footer.tsx
|
│ │ ├── Footer.tsx
|
||||||
│ │ └── LanguageSwitcher.tsx
|
│ │ ├── LanguageSwitcher.tsx
|
||||||
|
│ │ └── ThemeToggle.tsx
|
||||||
|
│ ├── hooks/
|
||||||
|
│ │ └── useTheme.ts
|
||||||
│ ├── i18n/
|
│ ├── i18n/
|
||||||
│ │ ├── index.ts ← setup do i18next
|
│ │ ├── index.ts
|
||||||
│ │ └── locales/{pt,en,es}.json
|
│ │ └── locales/{pt,en,es}.json
|
||||||
│ ├── lib/
|
│ ├── lib/
|
||||||
│ │ └── sections.ts ← ids/labels das seções de navegação
|
│ │ ├── logos.ts
|
||||||
|
│ │ ├── sections.ts # ids e labels da navegação
|
||||||
|
│ │ └── theme.ts
|
||||||
│ ├── styles/
|
│ ├── styles/
|
||||||
│ │ └── theme.css ← ⭐ arquivo único de customização
|
│ │ └── theme.css # tokens de design
|
||||||
│ ├── App.tsx
|
│ ├── App.tsx
|
||||||
│ ├── index.css
|
│ ├── index.css # componentes (.badge, .btn, …)
|
||||||
│ └── main.tsx
|
│ └── main.tsx
|
||||||
├── index.html
|
├── index.html
|
||||||
├── tailwind.config.ts ← consome variáveis de theme.css
|
├── tailwind.config.ts
|
||||||
├── postcss.config.js
|
|
||||||
├── vite.config.ts
|
├── vite.config.ts
|
||||||
└── tsconfig.json
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Seções da página
|
## Seções da página
|
||||||
|
|
||||||
1. **Hero** — proposta de valor + CTAs + visual com mini "editor"
|
1. **Hero** — proposta de valor e CTAs
|
||||||
2. **Services** — Outsourcing, Squads, Consultoria
|
2. **Services** — outsourcing, consultoria
|
||||||
3. **AdaptiveWork** — destaque para a especialização em Planview AdaptiveWork
|
3. **AdaptiveWork** — especialização Planview
|
||||||
4. **Why Us** — 4 pilares de diferenciação
|
4. **Why Us** — diferenciais
|
||||||
5. **Process** — 4 etapas (Diagnóstico → Proposta → Execução → Evolução)
|
5. **Process** — etapas do trabalho
|
||||||
6. **CTA** — bloco escuro com email/WhatsApp
|
6. **CTA** — contato (e-mail / WhatsApp)
|
||||||
7. **Footer** — navegação, contato, redes sociais
|
7. **Footer** — links, redes e copyright
|
||||||
|
|
||||||
|
Navegação por âncoras (`#services`, `#contact`, etc.) definidas em [`src/lib/sections.ts`](src/lib/sections.ts).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dicas
|
## Classes utilitárias (CSS)
|
||||||
|
|
||||||
- O menu mobile abre como drawer abaixo da navbar e bloqueia o scroll do body.
|
Definidas em [`src/index.css`](src/index.css):
|
||||||
- Todos os ícones vêm de [`lucide-react`](https://lucide.dev) — basta importar e usar.
|
|
||||||
- Animações de entrada usam `framer-motion` com `whileInView` (`once: true`) para não repetir.
|
- `.container-rl` — largura máxima e padding horizontal
|
||||||
- Acessibilidade: skip-link, `aria-label`, contrastes AA e foco visível.
|
- `.btn-primary` / `.btn-secondary` / `.btn-ghost`
|
||||||
|
- `.badge` — pills (cor do texto via `--color-badge-fg`)
|
||||||
|
- `.card` / `.card-hover`
|
||||||
|
- `.heading` — títulos de seção
|
||||||
|
|
||||||
|
Ícones: [Lucide React](https://lucide.dev). Animações: `framer-motion` com `whileInView` (`once: true`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Próximos passos sugeridos
|
## Deploy
|
||||||
|
|
||||||
- Integrar formulário de contato a um backend (ex.: Resend, Formspree).
|
```bash
|
||||||
- Adicionar página de cases / portfolio.
|
npm run build
|
||||||
- Configurar SEO com meta tags por idioma.
|
```
|
||||||
- Adicionar analytics (Plausible / Umami).
|
|
||||||
|
Publique o conteúdo de **`dist/`** (Netlify, Vercel, S3 + CloudFront, nginx estático, etc.).
|
||||||
|
Configure fallback para `index.html` se o host usar SPA routing (esta landing é single-page com âncoras).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Próximos passos (sugestões)
|
||||||
|
|
||||||
|
- Formulário de contato (Resend, Formspree, API própria)
|
||||||
|
- Página de cases / portfólio
|
||||||
|
- Meta tags e Open Graph por idioma
|
||||||
|
- Analytics (Plausible, Umami, etc.)
|
||||||
|
- Atualizar `theme-color` no `<meta>` conforme o tema ativo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Projeto privado — uso interno RivusLab.
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0F3A6C" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="RivusLab — Outsourcing de desenvolvimento de software e especialistas em Planview AdaptiveWork."
|
||||||
|
/>
|
||||||
|
<title>RivusLab — Engenharia de software sob medida</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var k = 'rivus-theme';
|
||||||
|
var stored = localStorage.getItem(k);
|
||||||
|
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
var dark = stored === 'dark' || (stored !== 'light' && prefersDark);
|
||||||
|
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||||
|
} catch (_) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2834
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "rivuslab-homepage",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.0.18",
|
||||||
|
"framer-motion": "^11.3.19",
|
||||||
|
"i18next": "^23.12.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
|
"lucide-react": "^0.418.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-i18next": "^15.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"postcss": "^8.4.40",
|
||||||
|
"tailwindcss": "^3.4.7",
|
||||||
|
"typescript": "^5.5.4",
|
||||||
|
"vite": "^5.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#0F3A6C"/>
|
||||||
|
<path
|
||||||
|
d="M18 50 C 22 36, 28 32, 34 32 C 40 32, 44 34, 44 40 C 44 46, 40 48, 34 48 L 28 48 L 38 56"
|
||||||
|
stroke="#23C4C0"
|
||||||
|
stroke-width="6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M14 22 C 20 18, 26 26, 32 22 C 38 18, 44 26, 50 22"
|
||||||
|
stroke="#23C4C0"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
fill="none"
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 534 B |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
+43
@@ -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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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' },
|
||||||
|
];
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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 */
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const rgb = (variable: string) => `rgb(var(${variable}) / <alpha-value>)`;
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
DEFAULT: rgb('--color-primary'),
|
||||||
|
fg: rgb('--color-primary-fg'),
|
||||||
|
soft: rgb('--color-primary-soft'),
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: rgb('--color-secondary'),
|
||||||
|
fg: rgb('--color-secondary-fg'),
|
||||||
|
},
|
||||||
|
accent: rgb('--color-accent'),
|
||||||
|
bg: {
|
||||||
|
DEFAULT: rgb('--color-bg'),
|
||||||
|
muted: rgb('--color-bg-muted'),
|
||||||
|
inverse: rgb('--color-bg-inverse'),
|
||||||
|
},
|
||||||
|
fg: {
|
||||||
|
DEFAULT: rgb('--color-fg'),
|
||||||
|
muted: rgb('--color-fg-muted'),
|
||||||
|
inverse: rgb('--color-fg-inverse'),
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
DEFAULT: rgb('--color-border'),
|
||||||
|
strong: rgb('--color-border-strong'),
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
fg: rgb('--color-badge-fg'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
sm: 'var(--radius-sm)',
|
||||||
|
DEFAULT: 'var(--radius-md)',
|
||||||
|
md: 'var(--radius-md)',
|
||||||
|
lg: 'var(--radius-lg)',
|
||||||
|
xl: 'var(--radius-xl)',
|
||||||
|
'2xl': 'var(--radius-2xl)',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-sans)'],
|
||||||
|
display: ['var(--font-display)'],
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
sm: 'var(--shadow-sm)',
|
||||||
|
md: 'var(--shadow-md)',
|
||||||
|
lg: 'var(--shadow-lg)',
|
||||||
|
glow: 'var(--shadow-glow)',
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
container: 'var(--container-max)',
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
section: 'var(--space-section-y)',
|
||||||
|
gutter: 'var(--space-container)',
|
||||||
|
},
|
||||||
|
transitionTimingFunction: {
|
||||||
|
smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/adaptivework.tsx","./src/components/cta.tsx","./src/components/footer.tsx","./src/components/hero.tsx","./src/components/languageswitcher.tsx","./src/components/navbar.tsx","./src/components/process.tsx","./src/components/services.tsx","./src/components/themetoggle.tsx","./src/components/whyus.tsx","./src/hooks/usetheme.ts","./src/i18n/index.ts","./src/lib/logos.ts","./src/lib/sections.ts","./src/lib/theme.ts"],"version":"5.9.3"}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfig;
|
||||||
|
export default _default;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user