limpieza de codigo
This commit is contained in:
-104
@@ -1,104 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fdfdfd;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.hero-vector {
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<svg class="hero-vector" viewBox="0 0 900 350" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="track-1" d="M 50 175 C 150 50, 250 300, 450 175 C 650 50, 750 300, 850 175 C 750 50, 650 300, 450 175 C 250 50, 150 300, 50 175 Z"
|
||||
stroke="#429b82" stroke-width="8" stroke-linecap="round" fill="none" />
|
||||
|
||||
<path id="track-2" d="M 50 175 C 150 300, 250 50, 450 175 C 650 300, 750 50, 850 175 C 750 300, 650 50, 450 175 C 250 300, 150 50, 50 175 Z"
|
||||
stroke="#b2c6ce" stroke-width="8" stroke-linecap="round" fill="none" opacity="0.6" />
|
||||
|
||||
<g id="dashed-lines"></g>
|
||||
|
||||
<g id="nodes-layer"></g>
|
||||
</svg>
|
||||
|
||||
<script>
|
||||
const track1 = document.getElementById('track-1');
|
||||
const track2 = document.getElementById('track-2');
|
||||
const len1 = track1.getTotalLength();
|
||||
const len2 = track2.getTotalLength();
|
||||
|
||||
const nodesLayer = document.getElementById('nodes-layer');
|
||||
const linesLayer = document.getElementById('dashed-lines');
|
||||
|
||||
// Configuración de los nodos móviles
|
||||
const nodesConfig = [
|
||||
{ track: track1, length: len1, progress: 0.00, speed: 0.0004, color: '#133543', radius: 11 },
|
||||
{ track: track1, length: len1, progress: 0.25, speed: 0.0004, color: '#429b82', radius: 9 },
|
||||
{ track: track1, length: len1, progress: 0.50, speed: 0.0004, color: '#133543', radius: 11 },
|
||||
{ track: track1, length: len1, progress: 0.75, speed: 0.0004, color: '#429b82', radius: 9 },
|
||||
{ track: track2, length: len2, progress: 0.12, speed: 0.0003, color: '#133543', radius: 10 },
|
||||
{ track: track2, length: len2, progress: 0.62, speed: 0.0003, color: '#429b82', radius: 10 }
|
||||
];
|
||||
|
||||
// Crear elementos SVG en el DOM
|
||||
const nodes = nodesConfig.map(cfg => {
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
circle.setAttribute('r', cfg.radius);
|
||||
circle.setAttribute('fill', cfg.color);
|
||||
nodesLayer.appendChild(circle);
|
||||
return { ...cfg, element: circle, x: 0, y: 0 };
|
||||
});
|
||||
|
||||
// Crear líneas punteadas para conectar pares de nodos consecutivos
|
||||
const lines = [];
|
||||
for (let i = 0; i < nodes.length - 1; i++) {
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('stroke', '#133543');
|
||||
line.setAttribute('stroke-width', '2');
|
||||
line.setAttribute('stroke-dasharray', '5,5');
|
||||
line.setAttribute('opacity', '0.7');
|
||||
linesLayer.appendChild(line);
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
// Bucle de animación (60fps)
|
||||
function animate() {
|
||||
// 1. Actualizar posición de cada nodo sobre su trayectoria
|
||||
nodes.forEach(node => {
|
||||
node.progress = (node.progress + node.speed) % 1;
|
||||
const point = node.track.getPointAtLength(node.progress * node.length);
|
||||
node.x = point.x;
|
||||
node.y = point.y;
|
||||
node.element.setAttribute('cx', point.x);
|
||||
node.element.setAttribute('cy', point.y);
|
||||
});
|
||||
|
||||
// 2. Conectar nodos ordenados por su eje X con las líneas punteadas
|
||||
const sortedNodes = [...nodes].sort((a, b) => a.x - b.x);
|
||||
for (let i = 0; i < sortedNodes.length - 1; i++) {
|
||||
const p1 = sortedNodes[i];
|
||||
const p2 = sortedNodes[i + 1];
|
||||
lines[i].setAttribute('x1', p1.x);
|
||||
lines[i].setAttribute('y1', p1.y);
|
||||
lines[i].setAttribute('x2', p2.x);
|
||||
lines[i].setAttribute('y2', p2.y);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Buscar en el sitio | IDE Consultores</title>
|
||||
<meta name="description" content="Buscá contenidos, soluciones, cursos e insights dentro del sitio de IDE Consultores.">
|
||||
<meta name="robots" content="noindex, follow">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<link rel="icon" type="image/x-icon" href="assets/img/favicon.png">
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header id="site-header"></header>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- Hero con el buscador -->
|
||||
<section class="search-hero">
|
||||
<div class="container container-narrow text-center">
|
||||
<span class="eyebrow">BUSCAR EN EL SITIO</span>
|
||||
<h1>¿Qué estás buscando?</h1>
|
||||
<p class="lead-text">
|
||||
Encontrá contenidos, soluciones, cursos del Ecosistema IDE e insights publicados.
|
||||
</p>
|
||||
|
||||
<form id="search-page-form" class="search-page-form" role="search">
|
||||
<div class="search-input-wrap">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
id="search-page-input"
|
||||
name="q"
|
||||
placeholder="Ej: ISO 9001, PMO, Academy, IA aplicada..."
|
||||
autocomplete="off"
|
||||
aria-label="Término de búsqueda"
|
||||
>
|
||||
</div>
|
||||
<button type="submit">Buscar</button>
|
||||
</form>
|
||||
|
||||
<p id="search-status" class="search-hint"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Resultados -->
|
||||
<section class="search-results-section">
|
||||
<div class="container">
|
||||
|
||||
<div id="search-suggestions" class="search-suggestions" hidden>
|
||||
<strong>Búsquedas frecuentes</strong>
|
||||
<a href="?q=ISO%209001" class="chip">ISO 9001</a>
|
||||
<a href="?q=PMO" class="chip">PMO</a>
|
||||
<a href="?q=Academy" class="chip">IDE Academy</a>
|
||||
<a href="?q=transformaci%C3%B3n%20digital" class="chip">Transformación digital</a>
|
||||
<a href="?q=IA" class="chip">IA aplicada</a>
|
||||
<a href="?q=sistemas%20de%20gestion" class="chip">Sistemas de gestión</a>
|
||||
<a href="?q=insights" class="chip">Insights</a>
|
||||
</div>
|
||||
|
||||
<div id="search-results" class="search-results-list"></div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer id="site-footer"></footer>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/* ==========================================================================
|
||||
PÁGINA DE BÚSQUEDA — buscar.html
|
||||
========================================================================== */
|
||||
|
||||
/* Hero con el campo de búsqueda */
|
||||
.search-hero {
|
||||
padding: 3.5rem 0 2.5rem;
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-hero .eyebrow {
|
||||
margin-bottom: 0.75rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.search-hero h1 {
|
||||
font-size: clamp(1.75rem, 4vw, 2.5rem);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.search-hero .lead-text {
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
max-width: 640px;
|
||||
margin: 0 auto 2rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* Form de búsqueda */
|
||||
.search-page-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-page-form .search-input-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.search-page-form .search-input-wrap svg {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-page-form input[type="search"] {
|
||||
width: 100%;
|
||||
padding: 0.9rem 1rem 0.9rem 2.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: var(--bg-body, #fff);
|
||||
color: var(--text-body);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.search-page-form input[type="search"]::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-page-form input[type="search"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--ide-primary);
|
||||
box-shadow: 0 0 0 3px rgba(19, 167, 138, 0.15);
|
||||
}
|
||||
|
||||
.search-page-form button[type="submit"] {
|
||||
padding: 0.9rem 1.75rem;
|
||||
background: var(--ide-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.2s ease, transform 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-page-form button[type="submit"]:hover {
|
||||
background: var(--ide-accent, #13A78A);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.search-page-form button[type="submit"]:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Status debajo del form */
|
||||
.search-hint {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 1rem;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
/* Sección de resultados */
|
||||
.search-results-section {
|
||||
padding: 2.5rem 0 4rem;
|
||||
}
|
||||
|
||||
.search-results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-surface);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
border-color: var(--ide-primary);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.search-result-item .result-category {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: var(--ide-accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.search-result-item h3 {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.search-result-item h3 a {
|
||||
color: var(--ide-primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.search-result-item h3 a:hover {
|
||||
color: var(--ide-accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.search-result-item p {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-result-item mark {
|
||||
background: rgba(19, 167, 138, 0.18);
|
||||
color: var(--ide-primary);
|
||||
font-weight: 600;
|
||||
padding: 0 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Estado vacío */
|
||||
.search-empty {
|
||||
text-align: center;
|
||||
padding: 3.5rem 1rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.search-empty svg {
|
||||
opacity: 0.35;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--ide-primary);
|
||||
}
|
||||
|
||||
.search-empty p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.search-empty p strong {
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.search-empty .search-hint {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.search-empty a {
|
||||
color: var(--ide-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Chips de sugerencias */
|
||||
.search-suggestions {
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
max-width: 640px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.search-suggestions strong {
|
||||
display: block;
|
||||
margin-bottom: 0.85rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.search-suggestions .chip {
|
||||
display: inline-block;
|
||||
padding: 0.45rem 0.9rem;
|
||||
margin: 0.2rem 0.25rem;
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-body);
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.search-suggestions .chip:hover {
|
||||
background: var(--ide-primary);
|
||||
color: #fff;
|
||||
border-color: var(--ide-primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.search-hero {
|
||||
padding: 2.5rem 0 1.75rem;
|
||||
}
|
||||
|
||||
.search-page-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-page-form button[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -386,7 +386,7 @@ p, li, .lead-text, .text-body, .text-muted {
|
||||
}
|
||||
|
||||
/* Switch de Tema Claro/Oscuro */
|
||||
.theme-toggle-btn {
|
||||
.theme-toggle-btn, .search-link-btn {
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-main);
|
||||
@@ -400,7 +400,7 @@ p, li, .lead-text, .text-body, .text-muted {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
.theme-toggle-btn:hover, .search-link-btn:hover {
|
||||
background: var(--border-color);
|
||||
color: var(--ide-accent);
|
||||
}
|
||||
@@ -2625,6 +2625,7 @@ canvas {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.footer-bg {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
[
|
||||
{
|
||||
"url": "index.html",
|
||||
"titulo": "Inicio — IDE Consultores",
|
||||
"categoria": "Institucional",
|
||||
"descripcion": "Consultoría en gestión, dirección de proyectos, formación y transformación digital con enfoque sistémico.",
|
||||
"keywords": "inicio home consultora ide sistemas gestion transformacion"
|
||||
},
|
||||
{
|
||||
"url": "desafios.html",
|
||||
"titulo": "Punto de Partida — Desafíos",
|
||||
"categoria": "Diagnóstico",
|
||||
"descripcion": "Identificá tu desafío actual: gestión, proyectos, equipos o tecnología. La Brújula IDE te orienta.",
|
||||
"keywords": "desafios problemas diagnostico punto de partida brujula orientacion quiz"
|
||||
},
|
||||
{
|
||||
"url": "soluciones.html",
|
||||
"titulo": "Soluciones — Excellence, Project, Digital",
|
||||
"categoria": "Servicios",
|
||||
"descripcion": "Soluciones en sistemas de gestión, dirección de proyectos y transformación digital adaptadas a tu contexto.",
|
||||
"keywords": "soluciones iso 9001 14001 45001 pmo proyectos transformacion digital ia inteligencia artificial"
|
||||
},
|
||||
{
|
||||
"url": "ecosistema.html",
|
||||
"titulo": "Ecosistema IDE",
|
||||
"categoria": "Institucional",
|
||||
"descripcion": "Excellence, Projects, Digital y Academy: un ecosistema integrado al servicio de la gestión organizacional.",
|
||||
"keywords": "ecosistema excellence projects digital academy integrado"
|
||||
},
|
||||
{
|
||||
"url": "ecosistema.html#excellence",
|
||||
"titulo": "IDE Excellence — Sistemas de gestión y calidad",
|
||||
"categoria": "Ecosistema",
|
||||
"descripcion": "Implementación y acompañamiento de sistemas de gestión ISO, mejora de procesos y auditorías con valor.",
|
||||
"keywords": "excellence calidad iso 9001 14001 45001 sistemas gestion auditoria procesos mejora continua"
|
||||
},
|
||||
{
|
||||
"url": "ecosistema.html#projects",
|
||||
"titulo": "IDE Project — Dirección y gestión de proyectos",
|
||||
"categoria": "Ecosistema",
|
||||
"descripcion": "Diseño de PMOs y acompañamiento en la dirección de proyectos para entregar en tiempo, costo y resultados.",
|
||||
"keywords": "project proyectos pmo direccion gestion pmbok agile metodologias"
|
||||
},
|
||||
{
|
||||
"url": "ecosistema.html#digital",
|
||||
"titulo": "IDE Digital — Transformación digital e IA aplicada",
|
||||
"categoria": "Ecosistema",
|
||||
"descripcion": "Identificamos dónde la IA y la digitalización generan valor real y acompañamos su implementación.",
|
||||
"keywords": "digital transformacion ia inteligencia artificial automatizacion tecnologia innovacion"
|
||||
},
|
||||
{
|
||||
"url": "ide-academy.html",
|
||||
"titulo": "IDE Academy — Formación profesional",
|
||||
"categoria": "Formación",
|
||||
"descripcion": "Programas de formación alineados con los desafíos reales de las organizaciones, en formato abierto o In-Company.",
|
||||
"keywords": "academy cursos formacion capacitacion in company liderazgo calidad proyectos ia"
|
||||
},
|
||||
{
|
||||
"url": "somos-ide.html",
|
||||
"titulo": "Somos IDE",
|
||||
"categoria": "Institucional",
|
||||
"descripcion": "Quiénes somos, nuestra mirada sistémica y el equipo que hace posible IDE Consultores.",
|
||||
"keywords": "nosotros equipo consultores historia valores mirada sistemica"
|
||||
},
|
||||
{
|
||||
"url": "insights.html",
|
||||
"titulo": "Insights — Artículos y perspectivas",
|
||||
"categoria": "Blog",
|
||||
"descripcion": "Artículos sobre gestión, calidad, proyectos y transformación digital para ampliar la mirada sobre la organización.",
|
||||
"keywords": "insights blog articulos notas pensamiento sistemico perspectivas conocimiento"
|
||||
},
|
||||
{
|
||||
"url": "trabaja-con-nosotros.html",
|
||||
"titulo": "Trabajá con nosotros",
|
||||
"categoria": "Institucional",
|
||||
"descripcion": "Sumate al equipo de IDE Consultores. Buscamos perfiles con mirada sistémica y vocación de transformación.",
|
||||
"keywords": "empleo trabajo postulacion busqueda laboral cv curriculum sumate"
|
||||
},
|
||||
{
|
||||
"url": "contacto.html",
|
||||
"titulo": "Contacto comercial",
|
||||
"categoria": "Contacto",
|
||||
"descripcion": "Coordinemos una primera conversación para entender tu contexto y encontrar el mejor punto de partida.",
|
||||
"keywords": "contacto hablar consulta cotizacion presupuesto conversacion"
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -603,7 +603,7 @@
|
||||
|
||||
<footer id="site-footer"></footer>
|
||||
<script src="js/main.js"></script>
|
||||
<script src="js/netschema2.js"></script>
|
||||
<script src="js/netschema.js"></script>
|
||||
<script src="js/modal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+24
-6
@@ -14,21 +14,38 @@
|
||||
|
||||
<!-- Enlaces de Navegación de Escritorio -->
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html" class="nav-link">Inicio</a></li>
|
||||
<li><a href="desafios.html" class="nav-link">Punto de Partida</a></li>
|
||||
<li><a href="soluciones.html" class="nav-link">Soluciones</a></li>
|
||||
<li><a href="ecosistema.html" class="nav-link">Ecosistema</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle">Nosotros</a>
|
||||
<a href="ecosistema.html" class="nav-link dropdown-toggle">Ecosistema</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="somos-ide.html" class="nav-link">Somos IDE</a></li>
|
||||
<li><a href="ide-academy.html" class="nav-link">IDE Academy</a></li>
|
||||
<li><a href="insights.html" class="nav-link">Insights</a></li>
|
||||
<li><a href="ecosistema.html#excellence" class="nav-link">Excellence</a></li>
|
||||
<li><a href="ecosistema.html#projects" class="nav-link">Projects</a></li>
|
||||
<li><a href="ecosistema.html#digital" class="nav-link">Digital</a></li>
|
||||
<li><a href="ide-academy.html" class="nav-link">Academy</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="somos-ide.html" class="nav-link">Somos IDE</a></li>
|
||||
<li><a href="insights.html" class="nav-link">Insights</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- Acciones: Switch de Tema y CTA Principal -->
|
||||
<!-- Acciones: Buscador, Switch de Tema y CTA Principal -->
|
||||
<div class="nav-actions">
|
||||
|
||||
<!-- Buscador: link directo a la página de búsqueda -->
|
||||
<a
|
||||
href="buscar.html"
|
||||
class="theme-toggle-btn search-link-btn"
|
||||
aria-label="Buscar en el sitio"
|
||||
title="Buscar en el sitio"
|
||||
>
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<button id="theme-toggle-btn" class="theme-toggle-btn" type="button" aria-label="Cambiar a modo oscuro" title="Cambiar tema">
|
||||
<!-- Ícono Luna (Modo Claro activo) -->
|
||||
<svg class="theme-icon-moon" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -95,6 +112,7 @@
|
||||
<li><a href="insights.html" class="nav-link">Insights</a></li>
|
||||
<li><a href="somos-ide.html" class="nav-link">Somos IDE</a></li>
|
||||
<li><a href="trabaja-con-nosotros.html" class="nav-link">Trabaja con nosotros</a></li>
|
||||
<li><a href="buscar.html" class="nav-link">Buscar en el sitio</a></li>
|
||||
<li><a href="contacto.html" class="nav-link" style="color: var(--ide-accent);">Contacto Comercial</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
+165
@@ -17,6 +17,7 @@ async function initApp() {
|
||||
initInsightsFeed();
|
||||
initContactForm();
|
||||
initCareersForm();
|
||||
initSearchPage();
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -1177,3 +1178,167 @@ function initHeroVideoSpeed() {
|
||||
video.playbackRate = targetSpeed;
|
||||
});
|
||||
}
|
||||
/* ==========================================================================
|
||||
12. BÚSQUEDA EN EL SITIO — Página buscar.html
|
||||
========================================================================== */
|
||||
const SEARCH_INDEX_URL = 'data/search-index.json';
|
||||
|
||||
async function initSearchPage() {
|
||||
const form = document.getElementById('search-page-form');
|
||||
const input = document.getElementById('search-page-input');
|
||||
const results = document.getElementById('search-results');
|
||||
const status = document.getElementById('search-status');
|
||||
const suggestions = document.getElementById('search-suggestions');
|
||||
|
||||
// Si no estamos en buscar.html, salir silenciosamente
|
||||
if (!form || !input || !results) return;
|
||||
|
||||
let index = [];
|
||||
|
||||
// Cargar índice
|
||||
try {
|
||||
const res = await fetch(SEARCH_INDEX_URL, { cache: 'no-store' });
|
||||
if (res.ok) {
|
||||
index = await res.json();
|
||||
} else {
|
||||
console.warn('No se pudo cargar search-index.json:', res.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error cargando search-index.json:', e);
|
||||
}
|
||||
|
||||
// Normalizar texto (sin acentos, minúsculas)
|
||||
const normalize = (str) =>
|
||||
(str || '')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
|
||||
// Escapar HTML
|
||||
const escapeHtml = (str) =>
|
||||
(str || '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[c]));
|
||||
|
||||
// Scoring por relevancia
|
||||
function scoreItem(item, terms) {
|
||||
const title = normalize(item.titulo);
|
||||
const desc = normalize(item.descripcion);
|
||||
const kw = normalize(item.keywords);
|
||||
let score = 0;
|
||||
|
||||
terms.forEach(term => {
|
||||
if (!term) return;
|
||||
if (title.includes(term)) score += 10;
|
||||
if (kw.includes(term)) score += 6;
|
||||
if (desc.includes(term)) score += 3;
|
||||
});
|
||||
|
||||
// Bonus si la frase completa aparece en el título
|
||||
const fullPhrase = terms.join(' ');
|
||||
if (fullPhrase && title.includes(fullPhrase)) score += 5;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// Resaltar coincidencias
|
||||
function highlight(text, terms) {
|
||||
let safe = escapeHtml(text);
|
||||
terms.forEach(term => {
|
||||
if (term.length < 2) return;
|
||||
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escaped})`, 'gi');
|
||||
safe = safe.replace(regex, '<mark>$1</mark>');
|
||||
});
|
||||
return safe;
|
||||
}
|
||||
|
||||
// Render de resultados
|
||||
function render(term) {
|
||||
results.innerHTML = '';
|
||||
const cleanTerm = (term || '').trim();
|
||||
|
||||
if (!cleanTerm) {
|
||||
if (status) status.textContent = '';
|
||||
if (suggestions) suggestions.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestions) suggestions.hidden = true;
|
||||
|
||||
const terms = normalize(cleanTerm).split(/\s+/).filter(Boolean);
|
||||
|
||||
const scored = index
|
||||
.map(item => ({ item, score: scoreItem(item, terms) }))
|
||||
.filter(r => r.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (scored.length === 0) {
|
||||
if (status) status.textContent = '';
|
||||
results.innerHTML = `
|
||||
<div class="search-empty">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||
</svg>
|
||||
<p>No encontramos resultados para <strong>${escapeHtml(cleanTerm)}</strong>.</p>
|
||||
<p class="search-hint">
|
||||
Probá con otros términos o <a href="contacto.html">contactanos directamente</a>.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
status.textContent = `${scored.length} resultado${scored.length !== 1 ? 's' : ''} para "${cleanTerm}"`;
|
||||
}
|
||||
|
||||
results.innerHTML = scored.map(({ item }) => `
|
||||
<article class="search-result-item">
|
||||
<div class="result-category">${escapeHtml(item.categoria)}</div>
|
||||
<h3><a href="${escapeHtml(item.url)}">${highlight(item.titulo, terms)}</a></h3>
|
||||
<p>${highlight(item.descripcion, terms)}</p>
|
||||
</article>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Leer ?q= de la URL al cargar
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const initialQuery = params.get('q') || '';
|
||||
|
||||
if (initialQuery) {
|
||||
input.value = initialQuery;
|
||||
render(initialQuery);
|
||||
} else {
|
||||
if (suggestions) suggestions.hidden = false;
|
||||
// Foco automático al llegar sin query
|
||||
setTimeout(() => input.focus(), 100);
|
||||
}
|
||||
|
||||
// Submit del form
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const q = input.value.trim();
|
||||
const newUrl = q
|
||||
? `?q=${encodeURIComponent(q)}`
|
||||
: window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
render(q);
|
||||
});
|
||||
|
||||
// Búsqueda en vivo con debounce
|
||||
let debounceTimer;
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
render(input.value);
|
||||
}, 220);
|
||||
});
|
||||
}
|
||||
|
||||
-302
@@ -1,302 +0,0 @@
|
||||
/**
|
||||
* Plexus 3D Hero - Red de Nodos Conectados para Hero
|
||||
* Integración con Three.js y soporte dinámico para Modo Claro / Oscuro vía CSS Variables.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
// Asegurarse de que el DOM esté listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPlexus);
|
||||
} else {
|
||||
initPlexus();
|
||||
}
|
||||
|
||||
function initPlexus() {
|
||||
// 1. Validar dependencia de Three.js
|
||||
if (typeof THREE === 'undefined') {
|
||||
console.warn('[Plexus 3D] Three.js no está cargado. Asegúrate de incluir Three.js antes de este script.');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('hero-3d-container');
|
||||
if (!container) {
|
||||
// Si el contenedor no existe en esta página, no ejecutar nada
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CONFIGURACIÓN GENERAL
|
||||
// =========================================================================
|
||||
const CONFIG = {
|
||||
nodeCount: 100, // Cantidad de nodos (puntos)
|
||||
radius: 120, // Radio general de la esfera/nube 3D
|
||||
maxDistance: 60, // Distancia máxima para conectar dos nodos con línea
|
||||
nodeSize: 6, // Tamaño del punto en píxeles
|
||||
driftSpeed: 0.0005, // Velocidad de flotación orgánica propia
|
||||
driftAmplitude: 12, // Rango máximo de oscilación (mantiene la forma)
|
||||
mouseFactor: 0.35, // Grado de rotación en respuesta al cursor
|
||||
lerpSpeed: 0.05, // Suavidad de la inercia del movimiento (0.01 - 0.1)
|
||||
|
||||
// Variables CSS que el script vigilará en tu :root / .dark
|
||||
cssVars: {
|
||||
nodeColor: '--ide-primary',
|
||||
lineColor: '--ide-primary'
|
||||
},
|
||||
|
||||
// Colores de respaldo en caso de que las variables no existan en el CSS
|
||||
fallbacks: {
|
||||
nodeColor: '#0a2540',
|
||||
lineColor: '#008080'
|
||||
},
|
||||
lineOpacity: 0.35 // Opacidad de las líneas
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// LECTURA REACTIVA DE VARIABLES CSS
|
||||
// =========================================================================
|
||||
function getCSSVariable(varName, fallback) {
|
||||
const val = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(varName)
|
||||
.trim();
|
||||
return val || fallback;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CONFIGURACIÓN THREE.JS (Escena, Cámara y Renderer)
|
||||
// =========================================================================
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const width = container.clientWidth || 400;
|
||||
const height = container.clientHeight || 450;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(50, width / height, 1, 1000);
|
||||
camera.position.z = 320;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
alpha: true, // Fondo 100% transparente para integrarse con el Hero
|
||||
antialias: true, // Suavizado de bordes
|
||||
powerPreference: 'high-performance'
|
||||
});
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
// Grupo principal para aplicar rotación y control de inercia
|
||||
const cloudGroup = new THREE.Group();
|
||||
scene.add(cloudGroup);
|
||||
|
||||
// =========================================================================
|
||||
// GENERACIÓN DE NODOS (Distribución Esférica Uniforme)
|
||||
// =========================================================================
|
||||
const nodes = [];
|
||||
const nodePositions = new Float32Array(CONFIG.nodeCount * 3);
|
||||
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
// Distribución equidistante en esfera (Espiral de Fibonacci 3D)
|
||||
const phi = Math.acos(-1 + (2 * i) / CONFIG.nodeCount);
|
||||
const theta = Math.sqrt(CONFIG.nodeCount * Math.PI) * phi;
|
||||
|
||||
// Variación radial controlada para darle volumen tridimensional
|
||||
const r = CONFIG.radius * (0.65 + 0.35 * Math.random());
|
||||
const baseX = r * Math.cos(theta) * Math.sin(phi);
|
||||
const baseY = r * Math.sin(theta) * Math.sin(phi);
|
||||
const baseZ = r * Math.cos(phi);
|
||||
|
||||
nodes.push({
|
||||
base: new THREE.Vector3(baseX, baseY, baseZ),
|
||||
current: new THREE.Vector3(baseX, baseY, baseZ),
|
||||
speedOffset: Math.random() * Math.PI * 2,
|
||||
axisFactors: [
|
||||
0.8 + Math.random() * 0.4,
|
||||
0.8 + Math.random() * 0.4,
|
||||
0.8 + Math.random() * 0.4
|
||||
]
|
||||
});
|
||||
|
||||
nodePositions[i * 3] = baseX;
|
||||
nodePositions[i * 3 + 1] = baseY;
|
||||
nodePositions[i * 3 + 2] = baseZ;
|
||||
}
|
||||
|
||||
// Geometría y textura circular suave para los nodos
|
||||
const pointsGeometry = new THREE.BufferGeometry();
|
||||
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(nodePositions, 3));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 28, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
const pointTexture = new THREE.CanvasTexture(canvas);
|
||||
|
||||
const pointsMaterial = new THREE.PointsMaterial({
|
||||
size: CONFIG.nodeSize,
|
||||
map: pointTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.05
|
||||
});
|
||||
|
||||
const pointCloud = new THREE.Points(pointsGeometry, pointsMaterial);
|
||||
cloudGroup.add(pointCloud);
|
||||
|
||||
// =========================================================================
|
||||
// LÍNEAS DE CONEXIÓN DINÁMICAS
|
||||
// =========================================================================
|
||||
const maxPossibleLines = (CONFIG.nodeCount * (CONFIG.nodeCount - 1)) / 2;
|
||||
const linePositions = new Float32Array(maxPossibleLines * 6);
|
||||
|
||||
const linesGeometry = new THREE.BufferGeometry();
|
||||
linesGeometry.setAttribute(
|
||||
'position',
|
||||
new THREE.BufferAttribute(linePositions, 3).setUsage(THREE.DynamicDrawUsage)
|
||||
);
|
||||
|
||||
const linesMaterial = new THREE.LineBasicMaterial({
|
||||
transparent: true,
|
||||
opacity: CONFIG.lineOpacity
|
||||
});
|
||||
|
||||
const lines = new THREE.LineSegments(linesGeometry, linesMaterial);
|
||||
cloudGroup.add(lines);
|
||||
|
||||
// =========================================================================
|
||||
// GESTIÓN DE COLORES Y OBSERVADORES DE TEMA (DARK / LIGHT MODE)
|
||||
// =========================================================================
|
||||
function updateThemeColors() {
|
||||
const nodeColorStr = getCSSVariable(CONFIG.cssVars.nodeColor, CONFIG.fallbacks.nodeColor);
|
||||
const lineColorStr = getCSSVariable(CONFIG.cssVars.lineColor, CONFIG.fallbacks.lineColor);
|
||||
|
||||
pointsMaterial.color.set(nodeColorStr);
|
||||
linesMaterial.color.set(lineColorStr);
|
||||
}
|
||||
|
||||
// Aplicar colores al arrancar
|
||||
updateThemeColors();
|
||||
|
||||
// 1. Observar cambios en atributos de <html> y <body> (classes, data-theme, etc.)
|
||||
const observer = new MutationObserver(() => {
|
||||
updateThemeColors();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'data-mode', 'style']
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-theme', 'data-mode', 'style']
|
||||
});
|
||||
|
||||
// 2. Escuchar cambios de preferencia de color del sistema operativo
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeColors);
|
||||
}
|
||||
|
||||
// Exponer función de refresco manual por si tu toggle dispara eventos personalizados
|
||||
window.refreshPlexusTheme = updateThemeColors;
|
||||
|
||||
// =========================================================================
|
||||
// INTERACCIÓN CON EL MOUSE (PARALAJE E INCLINACIÓN 3D)
|
||||
// =========================================================================
|
||||
let targetRotationX = 0;
|
||||
let targetRotationY = 0;
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
|
||||
// Cálculo de posición normalizada respecto al centro de la ventana o hero
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
const centerY = rect.top + rect.height / 2;
|
||||
|
||||
const normX = (e.clientX - centerX) / (window.innerWidth / 2);
|
||||
const normY = (e.clientY - centerY) / (window.innerHeight / 2);
|
||||
|
||||
targetRotationY = normX * CONFIG.mouseFactor;
|
||||
targetRotationX = -normY * CONFIG.mouseFactor;
|
||||
}, { passive: true });
|
||||
|
||||
// =========================================================================
|
||||
// BUCLE DE ANIMACIÓN
|
||||
// =========================================================================
|
||||
let time = 0;
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
time += CONFIG.driftSpeed * 1000;
|
||||
|
||||
// 1. Oscilación armónica de los nodos (flotación sin desarme)
|
||||
const currentPosArray = pointsGeometry.attributes.position.array;
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
const node = nodes[i];
|
||||
const offset = time * 0.0015 + node.speedOffset;
|
||||
|
||||
node.current.x = node.base.x + Math.sin(offset * node.axisFactors[0]) * CONFIG.driftAmplitude;
|
||||
node.current.y = node.base.y + Math.cos(offset * node.axisFactors[1]) * CONFIG.driftAmplitude;
|
||||
node.current.z = node.base.z + Math.sin(offset * node.axisFactors[2]) * CONFIG.driftAmplitude;
|
||||
|
||||
currentPosArray[i * 3] = node.current.x;
|
||||
currentPosArray[i * 3 + 1] = node.current.y;
|
||||
currentPosArray[i * 3 + 2] = node.current.z;
|
||||
}
|
||||
pointsGeometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// 2. Conectar nodos próximos
|
||||
let lineIndex = 0;
|
||||
const lineArray = linesGeometry.attributes.position.array;
|
||||
|
||||
for (let i = 0; i < CONFIG.nodeCount; i++) {
|
||||
for (let j = i + 1; j < CONFIG.nodeCount; j++) {
|
||||
const dist = nodes[i].current.distanceTo(nodes[j].current);
|
||||
|
||||
if (dist < CONFIG.maxDistance) {
|
||||
lineArray[lineIndex++] = nodes[i].current.x;
|
||||
lineArray[lineIndex++] = nodes[i].current.y;
|
||||
lineArray[lineIndex++] = nodes[i].current.z;
|
||||
|
||||
lineArray[lineIndex++] = nodes[j].current.x;
|
||||
lineArray[lineIndex++] = nodes[j].current.y;
|
||||
lineArray[lineIndex++] = nodes[j].current.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
linesGeometry.setDrawRange(0, lineIndex / 3);
|
||||
linesGeometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// 3. Rotación base continua + interpolación suave hacia el mouse (Lerp)
|
||||
cloudGroup.rotation.y += 0.0012;
|
||||
cloudGroup.rotation.y += (targetRotationY - cloudGroup.rotation.y) * CONFIG.lerpSpeed;
|
||||
cloudGroup.rotation.x += (targetRotationX - cloudGroup.rotation.x) * CONFIG.lerpSpeed;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
// =========================================================================
|
||||
// RESPONSIVE RESIZING (Evita deformaciones y recortes)
|
||||
// =========================================================================
|
||||
function handleResize() {
|
||||
if (!container) return;
|
||||
const newWidth = container.clientWidth;
|
||||
const newHeight = container.clientHeight;
|
||||
|
||||
if (newWidth === 0 || newHeight === 0) return;
|
||||
|
||||
camera.aspect = newWidth / newHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
// ResizeObserver para cambios precisos en el contenedor del Hero
|
||||
if (window.ResizeObserver) {
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(container);
|
||||
} else {
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
}
|
||||
})();
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
/**
|
||||
* Ripple Waves Background - Versión con Distorsión de Ruido Topográfico
|
||||
* Reemplaza o actualiza tu archivo ripple-bg.js
|
||||
*/
|
||||
(() => {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initRipple);
|
||||
} else {
|
||||
initRipple();
|
||||
}
|
||||
|
||||
function initRipple() {
|
||||
const canvas = document.getElementById('ripple-canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const parent = canvas.parentElement;
|
||||
|
||||
// =========================================================================
|
||||
// PARÁMETROS CONFIGURABLES
|
||||
// =========================================================================
|
||||
const CONFIG = {
|
||||
spacing: 36, // Separación entre anillos
|
||||
speed: 0.5, // Velocidad de expansión hacia afuera
|
||||
lineWidth: 2, // Grosor de las líneas
|
||||
maxAlpha: 0.42, // Opacidad máxima
|
||||
centerOffsetX: 0.5, // Centro horizontal (50%)
|
||||
centerOffsetY: 0.2, // Centro vertical (alineado con el h2)
|
||||
|
||||
// --- PARÁMETROS DE RUIDO / DISTORSIÓN ---
|
||||
distortionStrength: 14, // Qué tanto se deforma el círculo (en px)
|
||||
distortionFrequency: 3, // Cantidad de ondulaciones en el perímetro
|
||||
evolutionSpeed: 0.00001, // Velocidad con la que muta la forma de las ondas
|
||||
segmentsPerRing: 25, // Calidad/resolución del contorno (70 a 100)
|
||||
|
||||
cssVar: '--ripple-color', // Variable de tu CSS
|
||||
fallbackColor: 'rgba(24, 123, 141, 0.4)'
|
||||
};
|
||||
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let dpr = 1;
|
||||
let maxRadius = 0;
|
||||
let offset = 0;
|
||||
let time = 0;
|
||||
let currentColor = CONFIG.fallbackColor;
|
||||
|
||||
// --- Leer variable de color desde CSS ---
|
||||
function updateThemeColor() {
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(CONFIG.cssVar)
|
||||
.trim();
|
||||
currentColor = color || CONFIG.fallbackColor;
|
||||
}
|
||||
|
||||
updateThemeColor();
|
||||
|
||||
const observer = new MutationObserver(updateThemeColor);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] });
|
||||
observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] });
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeColor);
|
||||
}
|
||||
|
||||
// --- Adaptación de resolución Retina / HiDPI ---
|
||||
function resize() {
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
width = parent.clientWidth;
|
||||
height = parent.clientHeight;
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const cx = width * CONFIG.centerOffsetX;
|
||||
const cy = height * CONFIG.centerOffsetY;
|
||||
const dx = Math.max(cx, width - cx);
|
||||
const dy = Math.max(cy, height - cy);
|
||||
maxRadius = Math.sqrt(dx * dx + dy * dy) + CONFIG.spacing * 2;
|
||||
}
|
||||
|
||||
resize();
|
||||
|
||||
if (window.ResizeObserver) {
|
||||
new ResizeObserver(resize).observe(parent);
|
||||
} else {
|
||||
window.addEventListener('resize', resize);
|
||||
}
|
||||
|
||||
// --- Función de Pseudo-Ruido Armónico ---
|
||||
// Combina ondas senoidales a distintas frecuencias para simular turbulencia/ruido suave
|
||||
function getNoiseDeformation(angle, baseRadius, t) {
|
||||
const f = CONFIG.distortionFrequency;
|
||||
const w1 = Math.sin(angle * f + t + baseRadius * 0.015);
|
||||
const w2 = Math.cos(angle * (f + 2) - t * 0.8 + baseRadius * 0.02);
|
||||
const w3 = Math.sin(angle * 2 + t * 0.4);
|
||||
|
||||
// Crece ligeramente a medida que el anillo viaja hacia afuera
|
||||
const growthFactor = 0.5 + (baseRadius / maxRadius) * 0.5;
|
||||
|
||||
return (w1 * 0.55 + w2 * 0.35 + w3 * 0.1) * CONFIG.distortionStrength * growthFactor;
|
||||
}
|
||||
|
||||
// --- Loop de Animación ---
|
||||
function render() {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const cx = width * CONFIG.centerOffsetX;
|
||||
const cy = height * CONFIG.centerOffsetY;
|
||||
|
||||
offset = (offset + CONFIG.speed) % CONFIG.spacing;
|
||||
time += CONFIG.evolutionSpeed * 1000;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = currentColor;
|
||||
ctx.lineWidth = CONFIG.lineWidth;
|
||||
|
||||
const angleStep = (Math.PI * 2) / CONFIG.segmentsPerRing;
|
||||
|
||||
// Dibujar cada anillo deformado
|
||||
for (let r = offset; r < maxRadius; r += CONFIG.spacing) {
|
||||
if (r <= 2) continue;
|
||||
|
||||
const progress = r / maxRadius;
|
||||
|
||||
// Desvanecimiento suave en centro y bordes
|
||||
let alpha = 1;
|
||||
if (progress < 0.12) {
|
||||
alpha = progress / 0.12;
|
||||
} else {
|
||||
alpha = Math.cos(((progress - 0.12) / 0.88) * (Math.PI / 2));
|
||||
}
|
||||
|
||||
ctx.globalAlpha = Math.max(0, Math.min(1, alpha * CONFIG.maxAlpha));
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
// Trazar el contorno deformado punto a punto
|
||||
for (let i = 0; i <= CONFIG.segmentsPerRing; i++) {
|
||||
const angle = i * angleStep;
|
||||
const deformation = getNoiseDeformation(angle, r, time);
|
||||
const currentRadius = Math.max(0, r + deformation);
|
||||
|
||||
const px = cx + Math.cos(angle) * currentRadius;
|
||||
const py = cy + Math.sin(angle) * currentRadius;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(px, py);
|
||||
} else {
|
||||
ctx.lineTo(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
})();
|
||||
@@ -1,402 +0,0 @@
|
||||
/**
|
||||
* SnowballBackground
|
||||
* Círculos huecos flotantes que se fusionan en el mouse aumentando su radio
|
||||
* y se desintegran expulsando todas las partículas.
|
||||
*
|
||||
* Uso:
|
||||
* 1) Automático: agregá la clase .snowball-bg a cualquier contenedor
|
||||
* 2) Manual: new SnowballBackground({ containerId: 'mi-id' })
|
||||
*
|
||||
* Tema: lee variables CSS del contenedor o :root y reacciona a cambios de tema.
|
||||
*/
|
||||
class SnowballBackground {
|
||||
constructor(options = {}) {
|
||||
// --- Resolver contenedor (acepta elemento, ID o selector) ---
|
||||
if (options.container instanceof HTMLElement) {
|
||||
this.container = options.container;
|
||||
} else if (options.containerId) {
|
||||
this.container = document.getElementById(options.containerId);
|
||||
} else if (options.containerSelector) {
|
||||
this.container = document.querySelector(options.containerSelector);
|
||||
}
|
||||
|
||||
if (!this.container) {
|
||||
console.warn('SnowballBackground: no se encontró el contenedor.', options);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evitar doble inicialización en el mismo contenedor
|
||||
if (this.container._snowballInstance) {
|
||||
return;
|
||||
}
|
||||
this.container._snowballInstance = this;
|
||||
|
||||
// --- Configuración (defaults, se sobreescriben con CSS vars y options) ---
|
||||
this.config = {
|
||||
particleCount: 70,
|
||||
maxCapacity: 20,
|
||||
cooldownSeconds: 2.5,
|
||||
color: '#38bdf8',
|
||||
lineWidth: 1.5,
|
||||
minRadius: 5,
|
||||
maxRadius: 10
|
||||
};
|
||||
|
||||
// Leer variables CSS antes de aplicar options (options tienen prioridad)
|
||||
this.syncThemeColors();
|
||||
|
||||
// Aplicar overrides manuales
|
||||
if (options.particleCount) this.config.particleCount = options.particleCount;
|
||||
if (options.maxCapacity) this.config.maxCapacity = options.maxCapacity;
|
||||
if (options.cooldownSeconds) this.config.cooldownSeconds = options.cooldownSeconds;
|
||||
if (options.color) this.config.color = options.color;
|
||||
if (options.lineWidth) this.config.lineWidth = options.lineWidth;
|
||||
if (options.minRadius) this.config.minRadius = options.minRadius;
|
||||
if (options.maxRadius) this.config.maxRadius = options.maxRadius;
|
||||
|
||||
// --- Estados internos ---
|
||||
this.particles = [];
|
||||
this.absorbedParticles = [];
|
||||
this.disintegrationShards = [];
|
||||
this.mouse = { x: -1000, y: -1000, active: false };
|
||||
this.isCoolingDown = false;
|
||||
this.cooldownEndTime = 0;
|
||||
this.currentBallRadius = 0;
|
||||
|
||||
// --- Init ---
|
||||
this.initCanvas();
|
||||
this.initParticles();
|
||||
this.bindEvents();
|
||||
this.observeThemeChanges();
|
||||
this.loop();
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* LECTURA DE VARIABLES CSS
|
||||
* =========================================================== */
|
||||
readCSSVar(name) {
|
||||
// Primero busca en el contenedor, luego en :root
|
||||
let value = getComputedStyle(this.container).getPropertyValue(name).trim();
|
||||
if (!value) {
|
||||
value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
}
|
||||
return value || null;
|
||||
}
|
||||
|
||||
syncThemeColors() {
|
||||
const map = {
|
||||
'--snowball-color': { key: 'color', parse: String },
|
||||
'--snowball-particle-count': { key: 'particleCount', parse: parseInt },
|
||||
'--snowball-max-capacity': { key: 'maxCapacity', parse: parseInt },
|
||||
'--snowball-cooldown': { key: 'cooldownSeconds',parse: parseFloat },
|
||||
'--snowball-line-width': { key: 'lineWidth', parse: parseFloat },
|
||||
'--snowball-min-radius': { key: 'minRadius', parse: parseFloat },
|
||||
'--snowball-max-radius': { key: 'maxRadius', parse: parseFloat }
|
||||
};
|
||||
|
||||
for (const [cssVar, { key, parse }] of Object.entries(map)) {
|
||||
const raw = this.readCSSVar(cssVar);
|
||||
if (raw !== null) {
|
||||
const parsed = parse(raw, 10);
|
||||
// Solo aplicar si el parseo dio un valor válido
|
||||
if (typeof parsed === 'string' ? parsed : !isNaN(parsed)) {
|
||||
this.config[key] = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* OBSERVADOR DE CAMBIOS DE TEMA
|
||||
* =========================================================== */
|
||||
observeThemeChanges() {
|
||||
this._themeObserver = new MutationObserver(() => {
|
||||
this.syncThemeColors();
|
||||
});
|
||||
|
||||
// Observar <html> (cambios de data-theme, class, style)
|
||||
this._themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme', 'class', 'style']
|
||||
});
|
||||
|
||||
// Observar <body> también (algunos frameworks ponen el tema ahí)
|
||||
this._themeObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme', 'class', 'style']
|
||||
});
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* CANVAS
|
||||
* =========================================================== */
|
||||
initCanvas() {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.className = 'snowball-canvas';
|
||||
this.canvas.style.cssText =
|
||||
'position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:0;';
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// El contenedor debe ser posicionable
|
||||
const pos = getComputedStyle(this.container).position;
|
||||
if (pos === 'static') {
|
||||
this.container.style.position = 'relative';
|
||||
}
|
||||
|
||||
this.container.appendChild(this.canvas);
|
||||
this.resize();
|
||||
}
|
||||
|
||||
resize() {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.width = rect.width;
|
||||
this.height = rect.height;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
this.canvas.width = this.width * dpr;
|
||||
this.canvas.height = this.height * dpr;
|
||||
this.ctx.scale(dpr, dpr);
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* PARTÍCULAS
|
||||
* =========================================================== */
|
||||
initParticles() {
|
||||
this.particles = [];
|
||||
for (let i = 0; i < this.config.particleCount; i++) {
|
||||
const radius = this.randomRange(this.config.minRadius, this.config.maxRadius);
|
||||
this.particles.push({
|
||||
x: this.randomRange(radius, Math.max(radius + 1, this.width - radius)),
|
||||
y: this.randomRange(radius, Math.max(radius + 1, this.height - radius)),
|
||||
vx: (Math.random() - 0.5) * 1.6,
|
||||
vy: (Math.random() - 0.5) * 1.6,
|
||||
radius,
|
||||
state: 'FREE'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this._onResize = () => this.resize();
|
||||
window.addEventListener('resize', this._onResize);
|
||||
|
||||
this._onMouseMove = (e) => {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.mouse.x = e.clientX - rect.left;
|
||||
this.mouse.y = e.clientY - rect.top;
|
||||
this.mouse.active = true;
|
||||
};
|
||||
this.container.addEventListener('mousemove', this._onMouseMove);
|
||||
|
||||
this._onMouseLeave = () => { this.mouse.active = false; };
|
||||
this.container.addEventListener('mouseleave', this._onMouseLeave);
|
||||
}
|
||||
|
||||
randomRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* CONVERSIÓN DE COLOR UNIVERSAL
|
||||
* =========================================================== */
|
||||
toRgba(color, alpha = 1) {
|
||||
if (!color) return `rgba(0,0,0,${alpha})`;
|
||||
const c = color.trim();
|
||||
|
||||
// rgba(...) o rgb(...)
|
||||
if (c.startsWith('rgb')) {
|
||||
const parts = c.match(/[\d.]+/g);
|
||||
if (!parts || parts.length < 3) return `rgba(0,0,0,${alpha})`;
|
||||
const [r, g, b] = parts;
|
||||
const origA = parts[3] !== undefined ? parseFloat(parts[3]) : 1;
|
||||
return `rgba(${r},${g},${b},${(origA * alpha).toFixed(3)})`;
|
||||
}
|
||||
|
||||
// #hex (3, 4, 6 u 8 dígitos)
|
||||
if (c.startsWith('#')) {
|
||||
let hex = c.slice(1);
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
hex = hex.split('').map(ch => ch + ch).join('');
|
||||
}
|
||||
const n = parseInt(hex.slice(0, 6), 16);
|
||||
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
|
||||
let hexA = 1;
|
||||
if (hex.length === 8) hexA = parseInt(hex.slice(6, 8), 16) / 255;
|
||||
return `rgba(${r},${g},${b},${(hexA * alpha).toFixed(3)})`;
|
||||
}
|
||||
|
||||
// Fallback: dejar que el navegador resuelva (hsl, nombres, etc.)
|
||||
try {
|
||||
const tmp = document.createElement('canvas').getContext('2d');
|
||||
tmp.fillStyle = c;
|
||||
const resolved = tmp.fillStyle;
|
||||
if (resolved !== c) return this.toRgba(resolved, alpha);
|
||||
} catch (_) {}
|
||||
|
||||
return `rgba(0,0,0,${alpha})`;
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* EXPLOSIÓN
|
||||
* =========================================================== */
|
||||
explode() {
|
||||
const ex = this.mouse.x, ey = this.mouse.y;
|
||||
const br = this.currentBallRadius;
|
||||
const count = this.absorbedParticles.length;
|
||||
|
||||
// Fragmentos de desintegración
|
||||
const shardCount = 8;
|
||||
const arcStep = (Math.PI * 2) / shardCount;
|
||||
for (let i = 0; i < shardCount; i++) {
|
||||
const angle = i * arcStep;
|
||||
this.disintegrationShards.push({
|
||||
x: ex, y: ey, radius: br,
|
||||
startAngle: angle + 0.1,
|
||||
endAngle: angle + arcStep - 0.1,
|
||||
vx: Math.cos(angle + arcStep / 2) * 3,
|
||||
vy: Math.sin(angle + arcStep / 2) * 3,
|
||||
alpha: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Dispersar partículas
|
||||
this.absorbedParticles.forEach((p, i) => {
|
||||
const angle = (Math.PI * 2 / count) * i + (Math.random() - 0.5) * 0.3;
|
||||
const force = this.randomRange(10, 18);
|
||||
p.x = ex + Math.cos(angle) * (br * 0.8);
|
||||
p.y = ey + Math.sin(angle) * (br * 0.8);
|
||||
p.vx = Math.cos(angle) * force;
|
||||
p.vy = Math.sin(angle) * force;
|
||||
p.state = 'EXPLODED';
|
||||
});
|
||||
|
||||
this.absorbedParticles = [];
|
||||
this.currentBallRadius = 0;
|
||||
this.isCoolingDown = true;
|
||||
this.cooldownEndTime = performance.now() + this.config.cooldownSeconds * 1000;
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* UPDATE
|
||||
* =========================================================== */
|
||||
update() {
|
||||
const now = performance.now();
|
||||
if (this.isCoolingDown && now >= this.cooldownEndTime) {
|
||||
this.isCoolingDown = false;
|
||||
}
|
||||
|
||||
let targetRadius = 0;
|
||||
if (this.absorbedParticles.length > 0) {
|
||||
targetRadius = 14 + (this.absorbedParticles.length / this.config.maxCapacity) * 46;
|
||||
}
|
||||
this.currentBallRadius += (targetRadius - this.currentBallRadius) * 0.15;
|
||||
|
||||
const hitRadius = this.absorbedParticles.length === 0 ? 14 : this.currentBallRadius;
|
||||
|
||||
this.particles.forEach((p) => {
|
||||
if (p.state === 'FREE') {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
if (p.x - p.radius <= 0) { p.x = p.radius; p.vx *= -1; }
|
||||
if (p.x + p.radius >= this.width) { p.x = this.width - p.radius; p.vx *= -1; }
|
||||
if (p.y - p.radius <= 0) { p.y = p.radius; p.vy *= -1; }
|
||||
if (p.y + p.radius >= this.height) { p.y = this.height - p.radius; p.vy *= -1; }
|
||||
|
||||
if (this.mouse.active && !this.isCoolingDown) {
|
||||
const dx = p.x - this.mouse.x, dy = p.y - this.mouse.y;
|
||||
if (Math.sqrt(dx * dx + dy * dy) < hitRadius + p.radius) {
|
||||
p.state = 'ABSORBED';
|
||||
this.absorbedParticles.push(p);
|
||||
if (this.absorbedParticles.length >= this.config.maxCapacity) {
|
||||
this.explode();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (p.state === 'EXPLODED') {
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
p.vx *= 0.96; p.vy *= 0.96;
|
||||
if (p.x - p.radius <= 0) { p.x = p.radius; p.vx *= -1; }
|
||||
if (p.x + p.radius >= this.width) { p.x = this.width - p.radius; p.vx *= -1; }
|
||||
if (p.y - p.radius <= 0) { p.y = p.radius; p.vy *= -1; }
|
||||
if (p.y + p.radius >= this.height) { p.y = this.height - p.radius; p.vy *= -1; }
|
||||
if (Math.sqrt(p.vx * p.vx + p.vy * p.vy) < 1.6) p.state = 'FREE';
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = this.disintegrationShards.length - 1; i >= 0; i--) {
|
||||
const s = this.disintegrationShards[i];
|
||||
s.x += s.vx; s.y += s.vy;
|
||||
s.radius += 0.8;
|
||||
s.alpha -= 0.05;
|
||||
if (s.alpha <= 0) this.disintegrationShards.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* DRAW (todos los colores pasan por toRgba)
|
||||
* =========================================================== */
|
||||
draw() {
|
||||
this.ctx.clearRect(0, 0, this.width, this.height);
|
||||
const color = this.config.color;
|
||||
const lw = this.config.lineWidth;
|
||||
|
||||
// 1. Partículas libres / explotadas
|
||||
this.ctx.lineWidth = lw;
|
||||
this.ctx.strokeStyle = this.toRgba(color, 1);
|
||||
this.particles.forEach((p) => {
|
||||
if (p.state !== 'ABSORBED') {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Fragmentos de desintegración
|
||||
this.disintegrationShards.forEach((s) => {
|
||||
this.ctx.save();
|
||||
this.ctx.lineWidth = lw + 0.5;
|
||||
this.ctx.strokeStyle = this.toRgba(color, s.alpha);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(s.x, s.y, s.radius, s.startAngle, s.endAngle);
|
||||
this.ctx.stroke();
|
||||
this.ctx.restore();
|
||||
});
|
||||
|
||||
// 3. Bola en el mouse
|
||||
if (this.mouse.active && !this.isCoolingDown && this.currentBallRadius > 2) {
|
||||
this.ctx.save();
|
||||
this.ctx.lineWidth = lw + 0.5;
|
||||
this.ctx.strokeStyle = this.toRgba(color, 1);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(this.mouse.x, this.mouse.y, this.currentBallRadius, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* LOOP
|
||||
* =========================================================== */
|
||||
loop() {
|
||||
this.update();
|
||||
this.draw();
|
||||
requestAnimationFrame(() => this.loop());
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================================
|
||||
* AUTO-INICIALIZACIÓN
|
||||
* Busca todos los elementos con clase .snowball-bg
|
||||
* ============================================================== */
|
||||
function initSnowballBackgrounds() {
|
||||
document.querySelectorAll('.snowball-bg').forEach((el) => {
|
||||
if (!el._snowballInstance) {
|
||||
new SnowballBackground({ container: el });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initSnowballBackgrounds);
|
||||
} else {
|
||||
initSnowballBackgrounds();
|
||||
}
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Diagrama de Red y Modal</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Botón para probar la apertura del modal directamente -->
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<button type="button" class="btn-trigger-projects" id="btnOpenProjectsModal">
|
||||
Conocer IDE Projects
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contenedor del Diagrama Canvas -->
|
||||
<div class="canvas-wrapper">
|
||||
<canvas id="networkCanvas" width="1280" height="520"></canvas>
|
||||
|
||||
<!-- Tooltip flotante -->
|
||||
<div id="tooltip" class="network-tooltip hidden">
|
||||
<h5 id="tooltip-title"></h5>
|
||||
<p id="tooltip-desc"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL: IDE PROJECTS -->
|
||||
<div id="modal-ide-projects" class="ide-modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-projects-title">
|
||||
<div class="ide-modal-container">
|
||||
|
||||
<!-- Botón Cerrar (X) -->
|
||||
<button type="button" class="ide-modal-close" id="btnCloseModal" aria-label="Cerrar modal">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Envoltorio del contenido con scroll interno -->
|
||||
<div class="ide-modal-body-wrapper">
|
||||
<div class="ide-modal-scroll" id="modalScrollArea">
|
||||
|
||||
<!-- Encabezado -->
|
||||
<header class="ide-modal-header">
|
||||
<div class="ide-modal-pill">IDE PROJECTS</div>
|
||||
<h2 id="modal-projects-title" class="ide-modal-title">PMO & Project Services</h2>
|
||||
<p class="ide-modal-tagline">Convertir iniciativas en resultados.</p>
|
||||
</header>
|
||||
|
||||
<!-- Introducción -->
|
||||
<div class="ide-modal-lead">
|
||||
<p>Ayudamos a las organizaciones a fortalecer su capacidad para seleccionar, priorizar, planificar y ejecutar proyectos, programas y portafolios, conectando las iniciativas con los objetivos estratégicos y los recursos disponibles.</p>
|
||||
<p>Integramos metodologías, gobernanza, información y capacidades de gestión para acompañar todo el ciclo de vida de los proyectos y mejorar su contribución a los resultados de la organización.</p>
|
||||
</div>
|
||||
|
||||
<!-- Capacidades -->
|
||||
<section class="ide-modal-section">
|
||||
<h3 class="ide-section-title">Nuestras capacidades</h3>
|
||||
<div class="ide-grid-cards">
|
||||
|
||||
<div class="ide-feature-card">
|
||||
<h4>Gestión de proyectos</h4>
|
||||
<p>Planificación, organización, seguimiento y acompañamiento de proyectos para gestionar alcance, plazos, costos, recursos, riesgos, stakeholders y resultados.</p>
|
||||
</div>
|
||||
|
||||
<div class="ide-feature-card">
|
||||
<h4>PMO — Oficinas de Gestión de Proyectos</h4>
|
||||
<p>Diseño, implementación y fortalecimiento de PMO adaptadas al nivel de madurez, estrategia y necesidades de cada organización.</p>
|
||||
</div>
|
||||
|
||||
<div class="ide-feature-card">
|
||||
<h4>Programas y portafolios</h4>
|
||||
<p>Priorización y coordinación de iniciativas para mejorar la asignación de recursos, gestionar interdependencias y mantener alineadas las inversiones con los objetivos organizacionales.</p>
|
||||
</div>
|
||||
|
||||
<div class="ide-feature-card">
|
||||
<h4>Gobernanza y metodologías</h4>
|
||||
<p>Diseño de modelos de gobernanza, procesos, roles, indicadores y metodologías predictivas, ágiles o híbridas adaptadas al contexto de cada organización.</p>
|
||||
</div>
|
||||
|
||||
<div class="ide-feature-card ide-card-full">
|
||||
<h4>Diagnóstico y recuperación de proyectos</h4>
|
||||
<p>Evaluación de proyectos con dificultades o desvíos para identificar causas, redefinir prioridades y establecer planes de recuperación y seguimiento.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metodologías -->
|
||||
<section class="ide-modal-section">
|
||||
<h3 class="ide-section-title">Cómo trabajamos</h3>
|
||||
<div class="ide-tags-wrapper">
|
||||
<span class="ide-method-tag">Predictivo</span>
|
||||
<span class="ide-dot">·</span>
|
||||
<span class="ide-method-tag">Ágil</span>
|
||||
<span class="ide-dot">·</span>
|
||||
<span class="ide-method-tag">Híbrido</span>
|
||||
</div>
|
||||
<p class="ide-text-block">No partimos de una metodología predeterminada. Seleccionamos y combinamos prácticas de gestión según el tipo de proyecto, su complejidad, el contexto y las capacidades de la organización.</p>
|
||||
</section>
|
||||
|
||||
<!-- Propuesta de Valor -->
|
||||
<section class="ide-modal-callout">
|
||||
<blockquote class="ide-quote-highlight">
|
||||
“No se trata de gestionar más proyectos. Se trata de gestionar mejor las iniciativas que importan.”
|
||||
</blockquote>
|
||||
<p>Fortalecemos la capacidad de transformar prioridades estratégicas en iniciativas ejecutables, generar información para decidir y mantener el foco en los resultados.</p>
|
||||
</section>
|
||||
|
||||
<!-- Sinergia del Ecosistema -->
|
||||
<section class="ide-modal-synergy">
|
||||
<h4>Cuando el desafío requiere más</h4>
|
||||
<p>
|
||||
IDE Projects puede integrarse con <strong>IDE Excellence</strong>, <strong>IDE Academy</strong> e <strong>IDE Digital</strong> para rediseñar procesos, desarrollar capacidades, implementar sistemas o incorporar tecnología que fortalezca la gestión y ejecución de proyectos.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barra Inferior Fija (Footer) -->
|
||||
<footer class="ide-modal-footer-fixed">
|
||||
<span class="ide-scroll-hint" id="scrollHint">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M7 13l5 5 5-5M7 6l5 5 5-5"/>
|
||||
</svg>
|
||||
Desliza para leer más
|
||||
</span>
|
||||
<a href="#contacto" class="ide-btn-cta">
|
||||
<span>HABLEMOS</span>
|
||||
<span class="ide-btn-arrow">→</span>
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,73 +0,0 @@
|
||||
/* ==========================================================================
|
||||
CONTROLADOR DEL MODAL
|
||||
========================================================================== */
|
||||
function openModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
modal.classList.add("is-active");
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
const scrollArea = modal.querySelector(".ide-modal-scroll");
|
||||
if (scrollArea) scrollArea.scrollTop = 0;
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
modal.classList.remove("is-active");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
// Inicialización de eventos del modal una vez cargado el DOM
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// 1. Botón externo HTML
|
||||
const btnTrigger = document.getElementById("btnOpenProjectsModal");
|
||||
if (btnTrigger) {
|
||||
btnTrigger.addEventListener("click", () => {
|
||||
openModal("modal-ide-projects");
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Botón Cerrar (X)
|
||||
const btnClose = document.getElementById("btnCloseModal");
|
||||
if (btnClose) {
|
||||
btnClose.addEventListener("click", () => {
|
||||
closeModal("modal-ide-projects");
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Cerrar al hacer clic en el backdrop oscuro
|
||||
const modal = document.getElementById("modal-ide-projects");
|
||||
if (modal) {
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal("modal-ide-projects");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Cerrar con tecla Escape
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeModal("modal-ide-projects");
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Ocultar indicador "Desliza para leer más" al scrollear
|
||||
const scrollArea = document.getElementById("modalScrollArea");
|
||||
const scrollHint = document.getElementById("scrollHint");
|
||||
if (scrollArea && scrollHint) {
|
||||
scrollArea.addEventListener("scroll", () => {
|
||||
const isAtBottom = scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight < 40;
|
||||
if (isAtBottom) {
|
||||
scrollHint.classList.add("is-hidden");
|
||||
} else {
|
||||
scrollHint.classList.remove("is-hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Inicialización del motor Canvas
|
||||
//initNetwork();
|
||||
//requestAnimationFrame(animate);
|
||||
Reference in New Issue
Block a user