feat: 'Iridescent Apparatus' homepage redesign (WebGL, kinetic, brand system)
This commit is contained in:
parent
d4dbef11ed
commit
daeb3f6cea
17 changed files with 3420 additions and 312 deletions
|
|
@ -1,20 +1,34 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Cursor — a character cursor with a difference-blend "lens".
|
||||
*
|
||||
* A small dot tracks tightly; a larger ring trails with easing and inverts the
|
||||
* content beneath it (mix-blend-mode: difference). Over interactive elements it
|
||||
* grows; over elements carrying data-cursor it swaps in a contextual label
|
||||
* (e.g. "ver"). Disabled on touch / coarse pointers. Native cursor remains as a
|
||||
* fallback so the page is always usable.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { gsap } from "gsap";
|
||||
|
||||
export default function Cursor() {
|
||||
const dot = useRef<HTMLDivElement>(null);
|
||||
const ring = useRef<HTMLDivElement>(null);
|
||||
const label = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.matchMedia("(pointer: coarse)").matches) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
if (!dot.current || !ring.current) return;
|
||||
|
||||
const xD = gsap.quickTo(dot.current, "x", { duration: 0.15, ease: "power3" });
|
||||
const yD = gsap.quickTo(dot.current, "y", { duration: 0.15, ease: "power3" });
|
||||
const xR = gsap.quickTo(ring.current, "x", { duration: 0.5, ease: "power3" });
|
||||
const yR = gsap.quickTo(ring.current, "y", { duration: 0.5, ease: "power3" });
|
||||
document.body.classList.add("has-custom-cursor");
|
||||
|
||||
const xD = gsap.quickTo(dot.current, "x", { duration: 0.12, ease: "power3" });
|
||||
const yD = gsap.quickTo(dot.current, "y", { duration: 0.12, ease: "power3" });
|
||||
const xR = gsap.quickTo(ring.current, "x", { duration: 0.55, ease: "power3" });
|
||||
const yR = gsap.quickTo(ring.current, "y", { duration: 0.55, ease: "power3" });
|
||||
|
||||
const move = (e: MouseEvent) => {
|
||||
xD(e.clientX);
|
||||
|
|
@ -24,16 +38,30 @@ export default function Cursor() {
|
|||
};
|
||||
|
||||
const over = (e: Event) => {
|
||||
if ((e.target as HTMLElement).closest("a,button,.hoverable")) {
|
||||
ring.current?.classList.add("cursor-ring--big");
|
||||
const t = (e.target as HTMLElement).closest<HTMLElement>(
|
||||
"a,button,.hoverable,[data-cursor]"
|
||||
);
|
||||
if (!t) return;
|
||||
ring.current?.classList.add("cursor-ring--big");
|
||||
const text = t.getAttribute("data-cursor");
|
||||
if (text && label.current) {
|
||||
label.current.textContent = text;
|
||||
ring.current?.classList.add("cursor-ring--label");
|
||||
}
|
||||
};
|
||||
const out = () => ring.current?.classList.remove("cursor-ring--big");
|
||||
const out = (e: Event) => {
|
||||
const t = (e.target as HTMLElement).closest<HTMLElement>(
|
||||
"a,button,.hoverable,[data-cursor]"
|
||||
);
|
||||
if (!t) return;
|
||||
ring.current?.classList.remove("cursor-ring--big", "cursor-ring--label");
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", move);
|
||||
window.addEventListener("mousemove", move, { passive: true });
|
||||
document.addEventListener("mouseover", over);
|
||||
document.addEventListener("mouseout", out);
|
||||
return () => {
|
||||
document.body.classList.remove("has-custom-cursor");
|
||||
window.removeEventListener("mousemove", move);
|
||||
document.removeEventListener("mouseover", over);
|
||||
document.removeEventListener("mouseout", out);
|
||||
|
|
@ -42,8 +70,10 @@ export default function Cursor() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div ref={ring} className="cursor-ring" aria-hidden />
|
||||
<div ref={dot} className="cursor-dot" aria-hidden />
|
||||
<div ref={ring} className="cursor-ring" aria-hidden="true">
|
||||
<span ref={label} className="cursor-ring__label" />
|
||||
</div>
|
||||
<div ref={dot} className="cursor-dot" aria-hidden="true" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,107 +2,125 @@
|
|||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { gsap } from "gsap";
|
||||
|
||||
const line1 = "La mayoría de agencias alquilan sus herramientas.".split(" ");
|
||||
const line2 = "Nosotros construimos la nuestra.".split(" ");
|
||||
|
||||
function Words({ words, grad }: { words: string[]; grad?: boolean }) {
|
||||
return (
|
||||
<span className={"line" + (grad ? " grad" : "")}>
|
||||
{words.map((w, i) => (
|
||||
<span className="word" key={i}>
|
||||
<span className="word-in">{w}</span>{" "}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
import KineticText from "./KineticText";
|
||||
import PillMark from "./PillMark";
|
||||
import Magnetic from "./Magnetic";
|
||||
|
||||
export default function Hero() {
|
||||
const root = useRef<HTMLElement>(null);
|
||||
const mark = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = root.current;
|
||||
if (!el) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const tl = gsap.timeline({ defaults: { ease: "power4.out" } });
|
||||
tl.from(".eyebrow", { y: 20, opacity: 0, duration: 0.8 })
|
||||
tl.from(".hero__eyebrow", { y: 18, opacity: 0, duration: 0.9, delay: 0.15 })
|
||||
.from(".hero__sub", { y: 22, opacity: 0, duration: 0.9 }, "-=0.2")
|
||||
.from(
|
||||
".word-in",
|
||||
{ yPercent: 115, duration: 1.05, stagger: 0.045 },
|
||||
"-=0.4"
|
||||
".hero__actions > *",
|
||||
{ y: 20, opacity: 0, stagger: 0.1, duration: 0.7 },
|
||||
"-=0.5"
|
||||
)
|
||||
.from(".hero-sub", { y: 24, opacity: 0, duration: 0.9 }, "-=0.7")
|
||||
.from(".hero-actions > *", { y: 20, opacity: 0, stagger: 0.12, duration: 0.7 }, "-=0.6")
|
||||
.from(".bar", { scaleX: 0, transformOrigin: "left", stagger: 0.08, duration: 0.7, ease: "power3.inOut" }, "-=1.1");
|
||||
.from(".hero__meta", { opacity: 0, duration: 0.8 }, "-=0.4");
|
||||
|
||||
// floating pill-mark
|
||||
gsap.to(".bar", {
|
||||
y: "+=8",
|
||||
duration: 2.4,
|
||||
ease: "sine.inOut",
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
stagger: { each: 0.15, from: "random" },
|
||||
// parallax on the floating mark as you scroll the hero out
|
||||
gsap.to(".hero__mark", {
|
||||
yPercent: -22,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
gsap.to(".hero__display", {
|
||||
yPercent: 12,
|
||||
opacity: 0.25,
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: el,
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: true,
|
||||
},
|
||||
});
|
||||
|
||||
}, el);
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rx = (e.clientX / window.innerWidth - 0.5) * 2;
|
||||
const ry = (e.clientY / window.innerHeight - 0.5) * 2;
|
||||
gsap.to(mark.current, { x: rx * 22, y: ry * 22, duration: 0.8, ease: "power3" });
|
||||
gsap.to(".blob", { x: rx * 30, y: ry * 30, duration: 1.2, ease: "power3" });
|
||||
};
|
||||
window.addEventListener("mousemove", onMove);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
ctx.revert();
|
||||
};
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="hero" ref={root}>
|
||||
<div className="mesh" aria-hidden>
|
||||
<span className="blob b1" />
|
||||
<span className="blob b2" />
|
||||
<span className="blob b3" />
|
||||
<span className="blob b4" />
|
||||
</div>
|
||||
|
||||
<div className="hero-inner wrap">
|
||||
<p className="eyebrow">Agencia de marketing AI-native</p>
|
||||
|
||||
<div ref={mark} className="pillmark" aria-hidden>
|
||||
<span className="bar bar-grad" />
|
||||
<div className="bar-row">
|
||||
<span className="bar bar-ink" />
|
||||
<span className="bar bar-blue" />
|
||||
</div>
|
||||
<span className="bar bar-green" />
|
||||
</div>
|
||||
|
||||
<h1 className="hero-h1">
|
||||
<Words words={line1} />
|
||||
<Words words={line2} grad />
|
||||
</h1>
|
||||
|
||||
<p className="hero-sub">
|
||||
Estrategia humana + nuestra propia plataforma de IA. Web, SEO, ads y
|
||||
contenido — más rápido, más medible y a mejor coste que una agencia
|
||||
tradicional.
|
||||
<div className="hero__inner wrap">
|
||||
<p className="hero__eyebrow">
|
||||
<span className="dot" aria-hidden="true" />
|
||||
Agencia de marketing AI-native
|
||||
</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<a className="btn primary hoverable" href="#contacto">Habla con nosotros</a>
|
||||
<a className="btn ghost hoverable" href="#servicios">Ver qué hacemos</a>
|
||||
<h1 className="hero__display">
|
||||
<KineticText as="span" className="hero__line" text="No alquilamos" immediate delay={0.25} />
|
||||
<span className="hero__line hero__line--mixed">
|
||||
<KineticText as="span" text="las" immediate delay={0.4} />
|
||||
<em className="hero__serif"> herramientas.</em>
|
||||
</span>
|
||||
<KineticText
|
||||
as="span"
|
||||
className="hero__line hero__line--grad"
|
||||
text="Construimos la máquina."
|
||||
immediate
|
||||
delay={0.5}
|
||||
highlight={[0, 2]}
|
||||
/>
|
||||
</h1>
|
||||
|
||||
<div className="hero__mark" aria-hidden="true">
|
||||
<PillMark animate breathe />
|
||||
</div>
|
||||
|
||||
<p className="hero__sub">
|
||||
Estrategia humana sobre <strong>nuestra propia plataforma de IA</strong>.
|
||||
Web, SEO, ads y contenido — más rápido, más medible y a mejor coste que
|
||||
una agencia tradicional.
|
||||
</p>
|
||||
|
||||
<div className="hero__actions">
|
||||
<Magnetic strength={0.5}>
|
||||
<a className="btn btn--primary hoverable" href="#contacto">
|
||||
<span>Habla con nosotros</span>
|
||||
</a>
|
||||
</Magnetic>
|
||||
<Magnetic strength={0.3}>
|
||||
<a className="btn btn--ghost hoverable" href="#servicios">
|
||||
<span>Ver qué hacemos</span>
|
||||
</a>
|
||||
</Magnetic>
|
||||
</div>
|
||||
|
||||
<dl className="hero__meta">
|
||||
<div>
|
||||
<dt>Entrega</dt>
|
||||
<dd>en días, no semanas</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Reporting</dt>
|
||||
<dd>cada acción medida</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Infraestructura</dt>
|
||||
<dd>propia, no alquilada</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div className="scroll-hint" aria-hidden>scroll</div>
|
||||
<a className="hero__scroll hoverable" href="#ventaja" aria-label="Bajar a la siguiente sección">
|
||||
<span>scroll</span>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
||||
<path d="M12 4v16m0 0l-6-6m6 6l6-6" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</a>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
235
app/components/Iridescence.tsx
Normal file
235
app/components/Iridescence.tsx
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Iridescence — generative WebGL backdrop.
|
||||
*
|
||||
* A living interpretation of the brand cloudscape: domain-warped fractal noise
|
||||
* mapped onto an iridescent pastel palette (lavender -> blue -> rose -> mint),
|
||||
* with a faint rainbow refraction band and animated film grain. Drifts on its
|
||||
* own and leans toward the cursor. Falls back to a static CSS gradient when
|
||||
* WebGL is unavailable or the user prefers reduced motion.
|
||||
*
|
||||
* Renders behind everything (fixed, -z) and is purely decorative (aria-hidden).
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Renderer, Program, Mesh, Triangle, Vec2 } from "ogl";
|
||||
|
||||
const VERT = /* glsl */ `
|
||||
attribute vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const FRAG = /* glsl */ `
|
||||
precision highp float;
|
||||
|
||||
uniform float uTime;
|
||||
uniform vec2 uResolution;
|
||||
uniform vec2 uMouse; // 0..1, smoothed
|
||||
uniform float uIntensity; // global motion amount (0 for reduced motion)
|
||||
|
||||
// Brand palette anchors
|
||||
const vec3 VIOLET = vec3(0.545, 0.361, 0.965); // #8b5cf6
|
||||
const vec3 BLUE = vec3(0.231, 0.510, 0.965); // #3b82f6
|
||||
const vec3 ROSE = vec3(0.957, 0.475, 0.851); // soft pink
|
||||
const vec3 MINT = vec3(0.063, 0.725, 0.506); // #10b981 softened
|
||||
const vec3 HAZE = vec3(0.945, 0.949, 0.992); // near-white lavender
|
||||
|
||||
// -- hash / value noise --------------------------------------------------
|
||||
vec2 hash22(vec2 p) {
|
||||
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
||||
return fract(sin(p) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
float a = dot(hash22(i + vec2(0.0, 0.0)) - 0.5, f - vec2(0.0, 0.0));
|
||||
float b = dot(hash22(i + vec2(1.0, 0.0)) - 0.5, f - vec2(1.0, 0.0));
|
||||
float c = dot(hash22(i + vec2(0.0, 1.0)) - 0.5, f - vec2(0.0, 1.0));
|
||||
float d = dot(hash22(i + vec2(1.0, 1.0)) - 0.5, f - vec2(1.0, 1.0));
|
||||
return 0.5 + mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float v = 0.0;
|
||||
float amp = 0.55;
|
||||
mat2 rot = mat2(0.8, -0.6, 0.6, 0.8);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
v += amp * noise(p);
|
||||
p = rot * p * 2.0;
|
||||
amp *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// film grain
|
||||
float grain(vec2 uv, float t) {
|
||||
return fract(sin(dot(uv * (t + 1.0), vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / uResolution.xy;
|
||||
float aspect = uResolution.x / uResolution.y;
|
||||
vec2 p = uv;
|
||||
p.x *= aspect;
|
||||
|
||||
float t = uTime * 0.045 * (0.25 + uIntensity);
|
||||
|
||||
// mouse parallax (gentle)
|
||||
vec2 m = (uMouse - 0.5);
|
||||
p += m * 0.12 * uIntensity;
|
||||
|
||||
// domain warping for that liquid cloud feel
|
||||
vec2 q = vec2(fbm(p + vec2(0.0, t)), fbm(p + vec2(5.2, -t)));
|
||||
vec2 r = vec2(
|
||||
fbm(p + 1.7 * q + vec2(8.3, 2.8) + 0.15 * t),
|
||||
fbm(p + 1.7 * q + vec2(1.2, 6.5) - 0.12 * t)
|
||||
);
|
||||
float f = fbm(p + 2.0 * r);
|
||||
|
||||
// build the iridescent gradient from the field
|
||||
vec3 col = HAZE;
|
||||
col = mix(col, VIOLET, smoothstep(0.15, 0.85, f));
|
||||
col = mix(col, BLUE, smoothstep(0.30, 0.95, r.x));
|
||||
col = mix(col, ROSE, smoothstep(0.55, 1.05, q.y) * 0.65);
|
||||
col = mix(col, MINT, smoothstep(0.62, 1.0, r.y) * 0.30);
|
||||
|
||||
// central radiant bloom (echoes the cloudscape light source up high)
|
||||
vec2 center = vec2(0.5 * aspect, 0.86);
|
||||
float d = distance(p, center);
|
||||
float bloom = smoothstep(0.9, 0.0, d);
|
||||
col = mix(col, HAZE, bloom * 0.55);
|
||||
|
||||
// faint rainbow refraction band, lower-left like the asset
|
||||
float band = smoothstep(0.5, 0.0, abs((uv.x - uv.y) * 1.3 - 0.15 + 0.05 * sin(t * 2.0)));
|
||||
vec3 spectrum = 0.5 + 0.5 * cos(6.2831 * (vec3(0.0, 0.33, 0.67) + (uv.x + uv.y) * 0.7));
|
||||
col += spectrum * band * 0.06 * (0.4 + uIntensity);
|
||||
|
||||
// lift overall to keep it airy / pastel and protect text legibility
|
||||
col = mix(col, vec3(1.0), 0.18);
|
||||
|
||||
// soft vignette to anchor content readability
|
||||
float vig = smoothstep(1.25, 0.25, distance(uv, vec2(0.5)));
|
||||
col *= 0.82 + 0.18 * vig;
|
||||
|
||||
// animated grain to kill banding + add texture
|
||||
float g = grain(uv, floor(uTime * 12.0) * 0.5);
|
||||
col += (g - 0.5) * 0.035;
|
||||
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Iridescence() {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = ref.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
let renderer: Renderer;
|
||||
try {
|
||||
renderer = new Renderer({
|
||||
canvas,
|
||||
dpr: Math.min(window.devicePixelRatio, 1.75),
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
});
|
||||
} catch {
|
||||
// WebGL unavailable — CSS fallback (set on the host) remains visible.
|
||||
canvas.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0.95, 0.95, 0.99, 1);
|
||||
|
||||
const uniforms = {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vec2(1, 1) },
|
||||
uMouse: { value: new Vec2(0.5, 0.5) },
|
||||
uIntensity: { value: reduce ? 0 : 1 },
|
||||
};
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex: VERT,
|
||||
fragment: FRAG,
|
||||
uniforms,
|
||||
});
|
||||
const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
|
||||
|
||||
const resize = () => {
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
renderer.setSize(w, h);
|
||||
uniforms.uResolution.value.set(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
};
|
||||
resize();
|
||||
window.addEventListener("resize", resize);
|
||||
|
||||
// smoothed pointer
|
||||
const target = { x: 0.5, y: 0.5 };
|
||||
const onMove = (e: PointerEvent) => {
|
||||
target.x = e.clientX / window.innerWidth;
|
||||
target.y = 1 - e.clientY / window.innerHeight;
|
||||
};
|
||||
window.addEventListener("pointermove", onMove, { passive: true });
|
||||
|
||||
let raf = 0;
|
||||
let running = true;
|
||||
const start = performance.now();
|
||||
|
||||
const loop = (now: number) => {
|
||||
if (!running) return;
|
||||
raf = requestAnimationFrame(loop);
|
||||
uniforms.uTime.value = (now - start) / 1000;
|
||||
// ease pointer
|
||||
const m = uniforms.uMouse.value;
|
||||
m.x += (target.x - m.x) * 0.04;
|
||||
m.y += (target.y - m.y) * 0.04;
|
||||
renderer.render({ scene: mesh });
|
||||
};
|
||||
|
||||
if (reduce) {
|
||||
// render a single frame, then idle
|
||||
uniforms.uTime.value = 18;
|
||||
renderer.render({ scene: mesh });
|
||||
} else {
|
||||
raf = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
// pause when tab hidden (perf + battery)
|
||||
const onVis = () => {
|
||||
if (document.hidden) {
|
||||
running = false;
|
||||
cancelAnimationFrame(raf);
|
||||
} else if (!reduce) {
|
||||
running = true;
|
||||
raf = requestAnimationFrame(loop);
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", onVis);
|
||||
|
||||
return () => {
|
||||
running = false;
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", resize);
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
document.removeEventListener("visibilitychange", onVis);
|
||||
const ext = gl.getExtension("WEBGL_lose_context");
|
||||
ext?.loseContext();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="iridescence" aria-hidden="true">
|
||||
<canvas ref={ref} className="iridescence__canvas" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
app/components/KineticText.tsx
Normal file
83
app/components/KineticText.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* KineticText — word/line masked reveal driven by ScrollTrigger.
|
||||
*
|
||||
* Splits the given text into words wrapped in overflow-hidden masks (manual
|
||||
* spans — no premium SplitText plugin) and slides each word up from below as it
|
||||
* enters the viewport, with a stagger. Renders the text as real, selectable DOM
|
||||
* so it stays accessible and SEO-safe; animation only transforms (perf-friendly).
|
||||
*
|
||||
* With reduced-motion, the text simply appears (no transform).
|
||||
*/
|
||||
|
||||
import { createElement, useEffect, useRef, type ElementType } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
as?: ElementType;
|
||||
className?: string;
|
||||
/** Delay the whole reveal (s). */
|
||||
delay?: number;
|
||||
/** Start animation on mount instead of on scroll (for the hero). */
|
||||
immediate?: boolean;
|
||||
/** Mark a word index range as gradient-highlighted. */
|
||||
highlight?: [number, number];
|
||||
};
|
||||
|
||||
export default function KineticText({
|
||||
text,
|
||||
as: Tag = "span",
|
||||
className = "",
|
||||
delay = 0,
|
||||
immediate = false,
|
||||
highlight,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const words = text.split(" ");
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const inner = gsap.utils.toArray<HTMLElement>(".ktext__in", el);
|
||||
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
gsap.set(inner, { yPercent: 0, opacity: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.set(inner, { yPercent: 118 });
|
||||
const anim: gsap.TweenVars = {
|
||||
yPercent: 0,
|
||||
duration: 1.0,
|
||||
ease: "power4.out",
|
||||
stagger: 0.045,
|
||||
delay,
|
||||
};
|
||||
if (!immediate) {
|
||||
anim.scrollTrigger = { trigger: el, start: "top 88%" };
|
||||
}
|
||||
gsap.to(inner, anim);
|
||||
}, el);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [delay, immediate]);
|
||||
|
||||
const children = words.map((w, i) => {
|
||||
const hot =
|
||||
highlight && i >= highlight[0] && i <= highlight[1] ? " ktext__word--grad" : "";
|
||||
return (
|
||||
<span className={`ktext__word${hot}`} key={i}>
|
||||
<span className="ktext__in">{w}</span>
|
||||
{i < words.length - 1 ? " " : ""}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
return createElement(Tag, { ref, className: `ktext ${className}` }, children);
|
||||
}
|
||||
60
app/components/Magnetic.tsx
Normal file
60
app/components/Magnetic.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Magnetic — wraps a single interactive child and pulls it toward the cursor
|
||||
* within a radius, springing back on leave. Disabled for coarse pointers and
|
||||
* reduced-motion. Purely visual; does not alter semantics of the child.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { gsap } from "gsap";
|
||||
|
||||
export default function Magnetic({
|
||||
children,
|
||||
strength = 0.4,
|
||||
className = "",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
strength?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (window.matchMedia("(pointer: coarse)").matches) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const target = el.firstElementChild as HTMLElement | null;
|
||||
if (!target) return;
|
||||
|
||||
const xTo = gsap.quickTo(target, "x", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
|
||||
const yTo = gsap.quickTo(target, "y", { duration: 0.5, ease: "elastic.out(1, 0.4)" });
|
||||
|
||||
const move = (e: MouseEvent) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
const mx = e.clientX - (r.left + r.width / 2);
|
||||
const my = e.clientY - (r.top + r.height / 2);
|
||||
xTo(mx * strength);
|
||||
yTo(my * strength);
|
||||
};
|
||||
const leave = () => {
|
||||
xTo(0);
|
||||
yTo(0);
|
||||
};
|
||||
|
||||
el.addEventListener("mousemove", move);
|
||||
el.addEventListener("mouseleave", leave);
|
||||
return () => {
|
||||
el.removeEventListener("mousemove", move);
|
||||
el.removeEventListener("mouseleave", leave);
|
||||
};
|
||||
}, [strength]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className={`magnetic ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
40
app/components/Marquee.tsx
Normal file
40
app/components/Marquee.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Marquee — seamless looping band of disciplines, separated by the brand pill
|
||||
* glyph. Pure CSS animation (transform only); pauses under reduced-motion via
|
||||
* the stylesheet. Decorative.
|
||||
*/
|
||||
|
||||
const items = [
|
||||
"Web & Desarrollo",
|
||||
"SEO & GEO",
|
||||
"Paid Ads",
|
||||
"Contenido",
|
||||
"Diseño & Marca",
|
||||
"Automatización",
|
||||
];
|
||||
|
||||
function Row() {
|
||||
return (
|
||||
<div className="marquee__row" aria-hidden="true">
|
||||
{items.map((it) => (
|
||||
<span className="marquee__item" key={it}>
|
||||
{it}
|
||||
<span className="marquee__sep" />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Marquee() {
|
||||
return (
|
||||
<div className="marquee" aria-hidden="true">
|
||||
<div className="marquee__track">
|
||||
<Row />
|
||||
<Row />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
app/components/Packages.tsx
Normal file
98
app/components/Packages.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Packages — four tiers, each named for an outcome rather than a task list.
|
||||
* "Motor" is the featured tier (lifted, gradient spine). Cards reveal on scroll
|
||||
* and the featured one carries a continuously-shifting gradient border.
|
||||
*/
|
||||
|
||||
import Reveal from "./Reveal";
|
||||
|
||||
type Pkg = {
|
||||
n: string;
|
||||
who: string;
|
||||
price: string;
|
||||
f: string[];
|
||||
featured?: boolean;
|
||||
};
|
||||
|
||||
const packages: Pkg[] = [
|
||||
{
|
||||
n: "Sitio AI-native",
|
||||
who: "Necesitas web, ya",
|
||||
price: "Proyecto",
|
||||
f: ["Web en código, editable", "SEO base + tracking", "Entregada en tiempo récord"],
|
||||
},
|
||||
{
|
||||
n: "Base",
|
||||
who: "Estás arrancando",
|
||||
price: "Mensual",
|
||||
f: ["Fundamentos SEO", "Contenido inicial", "Analítica conectada"],
|
||||
},
|
||||
{
|
||||
n: "Motor",
|
||||
who: "Quieres crecer en serio",
|
||||
price: "Mensual",
|
||||
f: ["Ads + SEO + contenido", "Dashboard de resultados", "Optimización mensual", "Plataforma de IA incluida"],
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
n: "Partner",
|
||||
who: "Quieres escalar",
|
||||
price: "Retainer",
|
||||
f: ["Full-stack de crecimiento", "Automatización a medida", "Prioridad y roadmap", "Acceso directo al equipo"],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Packages() {
|
||||
return (
|
||||
<section id="paquetes" className="packages">
|
||||
<div className="wrap">
|
||||
<Reveal>
|
||||
<p className="kicker">04 — Paquetes</p>
|
||||
<h2 className="packages__title">
|
||||
Cada uno con un resultado. <span className="serif-em">No listas de tareas.</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
|
||||
<Reveal className="packages__grid" stagger={0.1}>
|
||||
{packages.map((p) => (
|
||||
<article
|
||||
className={`pkg hoverable${p.featured ? " pkg--featured" : ""}`}
|
||||
key={p.n}
|
||||
data-cursor={p.featured ? "el más elegido" : "ver"}
|
||||
>
|
||||
{p.featured && <span className="pkg__tag">Más elegido</span>}
|
||||
<header className="pkg__head">
|
||||
<span className="pkg__price">{p.price}</span>
|
||||
<h3 className="pkg__name">{p.n}</h3>
|
||||
<p className="pkg__who">{p.who}</p>
|
||||
</header>
|
||||
<ul className="pkg__features">
|
||||
{p.f.map((x) => (
|
||||
<li key={x}>
|
||||
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true">
|
||||
<path
|
||||
d="M4 10.5l3.5 3.5L16 6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>{x}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a className="pkg__cta hoverable" href="#contacto">
|
||||
Empezar con {p.n}
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
97
app/components/PillMark.tsx
Normal file
97
app/components/PillMark.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* PillMark — the brand imagotype as a living system.
|
||||
*
|
||||
* Reconstructs the 4-bar capsule grid (violet→blue gradient bar / ink bar +
|
||||
* blue square / emerald bar) as inline SVG so each pill can be animated
|
||||
* independently: it assembles on mount, then breathes. Used at hero scale and
|
||||
* as a compact mark in dividers / footer.
|
||||
*
|
||||
* Decorative by default (aria-hidden). Pass `title` to expose it as an image.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { gsap } from "gsap";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
/** Animate assembly on mount (hero). */
|
||||
animate?: boolean;
|
||||
/** Continuous breathing after assembly. */
|
||||
breathe?: boolean;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export default function PillMark({
|
||||
className = "",
|
||||
animate = false,
|
||||
breathe = false,
|
||||
title,
|
||||
}: Props) {
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
const bars = gsap.utils.toArray<SVGElement>(".pillmark__bar");
|
||||
|
||||
if (animate) {
|
||||
gsap.set(bars, { transformOrigin: "center center" });
|
||||
gsap.from(bars, {
|
||||
scaleX: 0,
|
||||
scaleY: 0.2,
|
||||
opacity: 0,
|
||||
duration: 1.1,
|
||||
ease: "elastic.out(1, 0.75)",
|
||||
stagger: { each: 0.09, from: "start" },
|
||||
delay: 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
if (breathe) {
|
||||
bars.forEach((bar, i) => {
|
||||
gsap.to(bar, {
|
||||
y: i % 2 === 0 ? "+=6" : "-=6",
|
||||
duration: 2.6 + i * 0.25,
|
||||
ease: "sine.inOut",
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
delay: 1.2 + i * 0.1,
|
||||
});
|
||||
});
|
||||
}
|
||||
}, el);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [animate, breathe]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
className={`pillmark ${className}`}
|
||||
viewBox="0 0 1192 1287"
|
||||
role={title ? "img" : "presentation"}
|
||||
aria-hidden={title ? undefined : "true"}
|
||||
aria-label={title}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
<defs>
|
||||
<linearGradient id="pm-grad" x1="0" y1="0" x2="1192" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#8b5cf6" />
|
||||
<stop offset="1" stopColor="#3b82f6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{/* top: full-width violet→blue gradient capsule */}
|
||||
<rect className="pillmark__bar" x="0" y="0" width="1192" height="348.64" rx="174.32" fill="url(#pm-grad)" />
|
||||
{/* middle row: ink wide capsule + blue square capsule */}
|
||||
<rect className="pillmark__bar" x="0.02" y="480.64" width="725.02" height="348.64" rx="174.32" fill="#111827" />
|
||||
<rect className="pillmark__bar" x="843" y="480.28" width="349.04" height="348.98" rx="174.32" fill="#3b82f6" />
|
||||
{/* bottom: full-width emerald capsule */}
|
||||
<rect className="pillmark__bar" x="0.04" y="938.21" width="1191.97" height="348.64" rx="174.32" fill="#10b981" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef, ReactNode } from "react";
|
||||
import { useEffect, useRef, type ReactNode } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
/**
|
||||
* Reveal — generic on-scroll entrance with personality (slide + soft clip),
|
||||
* not a plain fade-up. With `stagger`, animates direct children in sequence.
|
||||
* Respects reduced-motion (content shown immediately).
|
||||
*/
|
||||
export default function Reveal({
|
||||
children,
|
||||
className = "",
|
||||
stagger = 0,
|
||||
y = 40,
|
||||
y = 44,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
|
|
@ -22,13 +27,16 @@ export default function Reveal({
|
|||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||
|
||||
const targets = stagger > 0 ? Array.from(el.children) : [el];
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
gsap.from(targets, {
|
||||
y,
|
||||
opacity: 0,
|
||||
duration: 1,
|
||||
filter: "blur(8px)",
|
||||
duration: 1.05,
|
||||
ease: "power3.out",
|
||||
stagger,
|
||||
scrollTrigger: { trigger: el, start: "top 85%" },
|
||||
|
|
|
|||
163
app/components/ServicesJourney.tsx
Normal file
163
app/components/ServicesJourney.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* ServicesJourney — pinned horizontal-scroll track of services.
|
||||
*
|
||||
* On desktop the section pins and the panels scroll sideways as you scroll
|
||||
* down (GSAP ScrollTrigger pin + scrub), with a progress bar. On mobile / when
|
||||
* the pin would be awkward, and under reduced-motion, it degrades to a normal
|
||||
* vertical stack (no pin, panels just stack and reveal). Each panel is a real
|
||||
* <article> so the content is accessible regardless of layout.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { gsap } from "gsap";
|
||||
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const services = [
|
||||
{
|
||||
n: "01",
|
||||
t: "Web & Desarrollo",
|
||||
d: "Sitios construidos en código, veloces y 100% editables. Sin plantillas, sin atajos: arquitectura que escala contigo.",
|
||||
tags: ["Next.js", "Headless", "Core Web Vitals"],
|
||||
},
|
||||
{
|
||||
n: "02",
|
||||
t: "SEO & GEO",
|
||||
d: "Posicionamiento técnico y de contenido — para Google y para los motores de IA. Reporting real, no humo.",
|
||||
tags: ["Técnico", "Contenido", "Answer Engines"],
|
||||
},
|
||||
{
|
||||
n: "03",
|
||||
t: "Paid Ads",
|
||||
d: "Google, Meta, TikTok y LinkedIn. Creatividad asistida por IA y pujas optimizadas hacia el resultado, no el clic.",
|
||||
tags: ["Performance", "Creatividad IA", "Atribución"],
|
||||
},
|
||||
{
|
||||
n: "04",
|
||||
t: "Contenido",
|
||||
d: "Piezas pensadas para rankear y para convertir. Producción a escala sin perder la voz de tu marca.",
|
||||
tags: ["Editorial", "Vídeo", "Social"],
|
||||
},
|
||||
{
|
||||
n: "05",
|
||||
t: "Diseño & Marca",
|
||||
d: "Identidad y dirección de arte con criterio. Sistemas de diseño que viven en producto, no en un PDF.",
|
||||
tags: ["Identidad", "Design System", "Motion"],
|
||||
},
|
||||
{
|
||||
n: "06",
|
||||
t: "Automatización",
|
||||
d: "Flujos que devuelven horas: captación de leads, reporting automático y operativa conectada de punta a punta.",
|
||||
tags: ["Leads", "Reporting", "Ops"],
|
||||
},
|
||||
];
|
||||
|
||||
export default function ServicesJourney() {
|
||||
const section = useRef<HTMLElement>(null);
|
||||
const track = useRef<HTMLDivElement>(null);
|
||||
const bar = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const sec = section.current;
|
||||
const trk = track.current;
|
||||
if (!sec || !trk) return;
|
||||
|
||||
const mm = gsap.matchMedia();
|
||||
|
||||
// Desktop with motion allowed: pin + horizontal scroll.
|
||||
mm.add(
|
||||
"(min-width: 900px) and (prefers-reduced-motion: no-preference)",
|
||||
() => {
|
||||
const distance = () => trk.scrollWidth - window.innerWidth;
|
||||
|
||||
const tween = gsap.to(trk, {
|
||||
x: () => -distance(),
|
||||
ease: "none",
|
||||
scrollTrigger: {
|
||||
trigger: sec,
|
||||
start: "top top",
|
||||
end: () => "+=" + distance(),
|
||||
scrub: 0.6,
|
||||
pin: true,
|
||||
invalidateOnRefresh: true,
|
||||
onUpdate: (self) => {
|
||||
if (bar.current) {
|
||||
bar.current.style.transform = `scaleX(${self.progress})`;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Per-panel inner reveal as it slides into center.
|
||||
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
|
||||
gsap.from(panel.querySelectorAll(".sjourney__reveal"), {
|
||||
y: 40,
|
||||
opacity: 0,
|
||||
duration: 0.6,
|
||||
stagger: 0.06,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: {
|
||||
trigger: panel,
|
||||
containerAnimation: tween,
|
||||
start: "left 80%",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (bar.current) bar.current.style.transform = "scaleX(0)";
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Mobile / reduced-motion: simple stacked reveals.
|
||||
mm.add("(max-width: 899px)", () => {
|
||||
gsap.utils.toArray<HTMLElement>(".sjourney__panel").forEach((panel) => {
|
||||
gsap.from(panel, {
|
||||
y: 36,
|
||||
opacity: 0,
|
||||
duration: 0.7,
|
||||
ease: "power3.out",
|
||||
scrollTrigger: { trigger: panel, start: "top 88%" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return () => mm.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="servicios" className="sjourney" ref={section} aria-label="Servicios">
|
||||
<div className="sjourney__head wrap">
|
||||
<p className="kicker">02 — Qué hacemos</p>
|
||||
<h2 className="sjourney__title">
|
||||
Todo tu crecimiento, <span className="serif-em">un mismo sistema.</span>
|
||||
</h2>
|
||||
<div className="sjourney__progress" aria-hidden="true">
|
||||
<span ref={bar} className="sjourney__progress-bar" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sjourney__viewport">
|
||||
<div className="sjourney__track" ref={track}>
|
||||
{services.map((s) => (
|
||||
<article className="sjourney__panel hoverable" key={s.t} data-cursor="explorar">
|
||||
<span className="sjourney__n sjourney__reveal">{s.n}</span>
|
||||
<h3 className="sjourney__panel-title sjourney__reveal">{s.t}</h3>
|
||||
<p className="sjourney__panel-desc sjourney__reveal">{s.d}</p>
|
||||
<ul className="sjourney__tags sjourney__reveal">
|
||||
{s.tags.map((tag) => (
|
||||
<li key={tag}>{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
<span className="sjourney__pill" aria-hidden="true" />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
82
app/components/SiteHeader.tsx
Normal file
82
app/components/SiteHeader.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* SiteHeader — fixed top nav. Compacts (adds a glass backdrop) once you scroll
|
||||
* past the hero, and includes a mobile menu toggle. The wordmark is real text
|
||||
* ("feedback studios") with the brand mark inline.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import PillMark from "./PillMark";
|
||||
|
||||
const links = [
|
||||
{ href: "#ventaja", label: "Ventaja" },
|
||||
{ href: "#servicios", label: "Servicios" },
|
||||
{ href: "#sectores", label: "Sectores" },
|
||||
{ href: "#paquetes", label: "Paquetes" },
|
||||
];
|
||||
|
||||
export default function SiteHeader() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 80);
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
// Close the mobile menu on Escape.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={headerRef}
|
||||
className={`site-header${scrolled ? " is-scrolled" : ""}${open ? " is-open" : ""}`}
|
||||
>
|
||||
<div className="site-header__inner">
|
||||
<a className="brand hoverable" href="#top" aria-label="Feedback Studios, inicio">
|
||||
<PillMark className="brand__mark" title="Feedback Studios" />
|
||||
<span className="brand__word">
|
||||
feedback<span className="brand__word-2">studios</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav className="site-nav" aria-label="Principal">
|
||||
<ul>
|
||||
{links.map((l) => (
|
||||
<li key={l.href}>
|
||||
<a className="hoverable" href={l.href} onClick={() => setOpen(false)}>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a className="btn btn--sm btn--primary hoverable" href="#contacto" onClick={() => setOpen(false)}>
|
||||
<span>Hablemos</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="site-header__toggle hoverable"
|
||||
aria-expanded={open}
|
||||
aria-label={open ? "Cerrar menú" : "Abrir menú"}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,6 +9,12 @@ gsap.registerPlugin(ScrollTrigger);
|
|||
|
||||
export default function SmoothScroll() {
|
||||
useEffect(() => {
|
||||
// Respect reduced-motion: skip momentum scrolling entirely.
|
||||
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
ScrollTrigger.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.15,
|
||||
smoothWheel: true,
|
||||
|
|
@ -21,7 +27,22 @@ export default function SmoothScroll() {
|
|||
gsap.ticker.add(ticker);
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
// Anchor links should route through Lenis for the smooth glide.
|
||||
const onClick = (e: MouseEvent) => {
|
||||
const a = (e.target as HTMLElement).closest('a[href^="#"]');
|
||||
if (!a) return;
|
||||
const id = a.getAttribute("href");
|
||||
if (!id || id === "#") return;
|
||||
const target = document.querySelector(id);
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
lenis.scrollTo(target as HTMLElement, { offset: -20 });
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", onClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("click", onClick);
|
||||
gsap.ticker.remove(ticker);
|
||||
lenis.destroy();
|
||||
};
|
||||
|
|
|
|||
1367
app/globals.css
1367
app/globals.css
File diff suppressed because it is too large
Load diff
|
|
@ -24,13 +24,25 @@ export default function RootLayout({
|
|||
return (
|
||||
<html lang="es">
|
||||
<head>
|
||||
{/* Satoshi (brand) + Instrument Serif (editorial accent) */}
|
||||
<link rel="preconnect" href="https://api.fontshare.com" crossOrigin="" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap"
|
||||
href="https://api.fontshare.com/v2/css?f[]=satoshi@300,400,500,700,900&display=swap"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap"
|
||||
/>
|
||||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
|
||||
<meta name="theme-color" content="#0a0a12" />
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main" className="skip-link">
|
||||
Saltar al contenido
|
||||
</a>
|
||||
<SmoothScroll />
|
||||
<Cursor />
|
||||
{children}
|
||||
|
|
|
|||
235
app/page.tsx
235
app/page.tsx
|
|
@ -1,87 +1,118 @@
|
|||
import Iridescence from "./components/Iridescence";
|
||||
import SiteHeader from "./components/SiteHeader";
|
||||
import Hero from "./components/Hero";
|
||||
import Marquee from "./components/Marquee";
|
||||
import Reveal from "./components/Reveal";
|
||||
import KineticText from "./components/KineticText";
|
||||
import ServicesJourney from "./components/ServicesJourney";
|
||||
import Packages from "./components/Packages";
|
||||
import PillMark from "./components/PillMark";
|
||||
|
||||
const pillars = [
|
||||
{ k: "Más rápido", d: "Entregamos en días lo que una agencia tradicional tarda semanas." },
|
||||
{ k: "Más medible", d: "Cada acción con su dashboard y su resultado, no humo." },
|
||||
{ k: "Mejor coste", d: "Nuestra plataforma hace el trabajo pesado. Pagas resultado, no horas." },
|
||||
const advantages = [
|
||||
{
|
||||
k: "Más rápido",
|
||||
metric: "días",
|
||||
sub: "no semanas",
|
||||
d: "Nuestra plataforma hace el trabajo pesado. Entregamos en días lo que una agencia tradicional tarda en arrancar.",
|
||||
},
|
||||
{
|
||||
k: "Más medible",
|
||||
metric: "100%",
|
||||
sub: "trazable",
|
||||
d: "Cada acción tiene su dashboard y su resultado. Sabes qué funciona y qué no — sin humo ni informes de relleno.",
|
||||
},
|
||||
{
|
||||
k: "Mejor coste",
|
||||
metric: "0",
|
||||
sub: "horas vacías",
|
||||
d: "No pagas por horas: pagas por resultado. La IA absorbe lo repetitivo, el equipo se concentra en la estrategia.",
|
||||
},
|
||||
];
|
||||
|
||||
const services = [
|
||||
{ n: "01", t: "Web & Desarrollo", d: "Sitios en código, rápidos y editables. Sin plantillas." },
|
||||
{ n: "02", t: "SEO", d: "Posicionamiento técnico y de contenido, con reporting real." },
|
||||
{ n: "03", t: "Paid Ads", d: "Google, Meta, TikTok, LinkedIn — creatividad + IA." },
|
||||
{ n: "04", t: "Contenido", d: "Piezas optimizadas para Google y para IA (GEO)." },
|
||||
{ n: "05", t: "Diseño & Marca", d: "Identidad y dirección de arte con criterio." },
|
||||
{ n: "06", t: "Automatización", d: "Flujos que ahorran horas: leads, reporting, operativa." },
|
||||
];
|
||||
|
||||
const packages = [
|
||||
{ n: "Sitio AI-native", who: "Necesitas web ya", f: ["Web editable en código", "SEO base + tracking", "Listo en tiempo récord"] },
|
||||
{ n: "Base", who: "Estás arrancando", f: ["Fundamentos SEO", "Contenido inicial", "Analítica"] },
|
||||
{ n: "Motor", who: "Quieres crecer", f: ["Ads + SEO + contenido", "Dashboard de resultados", "Optimización mensual"], featured: true },
|
||||
{ n: "Partner", who: "Quieres escalar", f: ["Full-stack de crecimiento", "Automatización a medida", "Prioridad y plataforma"] },
|
||||
const sectors = [
|
||||
{ t: "Clínicas estéticas", d: "Captación de pacientes, reputación y agenda llena." },
|
||||
{ t: "Servicios profesionales", d: "Autoridad, leads cualificados y cierre." },
|
||||
{ t: "E-commerce", d: "ROAS sostenible y catálogo que convierte." },
|
||||
{ t: "SaaS & Tech", d: "Activación, retención y growth medible." },
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<header className="hd">
|
||||
<span className="logo">feedback<span className="logo-dot">studios</span></span>
|
||||
<nav className="hd-nav">
|
||||
<a href="#servicios" className="hoverable">Servicios</a>
|
||||
<a href="#paquetes" className="hoverable">Paquetes</a>
|
||||
<a href="#contacto" className="hd-cta hoverable">Habla con nosotros</a>
|
||||
</nav>
|
||||
</header>
|
||||
<Iridescence />
|
||||
<SiteHeader />
|
||||
|
||||
<main>
|
||||
<main id="main">
|
||||
<span id="top" />
|
||||
<Hero />
|
||||
|
||||
{/* MARQUEE */}
|
||||
<div className="marquee" aria-hidden>
|
||||
<div className="marquee-track">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<span key={i}>
|
||||
Web · SEO · Paid Ads · Contenido · Diseño · Automatización · IA ·
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Marquee />
|
||||
|
||||
{/* VENTAJA INJUSTA */}
|
||||
<section className="band ink">
|
||||
{/* VENTAJA INJUSTA — dark chapter */}
|
||||
<section id="ventaja" className="band band--ink advantage">
|
||||
<div className="wrap">
|
||||
<Reveal>
|
||||
<p className="kicker">01 — Ventaja injusta</p>
|
||||
<h2 className="band-h2">
|
||||
No alquilamos herramientas. <span className="grad">Construimos la máquina.</span>
|
||||
<p className="kicker kicker--light">01 — Ventaja injusta</p>
|
||||
<h2 className="band__title">
|
||||
<KineticText as="span" text="No alquilamos herramientas." />{" "}
|
||||
<KineticText
|
||||
as="span"
|
||||
className="band__title-grad"
|
||||
text="Construimos la máquina."
|
||||
highlight={[0, 2]}
|
||||
/>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal className="grid3" stagger={0.12}>
|
||||
{pillars.map((p) => (
|
||||
<div className="card" key={p.k}>
|
||||
<h3>{p.k}</h3>
|
||||
<p>{p.d}</p>
|
||||
</div>
|
||||
|
||||
<div className="advantage__grid">
|
||||
{advantages.map((a, i) => (
|
||||
<Reveal className="advantage__item" key={a.k} y={56}>
|
||||
<span className="advantage__index">0{i + 1}</span>
|
||||
<p className="advantage__metric">
|
||||
{a.metric}
|
||||
<span className="advantage__metric-sub">{a.sub}</span>
|
||||
</p>
|
||||
<h3 className="advantage__k">{a.k}</h3>
|
||||
<p className="advantage__d">{a.d}</p>
|
||||
</Reveal>
|
||||
))}
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* SERVICIOS */}
|
||||
<section id="servicios" className="band ink">
|
||||
<div className="wrap">
|
||||
<Reveal>
|
||||
<p className="kicker">02 — Qué hacemos</p>
|
||||
<h2 className="band-h2">Todo tu crecimiento, un mismo sistema.</h2>
|
||||
</Reveal>
|
||||
<Reveal className="svc-list" stagger={0.1}>
|
||||
{services.map((s) => (
|
||||
<div className="svc hoverable" key={s.t}>
|
||||
<span className="svc-n">{s.n}</span>
|
||||
<span className="svc-t">{s.t}</span>
|
||||
<span className="svc-d">{s.d}</span>
|
||||
{/* SERVICIOS — horizontal journey */}
|
||||
<ServicesJourney />
|
||||
|
||||
{/* SECTORES — deep dive */}
|
||||
<section id="sectores" className="band sectors">
|
||||
<div className="wrap sectors__wrap">
|
||||
<div className="sectors__intro">
|
||||
<Reveal>
|
||||
<p className="kicker">03 — Profundidad por sector</p>
|
||||
<h2 className="sectors__title">
|
||||
No hacemos marketing genérico.{" "}
|
||||
<span className="serif-em">Hablamos tu negocio.</span>
|
||||
</h2>
|
||||
<p className="sectors__lead">
|
||||
Construimos soluciones a medida de cada sector: el mismo motor de
|
||||
IA, afinado con el contexto, el vocabulario y los KPIs que de verdad
|
||||
importan en tu mercado.
|
||||
</p>
|
||||
<a className="link-arrow hoverable" href="#contacto">
|
||||
¿Tu sector no está? Cuéntanoslo
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</Reveal>
|
||||
</div>
|
||||
|
||||
<Reveal className="sectors__list" stagger={0.1}>
|
||||
{sectors.map((s) => (
|
||||
<div className="sector hoverable" key={s.t} data-cursor="ver">
|
||||
<PillMark className="sector__mark" />
|
||||
<div>
|
||||
<h3 className="sector__t">{s.t}</h3>
|
||||
<p className="sector__d">{s.d}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Reveal>
|
||||
|
|
@ -89,42 +120,62 @@ export default function Home() {
|
|||
</section>
|
||||
|
||||
{/* PAQUETES */}
|
||||
<section id="paquetes" className="band ink">
|
||||
<div className="wrap">
|
||||
<Reveal>
|
||||
<p className="kicker">03 — Paquetes</p>
|
||||
<h2 className="band-h2">Cada uno con un resultado. No listas de tareas.</h2>
|
||||
</Reveal>
|
||||
<Reveal className="grid4" stagger={0.1}>
|
||||
{packages.map((p) => (
|
||||
<div className={"pkg hoverable" + (p.featured ? " featured" : "")} key={p.n}>
|
||||
{p.featured && <span className="tag">Más popular</span>}
|
||||
<h3>{p.n}</h3>
|
||||
<p className="who">{p.who}</p>
|
||||
<ul>{p.f.map((x) => <li key={x}>{x}</li>)}</ul>
|
||||
</div>
|
||||
))}
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
<Packages />
|
||||
|
||||
{/* CTA */}
|
||||
{/* CTA — light iridescent close */}
|
||||
<section id="contacto" className="cta">
|
||||
<div className="mesh mesh-cta" aria-hidden>
|
||||
<span className="blob b1" /><span className="blob b3" />
|
||||
</div>
|
||||
<Reveal className="wrap">
|
||||
<h2 className="cta-h2">¿Hablamos de tu <span className="grad">crecimiento</span>?</h2>
|
||||
<p className="hero-sub">Cuéntanos dónde estás y te enseñamos cómo llegar al siguiente nivel.</p>
|
||||
<a className="btn primary big hoverable" href="mailto:feedback.studios.design@gmail.com">Habla con nosotros</a>
|
||||
<Reveal className="wrap cta__wrap">
|
||||
<PillMark className="cta__mark" animate breathe />
|
||||
<h2 className="cta__title">
|
||||
<KineticText as="span" text="¿Hablamos de tu" />{" "}
|
||||
<KineticText
|
||||
as="span"
|
||||
className="cta__title-grad"
|
||||
text="crecimiento?"
|
||||
highlight={[0, 0]}
|
||||
/>
|
||||
</h2>
|
||||
<p className="cta__lead">
|
||||
Cuéntanos dónde estás y te enseñamos, con datos, cómo llegar al
|
||||
siguiente nivel. Respuesta en menos de 24h.
|
||||
</p>
|
||||
<a
|
||||
className="btn btn--primary btn--lg hoverable"
|
||||
href="mailto:feedback.studios.design@gmail.com"
|
||||
data-cursor="escríbenos"
|
||||
>
|
||||
<span>Habla con nosotros</span>
|
||||
</a>
|
||||
<p className="cta__mail">
|
||||
o escríbenos a{" "}
|
||||
<a className="hoverable" href="mailto:feedback.studios.design@gmail.com">
|
||||
feedback.studios.design@gmail.com
|
||||
</a>
|
||||
</p>
|
||||
</Reveal>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="ft">
|
||||
<div className="wrap">
|
||||
<span className="logo">feedback<span className="logo-dot">studios</span></span>
|
||||
<span className="muted">© 2026 · Agencia de marketing AI-native · infraestructura propia</span>
|
||||
<footer className="site-footer">
|
||||
<div className="wrap site-footer__inner">
|
||||
<div className="site-footer__brand">
|
||||
<span className="brand__word brand__word--lg">
|
||||
feedback<span className="brand__word-2">studios</span>
|
||||
</span>
|
||||
<p className="site-footer__tag">
|
||||
La agencia de marketing AI-native. Estrategia humana,
|
||||
infraestructura propia.
|
||||
</p>
|
||||
</div>
|
||||
<nav className="site-footer__nav" aria-label="Pie">
|
||||
<a className="hoverable" href="#servicios">Servicios</a>
|
||||
<a className="hoverable" href="#paquetes">Paquetes</a>
|
||||
<a className="hoverable" href="#sectores">Sectores</a>
|
||||
<a className="hoverable" href="mailto:feedback.studios.design@gmail.com">Contacto</a>
|
||||
</nav>
|
||||
<p className="site-footer__legal">
|
||||
© 2026 Feedback Studios · Marketing AI-native con infraestructura propia
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
|
|
|
|||
999
package-lock.json
generated
Normal file
999
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,999 @@
|
|||
{
|
||||
"name": "agency-web",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agency-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"gsap": "^3.12.5",
|
||||
"lenis": "^1.1.14",
|
||||
"next": "15.1.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"split-type": "^0.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz",
|
||||
"integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz",
|
||||
"integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz",
|
||||
"integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz",
|
||||
"integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz",
|
||||
"integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz",
|
||||
"integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz",
|
||||
"integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz",
|
||||
"integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz",
|
||||
"integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
|
||||
"integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001799",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/client-only": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/gsap": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
|
||||
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
|
||||
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lenis": {
|
||||
"version": "1.3.23",
|
||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
|
||||
"integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"playground",
|
||||
"playground/*"
|
||||
],
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/darkroomengineering"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"vue": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz",
|
||||
"integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==",
|
||||
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.1.6",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.1.6",
|
||||
"@next/swc-darwin-x64": "15.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.6",
|
||||
"@next/swc-linux-arm64-musl": "15.1.6",
|
||||
"@next/swc-linux-x64-gnu": "15.1.6",
|
||||
"@next/swc-linux-x64-musl": "15.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.6",
|
||||
"@next/swc-win32-x64-msvc": "15.1.6",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"babel-plugin-react-compiler": "*",
|
||||
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
||||
"sass": "^1.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@playwright/test": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ogl": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz",
|
||||
"integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-darwin-x64": "0.33.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||
"@img/sharp-linux-arm": "0.33.5",
|
||||
"@img/sharp-linux-arm64": "0.33.5",
|
||||
"@img/sharp-linux-s390x": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||
"@img/sharp-wasm32": "0.33.5",
|
||||
"@img/sharp-win32-ia32": "0.33.5",
|
||||
"@img/sharp-win32-x64": "0.33.5"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split-type": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/split-type/-/split-type-0.3.4.tgz",
|
||||
"integrity": "sha512-otEk9vnD8qwfLsk3Lx0gz+qRkNIJCx0mlyL47ImP/DjMuV39d75Lpfwjn9fHteDRz0aoOblSzQjSNT9+Sswxcg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@babel/core": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
"gsap": "^3.12.5",
|
||||
"lenis": "^1.1.14",
|
||||
"next": "15.1.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
"react-dom": "19.0.0",
|
||||
"split-type": "^0.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
|
|
|
|||
Loading…
Reference in a new issue