feat(all-deployment)

This commit is contained in:
Emmanuel Rojas committed 2026-08-09 23:08:38 -05:00
1 parent e261394133
commit 4f0020bc10
39 files changed
+13151 -482

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "Landing",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5173,
"autoPort": true
}
]
}
+28 -2
View File
@@ -1,10 +1,36 @@
<!doctype html>
<html lang="en">
<html lang="es">
<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" />
<title>sitio-rynext</title>
<!-- SEO principal -->
<title>Rynext Technologies | Software Empresarial 100% Mexicano</title>
<meta name="description" content="Rynext Technologies diseña y construye software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en Cancún y toda México." />
<meta name="keywords" content="Rynext, Rynext Technologies, software empresarial México, sistemas a medida, desarrollo de software Cancún, software mexicano, ERP México" />
<meta name="author" content="Rynext Technologies" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://rynext.com.mx/" />
<!-- Open Graph (redes sociales) -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rynext.com.mx/" />
<meta property="og:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
<meta property="og:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
<meta property="og:locale" content="es_MX" />
<meta property="og:site_name" content="Rynext Technologies" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Rynext Technologies | Software Empresarial 100% Mexicano" />
<meta name="twitter:description" content="Diseñamos y construimos software empresarial 100% mexicano. Sistemas a medida, módulos y licencias para empresas en México." />
<!-- Geo -->
<meta name="geo.region" content="MX-ROO" />
<meta name="geo.placename" content="Cancún, Quintana Roo, México" />
<meta name="theme-color" content="#e50914" />
</head>
<body>
<div id="root"></div>
+4663 -6
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -15,6 +15,7 @@
"lucide-react": "^1.28.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router": "^8.3.0",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
@@ -29,6 +30,8 @@
"globals": "^17.7.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.65.0",
"vite": "^8.2.0"
"vite": "^8.2.0",
"vite-plugin-pwa": "^1.3.0",
"workbox-window": "^7.4.1"
}
}
+5985 -1
View File
File diff suppressed because it is too large. Load diff

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 449 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="8d756b956c"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="62b43949b4"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="3ac8341e25"><path d="M 0 648.902344 L 1500 648.902344 L 1500 851.402344 L 0 851.402344 Z M 0 648.902344 " clip-rule="nonzero"/></clipPath><mask id="60fb1d792f"><g filter="url(#8d756b956c)"><g filter="url(#62b43949b4)" transform="matrix(1.282226, 0, 0, 1.282471, -234.697857, -90.088596)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 90 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2000" zoomAndPan="magnify" viewBox="0 0 1500 1499.999933" height="2000" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><filter x="0%" y="0%" width="100%" height="100%" id="c829cef1fa"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0" color-interpolation-filters="sRGB"/></filter><filter x="0%" y="0%" width="100%" height="100%" id="22a7985332"><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.2126 0.7152 0.0722 0 0" color-interpolation-filters="sRGB"/></filter><clipPath id="63848e8728"><path d="M 0 694.328125 L 1500 694.328125 L 1500 865.328125 L 0 865.328125 Z M 0 694.328125 " clip-rule="nonzero"/></clipPath><mask id="17db18eb1d"><g filter="url(#c829cef1fa)"><g filter="url(#22a7985332)" transform="matrix(1.20166, 0, 0, 1.201904, -172.876855, -40.289897)"><image x="0" y="0" width="1536" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 245 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="18" y="18" width="124" height="124" rx="24" fill="#EAF6FF"/>
<path d="M54 46H106C112.627 46 118 51.3726 118 58V68C118 74.6274 112.627 80 106 80H54C47.3726 80 42 74.6274 42 68V58C42 51.3726 47.3726 46 54 46Z" fill="#0077C2" opacity="0.16"/>
<path d="M54 46H106C112.627 46 118 51.3726 118 58V68C118 74.6274 112.627 80 106 80H54C47.3726 80 42 74.6274 42 68V58C42 51.3726 47.3726 46 54 46Z" stroke="#0077C2" stroke-width="6"/>
<path d="M42 58C42 64.6274 47.3726 70 54 70H106C112.627 70 118 64.6274 118 58" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<path d="M42 68C42 74.6274 47.3726 80 54 80H106C112.627 80 118 74.6274 118 68" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<path d="M42 78C42 84.6274 47.3726 90 54 90H106C112.627 90 118 84.6274 118 78" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<path d="M42 88C42 94.6274 47.3726 100 54 100H106C112.627 100 118 94.6274 118 88" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<path d="M54 108H106" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<path d="M54 118H92" stroke="#0077C2" stroke-width="6" stroke-linecap="round"/>
<text x="80" y="112" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="20" font-weight="700" fill="#0077C2">SQL</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://rynext.com.mx/sitemap.xml
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://rynext.com.mx/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://rynext.com.mx/sobre-nosotros</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rynext.com.mx/productos</loc>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://rynext.com.mx/blog</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
</urlset>
+27 -35
View File
@@ -1,48 +1,40 @@
import { useState } from 'react';
import { Routes, Route } from 'react-router';
import { Navbar } from './components/Navbar';
import { Hero } from './components/Hero';
import { Services } from './components/Services';
import { Process } from './components/Process';
import { PlatformAdvantage } from './components/PlatformAdvantage';
import { CtaBanner } from './components/CtaBanner';
import { Footer } from './components/Footer';
import { ContactModal } from './components/ContactModal';
import { FloatingButtons } from './components/FloatingButtons';
import { CookieBanner } from './components/CookieBanner';
import { Home } from './pages/Home';
import { SobreNosotros } from './pages/SobreNosotros';
import { Productos } from './pages/Productos';
import { Blog } from './pages/Blog';
const CONTACT_EMAIL = '[email protected]';
const openContact = () => {
window.location.href = `mailto:${CONTACT_EMAIL}?subject=Hola%2C%20tengo%20un%20proyecto`;
};
export default function App() {
const [isNavyBg, setIsNavyBg] = useState<boolean>(false);
const [isContactOpen, setIsContactOpen] = useState<boolean>(false);
const isNavyBg = false;
const setIsNavyBg = (_: boolean | ((prev: boolean) => boolean)) => {};
return (
<div className={`min-h-screen overflow-x-hidden transition-colors duration-500 font-sans ${
isNavyBg ? 'bg-[#071b49] text-white' : 'bg-[#f4f9ff] text-brand-black'
}`}>
<Navbar
isNavyBg={isNavyBg}
setIsNavyBg={setIsNavyBg}
onOpenContact={() => setIsContactOpen(true)}
/>
<div
className="min-h-screen overflow-x-hidden font-sans transition-colors duration-500"
style={{ background: '#f9f9f7' }}
>
<Navbar onOpenContact={openContact} />
<main>
<Hero
isNavyBg={isNavyBg}
onOpenContact={() => setIsContactOpen(true)}
/>
<Services isNavyBg={isNavyBg} />
<Process isNavyBg={isNavyBg} />
<PlatformAdvantage isNavyBg={isNavyBg} />
<CtaBanner
isNavyBg={isNavyBg}
onOpenContact={() => setIsContactOpen(true)}
/>
</main>
<Routes>
<Route path="/" element={<Home isNavyBg={isNavyBg} onOpenContact={openContact} />} />
<Route path="/sobre-nosotros" element={<SobreNosotros />} />
<Route path="/productos" element={<Productos />} />
<Route path="/blog" element={<Blog />} />
</Routes>
<Footer isNavyBg={isNavyBg} />
<ContactModal
isOpen={isContactOpen}
onClose={() => setIsContactOpen(false)}
isNavyBg={isNavyBg}
/>
<FloatingButtons isNavyBg={isNavyBg} setIsNavyBg={setIsNavyBg} />
<CookieBanner isNavyBg={isNavyBg} />
</div>
);
}
+130
View File
@@ -0,0 +1,130 @@
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
const layers = [
{
id: 'presentation',
num: '01',
title: 'PRESENTACIÓN',
subtitle: 'La interfaz donde opera el usuario',
description:
'Portales de administración, apps web, paneles de monitoreo y puntos de venta. Construidos para adopción real, no para demos.',
tech: ['React', 'Next.js', 'TypeScript', 'Tailwind', 'Mobile Web'],
},
{
id: 'logic',
num: '02',
title: 'LÓGICA DE NEGOCIO',
subtitle: 'Las reglas que gobiernan la operación',
description:
'Validaciones, cálculos, flujos de aprobación y orquestación de procesos. El código que representa cómo funciona tu negocio.',
tech: ['Node.js', 'Python', 'REST APIs', 'GraphQL', 'Microservicios'],
},
{
id: 'data',
num: '03',
title: 'DATOS',
subtitle: 'Persistencia y acceso a la información',
description:
'Modelo de datos diseñado para tu operación. Bases relacionales, NoSQL, caché y búsqueda de texto. Una sola fuente de verdad.',
tech: ['PostgreSQL', 'MongoDB', 'Redis', 'Elasticsearch', 'Prisma'],
},
{
id: 'infra',
num: '04',
title: 'INFRAESTRUCTURA',
subtitle: 'La base que soporta el sistema',
description:
'Cloud, contenedores, pipelines de CI/CD y observabilidad. Diseñada para que el sistema esté siempre disponible y sea fácil de operar.',
tech: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Monitoring'],
},
{
id: 'integrations',
num: '05',
title: 'INTEGRACIONES',
subtitle: 'Conexión con el ecosistema externo',
description:
'ERPs, sistemas de pago, servicios de mensajería, proveedores y APIs externas. El sistema habla con todo lo que el negocio necesita.',
tech: ['Stripe', 'SAP / Odoo', 'OAuth 2.0', 'Webhooks', 'SMS / Email'],
},
];
export const Anatomy = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : '#dddbd5';
return (
<section
id="anatomy"
aria-labelledby="anatomy-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
{/* Header */}
<div className="mb-16">
<h2
id="anatomy-heading"
className="text-headline"
style={{ maxWidth: '24ch' }}
>
Todo sistema tiene<br /> las mismas capas.
</h2>
</div>
{/* Layers — always visible */}
<div style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}>
{layers.map((layer, i) => (
<motion.div
key={layer.id}
initial={{ opacity: 0, y: 14 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: i * 0.07 }}
style={{
borderBottom: i < layers.length - 1 ? `1px solid ${border}` : 'none',
display: 'grid',
gridTemplateColumns: '120px 1fr',
gap: '0 32px',
alignItems: 'start',
padding: '32px 32px',
}}
>
{/* Number */}
<span
className="font-display font-black"
style={{ fontSize: 64, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
>
{layer.num}
</span>
{/* Title + description */}
<div style={{ paddingTop: 8 }}>
<p
className="font-display font-black mb-1"
style={{ fontSize: 13, letterSpacing: '0.04em', color: text }}
>
{layer.title}
</p>
<p className="text-xs mb-3" style={{ color: sub }}>
{layer.subtitle}
</p>
<p className="text-sm leading-relaxed" style={{ color: sub, maxWidth: '60ch' }}>
{layer.description}
</p>
</div>
</motion.div>
))}
</div>
</motion.div>
</section>
);
};
+29
View File
@@ -0,0 +1,29 @@
import { type ReactNode } from 'react';
interface ButtonProps {
children: ReactNode;
onClick?: () => void;
href?: string;
variant?: 'primary' | 'secondary';
className?: string;
isNavyBg?: boolean;
}
export const Button = ({ children, onClick, href, variant = 'primary', className = '', isNavyBg = false }: ButtonProps) => {
const base = 'inline-flex items-center gap-3 px-8 py-4 text-base font-bold transition hover:scale-[1.02] cursor-pointer';
const styles = {
primary: 'bg-brand-red text-white shadow-lg shadow-red-500/20 hover:bg-[#b60014]',
secondary: isNavyBg
? 'border border-white/15 bg-white/5 text-white hover:bg-white/10'
: 'border border-brand-navy/15 text-brand-navy hover:border-brand-blue hover:text-brand-blue',
};
const cls = `${base} ${styles[variant]} ${className}`;
if (href) {
return <a href={href} className={cls}>{children}</a>;
}
return <button onClick={onClick} className={cls}>{children}</button>;
};
+9 -5
View File
@@ -2,7 +2,7 @@ import { X, Mail } from 'lucide-react';
import type { ModalProps } from '../types';
const supportEmail = '[email protected]';
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta RyNext')}&body=${encodeURIComponent('Hola RyNext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
const mailToHref = `mailto:${supportEmail}?subject=${encodeURIComponent('Consulta Rynext')}&body=${encodeURIComponent('Hola Rynext,\n\nQuiero agendar una conversación sobre un proyecto.\n\nGracias.')}`;
export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
if (!isOpen) return null;
@@ -28,7 +28,7 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-brand-blue/20 text-brand-blue">
<Mail size={24} />
</div>
<h3 className="mb-2 text-2xl font-bold">Ponte en Contacto</h3>
<h3 className="mb-2 text-2xl font-bold">Hablemos de tu proyecto</h3>
<p className={`mb-6 text-sm ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
Puedes escribirnos directamente a nuestro correo oficial de atención y tu cliente de correo se abrirá automáticamente.
</p>
@@ -40,7 +40,7 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
<a
href={mailToHref}
className="block font-mono text-base font-medium underline decoration-brand-blue/50 underline-offset-4"
aria-label="Enviar correo a soporte de RyNext"
aria-label="Enviar correo a soporte de Rynext"
>
{supportEmail}
</a>
@@ -55,9 +55,13 @@ export const ContactModal = ({ isOpen, onClose, isNavyBg }: ModalProps) => {
</a>
<button
onClick={onClose}
className="cursor-pointer rounded-xl border border-brand-navy/15 bg-white px-4 py-3 text-sm font-bold text-brand-navy transition hover:border-brand-blue hover:text-brand-blue"
className={`cursor-pointer rounded-xl border px-4 py-3 text-sm font-bold transition hover:border-brand-blue hover:text-brand-blue ${
isNavyBg
? 'border-white/20 bg-white/5 text-white'
: 'border-brand-navy/15 bg-white text-brand-navy'
}`}
>
Entendido
Cerrar
</button>
</div>
</div>
+55
View File
@@ -0,0 +1,55 @@
import { useState, useEffect } from 'react';
import type { ThemeProps } from '../types';
export const CookieBanner = ({ isNavyBg }: ThemeProps) => {
const [visible, setVisible] = useState(false);
useEffect(() => {
if (!localStorage.getItem('rynext_cookies')) {
const t = setTimeout(() => setVisible(true), 1200);
return () => clearTimeout(t);
}
}, []);
const accept = () => {
localStorage.setItem('rynext_cookies', 'accepted');
setVisible(false);
};
if (!visible) return null;
const bg = isNavyBg ? 'rgba(0,16,64,0.97)' : 'rgba(10,10,10,0.97)';
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.08)';
return (
<div
role="dialog"
aria-label="Aviso de cookies"
className="fixed bottom-6 left-1/2 z-50 w-[calc(100%-3rem)] max-w-xl -translate-x-1/2 px-6 py-4 shadow-2xl"
style={{
background: bg,
border: `1px solid ${border}`,
borderRadius: 6,
backdropFilter: 'blur(16px)',
WebkitBackdropFilter: 'blur(16px)',
}}
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-relaxed" style={{ color: 'rgba(255,255,255,0.6)', maxWidth: '42ch' }}>
Usamos cookies para mejorar tu experiencia en el sitio.
Al continuar, aceptas su uso.{' '}
<a href="/privacidad" className="underline transition hover:text-white" style={{ color: 'rgba(255,255,255,0.45)' }}>
Política de privacidad
</a>
</p>
<button
onClick={accept}
className="shrink-0 px-5 py-2 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: '#e50914', borderRadius: 3, border: 'none', cursor: 'pointer' }}
>
Aceptar
</button>
</div>
</div>
);
};
+55 -19
View File
@@ -1,27 +1,63 @@
import { motion } from 'framer-motion';
import { ArrowRight } from 'lucide-react';
import { Button } from './Button';
import type { ThemeProps } from '../types';
export const CtaBanner = ({ onOpenContact }: ThemeProps & { onOpenContact: () => void }) => {
return (
<section className="relative overflow-hidden bg-[linear-gradient(135deg,#001f5b,#0d2d78_50%,#005596)] px-6 py-24 text-white md:px-12">
<div className="absolute -left-10 top-0 h-60 w-60 rounded-full bg-brand-red/20 blur-[120px]" />
<div className="absolute bottom-0 right-0 h-72 w-72 rounded-full bg-white/10 blur-[120px]" />
<div className="absolute inset-x-0 top-0 h-16 rounded-b-[100%] bg-white/5" />
interface CtaBannerProps extends ThemeProps {
onOpenContact: () => void;
}
<div className="relative z-10 mx-auto max-w-5xl space-y-8 text-center">
<h2 className="text-3xl font-black leading-tight tracking-tight font-display sm:text-6xl">
¿Listo para dejar de construir con ruido y empezar con dirección?
</h2>
<p className="mx-auto max-w-2xl text-lg font-normal text-sky-100 sm:text-xl">
Agenda una conversación técnica con RyNext para revisar tu proyecto, tu stack y la ruta de evolución ideal.
</p>
<button
onClick={onOpenContact}
className="inline-flex cursor-pointer items-center gap-3 rounded-xl bg-white px-9 py-4 text-base font-bold text-brand-navy shadow-xl transition hover:scale-[1.03] hover:bg-gray-100"
export const CtaBanner = ({ onOpenContact, isNavyBg }: CtaBannerProps) => {
const bg = isNavyBg ? '#001f5b' : '#f9f9f7';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)';
return (
<section
className="relative overflow-hidden px-8 py-32 md:px-14 transition-colors duration-500"
style={{ background: bg, color: text, borderBottom: `1px solid ${isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}` }}
aria-labelledby="cta-heading"
>
<span>Agendar</span>
<ArrowRight size={18} />
</button>
{/* Subtle grid */}
<div
className="pointer-events-none absolute inset-0 bg-grid-lines text-white/[0.03]"
aria-hidden="true"
/>
<div className="relative mx-auto max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7 }}
className="text-center"
>
<h2
id="cta-heading"
className="text-display mb-8"
style={{ color: text }}
>
Construimos el sistema.<br /> Tú operas el negocio.
</h2>
<p className="mx-auto mb-12 max-w-md text-base leading-relaxed" style={{ color: sub }}>
Agenda una sesión técnica con nuestro equipo. Analizamos tu operación, identificamos los cuellos de botella y proponemos una ruta concreta de implementación.
</p>
<div className="flex flex-wrap items-center justify-center gap-5">
<Button
variant="primary"
onClick={onOpenContact}
className={isNavyBg ? 'shadow-xl shadow-red-900/30' : ''}
>
Hablemos de tu proyecto <ArrowRight size={16} />
</Button>
<Button variant="secondary" href="mailto:[email protected]" isNavyBg={isNavyBg}>
contacto@rynext.com.mx
</Button>
</div>
</motion.div>
</div>
</section>
);
+120
View File
@@ -0,0 +1,120 @@
import { ArrowUp } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import type { ThemeProps } from '../types';
import { type Dispatch, type SetStateAction, useState, useEffect } from 'react';
const WHATSAPP_NUMBER = '521XXXXXXXXXX'; // ← reemplaza con el número real
const WHATSAPP_MSG = encodeURIComponent('¿En qué podemos ayudarte?');
interface FloatingButtonsProps extends ThemeProps {
setIsNavyBg: Dispatch<SetStateAction<boolean>>;
}
export const FloatingButtons = ({ isNavyBg, setIsNavyBg: _setIsNavyBg }: FloatingButtonsProps) => {
const [showTop, setShowTop] = useState(false);
const [showBubble, setShowBubble] = useState(false);
const toggleBg = isNavyBg ? 'rgba(0,31,91,0.95)' : 'rgba(255,255,255,0.95)';
const toggleBorder = isNavyBg ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.12)';
const toggleColor = isNavyBg ? '#fbbf24' : '#001f5b';
useEffect(() => {
const onScroll = () => setShowTop(window.scrollY > 400);
window.addEventListener('scroll', onScroll, { passive: true });
const t = setTimeout(() => setShowBubble(true), 3000);
return () => { window.removeEventListener('scroll', onScroll); clearTimeout(t); };
}, []);
return (
<>
{/* Scroll to top — izquierda */}
{showTop && (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
aria-label="Volver arriba"
className="fixed bottom-6 left-6 z-50 flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95"
style={{
background: toggleBg,
border: `1px solid ${toggleBorder}`,
backdropFilter: 'blur(8px)',
cursor: 'pointer',
color: toggleColor,
}}
>
<ArrowUp size={16} />
</button>
)}
<div
className="fixed bottom-6 right-6 z-50 flex flex-col items-center gap-3"
aria-label="Acciones rápidas"
>
{/* WhatsApp + bubble wrapper */}
<div style={{ position: 'relative' }}>
<AnimatePresence>
{showBubble && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.92 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 6, scale: 0.95 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
style={{
position: 'absolute',
bottom: 'calc(100% + 10px)',
right: 0,
filter: 'drop-shadow(0 4px 16px rgba(0,0,0,0.12))',
}}>
<div style={{
background: '#fff',
borderRadius: 16,
padding: '12px 36px 12px 16px',
fontSize: 14,
fontWeight: 500,
color: '#1a1a2e',
whiteSpace: 'nowrap',
position: 'relative',
}}>
¡Hola! ¿En qué podemos ayudarte?
<button
onClick={() => setShowBubble(false)}
aria-label="Cerrar"
style={{
position: 'absolute', top: 6, right: 8,
width: 20, height: 20, cursor: 'pointer', fontSize: 10,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#888',
}}
>✕</button>
</div>
{/* tail apuntando al botón */}
<div style={{
width: 14, height: 10,
background: '#fff',
clipPath: 'polygon(0 0, 100% 0, 100% 100%)',
position: 'absolute',
bottom: -9,
right: 16,
}} />
</motion.div>
)}
</AnimatePresence>
{/* WhatsApp */}
<a
href={`https://wa.me/${WHATSAPP_NUMBER}?text=${WHATSAPP_MSG}`}
target="_blank"
rel="noopener noreferrer"
aria-label="Contactar por WhatsApp"
className="flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-transform hover:scale-110 active:scale-95"
style={{ background: '#25D366' }}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="white" aria-hidden="true">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
<path d="M12 0C5.373 0 0 5.373 0 12c0 2.122.554 4.112 1.523 5.837L.057 23.882a.5.5 0 0 0 .611.61l6.109-1.457A11.945 11.945 0 0 0 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 21.75a9.721 9.721 0 0 1-4.965-1.362l-.356-.211-3.687.88.897-3.583-.231-.369A9.718 9.718 0 0 1 2.25 12C2.25 6.615 6.615 2.25 12 2.25S21.75 6.615 21.75 12 17.385 21.75 12 21.75z"/>
</svg>
</a>
</div>
</div>
</>
);
};
+178 -66
View File
@@ -1,93 +1,205 @@
import { Mail, MapPin, MessageCircle } from 'lucide-react';
import type { ThemeProps } from '../types';
const navLinks = [
{ label: 'Sistemas', href: '#machinery' },
{ label: 'Anatomía', href: '#anatomy' },
{ label: 'Proceso', href: '#proceso' },
{ label: 'Principios', href: '#principios' },
];
export const Footer = ({ isNavyBg }: ThemeProps) => {
const isDark = isNavyBg;
const bg = isNavyBg ? '#001040' : '#0a0a0a';
const sub = 'rgba(255,255,255,0.45)';
const border = 'rgba(255,255,255,0.08)';
const handleNav = (href: string) => {
const el = document.querySelector(href);
if (el) el.scrollIntoView({ behavior: 'smooth' });
};
return (
<footer className={`border-t px-6 py-14 md:px-12 transition-colors duration-500 ${
isDark
? 'border-white/10 bg-[linear-gradient(135deg,#001f5b,#0a2b78)] text-white'
: 'border-brand-navy/10 bg-white text-brand-black'
}`}>
<div className={`mx-auto grid max-w-7xl gap-8 text-sm md:grid-cols-5 ${isDark ? 'text-white' : 'text-brand-navy'}`}>
<div className="md:col-span-2">
<div className="mb-4 flex items-center gap-3">
<footer
style={{ background: bg, borderTop: `1px solid ${border}` }}
className="px-8 pt-20 pb-10 md:px-14 transition-colors duration-500"
role="contentinfo"
>
<div className="mx-auto max-w-7xl">
{/* Main grid */}
<div className="grid gap-14 md:grid-cols-[1.8fr_1fr_1.2fr]">
{/* Brand */}
<div>
<div className="mb-6 flex items-center gap-3">
<img
src={isDark ? '/icons/Logo_blanco.svg' : '/icons/Logo_negro.svg'}
alt="RyNext logo"
src="/icons/Logo_blanco.svg"
alt="Rynext logo"
className="h-9 w-auto object-contain"
/>
<div className="flex items-center">
<span className={`font-display text-xl font-extrabold tracking-tight ${
isDark ? 'text-white' : 'text-brand-black'
}`}>
Ry<span className="text-brand-red">Next</span>
</span>
<span className={`ml-2 text-xs font-semibold ${isDark ? 'text-slate-300' : 'text-brand-navy/75'}`}>
— Software Studio
</span>
<img
src="/icons/Text_Logo_Blanco.svg"
alt="Rynext"
style={{
width: "130px",
height: "18px",
objectFit: "cover",
objectPosition: "center 52%",
display: "block",
}}
/>
</div>
</div>
<p className={`max-w-md text-[13px] leading-6 ${isDark ? 'text-slate-200' : 'text-brand-navy/75'}`}>
Diseñamos productos y plataformas con foco en velocidad, estabilidad y crecimiento real de negocio.
<p
className="text-sm leading-relaxed mb-8"
style={{ color: sub, maxWidth: "30ch" }}
>
Diseñamos y construimos sistemas empresariales a medida,
arquitectura real, sin atajos técnicos.
</p>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
<div className={`rounded-xl border px-3 py-2 ${isDark ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
<p className="text-[11px] font-semibold tracking-[0.08em] text-sky-500">Operación</p>
<p className={`mt-1 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>MX • LATAM • remoto</p>
</div>
<div className={`rounded-xl border px-3 py-2 ${isDark ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
<p className="text-[11px] font-semibold tracking-[0.08em] text-sky-500">Entrega</p>
<p className={`mt-1 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>Sprints • roadmap • support</p>
</div>
{/* Social */}
<div className="flex items-center gap-3">
<a
href="https://linkedin.com/company/rynext"
target="_blank"
rel="noopener noreferrer"
aria-label="Rynext en LinkedIn"
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
onMouseEnter={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.14)";
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.07)";
(e.currentTarget as HTMLAnchorElement).style.color = sub;
}}
>
{/* LinkedIn SVG */}
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" />
<rect x="2" y="9" width="4" height="12" />
<circle cx="4" cy="4" r="2" />
</svg>
</a>
<a
href={`https://wa.me/529981234567?text=${encodeURIComponent('¿En qué podemos ayudarte?')}`}
target="_blank"
rel="noopener noreferrer"
aria-label="Rynext en WhatsApp"
className="flex h-9 w-9 items-center justify-center rounded transition-colors"
style={{ background: "rgba(255,255,255,0.07)", color: sub }}
onMouseEnter={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.14)";
(e.currentTarget as HTMLAnchorElement).style.color = "#fff";
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLAnchorElement).style.background =
"rgba(255,255,255,0.07)";
(e.currentTarget as HTMLAnchorElement).style.color = sub;
}}
>
<MessageCircle size={15} />
</a>
</div>
</div>
{/* Columna Servicios */}
{/* Navegación */}
<div>
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Servicios</p>
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
<li>Software a medida</li>
<li>Plataformas web</li>
<li>Integraciones y APIs</li>
<li>Arquitectura cloud</li>
<p
className="text-mono-label mb-6"
style={{
color: "rgba(255,255,255,0.25)",
letterSpacing: "0.12em",
}}
>
Navegación
</p>
<ul className="flex flex-col gap-3.5">
{navLinks.map((link) => (
<li key={link.href}>
<button
onClick={() => handleNav(link.href)}
className="text-sm font-medium transition-colors"
style={{
color: sub,
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
}}
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
>
{link.label}
</button>
</li>
))}
</ul>
</div>
{/* Columna Contacto */}
{/* Contacto */}
<div>
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Contacto</p>
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
<li>
<a href="mailto:[email protected]?subject=Consulta%20RyNext" className="underline decoration-current/40 underline-offset-4 transition hover:text-brand-blue">
soporte@rynext.com.mx
<p
className="text-mono-label mb-6"
style={{
color: "rgba(255,255,255,0.25)",
letterSpacing: "0.12em",
}}
>
Contacto
</p>
<ul className="flex flex-col gap-4">
<li className="flex items-start gap-3">
<MapPin
size={14}
style={{ color: "#e50914", marginTop: 2, flexShrink: 0 }}
/>
<span className="text-sm leading-snug" style={{ color: sub }}>
Cancún, Quintana Roo
<br />
México
</span>
</li>
<li className="flex items-center gap-3">
<Mail size={14} style={{ color: "#e50914", flexShrink: 0 }} />
<a
href="mailto:[email protected]"
className="text-sm font-medium transition-colors"
style={{ color: sub }}
onMouseEnter={(e) => (e.currentTarget.style.color = "#fff")}
onMouseLeave={(e) => (e.currentTarget.style.color = sub)}
>
contacto@rynext.com.mx
</a>
</li>
<li className="mt-2">
<a
href="mailto:[email protected]"
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: "#e50914", borderRadius: 3 }}
>
Hablemos de tu proyecto
</a>
</li>
<li>Cancún, Quintana Roo, México</li>
<li>Reserva de proyecto en 48h</li>
</ul>
</div>
{/* Columna Horario */}
<div>
<p className="mb-3 text-[11px] font-semibold tracking-[0.08em] text-sky-500">Horario</p>
<ul className={`space-y-2 text-[13px] ${isDark ? 'text-slate-200' : 'text-brand-navy/80'}`}>
<li>Lun–Vie 9:00–18:00</li>
<li>Reuniones por agenda</li>
<li>Soporte de proyecto</li>
<li>Consultoría técnica</li>
</ul>
</div>
</div>
{/* Copyright */}
<div className={`mx-auto mt-8 flex max-w-7xl flex-col items-center justify-between gap-4 border-t pt-6 text-[13px] sm:flex-row ${isDark ? 'border-white/10' : 'border-brand-navy/10'}`}>
<div className={isDark ? 'text-slate-200' : 'text-brand-navy/80'}>
© {new Date().getFullYear()} RyNext. Todos los derechos reservados.
{/* Bottom bar */}
<div
className="mt-16 border-t pt-8 text-center"
style={{ borderColor: border }}
>
<p className="text-xs" style={{ color: "rgba(255,255,255,0.25)" }}>
© {new Date().getFullYear()} Rynext Technologies S.A.S. | Todos los
derechos reservados.
</p>
</div>
</div>
</footer>
+92 -91
View File
@@ -1,108 +1,109 @@
import { motion } from 'framer-motion';
import { ArrowRight, CheckCircle2, Shield } from 'lucide-react';
import { ArrowRight } from 'lucide-react';
import type { HeroProps } from '../types';
import { Button } from './Button';
import { SystemMap } from './SystemMap';
export const Hero = ({ isNavyBg, onOpenContact }: HeroProps) => {
const ease = [0.22, 1, 0.36, 1] as const;
function RevealLine({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
return (
<section className={`section-wave relative flex min-h-screen items-center overflow-hidden px-6 pb-24 pt-36 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#081b46] text-white' : 'bg-[#f4f9ff] text-brand-black'
}`}>
<div className={`absolute inset-x-0 top-0 h-44 ${
isNavyBg
? 'bg-[radial-gradient(circle_at_top_left,rgba(0,119,194,0.24),transparent_34%),radial-gradient(circle_at_top_right,rgba(229,9,20,0.16),transparent_30%)]'
: 'bg-[radial-gradient(circle_at_top_left,rgba(0,119,194,0.20),transparent_34%),radial-gradient(circle_at_top_right,rgba(229,9,20,0.10),transparent_30%)]'
}`} />
<div className="mx-auto grid w-full max-w-7xl grid-cols-1 items-center gap-12 lg:grid-cols-12 lg:gap-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
<motion.span
className="block"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7 }}
className="space-y-8 lg:col-span-12"
transition={{ duration: 0.75, delay, ease }}
>
<div className="space-y-4">
{/* Título uniforme de un solo color */}
<h1 className={`text-hero-dacodes max-w-5xl font-display transition-colors duration-500 ${
isNavyBg ? 'text-white' : 'text-[#001f5b]'
}`}>
Diseñamos software
<span className="block">
que mantiene el negocio en marcha.
</span>
{children}
</motion.span>
);
}
export const Hero = ({ onOpenContact, isNavyBg }: HeroProps) => {
const bg = isNavyBg ? '#001f5b' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.45)';
const gridLine = isNavyBg ? 'rgba(255,255,255,0.025)' : 'rgba(0,0,0,0.04)';
return (
<section
className="relative flex min-h-screen flex-col overflow-hidden transition-colors duration-500"
style={{ background: bg, color: text, borderBottom: isNavyBg ? '1px solid rgba(255,255,255,0.1)' : '1px solid rgba(0,0,0,0.1)' }}
aria-label="Rynext Technologies — Hero"
>
{/* Subtle grid bg */}
<div
className="pointer-events-none absolute inset-0 bg-grid-lines"
style={{ color: gridLine }}
aria-hidden="true"
/>
{/* Main content */}
<div className="relative mx-auto grid w-full max-w-7xl flex-1 items-center gap-12 px-8 pb-24 pt-36 md:px-14 lg:grid-cols-[1fr_1fr] lg:gap-20">
{/* Left column */}
<div>
{/* Headline */}
<h1 className="text-display mb-8" style={{ color: text }}>
<RevealLine delay={0.08}>Construimos</RevealLine>
<RevealLine delay={0.16}>el sistema</RevealLine>
<RevealLine delay={0.24}>
<span className="text-brand-red">detrás</span> del
</RevealLine>
<RevealLine delay={0.32}>negocio.</RevealLine>
</h1>
<p className={`max-w-2xl text-lg leading-relaxed sm:text-xl ${
isNavyBg ? 'text-slate-200' : 'text-gray-700'
}`}>
RyNext combina arquitectura, producto y operación para lanzar plataformas que se entiendan bien, corran rápido y puedan crecer sin romperse.
</p>
</div>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.5, ease }}
className="mb-10 max-w-sm text-base leading-relaxed"
style={{ color: sub }}
>
Ingeniería de software empresarial diseñada desde la arquitectura.
No herramientas genéricas, sistemas construidos para
la operación real de tu negocio.
</motion.p>
<div className="flex flex-wrap items-center gap-4 pt-2">
{/* CTAs */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.65, ease }}
className="flex flex-wrap items-center gap-6"
>
<Button variant="primary" onClick={onOpenContact}>
Hablemos de tu proyecto <ArrowRight size={16} />
</Button>
<button
onClick={onOpenContact}
className="group inline-flex items-center gap-3 bg-brand-red px-8 py-4 text-base font-bold text-white shadow-lg shadow-red-500/20 transition hover:scale-[1.02] hover:bg-[#b60014]"
onClick={() => {
document.getElementById('machinery')?.scrollIntoView({ behavior: 'smooth' });
}}
className="group flex items-center gap-2 text-sm font-semibold transition"
style={{ color: sub }}
>
<span>Agendar</span>
<ArrowRight size={18} className="transition group-hover:translate-x-1" />
Explorar la maquinaria
<ArrowRight
size={14}
className="transition-transform group-hover:translate-x-0.5"
/>
</button>
<a
href="#servicios"
className={`border px-8 py-4 text-base font-semibold transition hover:scale-[1.02] ${
isNavyBg
? 'border-white/20 bg-white/5 text-white hover:bg-white/10'
: 'border-brand-navy/15 bg-white text-brand-navy hover:border-brand-blue hover:text-brand-blue'
}`}
>
Ver servicios
</a>
</div>
<div className="grid gap-4 pt-4 lg:grid-cols-3">
<div className={`rounded-[18px] border-[1.5px] p-4 ${
isNavyBg
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
}`}>
<div className="mb-2 flex items-center gap-2">
<CheckCircle2 size={16} className="text-brand-blue" />
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Time to value</span>
</div>
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
Entregas visibles desde las primeras semanas con objetivos claros.
</p>
</div>
<div className={`rounded-[18px] border-[1.5px] p-4 ${
isNavyBg
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
}`}>
<div className="mb-2 flex items-center gap-2">
<CheckCircle2 size={16} className="text-brand-red" />
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Sistemas seguros</span>
</div>
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
Infraestructura preparada para escalar con confianza y control.
</p>
</div>
<div className={`rounded-[18px] border-[1.5px] p-4 ${
isNavyBg
? 'border-white/15 bg-white/5 shadow-[0_8px_30px_rgba(0,0,0,0.18)]'
: 'border-brand-navy/15 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'
}`}>
<div className="mb-2 flex items-center gap-2">
<Shield size={16} className="text-brand-blue" />
<span className={`text-sm font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>Operación estable</span>
</div>
<p className={`text-sm ${isNavyBg ? 'text-slate-200' : 'text-gray-700'}`}>
Alianzas con dirección técnica, roadmap claro y sostenibilidad real.
</p>
</div>
</div>
</motion.div>
</div>
{/* Right column — System Map */}
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.4, ease }}
className="hidden lg:block"
>
<SystemMap isNavyBg={isNavyBg} />
</motion.div>
</div>
</section>
);
};
+139
View File
@@ -0,0 +1,139 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
import { machineryData } from '../data/machineryData';
export const Machinery = ({ isNavyBg }: ThemeProps) => {
const [hovered, setHovered] = useState<number | null>(null);
const bg = isNavyBg ? '#001f5b' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : '#6a6866';
const cardBg = isNavyBg ? 'rgba(255,255,255,0.06)' : '#fafaf9';
const cardBdr = isNavyBg ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.09)';
const numColor = isNavyBg ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.04)';
return (
<section
id="machinery"
aria-labelledby="machinery-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${cardBdr}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
{/* Header */}
<div className="mb-16">
<h2
id="machinery-heading"
className="text-headline"
style={{ maxWidth: '22ch', color: text }}
>
Los sistemas que<br className="hidden sm:block" /> mueven la operación.
</h2>
</div>
{/* Cards grid */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{machineryData.map((item, i) => {
const isHov = hovered === item.id;
return (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.45, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
onMouseEnter={() => setHovered(item.id)}
onMouseLeave={() => setHovered(null)}
role="article"
aria-label={item.title}
style={{
background: isHov
? isNavyBg ? 'rgba(229,9,20,0.06)' : 'rgba(229,9,20,0.03)'
: cardBg,
border: `1px solid ${isHov ? 'rgba(229,9,20,0.25)' : cardBdr}`,
borderRadius: 6,
padding: '28px 28px 32px',
position: 'relative',
overflow: 'hidden',
cursor: 'default',
transition: 'background 220ms, border-color 220ms, transform 220ms, box-shadow 220ms',
transform: isHov ? 'translateY(-2px)' : 'translateY(0)',
boxShadow: isHov
? '0 8px 28px rgba(0,0,0,0.10)'
: '0 1px 3px rgba(0,0,0,0.04)',
}}
>
{/* Faint background number */}
<span
aria-hidden="true"
style={{
position: 'absolute',
top: -8,
right: 12,
fontFamily: 'var(--font-display)',
fontSize: 96,
fontWeight: 900,
lineHeight: 1,
color: isHov ? 'rgba(229,9,20,0.07)' : numColor,
transition: 'color 220ms',
userSelect: 'none',
pointerEvents: 'none',
}}
>
{item.num}
</span>
{/* Red accent top bar on hover */}
<span
aria-hidden="true"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: 2,
background: '#e50914',
opacity: isHov ? 1 : 0,
transition: 'opacity 220ms',
borderRadius: '6px 6px 0 0',
}}
/>
{/* Content */}
<div style={{ position: 'relative' }}>
<h3
className="font-display font-black tracking-tight mb-3"
style={{
fontSize: 'clamp(1.1rem, 2vw, 1.3rem)',
letterSpacing: '-0.02em',
color: isHov ? '#e50914' : text,
transition: 'color 220ms',
}}
>
{item.title}
</h3>
<p
className="text-sm leading-relaxed"
style={{ color: sub, transition: 'color 220ms' }}
>
{item.description}
</p>
</div>
</motion.div>
);
})}
</div>
</motion.div>
</section>
);
};
+123 -54
View File
@@ -1,67 +1,136 @@
import { Sun, Moon, ArrowRight } from 'lucide-react';
import type { NavbarProps } from '../types';
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Link, useLocation } from "react-router";
import type { NavbarProps } from "../types";
const pageLinks = [
{ label: "Sobre Nosotros", href: "/sobre-nosotros" },
];
export const Navbar = ({ onOpenContact }: NavbarProps) => {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 80);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
setMenuOpen(false);
}, [location.pathname]);
export const Navbar = ({ isNavyBg, setIsNavyBg, onOpenContact }: NavbarProps) => {
return (
<header className="fixed top-0 left-0 right-0 z-50 w-full">
<div className={`w-full border-b backdrop-blur-md transition-all duration-500 ${
isNavyBg
? 'border-white/10 bg-[#001f5b]/95 text-white shadow-lg shadow-black/10'
: 'border-brand-navy/10 bg-white/95 text-brand-black shadow-sm'
}`}>
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4 md:px-12">
{/* Identidad / Logo */}
<div className="flex items-center gap-3">
<img
src={isNavyBg ? '/icons/Logo_blanco.svg' : '/icons/Logo_negro.svg'}
alt="RyNext logo"
className="h-10 w-auto object-contain"
/>
<span className={`font-display text-2xl font-extrabold tracking-tight ${
isNavyBg ? 'text-white' : 'text-brand-black'
}`}>
Ry<span className="text-brand-red">Next</span>
</span>
</div>
{/* Menú y Acciones (Sin 'Stack') */}
<div className="flex items-center gap-4 sm:gap-6">
<a href="#servicios" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
Servicios
</a>
<a href="#metodo" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
Método
</a>
<a href="#conveniencia" className="hidden text-sm font-semibold transition-colors hover:text-brand-red sm:block">
Conveniencia
</a>
{/* Switch de Fondo */}
<button
onClick={() => setIsNavyBg((prev) => !prev)}
className={`flex cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-xs font-semibold transition-all ${
isNavyBg
? 'border-white/20 bg-white/5 text-white hover:bg-white/10'
: 'border-brand-navy/10 bg-[#001f5b]/5 text-brand-navy hover:bg-[#001f5b]/10'
}`}
<>
<motion.header
className="fixed top-0 left-0 right-0 z-50"
initial={{ y: -72, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
role="banner"
>
{isNavyBg ? <Sun size={15} className="text-amber-400" /> : <Moon size={15} className="text-brand-navy" />}
<span className="hidden md:inline">{isNavyBg ? 'Fondo blanco' : 'Fondo azul'}</span>
</button>
<div
style={{
background: scrolled ? "rgba(255,255,255,0.97)" : "transparent",
borderBottom: scrolled ? "1px solid rgba(0,0,0,0.08)" : "1px solid transparent",
backdropFilter: scrolled ? "blur(16px)" : "none",
WebkitBackdropFilter: scrolled ? "blur(16px)" : "none",
transition: "background 200ms, border-color 200ms",
}}
>
<div className="mx-auto flex max-w-7xl items-center justify-between px-8 py-4 md:px-14">
{/* Logo — siempre modo claro */}
<Link to="/" aria-label="Rynext Technologies — inicio" className="flex items-center gap-3">
<img src="/icons/Logo_negro.svg" alt="Rynext logo" className="h-9 w-auto object-contain" />
<img
src="/icons/Text_Logo_Negro.svg"
alt="Rynext"
style={{ width: "130px", height: "18px", objectFit: "cover", objectPosition: "center 52%", display: "block" }}
/>
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-8 md:flex" aria-label="Navegación principal">
{pageLinks.map((link) => {
const active = location.pathname === link.href;
return (
<Link
key={link.href}
to={link.href}
className="text-sm font-semibold transition-colors"
style={{ color: active ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
onMouseEnter={(e) => (e.currentTarget.style.color = "#0a0a0a")}
onMouseLeave={(e) => (e.currentTarget.style.color = active ? "#0a0a0a" : "#6a6866")}
>
{link.label}
</Link>
);
})}
{/* Botón CTA */}
<button
onClick={onOpenContact}
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-brand-red px-5 py-2.5 text-sm font-bold text-white shadow-md shadow-red-500/20 transition hover:scale-[1.02] hover:bg-[#b60014]"
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-bold text-white transition hover:scale-[1.02] hover:opacity-90"
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
>
<span>Agendar</span>
<ArrowRight size={16} />
Hablemos
</button>
</nav>
{/* Mobile menu button */}
<button
onClick={() => setMenuOpen((v) => !v)}
className="flex items-center justify-center rounded p-2 md:hidden"
style={{ color: "#0a0a0a", background: "none", border: "none", cursor: "pointer" }}
aria-label={menuOpen ? "Cerrar menú" : "Abrir menú"}
aria-expanded={menuOpen}
>
{menuOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
</div>
</motion.header>
{/* Mobile menu */}
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
className="fixed inset-x-0 top-[65px] z-40 px-8 py-6 md:hidden"
style={{ background: "#ffffff", borderBottom: "1px solid #dddbd5" }}
role="dialog"
aria-modal="true"
aria-label="Menú de navegación"
>
<nav className="flex flex-col gap-5">
{pageLinks.map((link) => (
<Link
key={link.href}
to={link.href}
className="text-left text-base font-semibold"
style={{ color: location.pathname === link.href ? "#0a0a0a" : "#6a6866", textDecoration: "none" }}
>
{link.label}
</Link>
))}
<div className="pt-2">
<button
onClick={onOpenContact}
className="w-full py-3 text-center text-sm font-bold text-white"
style={{ background: "#e50914", border: "none", cursor: "pointer", borderRadius: 3 }}
>
Hablemos
</button>
</div>
</div>
</header>
</nav>
</motion.div>
)}
</AnimatePresence>
</>
);
};
+68 -68
View File
@@ -1,5 +1,5 @@
import { motion } from 'framer-motion';
import { ArrowRight, Building2, Layers3, ShieldCheck } from 'lucide-react';
import { Building2, Layers3, ShieldCheck, X, Check } from 'lucide-react';
import type { ThemeProps } from '../types';
const comparisonRows = [
@@ -10,7 +10,7 @@ const comparisonRows = [
},
{
criterion: 'Propiedad y evolución',
saas: 'Depende del proveedor y de su roadmap.',
saas: 'Depende del proveedor y su roadmap.',
ry: 'Roadmap compartido con autonomía técnica real.',
},
{
@@ -21,7 +21,7 @@ const comparisonRows = [
{
criterion: 'Soporte y continuidad',
saas: 'Respuesta general, soporte tipo ticket.',
ry: 'Equipo senior con acompañamiento directo y operación.',
ry: 'Equipo senior con acompañamiento directo.',
},
];
@@ -37,7 +37,7 @@ const proofCards = [
icon: ShieldCheck,
},
{
title: 'Equipo senior detrás',
title: 'Equipo senior',
description: 'Product owner, ingeniería y operación trabajando con criterio empresarial.',
icon: Building2,
},
@@ -45,110 +45,110 @@ const proofCards = [
export const PlatformAdvantage = ({ isNavyBg }: ThemeProps) => {
return (
<section id="conveniencia" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#071b49] text-white' : 'bg-[#f7fbff] text-brand-black'
}`}>
<section
id="conveniencia"
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black'
}`}
>
<div className="mx-auto max-w-7xl">
{/* Header */}
<div className="mb-14 flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-3xl">
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
isNavyBg ? 'text-sky-400' : 'text-brand-blue'
}`}>
Conveniencia RyNext
Por qué Rynext
</span>
<h2 className="text-section-title">
Detrás de cada plataforma hay un equipo senior, una arquitectura clara y una ruta con sentido.
</h2>
</div>
<p className={`max-w-xl text-base leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
<p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
Un sistema no solo se construye para operar: se diseña para sostener negocio, seguridad, adopción y evolución continua.
</p>
</div>
<div className={`overflow-hidden rounded-[22px] border ${isNavyBg ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-white'}`}>
<div className={`grid grid-cols-1 border-b text-sm font-semibold md:grid-cols-[1.1fr_0.95fr_0.95fr] ${isNavyBg ? 'border-white/10 text-sky-100' : 'border-brand-navy/10 text-brand-navy'}`}>
<div className="px-5 py-4">Criterio</div>
<div className="px-5 py-4 border-t md:border-t-0 md:border-l">Licencia SaaS</div>
<div className="px-5 py-4 border-t md:border-t-0 md:border-l">RyNext software</div>
{/* Comparison table */}
<div className={`overflow-hidden rounded-2xl border ${
isNavyBg ? 'border-white/8 bg-white/[0.03]' : 'border-gray-200 bg-white'
}`}>
{/* Table header */}
<div className={`grid grid-cols-1 text-xs font-semibold uppercase tracking-[0.1em] md:grid-cols-[1.2fr_1fr_1fr] ${
isNavyBg ? 'border-b border-white/8 bg-white/[0.04]' : 'border-b border-gray-100 bg-gray-50'
}`}>
<div className={`px-6 py-4 ${isNavyBg ? 'text-slate-400' : 'text-gray-400'}`}>Criterio</div>
<div className={`border-t px-6 py-4 md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/8 text-slate-400' : 'border-gray-100 text-gray-400'
}`}>
Licencia SaaS
</div>
<div className={`border-t px-6 py-4 font-bold md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/8 text-sky-400' : 'border-gray-100 text-brand-blue'
}`}>
Rynext software
</div>
</div>
{/* Rows */}
{comparisonRows.map((row, index) => (
<div key={row.criterion} className={`grid grid-cols-1 md:grid-cols-[1.1fr_0.95fr_0.95fr] ${isNavyBg ? 'text-slate-200' : 'text-brand-navy/80'}`}>
<div className={`px-5 py-4 border-b font-semibold ${isNavyBg ? 'border-white/10 text-white' : 'border-brand-navy/10 text-brand-navy'}`}>
<div
key={row.criterion}
className={`grid grid-cols-1 text-sm md:grid-cols-[1.2fr_1fr_1fr] ${
index < comparisonRows.length - 1
? isNavyBg ? 'border-b border-white/5' : 'border-b border-gray-100'
: ''
}`}
>
<div className={`px-6 py-5 font-semibold ${isNavyBg ? 'text-white' : 'text-brand-navy'}`}>
{row.criterion}
</div>
<div className={`px-5 py-4 border-b md:border-l ${isNavyBg ? 'border-white/10' : 'border-brand-navy/10'}`}>
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/5 text-slate-400' : 'border-gray-100 text-gray-500'
}`}>
<X size={13} className="mt-0.5 shrink-0 text-gray-400" />
{row.saas}
</div>
<div className={`px-5 py-4 border-b md:border-l ${isNavyBg ? 'border-white/10' : 'border-brand-navy/10'}`}>
<div className={`flex items-start gap-2.5 border-t px-6 py-5 md:border-t-0 md:border-l ${
isNavyBg ? 'border-white/5 text-slate-200' : 'border-gray-100 text-brand-navy'
}`}>
<Check size={13} className={`mt-0.5 shrink-0 ${isNavyBg ? 'text-sky-400' : 'text-brand-blue'}`} />
{row.ry}
</div>
{index === comparisonRows.length - 1 && (
<div className={`md:col-span-3 px-5 py-4 text-xs tracking-[0.08em] ${isNavyBg ? 'text-sky-100' : 'text-brand-navy'}`}>
Resultado: menos fricción, más control y más valor real.
</div>
)}
</div>
))}
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-3">
{/* Proof cards */}
<div className="mt-6 grid gap-4 lg:grid-cols-3">
{proofCards.map((card, index) => {
const Icon = card.icon;
return (
<motion.article
key={card.title}
initial={{ opacity: 0, y: 24 }}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.08 }}
className={`rounded-[18px] border p-5 ${isNavyBg
? 'border-white/10 bg-white/5 shadow-[0_10px_30px_rgba(0,0,0,0.16)]'
: 'border-brand-navy/10 bg-white shadow-[0_10px_30px_rgba(0,31,91,0.08)]'}`}
className={`rounded-2xl border p-6 ${
isNavyBg
? 'border-white/8 bg-white/[0.04]'
: 'border-gray-200 bg-white shadow-sm'
}`}
>
<div className="mb-4 flex items-center gap-3">
<div className={`rounded-xl p-3 ${isNavyBg ? 'bg-white/10 text-white' : 'bg-brand-blue/10 text-brand-blue'}`}>
<Icon size={18} />
<div className={`mb-5 inline-flex rounded-xl p-3 ${
isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/8 text-brand-blue'
}`}>
<Icon size={20} />
</div>
<span className={`text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>
Capacidad real
</span>
</div>
<h3 className="mb-2 text-xl font-bold font-display">{card.title}</h3>
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{card.description}</p>
<h3 className="mb-2 font-display text-lg font-bold">{card.title}</h3>
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
{card.description}
</p>
</motion.article>
);
})}
</div>
<div className={`mt-8 grid gap-4 rounded-[20px] border p-5 lg:grid-cols-[1.1fr_0.9fr] ${isNavyBg ? 'border-white/10 bg-white/5' : 'border-brand-navy/10 bg-white'}`}>
<div>
<p className={`mb-3 text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>
Visión operativa
</p>
<h3 className="mb-3 text-2xl font-bold font-display">Una plataforma respaldada por una operación empresarial madura.</h3>
<p className={`max-w-2xl text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
No entregamos simplemente una solución “bonita”. Dejanos una base técnica diseñada para crecer, sostener usuarios, automatizar procesos y poner a tu negocio en una ruta clara.
</p>
</div>
<div className={`rounded-[18px] border p-4 ${isNavyBg ? 'border-white/10 bg-[#081b46]' : 'border-brand-navy/10 bg-[#001f5b]/5'}`}>
<div className="mb-3 flex items-center justify-between">
<span className={`text-[10px] font-semibold tracking-[0.08em] ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>Resumen ejecutivo</span>
<ArrowRight size={16} className={isNavyBg ? 'text-sky-200' : 'text-brand-blue'} />
</div>
<div className="space-y-3 text-sm">
<div className={`rounded-xl border px-3 py-2 ${isNavyBg ? 'border-white/10 bg-white/5 text-slate-200' : 'border-brand-navy/10 bg-white text-brand-navy'}`}>
<strong className="font-bold">Sin bloqueo del proveedor</strong>
<div className="mt-1">Tu roadmap y evolución no dependen de una licencia estándar.</div>
</div>
<div className={`rounded-xl border px-3 py-2 ${isNavyBg ? 'border-white/10 bg-white/5 text-slate-200' : 'border-brand-navy/10 bg-white text-brand-navy'}`}>
<strong className="font-bold">Equipo con criterio</strong>
<div className="mt-1">Diseño, ingeniería, integración y soporte en una sola visión.</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
+106
View File
@@ -0,0 +1,106 @@
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
const principles = [
{
title: 'Simple por diseño',
description: 'La complejidad existe solo cuando aporta valor real. Todo lo demás se elimina.',
},
{
title: 'Diseñado para fallar',
description: 'Los sistemas asumen que algo fallará. La resiliencia es diseñada, no improvisada.',
},
{
title: 'Una sola fuente de verdad',
description: 'La información crítica tiene una única fuente confiable. Sin duplicidad de datos.',
},
{
title: 'Automatiza lo repetitivo',
description: 'Si una persona lo hace manualmente diez veces, el sistema debe hacerlo.',
},
{
title: 'Construido para cambiar',
description: 'El software debe poder evolucionar. La arquitectura anticipa el cambio inevitable.',
},
];
export const Principles = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)';
const gridGap = isNavyBg ? 'rgba(255,255,255,0.07)' : 'rgba(0,0,0,0.07)';
return (
<section
id="principios"
aria-labelledby="principles-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${gridGap}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
{/* Header */}
<div className="mb-16">
<h2
id="principles-heading"
className="text-headline"
style={{ maxWidth: '24ch', color: text }}
>
Principios, no valores<br className="hidden sm:block" /> corporativos.
</h2>
</div>
{/* Principles — 5 columns */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(5, 1fr)',
border: `1px solid ${gridGap}`,
borderRadius: 4,
overflow: 'hidden',
}}
>
{principles.map((p, i) => (
<motion.div
key={p.title}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: i * 0.06 }}
style={{
padding: '32px 28px 36px',
borderRight: i < principles.length - 1 ? `1px solid ${gridGap}` : 'none',
borderTop: '3px solid #e50914',
}}
>
<span
className="font-display font-black block mb-5"
style={{ fontSize: 56, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
>
{String(i + 1).padStart(2, '0')}
</span>
<h3
className="font-display font-black mb-3"
style={{ fontSize: 15, letterSpacing: '-0.01em', color: text, lineHeight: 1.25 }}
>
{p.title}
</h3>
<p
className="text-sm leading-relaxed"
style={{ color: sub }}
>
{p.description}
</p>
</motion.div>
))}
</div>
</motion.div>
</section>
);
};
+80
View File
@@ -0,0 +1,80 @@
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
export const Problem = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0d1117' : '#f5f5f3';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.4)';
const border = isNavyBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)';
return (
<section
aria-labelledby="problem-heading"
className="px-8 py-24 md:px-14 transition-colors duration-500"
style={{ background: bg, borderBottom: `1px solid ${border}` }}
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.15 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
<div>
{/* Headline */}
<h2
id="problem-heading"
className="text-display"
style={{ lineHeight: 1.0, letterSpacing: '-0.02em', color: text }}
>
{['La mayoría', 'de los problemas', 'operativos son'].map((line, i) => (
<motion.span
key={line}
className="block"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: i * 0.07, ease: [0.22, 1, 0.36, 1] }}
>
{line}
</motion.span>
))}
<motion.span
className="block text-brand-red"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.24, ease: [0.22, 1, 0.36, 1] }}
>
problemas de sistema.
</motion.span>
</h2>
{/* Descripción */}
<motion.div
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.45 }}
className="mt-12 flex gap-4"
style={{ maxWidth: '56ch' }}
>
<span
className="mt-1 shrink-0 block h-full w-px"
style={{ background: '#e50914', minHeight: '100%', width: 2, borderRadius: 2, alignSelf: 'stretch', opacity: 0.7 }}
/>
<p className="text-base leading-relaxed" style={{ color: sub }}>
Software que impone sus propias reglas en lugar de adaptarse
a la operación. Procesos que permanecen manuales porque nadie
los diseñó para escalar. Ambos tienen la misma raíz: ausencia
de arquitectura.
</p>
</motion.div>
</div>
</motion.div>
</section>
);
};
+77 -29
View File
@@ -2,50 +2,98 @@ import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
const steps = [
{ step: '01', title: 'Diagnóstico y mapa de valor', desc: 'Auditamos tu stack actual, objetivos de negocio y fricción operativa para definir una ruta técnica precisa.' },
{ step: '02', title: 'Diseño de solución', desc: 'Estructuramos arquitectura, alcance, experiencia y entregables con una visión clara, realista y escalable.' },
{ step: '03', title: 'Construcción con control', desc: 'Desarrollamos en sprints con entregables medibles, calidad técnica y coordinación constante con tu equipo.' },
{ step: '04', title: 'Entrega y evolución', desc: 'Publicamos en la nube, transferimos conocimiento y dejamos una plataforma lista para crecer con seguridad.' },
{
num: '01',
title: 'ENTENDER',
desc: 'Auditamos la operación, el stack y la fricción real antes de proponer nada. Sin suposiciones.',
},
{
num: '02',
title: 'MODELAR',
desc: 'Diseñamos la arquitectura del sistema: flujos, interfaces, datos y dependencias.',
},
{
num: '03',
title: 'CONSTRUIR',
desc: 'Desarrollamos en sprints con entregables medibles y revisión técnica constante.',
},
{
num: '04',
title: 'CONECTAR',
desc: 'Integramos el sistema con el ecosistema del negocio: ERPs, pagos, APIs externas.',
},
{
num: '05',
title: 'OPERAR',
desc: 'Despliegue a producción, monitoreo, soporte y evolución continua del sistema.',
},
];
export const Process = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#001f5b' : '#ffffff';
const text = isNavyBg ? '#ffffff' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.4)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.1)' : '#dddbd5';
return (
<section id="metodo" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 ${
isNavyBg ? 'bg-[#0a2b7a] text-white' : 'bg-[#f7fbff] text-brand-black'
}`}>
<div className="mx-auto max-w-7xl">
<section
id="proceso"
aria-labelledby="process-heading"
style={{ background: bg, color: text, borderBottom: `1px solid ${border}` }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<motion.div
className="mx-auto max-w-7xl"
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.1 }}
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
>
{/* Header */}
<div className="mb-16">
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
}`}>
Método RyNext
</span>
<h2 className="max-w-3xl text-section-title">
Un proceso que mantiene foco, velocidad y resultados concretos.
<h2
id="process-heading"
className="text-headline"
style={{ maxWidth: '28ch' }}
>
De la problemática<br className="hidden sm:block" /> al sistema en producción.
</h2>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{steps.map((item, index) => (
{/* Steps */}
<div className="grid gap-0 md:grid-cols-3 lg:grid-cols-5" style={{ border: `1px solid ${border}`, borderRadius: 4, overflow: 'hidden' }}>
{steps.map((step, i) => (
<motion.div
key={item.step}
initial={{ opacity: 0, y: 30 }}
key={step.num}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={`rounded-[16px] border p-6 ${
isNavyBg
? 'border-white/10 bg-white/5'
: 'border-gray-200 bg-white shadow-sm'
}`}
transition={{ duration: 0.4, delay: i * 0.08 }}
style={{
padding: '32px 28px',
borderRight: i < steps.length - 1 ? `1px solid ${border}` : 'none',
}}
>
<div className={`mb-4 text-4xl font-black ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>{item.step}</div>
<h3 className="mb-3 text-xl font-bold font-display">{item.title}</h3>
<p className={`text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{item.desc}</p>
<span
className="font-display font-black block mb-6"
style={{ fontSize: 52, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}
>
{step.num}
</span>
<h3
className="font-display font-black mb-3"
style={{ fontSize: 13, letterSpacing: '0.04em', color: text }}
>
{step.title}
</h3>
<p className="text-sm leading-relaxed" style={{ color: sub }}>
{step.desc}
</p>
</motion.div>
))}
</div>
</div>
</motion.div>
</section>
);
};
+55 -23
View File
@@ -1,59 +1,91 @@
import { motion } from 'framer-motion';
import { ArrowUpRight } from 'lucide-react';
import { Check } from 'lucide-react';
import type { ThemeProps } from '../types';
import { servicesData } from '../data/servicesData';
export const Services = ({ isNavyBg }: ThemeProps) => {
return (
<section id="servicios" className={`section-wave border-t border-current/10 px-6 py-28 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#0a2b7a] text-white' : 'bg-[#f7fbff] text-brand-black'
}`}>
<section
id="servicios"
className={`section-wave px-6 py-28 md:px-12 transition-colors duration-500 ${
isNavyBg ? 'bg-[#001749] text-white' : 'bg-slate-50 text-brand-black'
}`}
>
<div className="mx-auto max-w-7xl">
{/* Header */}
<div className="mb-16 flex flex-col justify-between gap-8 md:flex-row md:items-end">
<div>
<span className={`mb-3 block text-xs font-semibold tracking-[0.08em] ${
isNavyBg ? 'text-sky-200' : 'text-brand-blue'
<span className={`mb-3 block text-xs font-semibold uppercase tracking-[0.14em] ${
isNavyBg ? 'text-sky-400' : 'text-brand-blue'
}`}>
Servicios RyNext
Servicios
</span>
<h2 className="max-w-2xl text-section-title">
Soluciones que conectan producto, infraestructura y crecimiento.
</h2>
</div>
<p className={`max-w-xl text-base leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>
No trabajamos con plantillas. Diseñamos ecosistemas de tecnología a medida para que tu operación gane velocidad, claridad y control.
<p className={`max-w-sm text-base leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
No trabajamos con plantillas. Cada ecosistema tecnológico se diseña para la operación específica del cliente.
</p>
</div>
<div className="grid gap-5 md:grid-cols-2">
{/* Cards grid */}
<div className="grid gap-4 md:grid-cols-12">
{servicesData.map((service, index) => {
const Icon = service.icon;
const accentClass = service.accent === 'blue' ? 'bg-brand-blue' : 'bg-brand-red';
const iconBgClass = service.accent === 'blue'
? (isNavyBg ? 'bg-brand-blue/15 text-sky-300' : 'bg-brand-blue/10 text-brand-blue')
: (isNavyBg ? 'bg-brand-red/15 text-red-300' : 'bg-brand-red/10 text-brand-red');
return (
<motion.article
key={service.id}
initial={{ opacity: 0, y: 30 }}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-40px' }}
transition={{ duration: 0.5, delay: index * 0.08 }}
className={`group rounded-[16px] border p-6 transition-all duration-300 hover:-translate-y-1 ${
className={`group relative overflow-hidden rounded-2xl border p-7 transition-all duration-300 hover:-translate-y-0.5 ${service.gridCols} ${
isNavyBg
? 'border-white/10 bg-white/5 hover:border-brand-blue/50 hover:bg-white/10'
: 'border-gray-200 bg-white hover:border-brand-blue/40 hover:shadow-xl'
? 'border-white/8 bg-white/[0.04] hover:border-white/15 hover:bg-white/[0.07]'
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-xl hover:shadow-gray-100'
}`}
>
<div className="mb-5 flex items-center justify-between">
<div className="flex items-center gap-4">
<span className={`text-sm font-semibold ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'}`}>0{service.id}</span>
<div className={`rounded-xl p-3 ${isNavyBg ? 'bg-white/10 text-white' : 'bg-brand-blue/10 text-brand-blue'}`}>
<Icon size={20} />
{/* Top accent line */}
<div className={`absolute inset-x-0 top-0 h-[2px] ${accentClass} opacity-60`} />
{/* Header row */}
<div className="mb-6 flex items-start justify-between">
<div className={`rounded-xl p-3 ${iconBgClass}`}>
<Icon size={22} />
</div>
</div>
<ArrowUpRight size={18} className={`opacity-70 transition ${isNavyBg ? 'text-sky-200' : 'text-brand-blue'} group-hover:translate-x-0.5 group-hover:-translate-y-0.5`} />
<span className={`font-mono text-xs font-bold ${isNavyBg ? 'text-white/20' : 'text-gray-200'}`}>
0{service.id}
</span>
</div>
<h3 className="mb-3 text-2xl font-bold font-display">{service.title}</h3>
<p className={`max-w-lg text-sm leading-relaxed ${isNavyBg ? 'text-slate-200' : 'text-gray-600'}`}>{service.description}</p>
<h3 className="mb-3 font-display text-2xl font-bold">{service.title}</h3>
<p className={`mb-6 text-sm leading-relaxed ${isNavyBg ? 'text-slate-400' : 'text-gray-500'}`}>
{service.description}
</p>
{/* Feature list */}
<ul className="space-y-2">
{service.features.map((f: string) => (
<li key={f} className="flex items-center gap-2.5">
<Check
size={13}
className={`shrink-0 ${service.accent === 'blue'
? (isNavyBg ? 'text-sky-400' : 'text-brand-blue')
: (isNavyBg ? 'text-red-400' : 'text-brand-red')
}`}
/>
<span className={`text-xs font-medium ${isNavyBg ? 'text-slate-300' : 'text-gray-600'}`}>
{f}
</span>
</li>
))}
</ul>
</motion.article>
);
})}
+116
View File
@@ -0,0 +1,116 @@
import { motion } from 'framer-motion';
import type { ThemeProps } from '../types';
const stackGroups = [
{
label: 'FRONTEND',
items: ['React', 'TypeScript', 'Next.js', 'Tailwind CSS', 'Vite'],
},
{
label: 'BACKEND',
items: ['Node.js', 'Python', 'Express', 'FastAPI', 'NestJS'],
},
{
label: 'DATOS',
items: ['PostgreSQL', 'MongoDB', 'Redis', 'Prisma', 'Elasticsearch'],
},
{
label: 'CLOUD & OPS',
items: ['AWS', 'Docker', 'GitHub Actions', 'Terraform', 'Linux'],
},
{
label: 'INTEGRACIONES',
items: ['REST', 'GraphQL', 'Stripe', 'OAuth 2.0', 'Webhooks'],
},
];
export const Stack = ({ isNavyBg }: ThemeProps) => {
const bg = isNavyBg ? '#0f1520' : '#f9f9f7';
const text = isNavyBg ? '#eef0f4' : '#0a0a0a';
const sub = isNavyBg ? 'rgba(255,255,255,0.35)' : '#6a6866';
const border = isNavyBg ? 'rgba(255,255,255,0.07)' : '#dddbd5';
const tagBg = isNavyBg ? 'rgba(255,255,255,0.04)' : '#f0efec';
return (
<section
id="stack"
aria-labelledby="stack-heading"
style={{ background: bg, color: text }}
className="px-8 py-28 md:px-14 transition-colors duration-500"
>
<div className="mx-auto max-w-7xl">
{/* Header */}
<div className="mb-16 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<div className="mb-4 flex items-center gap-3">
<span className="h-px w-8 bg-brand-red" />
<span className="text-mono-label" style={{ color: '#e50914' }}>Las herramientas son el medio</span>
</div>
<h2
id="stack-heading"
className="text-headline"
style={{ maxWidth: '26ch' }}
>
Las herramientas son el medio.<br className="hidden sm:block" /> El sistema es el fin.
</h2>
</div>
<p className="max-w-xs text-sm leading-relaxed" style={{ color: sub }}>
Elegimos la tecnología correcta para cada problema. No hay un stack universal.
</p>
</div>
{/* Stack groups */}
<div
style={{ borderTop: `1px solid ${border}` }}
>
{stackGroups.map((group, i) => (
<motion.div
key={group.label}
initial={{ opacity: 0, x: -8 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: i * 0.06 }}
className="flex flex-col gap-4 py-6 sm:flex-row sm:items-center sm:gap-10"
style={{ borderBottom: `1px solid ${border}` }}
>
<span
className="text-mono-label shrink-0"
style={{ color: sub, width: 120 }}
>
{group.label}
</span>
<div className="flex flex-wrap gap-2">
{group.items.map(item => (
<span
key={item}
className="text-sm font-medium px-3 py-1 rounded-sm"
style={{
background: tagBg,
border: `1px solid ${border}`,
color: text,
}}
>
{item}
</span>
))}
</div>
</motion.div>
))}
</div>
{/* Footnote */}
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
className="mt-10 text-xs leading-relaxed"
style={{ color: sub, maxWidth: '60ch' }}
>
Estas son las herramientas con las que trabajamos actualmente. La elección depende siempre del problema, no de la preferencia.
</motion.p>
</div>
</section>
);
};
+226
View File
@@ -0,0 +1,226 @@
import { useState } from 'react';
interface Node {
id: string;
label: string;
x: number;
y: number;
w: number;
h: number;
type: 'main' | 'side';
desc: string;
}
interface Edge {
from: string;
to: string;
type: 'main' | 'side';
}
const NODES: Node[] = [
{ id: 'frontend', label: 'FRONTEND', x: 80, y: 16, w: 160, h: 38, type: 'main', desc: 'Portales, apps y paneles:interfaces donde el usuario opera' },
{ id: 'api', label: 'API GATEWAY', x: 80, y: 108, w: 160, h: 38, type: 'main', desc: 'Seguridad, ruteo, rate limiting y versionado unificados' },
{ id: 'logic', label: 'LÓGICA DE NEGOCIO', x: 80, y: 200, w: 160, h: 38, type: 'main', desc: 'Reglas de negocio, validaciones, procesamiento y orquestación' },
{ id: 'db', label: 'BASE DE DATOS', x: 80, y: 292, w: 160, h: 38, type: 'main', desc: 'Persistencia diseñada para tu modelo:SQL, NoSQL, caché' },
{ id: 'infra', label: 'INFRAESTRUCTURA', x: 80, y: 384, w: 160, h: 36, type: 'main', desc: 'Nube, contenedores, CI/CD y observabilidad operacional' },
{ id: 'payments', label: 'PAGOS', x: 8, y: 172, w: 62, h: 28, type: 'side', desc: 'Stripe, Conekta:flujo de cobro integrado' },
{ id: 'erp', label: 'ERP / CRM', x: 8, y: 222, w: 62, h: 28, type: 'side', desc: 'SAP, Odoo:sincronización bidireccional' },
{ id: 'auth', label: 'ACCESO', x: 250, y: 84, w: 62, h: 28, type: 'side', desc: 'SSO, JWT, OAuth 2.0:identidad y permisos por rol' },
{ id: 'external', label: 'EXTERNOS', x: 250, y: 134, w: 62, h: 28, type: 'side', desc: 'APIs de terceros:logística, notificaciones, reportes' },
];
const EDGES: Edge[] = [
{ from: 'frontend', to: 'api', type: 'main' },
{ from: 'api', to: 'logic', type: 'main' },
{ from: 'logic', to: 'db', type: 'main' },
{ from: 'db', to: 'infra', type: 'main' },
{ from: 'payments', to: 'logic', type: 'side' },
{ from: 'erp', to: 'logic', type: 'side' },
{ from: 'auth', to: 'api', type: 'side' },
{ from: 'external', to: 'api', type: 'side' },
];
function nodeCenter(n: Node) {
return { cx: n.x + n.w / 2, cy: n.y + n.h / 2 };
}
function edgeLine(edge: Edge): { x1: number; y1: number; x2: number; y2: number } {
const from = NODES.find(n => n.id === edge.from)!;
const to = NODES.find(n => n.id === edge.to)!;
const fc = nodeCenter(from);
const tc = nodeCenter(to);
if (edge.type === 'main') {
return { x1: fc.cx, y1: from.y + from.h, x2: tc.cx, y2: to.y };
}
if (from.x < to.x) {
return { x1: from.x + from.w, y1: fc.cy, x2: to.x, y2: tc.cy };
}
return { x1: from.x, y1: fc.cy, x2: to.x + to.w, y2: tc.cy };
}
function isConnected(nodeId: string, activeId: string | null) {
if (!activeId || nodeId === activeId) return false;
return EDGES.some(e =>
(e.from === activeId && e.to === nodeId) ||
(e.to === activeId && e.from === nodeId),
);
}
function isEdgeActive(edge: Edge, activeId: string | null) {
if (!activeId) return false;
return edge.from === activeId || edge.to === activeId;
}
const TOOLTIP_W = 172;
const TAIL_H = 9;
const TAIL_W = 14;
function tooltipHeight(desc: string): number {
const charsPerLine = Math.floor((TOOLTIP_W - 26) / 6.3); // ~aprox chars por línea a 10.5px
const lines = Math.ceil(desc.length / charsPerLine);
return 14 + 16 + lines * 17 + 14; // paddingTop + label + líneas + paddingBottom
}
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
const [active, setActive] = useState<string | null>(null);
const RED = '#e50914';
const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)';
const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)';
const connStroke = 'rgba(229,9,20,0.45)';
const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
void tooltipBorder;
const activeDesc = NODES.find(n => n.id === active);
return (
<div className="w-full" style={{ overflow: 'visible' }}>
<svg
viewBox="0 0 320 434"
width="100%"
aria-label="Mapa de sistema interactivo:hover para explorar capas"
role="img"
style={{ maxWidth: 340, display: 'block', margin: '0 auto', overflow: 'visible' }}
onMouseLeave={() => setActive(null)}
>
<defs>
<filter id="bubble-shadow" x="-15%" y="-15%" width="130%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="4" floodColor="rgba(0,0,0,0.18)" />
</filter>
</defs>
{/* Edges */}
{EDGES.map((edge, i) => {
const { x1, y1, x2, y2 } = edgeLine(edge);
const edgeActive = isEdgeActive(edge, active);
return (
<line
key={i}
x1={x1} y1={y1} x2={x2} y2={y2}
stroke={edgeActive ? RED : edge.type === 'side' ? connStroke : defStroke}
strokeWidth={edgeActive ? 1.5 : 1}
strokeDasharray={edge.type === 'side' ? '3 3' : undefined}
style={{ transition: 'stroke 200ms, stroke-width 200ms' }}
/>
);
})}
{/* Nodes */}
{NODES.map(node => {
const isActive = active === node.id;
const isConn = isConnected(node.id, active);
const isDim = active !== null && !isActive && !isConn;
const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke;
const fillOpacity = isActive ? 0.12 : 0;
const textColor = isActive ? RED : isDim
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
: (isNavyBg ? 'rgba(255,255,255,0.65)' : 'rgba(0,31,91,0.75)');
const { cx } = nodeCenter(node);
return (
<g
key={node.id}
onMouseEnter={() => setActive(node.id)}
onFocus={() => setActive(node.id)}
onBlur={() => setActive(null)}
style={{ cursor: 'default', outline: 'none' }}
tabIndex={0}
role="button"
aria-label={`${node.label}: ${node.desc}`}
>
<rect
x={node.x} y={node.y}
width={node.w} height={node.h}
fill={`rgba(229,9,20,${fillOpacity})`}
stroke={stroke}
strokeWidth={isActive ? 1.5 : 1}
rx={2}
style={{ transition: 'fill 200ms, stroke 200ms' }}
/>
<text
x={cx}
y={node.y + node.h / 2 + 1}
textAnchor="middle"
dominantBaseline="middle"
fill={textColor}
fontFamily="'SF Mono','Fira Code',Consolas,monospace"
fontSize={node.type === 'main' ? 10 : 8}
fontWeight={600}
letterSpacing={node.type === 'main' ? 1.2 : 0.8}
style={{ transition: 'fill 200ms', pointerEvents: 'none' }}
>
{node.label}
</text>
</g>
);
})}
{/* Tooltip burbuja estilo chat */}
{activeDesc && (() => {
const node = NODES.find(n => n.id === active)!;
const TOOLTIP_H = tooltipHeight(activeDesc.desc);
const cy = node.y + node.h / 2;
// nodos izquierda → burbuja a la derecha
// nodos derecha → burbuja a la izquierda
// nodos centro → burbuja a la derecha
const showLeft = node.x > 160; // solo los nodos de la derecha van a la izquierda
let tx: number, ty: number, tailPoints: string;
if (showLeft) {
tx = node.x - TOOLTIP_W - TAIL_H - 4;
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
tailPoints = `${tx + TOOLTIP_W},${tailCy - TAIL_W / 2} ${tx + TOOLTIP_W},${tailCy + TAIL_W / 2} ${tx + TOOLTIP_W + TAIL_H},${tailCy}`;
} else {
tx = node.x + node.w + TAIL_H + 4;
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
tailPoints = `${tx},${tailCy - TAIL_W / 2} ${tx},${tailCy + TAIL_W / 2} ${tx - TAIL_H},${tailCy}`;
}
return (
<g style={{ pointerEvents: 'none' }}>
<g filter="url(#bubble-shadow)">
<rect x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} fill={tooltipBg} rx={12} />
<polygon points={tailPoints} fill={tooltipBg} />
</g>
<foreignObject x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} style={{ overflow: 'visible' }}>
<div style={{ padding: '10px 13px' }}>
<p style={{ fontFamily: "'SF Mono',Consolas,monospace", fontSize: 8, letterSpacing: '0.12em', textTransform: 'uppercase', color: RED, marginBottom: 4, fontWeight: 600 }}>
{activeDesc.label}
</p>
<p style={{ fontSize: 10.5, color: descText, lineHeight: 1.5, margin: 0 }}>
{activeDesc.desc}
</p>
</div>
</foreignObject>
</g>
);
})()}
</svg>
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
export const machineryData = [
{
id: 1,
num: '01',
title: 'OPERACIONES',
description: 'Plataformas internas, automatización de flujos y control de procesos críticos. Sistemas que eliminan la fricción operativa y dan visibilidad en tiempo real.',
tags: ['Portales de gestión', 'Automatización de flujos', 'Reportes operacionales', 'Control de acceso por rol'],
},
{
id: 2,
num: '02',
title: 'VENTAS',
description: 'Sistemas de venta, cobro y facturación integrados al núcleo del negocio. Desde el punto de contacto con el cliente hasta el comprobante fiscal.',
tags: ['E-commerce a medida', 'Integración de pagos', 'Facturación y CFDI', 'Recuperación de carritos'],
},
{
id: 3,
num: '03',
title: 'INTEGRACIONES',
description: 'Conectamos sistemas heterogéneos en un ecosistema coherente: ERPs, CRMs, proveedores y APIs de terceros. Sin silos, sin duplicidad de datos.',
tags: ['ERP / SAP / Odoo', 'APIs de terceros', 'Sincronización bidireccional', 'Webhooks y eventos'],
},
{
id: 4,
num: '04',
title: 'AUTOMATIZACIÓN',
description: 'Eliminamos intervención humana donde no aporta valor: aprobaciones, notificaciones, sincronizaciones y pipelines de datos que corren solos.',
tags: ['Bots y pipelines', 'Procesamiento de datos', 'Notificaciones inteligentes', 'Triggers y colas'],
},
{
id: 5,
num: '05',
title: 'DATOS',
description: 'Instrumentación del negocio para tomar decisiones con evidencia. Dashboards ejecutivos, modelos analíticos y alertas que activan antes de que el problema escale.',
tags: ['Dashboards ejecutivos', 'Pipelines de datos', 'KPIs e instrumentación', 'Alertas y umbrales'],
},
];
+15 -7
View File
@@ -5,29 +5,37 @@ export const servicesData: ServiceItem[] = [
{
id: 1,
title: 'Desarrollo de software',
description: 'Arquitecturas a medida, código mantenible y desarrollo de plataformas de alto rendimiento.',
description: 'Plataformas web y APIs construidas con arquitectura limpia, código mantenible y enfoque en rendimiento real.',
features: ['APIs REST & GraphQL', 'Microservicios y monolitos modulares', 'Code review y auditorías técnicas'],
icon: Code,
gridCols: 'md:col-span-7',
accent: 'blue',
},
{
id: 2,
title: 'Soluciones en la nube',
description: 'Infraestructura cloud escalable, migración y alta disponibilidad 24/7 sin interrupciones.',
title: 'Infraestructura en la nube',
description: 'Diseño, migración y operación de infraestructura cloud escalable con alta disponibilidad.',
features: ['AWS, GCP y Azure', 'CI/CD y DevOps', 'Monitoreo y alertas 24/7'],
icon: Cloud,
gridCols: 'md:col-span-5',
accent: 'blue',
},
{
id: 3,
title: 'Tecnología segura',
description: 'Protección de datos corporativos, cifrado avanzado y protocolos de ciberseguridad activa.',
title: 'Ciberseguridad aplicada',
description: 'Protección de datos, auditorías de seguridad y protocolos que cumplen estándares empresariales.',
features: ['Pruebas de penetración', 'Cifrado end-to-end', 'Cumplimiento GDPR / ISO'],
icon: ShieldCheck,
gridCols: 'md:col-span-5',
accent: 'red',
},
{
id: 4,
title: 'Innovación que impulsa negocios',
description: 'Estrategias tecnológicas orientadas a optimizar operaciones y acelerar el crecimiento comercial.',
title: 'Producto y crecimiento',
description: 'Estrategia tecnológica alineada al negocio para lanzar, medir y escalar con datos reales.',
features: ['Roadmap técnico y de producto', 'Analytics e instrumentación', 'Optimización de conversión'],
icon: TrendingUp,
gridCols: 'md:col-span-7',
accent: 'red',
},
];
+60
View File
@@ -0,0 +1,60 @@
import { useEffect, useRef } from 'react';
export function useScrollSnap() {
const locked = useRef(false);
const current = useRef(0);
useEffect(() => {
const getSections = () =>
Array.from(document.querySelectorAll('section, footer')) as HTMLElement[];
const goTo = (index: number) => {
const sections = getSections();
if (index < 0 || index >= sections.length) return;
current.current = index;
locked.current = true;
sections[index].scrollIntoView({ behavior: 'smooth', block: 'start' });
setTimeout(() => { locked.current = false; }, 900);
};
const updateCurrent = () => {
const sections = getSections();
const mid = window.innerHeight / 2;
sections.forEach((s, i) => {
const rect = s.getBoundingClientRect();
if (rect.top <= mid && rect.bottom > mid) current.current = i;
});
};
const onWheel = (e: WheelEvent) => {
if (locked.current) { e.preventDefault(); return; }
updateCurrent();
const dir = e.deltaY > 0 ? 1 : -1;
const next = current.current + dir;
const sections = getSections();
if (next >= 0 && next < sections.length) {
e.preventDefault();
goTo(next);
}
};
const onKeyDown = (e: KeyboardEvent) => {
if (locked.current) return;
updateCurrent();
if (e.key === 'ArrowDown' || e.key === 'PageDown') {
e.preventDefault();
goTo(current.current + 1);
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault();
goTo(current.current - 1);
}
};
window.addEventListener('wheel', onWheel, { passive: false });
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('wheel', onWheel);
window.removeEventListener('keydown', onKeyDown);
};
}, []);
}
+124 -46
View File
@@ -1,34 +1,89 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@700;800;900&family=Inter:wght@300;400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,700;0,800;0,900;1,800&family=Inter:wght@300;400;500;600&display=swap');
@import "tailwindcss";
@theme {
--color-brand-blue: #0077c2;
--color-brand-navy: #001f5b;
--color-brand-black: #000000;
--color-brand-black: #0a0a0a;
--color-brand-red: #e50914;
--color-bg-light: #f9f9f7;
--color-bg-alt: #f0efec;
--color-border-light: #dddbd5;
--color-text-secondary: #6a6866;
--color-dark-bg: #080c14;
--color-dark-surface: #0f1520;
--color-dark-border: #1c2535;
--font-sans: 'Inter', sans-serif;
--font-display: 'Plus Jakarta Sans', sans-serif;
--font-sans: 'Inter', system-ui, sans-serif;
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', Consolas, monospace;
}
::selection {
background: rgba(229, 9, 20, 0.18);
color: inherit;
}
@layer base {
body {
@apply font-sans antialiased m-0 p-0 overflow-x-hidden selection:bg-brand-blue selection:text-white;
@apply font-sans antialiased m-0 p-0 overflow-x-hidden;
background-color: #f9f9f7;
color: #0a0a0a;
}
h1,
h2,
h3,
h4,
.font-display {
h1, h2, h3, h4, .font-display {
font-family: var(--font-display);
}
*:focus-visible {
outline: 2px solid #e50914;
outline-offset: 2px;
}
::selection {
background: #001f5b;
color: #fff;
}
}
/* Typography scale */
.text-display {
font-family: var(--font-display);
font-size: clamp(2.6rem, 6vw, 5.5rem);
line-height: 1.02;
font-weight: 900;
letter-spacing: -0.04em;
}
.text-headline {
font-family: var(--font-display);
font-size: clamp(1.8rem, 4vw, 3.2rem);
line-height: 1.1;
font-weight: 800;
letter-spacing: -0.03em;
}
.text-section-title {
font-family: var(--font-display);
font-size: clamp(1.5rem, 3vw, 2.4rem);
line-height: 1.15;
font-weight: 800;
letter-spacing: -0.025em;
}
.text-mono-label {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 500;
}
/* Section wave divider */
.section-wave {
position: relative;
}
.section-wave::before {
content: '';
position: absolute;
@@ -36,51 +91,74 @@
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(0, 31, 91, 0.24), transparent);
opacity: 0.55;
background: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0.12;
pointer-events: none;
}
.visual-shell {
position: relative;
/* Bg patterns */
.bg-dot-grid {
background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
background-size: 24px 24px;
}
.bg-grid-lines {
background-image:
linear-gradient(currentColor 1px, transparent 1px),
linear-gradient(to right, currentColor 1px, transparent 1px);
background-size: 48px 48px;
}
/* Ticker */
@keyframes ticker {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.animate-ticker {
animation: ticker 32s linear infinite;
}
/* Anatomy accordion */
.anatomy-body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 320ms cubic-bezier(0.4, 0, 0.2, 1);
}
.anatomy-body.open {
grid-template-rows: 1fr;
}
.anatomy-body-inner {
overflow: hidden;
}
.image-tile {
border-radius: 18px;
/* Principles hover */
.principle-desc {
max-height: 0;
overflow: hidden;
transition: max-height 320ms cubic-bezier(0.4, 0, 0.2, 1);
}
.principle-item:hover .principle-desc,
.principle-item:focus-within .principle-desc {
max-height: 120px;
}
.visual-shell::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at top right, rgba(229, 9, 20, 0.18), transparent 24%),
radial-gradient(circle at bottom left, rgba(0, 119, 194, 0.24), transparent 26%);
pointer-events: none;
/* Modal fade */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fadeIn {
animation: fadeIn 200ms ease;
}
.visual-grid {
position: relative;
z-index: 1;
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.placeholder-tile {
backdrop-filter: blur(8px);
.animate-ticker {
animation: none;
}
.text-hero-dacodes {
font-family: var(--font-display);
font-size: clamp(2.5rem, 6vw, 5rem);
line-height: 1.05;
font-weight: 900;
letter-spacing: -0.035em;
}
.text-section-title {
font-family: var(--font-display);
font-size: clamp(2rem, 4vw, 3.5rem);
line-height: 1.1;
font-weight: 800;
letter-spacing: -0.03em;
}
+3
View File
@@ -1,10 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
+151
View File
@@ -0,0 +1,151 @@
import { useEffect } from 'react';
import { motion } from 'framer-motion';
const fadeUp = {
hidden: { opacity: 0, y: 28 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const } },
};
const articulos = [
{
categoria: 'ARQUITECTURA',
titulo: 'Por qué el software empresarial fracasa antes de llegar a producción',
desc: 'La mayoría de los proyectos de software empresarial no fallan en el código, fallan en el diseño. Exploramos las causas estructurales más comunes y cómo evitarlas desde el inicio.',
fecha: 'Próximamente',
img: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800&q=80',
},
{
categoria: 'INTEGRACIONES',
titulo: 'ERP vs. sistema a medida: cuándo tiene sentido cada uno',
desc: 'La respuesta no es simple. Depende del tamaño de la operación, la madurez del proceso y la capacidad técnica interna. Aquí el marco de decisión que usamos con nuestros clientes.',
fecha: 'Próximamente',
img: 'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=800&q=80',
},
{
categoria: 'DATOS',
titulo: 'Instrumentación operacional: medir lo que importa',
desc: 'Tener datos no es lo mismo que tener visibilidad. La diferencia está en qué se mide, cómo se almacena y cuándo se activa una alerta. Una guía práctica para equipos de operaciones.',
fecha: 'Próximamente',
img: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&q=80',
},
];
export const Blog = () => {
useEffect(() => {
document.title = 'Blog | Rynext Technologies';
}, []);
return (
<main id="main-content">
{/* Hero */}
<section
style={{
background: '#ffffff',
borderBottom: '1px solid #dddbd5',
paddingTop: '160px',
paddingBottom: '100px',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.p className="text-mono-label mb-6"
style={{ color: '#e50914', letterSpacing: '0.18em' }}
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
RECURSOS
</motion.p>
<motion.h1 className="text-display" style={{ color: '#0a0a0a', maxWidth: '16ch' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
Pensamiento de ingeniería.
</motion.h1>
<motion.p className="mt-8 text-lg leading-relaxed"
style={{ color: '#6a6866', maxWidth: '52ch' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}>
Perspectivas sobre arquitectura de software, sistemas empresariales y decisiones técnicas
que impactan la operación real de los negocios.
</motion.p>
</div>
</section>
{/* Artículos */}
<section
style={{
background: '#f5f5f3',
borderBottom: '1px solid #dddbd5',
padding: '100px 0',
minHeight: '60vh',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.p className="text-mono-label mb-12"
style={{ color: '#e50914', letterSpacing: '0.18em' }}
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
PRÓXIMOS ARTÍCULOS
</motion.p>
<div className="grid gap-6 md:grid-cols-3">
{articulos.map((art, i) => (
<motion.article key={i}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] } } }}
style={{
background: '#ffffff',
border: '1px solid #dddbd5',
borderRadius: 4,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
opacity: 0.82,
}}
>
{/* Thumbnail */}
<div style={{ aspectRatio: '16/9', background: '#f0efec', overflow: 'hidden', position: 'relative' }}>
<img
src={art.img}
alt={art.titulo}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => { (e.currentTarget as HTMLImageElement).style.display = 'none'; }}
/>
{/* Categoria badge encima */}
<span
style={{
position: 'absolute', top: 14, left: 14,
background: '#e50914', color: '#fff',
fontFamily: 'monospace', fontSize: 10, letterSpacing: '0.12em',
padding: '3px 8px', borderRadius: 2,
}}
>{art.categoria}</span>
</div>
{/* Contenido */}
<div style={{ padding: '28px 28px 24px', display: 'flex', flexDirection: 'column', gap: 12, flexGrow: 1 }}>
<h2 className="font-display font-bold text-lg leading-snug" style={{ color: '#0a0a0a' }}>
{art.titulo}
</h2>
<p className="text-sm leading-relaxed" style={{ color: '#6a6866', flexGrow: 1 }}>
{art.desc}
</p>
<p className="text-mono-label" style={{ color: '#aaa8a4', letterSpacing: '0.12em', marginTop: 4 }}>
{art.fecha}
</p>
</div>
</motion.article>
))}
</div>
<motion.div
className="mt-20 text-center"
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.6, delay: 0.3 } } }}
>
<p className="text-sm" style={{ color: '#aaa8a4' }}>
El blog está en preparación. Los artículos llegarán pronto.
</p>
</motion.div>
</div>
</section>
</main>
);
};
+24
View File
@@ -0,0 +1,24 @@
import { Hero } from '../components/Hero';
import { Problem } from '../components/Problem';
import { Machinery } from '../components/Machinery';
import { Anatomy } from '../components/Anatomy';
import { Process } from '../components/Process';
import { Principles } from '../components/Principles';
import { CtaBanner } from '../components/CtaBanner';
import type { ThemeProps } from '../types';
interface HomeProps extends ThemeProps {
onOpenContact: () => void;
}
export const Home = ({ isNavyBg, onOpenContact }: HomeProps) => (
<main id="main-content">
<Hero isNavyBg={isNavyBg} onOpenContact={onOpenContact} />
<Problem isNavyBg={isNavyBg} />
<Machinery isNavyBg={isNavyBg} />
<Anatomy isNavyBg={isNavyBg} />
<Process isNavyBg={isNavyBg} />
<Principles isNavyBg={isNavyBg} />
<CtaBanner isNavyBg={isNavyBg} onOpenContact={onOpenContact} />
</main>
);
+110
View File
@@ -0,0 +1,110 @@
import { useEffect } from 'react';
import { motion } from 'framer-motion';
const fadeUp = {
hidden: { opacity: 0, y: 28 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as const } },
};
export const Productos = () => {
useEffect(() => {
document.title = 'Productos | Rynext Technologies';
}, []);
return (
<main id="main-content">
{/* Hero */}
<section
style={{
background: '#ffffff',
borderBottom: '1px solid #dddbd5',
paddingTop: '160px',
paddingBottom: '100px',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.p className="text-mono-label mb-6"
style={{ color: '#e50914', letterSpacing: '0.18em' }}
initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeUp}>
PRODUCTOS
</motion.p>
<motion.h1 className="text-display" style={{ color: '#0a0a0a', maxWidth: '18ch' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
Software diseñado para operar.
</motion.h1>
<motion.p className="mt-8 text-lg leading-relaxed"
style={{ color: '#6a6866', maxWidth: '52ch' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}>
Módulos y licencias listos para implementar, respaldados por la misma ingeniería
con la que construimos sistemas a medida.
</motion.p>
</div>
</section>
{/* Producto — comentado hasta tener contenido real */}
{/* <section
style={{
background: '#f5f5f3',
borderBottom: '1px solid #dddbd5',
padding: '100px 0',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.div
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] } } }}
style={{
background: '#ffffff',
border: '1px solid #dddbd5',
borderRadius: 4,
padding: '60px 56px',
display: 'flex',
flexDirection: 'column',
gap: 32,
maxWidth: 720,
}}
>
<div style={{
width: 72, height: 72,
background: '#f0efec',
border: '1.5px dashed #c8c5bf',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#c8c5bf" strokeWidth="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<div>
<p className="text-mono-label mb-2" style={{ color: '#aaa8a4', letterSpacing: '0.14em' }}>NOMBRE DEL PRODUCTO</p>
<h2 className="text-headline" style={{ color: '#0a0a0a' }}>Tu producto aquí</h2>
</div>
<p className="text-base leading-relaxed" style={{ color: '#6a6866', maxWidth: '48ch' }}>
Descripción del producto. Qué resuelve, para quién está diseñado y qué lo hace diferente.
Agrega aquí los detalles que quieres que el cliente vea antes de solicitar una demo.
</p>
<div>
<a
href="mailto:[email protected]?subject=Solicitud%20de%20demo"
className="inline-flex items-center gap-2 px-7 py-3.5 text-sm font-bold text-white transition hover:opacity-90"
style={{ background: '#e50914', borderRadius: 3 }}
>
Solicitar demo
</a>
</div>
</motion.div>
</div>
</section> */}
</main>
);
};
+173
View File
@@ -0,0 +1,173 @@
import { useEffect } from 'react';
import { motion } from 'framer-motion';
const valores = [
{ num: '01', titulo: 'Arquitectura primero', desc: 'Cada sistema empieza con un diseño estructural claro antes de escribir la primera línea de código.' },
{ num: '02', titulo: 'Sin atajos técnicos', desc: 'No usamos soluciones genéricas donde la operación exige algo específico. La deuda técnica cuesta más a largo plazo.' },
{ num: '03', titulo: 'Código como contrato', desc: 'El software que entregamos es la documentación viva de la operación del cliente. Tiene que ser legible y mantenible.' },
{ num: '04', titulo: 'Iteración con propósito', desc: 'Construimos en ciclos cortos y medibles. Cada entrega aporta valor real y permite ajustar la dirección.' },
{ num: '05', titulo: 'Responsabilidad completa', desc: 'Desde el análisis hasta la producción, somos dueños del resultado. No transferimos riesgo al cliente.' },
];
const TEAM_IMAGE = 'https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200&q=80';
export const SobreNosotros = () => {
useEffect(() => {
document.title = 'Sobre Nosotros | Rynext Technologies';
}, []);
return (
<main id="main-content">
{/* Hero — texto + imagen lado a lado */}
<section
style={{
background: '#ffffff',
borderBottom: '1px solid #dddbd5',
paddingTop: '140px',
paddingBottom: '100px',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl grid gap-16 lg:grid-cols-2 lg:items-center">
{/* Texto */}
<div>
<motion.h1
className="text-display mb-8"
style={{ color: '#0a0a0a' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 32 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}
>
Software 100%{' '}
<span style={{ position: 'relative', display: 'inline-block' }}>
<span style={{ color: '#e50914' }}>Mexicano</span>
<svg
viewBox="0 0 200 12"
preserveAspectRatio="none"
aria-hidden="true"
style={{ position: 'absolute', bottom: -4, left: 0, width: '100%', height: 12 }}
>
<path d="M2 8 C50 2, 150 2, 198 8" stroke="#e50914" strokeWidth="3" strokeLinecap="round" fill="none" />
</svg>
</span>{' '}
de clase mundial.
</motion.h1>
<motion.p
className="text-lg leading-relaxed"
style={{ color: '#6a6866' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.2, ease: [0.22, 1, 0.36, 1] } } }}
>
Rynext Technologies nació de una convicción: México tiene el talento para construir
software empresarial que compita con cualquier firma en el mundo. No adaptamos
herramientas extranjeras diseñamos sistemas propios, con las tecnologías más
actuales, para la realidad operativa de los negocios mexicanos.
</motion.p>
</div>
{/* Imagen del equipo */}
<motion.div
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, x: 24 }, visible: { opacity: 1, x: 0, transition: { duration: 0.7, delay: 0.15, ease: [0.22, 1, 0.36, 1] } } }}
style={{ position: 'relative' }}
>
{/* Acento rojo desplazado atrás */}
<div style={{
position: 'absolute',
top: 16, left: -16,
width: '100%', height: '100%',
border: '3px solid #e50914',
borderRadius: 2,
zIndex: 0,
}} />
{/* Imagen con corte diagonal */}
<img
src={TEAM_IMAGE}
alt="Equipo Rynext Technologies"
style={{
position: 'relative',
zIndex: 1,
width: '100%',
aspectRatio: '4/3',
objectFit: 'cover',
display: 'block',
clipPath: 'polygon(0 0, 100% 0, 100% 88%, 94% 100%, 0 100%)',
}}
/>
</motion.div>
</div>
</section>
{/* Por qué Rynext */}
<section
style={{
background: '#f5f5f3',
borderBottom: '1px solid #dddbd5',
padding: '100px 0',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.h2 className="text-display mb-20" style={{ color: '#0a0a0a', maxWidth: '22ch' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 28 }, visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] } } }}>
¿Por qué <span style={{ color: '#e50914' }}>Rynext Technologies?</span>
</motion.h2>
<div className="flex flex-col divide-y" style={{ borderTop: '1px solid #dddbd5', borderBottom: '1px solid #dddbd5' }}>
{[
{ num: '01', titulo: 'Diseño desde la raíz', desc: 'Cada sistema empieza con un análisis profundo de la operación. No adaptamos plantillas, diseñamos desde cero para el contexto específico del negocio.' },
{ num: '02', titulo: 'Equipo técnico propio', desc: 'Sin intermediarios. El equipo que analiza tu operación es el mismo que diseña la arquitectura y escribe el código.' },
{ num: '03', titulo: 'Enfoque en producción', desc: 'No entregamos prototipos. Entregamos sistemas listos para operar con carga real, datos reales y usuarios reales.' },
].map((item, i) => (
<motion.div key={i}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 16 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] } } }}
className="grid py-10 md:grid-cols-[80px_1fr_2fr] gap-6 items-start"
style={{ borderColor: '#dddbd5' }}
>
<span className="font-display font-black" style={{ fontSize: 48, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}>{item.num}</span>
<h3 className="font-display font-bold text-xl" style={{ color: '#0a0a0a' }}>{item.titulo}</h3>
<p className="text-base leading-relaxed" style={{ color: '#6a6866', maxWidth: '52ch' }}>{item.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
{/* Valores */}
<section
style={{
background: '#ffffff',
borderBottom: '1px solid #dddbd5',
padding: '100px 0',
}}
className="px-8 md:px-14"
>
<div className="mx-auto max-w-7xl">
<motion.h2 className="text-headline mb-16" style={{ color: '#0a0a0a' }}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 28 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6, delay: 0.1, ease: [0.22, 1, 0.36, 1] } } }}>
Los principios que guían cada proyecto.
</motion.h2>
<div className="grid gap-8 md:grid-cols-5">
{valores.map((v, i) => (
<motion.div key={i}
initial="hidden" whileInView="visible" viewport={{ once: true }}
variants={{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, delay: i * 0.08, ease: [0.22, 1, 0.36, 1] } } }}
style={{ borderTop: '3px solid #e50914', paddingTop: 24 }}
>
<span className="font-display font-black block mb-4"
style={{ fontSize: 48, lineHeight: 1, color: '#e50914', letterSpacing: '-0.04em' }}>
{v.num}
</span>
<h3 className="font-display font-bold text-base mb-3" style={{ color: '#0a0a0a' }}>{v.titulo}</h3>
<p className="text-sm leading-relaxed" style={{ color: '#6a6866' }}>{v.desc}</p>
</motion.div>
))}
</div>
</div>
</section>
</main>
);
};
+19 -15
View File
@@ -1,27 +1,31 @@
import { type ComponentType, type Dispatch, type SetStateAction } from 'react';
export interface ServiceItem {
id: number;
title: string;
description: string;
icon: ComponentType<{ size?: number; className?: string }>;
gridCols: string;
}
import type React from 'react';
import { type Dispatch, type SetStateAction } from 'react';
export interface ThemeProps {
isNavyBg: boolean;
}
export interface NavbarProps {
onOpenContact: () => void;
}
export interface HeroProps extends ThemeProps {
onOpenContact: () => void;
}
export interface ModalProps extends ThemeProps {
isOpen: boolean;
onClose: () => void;
}
export interface NavbarProps extends ThemeProps {
setIsNavyBg: Dispatch<SetStateAction<boolean>>;
onOpenContact: () => void;
export interface ServiceItem {
id: number;
title: string;
description: string;
features: string[];
icon: React.ElementType;
accent: 'blue' | 'red';
gridCols: string;
}
export interface HeroProps extends ThemeProps {
onOpenContact: () => void;
}
export type { Dispatch, SetStateAction };
+29 -2
View File
@@ -1,8 +1,35 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [
react(),
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\./i,
handler: 'CacheFirst',
options: { cacheName: 'google-fonts', expiration: { maxEntries: 10, maxAgeSeconds: 60 * 60 * 24 * 365 } },
},
],
},
manifest: {
name: 'Rynext Technologies',
short_name: 'Rynext',
description: 'Ingeniería de software empresarial desde la arquitectura.',
theme_color: '#001f5b',
background_color: '#ffffff',
display: 'standalone',
icons: [
{ src: '/icons/Logo_blanco.svg', sizes: 'any', type: 'image/svg+xml' },
],
},
}),
],
})