From 959de3b809f79dba14ffe0631f4dd503d2a759e4 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 00:28:38 -0700
Subject: [PATCH 01/11] =?UTF-8?q?feat(site):=20bright=20"Inspector"=20rede?=
=?UTF-8?q?sign=20=E2=80=94=20interactive=20verdict,=20lens=20sheen,=20the?=
=?UTF-8?q?me-scan?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Drop the warm manila/amber "Case File" palette (kept reading as brown/Claude-ish)
for a clean, bright direction the lens-mascot actually belongs in.
- Palette: cool near-white base + near-black ink + vivid blue (#2563ff) lead, a
cyan scanner glint, and a hot-coral pop (kicker dot, wide-feature lenses). Dark
mode recast as a cool slate, not a warm noir. All driven by global.css tokens
so marketing + docs reskin together; AA contrast preserved.
- Verdict demo is now a live tablist: 7 clickable tabs crossfade the card body,
real tab/tabpanel a11y + arrow-key nav, and the "Strong fit" stamp slams in on
scroll (the previously-unused --ease-stamp curve).
- Hero: bigger Fraunces type contrast, a soft specular lens sheen drifting over
the mascot port (replaced a cheap cyan scan-line), CASE No. kicker.
- Bento: accent edge-light + lens-icon tilt on hover; section titles wipe in.
- Theme toggle: a top->bottom scanline "develops" the page into the new theme
via the View Transitions API (flushSync + clip-path reveal, cyan glow edge),
with an instant-flip fallback for unsupported browsers and reduced-motion.
All motion is reduced-motion gated. Production build, typecheck, and lint pass.
---
website/app/(home)/styles/home.css | 330 ++++++++++++++++++++++--
website/app/(home)/styles/shell.css | 6 +-
website/app/global.css | 263 +++++++++++--------
website/app/icon.svg | 8 +-
website/components/home/Hero.tsx | 4 +-
website/components/home/SiteMotion.tsx | 21 ++
website/components/home/VerdictDemo.tsx | 255 ++++++++++++++++--
website/components/site/ThemeToggle.tsx | 26 +-
8 files changed, 747 insertions(+), 166 deletions(-)
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index 473c0bc..1fdf943 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -57,6 +57,49 @@
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35),
inset 0 -56px 60px -34px var(--accent-glow);
}
+/* Lens sheen — a soft specular glint drifts across the glass, like light
+ catching a lens. No hard scan-line. Slow, with a long calm pause between
+ passes. Reduced-motion: nothing moves. */
+.hero-mascot-stage::before {
+ content: '';
+ position: absolute;
+ inset: -30%;
+ z-index: 2;
+ pointer-events: none;
+ opacity: 0;
+ background: linear-gradient(
+ 100deg,
+ transparent 41%,
+ rgba(226, 246, 255, 0.1) 47%,
+ rgba(240, 251, 255, 0.34) 50%,
+ rgba(226, 246, 255, 0.1) 53%,
+ transparent 59%
+ );
+ mix-blend-mode: screen;
+ transform: translateX(-58%);
+}
+@media (prefers-reduced-motion: no-preference) {
+ .hero-mascot-stage::before {
+ animation: lensSheen 7s var(--ease-out) infinite;
+ }
+}
+@keyframes lensSheen {
+ 0% {
+ transform: translateX(-58%);
+ opacity: 0;
+ }
+ 6% {
+ opacity: 1;
+ }
+ 19% {
+ opacity: 1;
+ }
+ 27%,
+ 100% {
+ transform: translateX(58%);
+ opacity: 0;
+ }
+}
.hero-say {
font-size: 0.9rem;
color: var(--muted);
@@ -66,14 +109,21 @@
text-align: center;
}
.hero-title {
- font-size: clamp(2.5rem, 1rem + 5vw, 4.6rem);
+ font-size: clamp(2.6rem, 0.9rem + 5.4vw, 5rem);
font-weight: 800;
- letter-spacing: -0.042em;
- line-height: 0.98;
+ letter-spacing: -0.044em;
+ line-height: 0.94;
color: var(--text-strong);
margin: 18px 0 0;
text-align: left;
}
+
+/* The verdict keyword — vivid blue, the one word the headline turns on. */
+.hero-mark {
+ display: inline-block;
+ color: var(--warm);
+ white-space: nowrap;
+}
.hero-sub {
max-width: 52ch;
margin: 22px 0 0;
@@ -158,6 +208,7 @@
}
.vd-card {
+ position: relative;
border-radius: 18px;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--border-2);
@@ -229,28 +280,58 @@
color: var(--muted);
margin-top: 5px;
}
+/* Tabs are a real tablist now — horizontally scrollable, with a fade mask on
+ the overflow edge so it reads as "more to the right". */
.vd-tabs {
display: flex;
gap: 16px;
padding: 0 20px;
border-bottom: 1px solid var(--border);
- overflow: hidden;
- mask-image: linear-gradient(90deg, #000 86%, transparent);
+ overflow-x: auto;
+ scrollbar-width: none;
+ scroll-snap-type: x proximity;
+ mask-image: linear-gradient(90deg, #000 92%, transparent);
+}
+.vd-tabs::-webkit-scrollbar {
+ display: none;
}
.vd-tab {
- font-size: 0.82rem;
- font-weight: 600;
+ font: 600 0.82rem/1 var(--sans);
color: var(--muted);
- padding: 11px 0;
+ padding: 11px 1px 10px;
white-space: nowrap;
+ scroll-snap-align: start;
+ background: none;
+ border: none;
border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: color var(--dur-fast) var(--ease-out),
+ border-color var(--dur-fast) var(--ease-out);
+}
+.vd-tab:hover {
+ color: var(--text-strong);
}
.vd-tab.is-active {
color: var(--text-strong);
border-bottom-color: var(--warm);
}
+.vd-tab:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
+ border-radius: 4px;
+}
+
+/* The swappable card body. A fixed min-height keeps the card from jumping as
+ panels of different lengths cross-fade. */
+.vd-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px 20px 20px;
+ min-height: 178px;
+}
+
.vd-verdict {
- margin: 18px 20px 0;
display: flex;
align-items: center;
gap: 16px;
@@ -278,7 +359,6 @@
color: var(--sub);
}
.vd-bottom {
- margin: 12px 20px 0;
padding: 14px 16px;
border-radius: 12px;
background: var(--bg-2);
@@ -299,13 +379,13 @@
display: flex;
align-items: center;
gap: 10px;
- padding: 14px 20px 20px;
+ margin-top: auto;
}
.vd-lang-chip {
font: 700 11px/1 var(--mono);
- color: var(--warn);
- background: color-mix(in srgb, var(--warn) 16%, transparent);
- border: 1px solid color-mix(in srgb, var(--warn) 36%, transparent);
+ color: var(--accent);
+ background: var(--accent-weak);
+ border: 1px solid var(--accent-line);
border-radius: 6px;
padding: 6px 9px;
}
@@ -314,6 +394,189 @@
color: var(--faint);
}
+/* ── Verdict panel content (ELI5 / Technical / Use Cases / Health / etc.) ── */
+.vd-prose p {
+ font-size: 0.92rem;
+ color: var(--text);
+ margin: 0 0 10px;
+}
+.vd-prose p:last-child {
+ margin-bottom: 0;
+}
+.vd-prose code,
+.vd-list code {
+ font: 600 0.84em/1 var(--mono);
+ color: var(--accent);
+ background: var(--accent-weak);
+ border-radius: 4px;
+ padding: 1px 5px;
+}
+.vd-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.vd-list li {
+ position: relative;
+ padding-left: 22px;
+ font-size: 0.9rem;
+ color: var(--sub);
+}
+.vd-list li::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0.5em;
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
+ background: var(--warm);
+ transform: rotate(45deg);
+}
+.vd-list strong {
+ color: var(--text-strong);
+ font-weight: 700;
+}
+.vd-cases {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.vd-case {
+ font: 600 0.84rem/1 var(--sans);
+ color: var(--text);
+ background: var(--surface);
+ border: 1px solid var(--border-2);
+ border-radius: 999px;
+ padding: 9px 14px;
+}
+.vd-bars {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.vd-bar {
+ display: grid;
+ grid-template-columns: 9.5ch 1fr 2.5ch;
+ align-items: center;
+ gap: 12px;
+}
+.vd-bar-k {
+ font: 600 0.8rem/1 var(--sans);
+ color: var(--sub);
+}
+.vd-bar-track {
+ height: 7px;
+ border-radius: 999px;
+ background: var(--bg-2);
+ border: 1px solid var(--border);
+ overflow: hidden;
+}
+.vd-bar-fill {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--warm-2), var(--warm));
+}
+.vd-bar-v {
+ font: 700 0.82rem/1 var(--mono);
+ color: var(--text-strong);
+ text-align: right;
+}
+.vd-clean {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 16px;
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--ok) 9%, var(--bg-2));
+ border: 1px solid color-mix(in srgb, var(--ok) 32%, transparent);
+}
+.vd-clean-mark {
+ display: inline-grid;
+ place-items: center;
+ width: 36px;
+ height: 36px;
+ flex-shrink: 0;
+ border-radius: 50%;
+ color: var(--ok);
+ background: color-mix(in srgb, var(--ok) 16%, transparent);
+}
+.vd-clean-k {
+ font: 700 0.95rem/1.2 var(--sans);
+ color: var(--text-strong);
+ margin-bottom: 4px;
+}
+.vd-clean p {
+ font-size: 0.88rem;
+ color: var(--sub);
+}
+.vd-stack {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 11px;
+}
+.vd-stack > div {
+ display: grid;
+ grid-template-columns: 11ch 1fr;
+ gap: 14px;
+ padding-bottom: 11px;
+ border-bottom: 1px solid var(--border);
+}
+.vd-stack > div:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+.vd-stack dt {
+ font: 700 10px/1.5 var(--mono);
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--muted);
+}
+.vd-stack dd {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--text);
+}
+
+/* Panel crossfade on tab switch + the verdict stamp's heavy slam on scroll-in.
+ Both gated — reduced-motion visitors get an instant swap and a static stamp. */
+@media (prefers-reduced-motion: no-preference) {
+ .vd-panel {
+ animation: vdPanelIn 260ms var(--ease-out);
+ }
+ .vd-panel.is-in .vd-stamp {
+ animation: vdStampSlam 460ms var(--ease-stamp) both;
+ }
+}
+@keyframes vdPanelIn {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+@keyframes vdStampSlam {
+ 0% {
+ opacity: 0;
+ transform: rotate(11deg) scale(2.3);
+ }
+ 68% {
+ opacity: 0.9;
+ }
+ 100% {
+ opacity: 0.9;
+ transform: rotate(-7deg) scale(1);
+ }
+}
+
/* ── Feature bento ───────────────────────────────────────────────────── */
.bento {
margin-top: 32px;
@@ -322,6 +585,8 @@
gap: 14px;
}
.feat {
+ position: relative;
+ overflow: hidden;
grid-column: span 1;
border-radius: 16px;
padding: 22px 20px;
@@ -330,11 +595,25 @@
transition: transform var(--dur) var(--ease-out), border-color var(--dur) var(--ease-out),
box-shadow var(--dur) var(--ease-out);
}
+/* Accent edge-light — a brass hairline that draws across the top on hover. */
+.feat::after {
+ content: '';
+ position: absolute;
+ inset: 0 0 auto 0;
+ height: 2px;
+ background: var(--grad-soft);
+ transform: scaleX(0);
+ transform-origin: left;
+ transition: transform var(--dur-slow) var(--ease-out);
+}
.feat:hover {
- transform: translateY(-4px);
- border-color: var(--border-2);
+ transform: translateY(-5px);
+ border-color: var(--accent-line);
box-shadow: var(--shadow-md);
}
+.feat:hover::after {
+ transform: scaleX(1);
+}
.feat-wide {
grid-column: span 2;
background: radial-gradient(420px 200px at 100% 0%, var(--accent-weak), transparent 62%),
@@ -349,15 +628,26 @@
color: var(--accent);
background: var(--accent-weak);
border: 1px solid var(--accent-line);
+ transition: transform var(--dur) var(--ease-spring), background var(--dur) var(--ease-out),
+ border-color var(--dur) var(--ease-out);
+}
+/* The lens tilts and lifts as the card is inspected. */
+.feat:hover .feat-ic {
+ transform: translateY(-2px) scale(1.07) rotate(-4deg);
+ background: var(--accent-weak);
+ border-color: var(--accent);
+}
+.feat-wide:hover .feat-ic {
+ border-color: var(--pop);
}
.feat-ic .icon {
display: block;
}
-/* The wide "hero" feature tiles take the warm accent — the complementary pop. */
+/* The wide "hero" feature tiles take the hot-coral pop — the complementary jolt. */
.feat-wide .feat-ic {
- color: var(--warm);
- background: var(--warm-weak);
- border-color: var(--warm-line);
+ color: var(--pop);
+ background: var(--pop-weak);
+ border-color: var(--pop-line);
}
/* Line icons are drawn in by GSAP on scroll (pathLength=1); fully drawn by
default so they're visible without JS / under reduced-motion. */
diff --git a/website/app/(home)/styles/shell.css b/website/app/(home)/styles/shell.css
index 111e06e..f47ec49 100644
--- a/website/app/(home)/styles/shell.css
+++ b/website/app/(home)/styles/shell.css
@@ -53,7 +53,7 @@
inset: 0;
z-index: 60;
pointer-events: none;
- opacity: 0.04;
+ opacity: 0.02;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 180px 180px;
}
@@ -332,8 +332,8 @@
width: 7px;
height: 7px;
border-radius: 50%;
- background: var(--accent);
- box-shadow: 0 0 10px var(--accent);
+ background: var(--pop);
+ box-shadow: 0 0 10px var(--pop);
}
.site-root .section-title {
font-size: clamp(1.7rem, 1.1rem + 1.8vw, 2.6rem);
diff --git a/website/app/global.css b/website/app/global.css
index 4f55598..65e44c5 100644
--- a/website/app/global.css
+++ b/website/app/global.css
@@ -5,10 +5,11 @@
@source '../components';
/* ════════════════════════════════════════════════════════════════════════
- "The Case File" — a detective/inspector identity for RepoLens.
- Leads LIGHT (warm manila + ink), with an amber/brass lens-glint accent and a
- detective red for risk. Dark survives as a warm "night stakeout" (.dark), not
- a cold terminal. next-themes flips .dark; tokens drive marketing + docs.
+ RepoLens — "Inspector" identity, bright & alive.
+ Leads LIGHT on a clean cool near-white (no warmth — brown reads cheap), with
+ near-black ink, a VIVID blue lead, a cyan scanner glint, and a hot-coral pop
+ for life. Green = FIT, red = RISK. Dark is a cool slate complement, not a
+ warm noir and not an AI terminal. next-themes flips .dark; tokens drive all.
──────────────────────────────────────────────────────────────────────── */
:root {
@@ -25,120 +26,158 @@
/* the stamp's heavy "slam" */
--ease-stamp: cubic-bezier(0.5, 1.8, 0.4, 0.95);
- /* Amber/brass lens-glint accent + detective red. One ramp tuned for paper;
- .dark brightens it for the night-stakeout surfaces. */
- --accent: #9e530e;
- --accent-2: #934a0c;
- --accent-3: #d9822b;
- --grad: linear-gradient(120deg, #d9822b 0%, #9e530e 60%, #934a0c 120%);
- --grad-soft: linear-gradient(120deg, #d9822b, #9e530e);
- --accent-glow: rgba(179, 94, 16, 0.35);
- --accent-weak: rgba(179, 94, 16, 0.1);
- --accent-line: rgba(179, 94, 16, 0.28);
+ /* Vivid blue lead + cyan scanner glint + hot-coral pop. Green=FIT, red=RISK.
+ .dark recasts these brighter on cool slate. */
+ --accent: #2563ff;
+ --accent-2: #1d4ed8;
+ --accent-3: #06b6d4;
+ --glint: #06b6d4;
+ --grad: linear-gradient(120deg, #06b6d4 0%, #2563ff 58%, #1d4ed8 120%);
+ --grad-soft: linear-gradient(120deg, #3b82f6, #2563ff);
+ --accent-glow: rgba(37, 99, 255, 0.3);
+ --accent-weak: rgba(37, 99, 255, 0.08);
+ --accent-line: rgba(37, 99, 255, 0.22);
- /* warm = the amber CTA; red = the RISKY stamp / red flags */
- --warm: #c2691c;
- --warm-2: #d9822b;
- --warm-ink: #2a1605;
- --warm-glow: rgba(194, 105, 28, 0.4);
- --warm-weak: rgba(194, 105, 28, 0.12);
- --warm-line: rgba(194, 105, 28, 0.34);
- --red: #c0392b;
- --red-weak: rgba(192, 57, 43, 0.12);
+ /* warm = the primary CTA (vivid→deep blue, white text); pop = hot-coral life */
+ --warm: #2563ff;
+ --warm-2: #1d4ed8;
+ --warm-ink: #ffffff;
+ --warm-glow: rgba(37, 99, 255, 0.38);
+ --warm-weak: rgba(37, 99, 255, 0.1);
+ --warm-line: rgba(37, 99, 255, 0.26);
+ --pop: #ff5a5f;
+ --pop-weak: rgba(255, 90, 95, 0.12);
+ --pop-line: rgba(255, 90, 95, 0.34);
+ --red: #e5484d;
+ --red-weak: rgba(229, 72, 77, 0.12);
- /* Case File — warm manila paper, sepia ink */
- --bg: #f4ead4;
- --bg-2: #ecdfc2;
- --surface: #fbf5e7;
- --surface-2: #f5ecd6;
- --border: #ddccab;
- --border-2: #c8b285;
- --text: #2a2114;
- --text-strong: #17110a;
- --sub: #5d5034;
- --muted: #756544;
- --faint: #a89571;
- --ok: #2c7631;
- --info: #1d6fa3;
- --warn: #b4690a;
- --bad: #c0392b;
- --shadow-lg: 0 26px 70px -34px rgba(60, 42, 16, 0.38);
- --shadow-md: 0 16px 38px -22px rgba(60, 42, 16, 0.3);
- --paper-grain: 0.5;
+ /* Clean paper — cool near-white, ink near-black */
+ --bg: #f7f9fd;
+ --bg-2: #eef2f9;
+ --surface: #ffffff;
+ --surface-2: #f9fbfe;
+ --border: #e3e8f2;
+ --border-2: #ccd6e6;
+ --text: #1b2230;
+ --text-strong: #0b0e14;
+ --sub: #495264;
+ --muted: #6b7384;
+ --faint: #97a0b2;
+ --ok: #16a34a;
+ --info: #2563ff;
+ --warn: #06b6d4;
+ --bad: #e5484d;
+ --shadow-lg: 0 28px 64px -30px rgba(15, 30, 70, 0.3);
+ --shadow-md: 0 16px 38px -22px rgba(15, 30, 70, 0.22);
+ --paper-grain: 0;
- /* fumadocs remap → Case File light */
- --color-fd-background: #f4ead4;
- --color-fd-foreground: #2a2114;
- --color-fd-muted: #ecdfc2;
- --color-fd-muted-foreground: #6a5c3d;
- --color-fd-popover: #fbf5e7;
- --color-fd-popover-foreground: #17110a;
- --color-fd-card: #fbf5e7;
- --color-fd-card-foreground: #17110a;
- --color-fd-border: #ddccab;
- --color-fd-primary: #9e530e;
- --color-fd-primary-foreground: #fbf5e7;
- --color-fd-secondary: #efe3ca;
- --color-fd-secondary-foreground: #2a2114;
- --color-fd-accent: #ecdfc2;
- --color-fd-accent-foreground: #17110a;
- --color-fd-ring: #9e530e;
+ /* fumadocs remap → bright light */
+ --color-fd-background: #f7f9fd;
+ --color-fd-foreground: #1b2230;
+ --color-fd-muted: #eef2f9;
+ --color-fd-muted-foreground: #6b7384;
+ --color-fd-popover: #ffffff;
+ --color-fd-popover-foreground: #0b0e14;
+ --color-fd-card: #ffffff;
+ --color-fd-card-foreground: #0b0e14;
+ --color-fd-border: #e3e8f2;
+ --color-fd-primary: #2563ff;
+ --color-fd-primary-foreground: #ffffff;
+ --color-fd-secondary: #eef2f9;
+ --color-fd-secondary-foreground: #1b2230;
+ --color-fd-accent: #e8eeff;
+ --color-fd-accent-foreground: #0b0e14;
+ --color-fd-ring: #2563ff;
}
.dark {
- /* Night Stakeout — warm noir, not a cold terminal */
- --bg: #1b1610;
- --bg-2: #221b12;
- --surface: #2a2116;
- --surface-2: #32281a;
- --border: #43361f;
- --border-2: #5e4b2c;
- --text: #ece1cd;
- --text-strong: #fff8ec;
- --sub: #c3b094;
- --muted: #9c8a6a;
- --faint: #6f6045;
+ /* Cool slate complement — clean dark, not a warm noir, not an AI terminal */
+ --bg: #0b0f17;
+ --bg-2: #121826;
+ --surface: #151c2b;
+ --surface-2: #1b2336;
+ --border: #283146;
+ --border-2: #3a4358;
+ --text: #e6ebf3;
+ --text-strong: #ffffff;
+ --sub: #aeb6c4;
+ --muted: #7f8799;
+ --faint: #596173;
- /* brighter amber + red read better on the warm-dark surfaces */
- --accent: #e0934a;
- --accent-2: #cf7a2a;
- --accent-3: #f0aa63;
- --grad: linear-gradient(120deg, #f0aa63 0%, #e0934a 60%, #cf7a2a 120%);
- --grad-soft: linear-gradient(120deg, #f0aa63, #e0934a);
- --accent-glow: rgba(224, 147, 74, 0.45);
- --accent-weak: rgba(224, 147, 74, 0.14);
- --accent-line: rgba(224, 147, 74, 0.32);
- --warm: #e0934a;
- --warm-2: #f0aa63;
- --warm-ink: #2a1605;
- --warm-glow: rgba(224, 147, 74, 0.45);
- --warm-weak: rgba(224, 147, 74, 0.16);
- --warm-line: rgba(224, 147, 74, 0.36);
- --red: #e06a5c;
- --red-weak: rgba(224, 106, 92, 0.16);
- --ok: #5cc05f;
- --info: #56b6e8;
- --warn: #e0a341;
- --bad: #e06a5c;
- --shadow-lg: 0 26px 70px -34px rgba(0, 0, 0, 0.72);
- --shadow-md: 0 16px 44px -26px rgba(0, 0, 0, 0.66);
- --paper-grain: 0.7;
+ /* brighter blue + cyan + coral on the slate surfaces */
+ --accent: #5a8cff;
+ --accent-2: #3b6ef0;
+ --accent-3: #34d6e8;
+ --glint: #34d6e8;
+ --grad: linear-gradient(120deg, #34d6e8 0%, #5a8cff 58%, #3b6ef0 120%);
+ --grad-soft: linear-gradient(120deg, #6f9bff, #5a8cff);
+ --accent-glow: rgba(90, 140, 255, 0.45);
+ --accent-weak: rgba(90, 140, 255, 0.16);
+ --accent-line: rgba(90, 140, 255, 0.32);
+ --warm: #5a8cff;
+ --warm-2: #4f86ff;
+ --warm-ink: #06122a;
+ --warm-glow: rgba(90, 140, 255, 0.5);
+ --warm-weak: rgba(90, 140, 255, 0.18);
+ --warm-line: rgba(90, 140, 255, 0.36);
+ --pop: #ff6b70;
+ --pop-weak: rgba(255, 107, 112, 0.16);
+ --pop-line: rgba(255, 107, 112, 0.4);
+ --red: #ff6b6b;
+ --red-weak: rgba(255, 107, 107, 0.16);
+ --ok: #3ecf7e;
+ --info: #5a8cff;
+ --warn: #34d6e8;
+ --bad: #ff6b6b;
+ --shadow-lg: 0 28px 64px -30px rgba(0, 0, 0, 0.72);
+ --shadow-md: 0 16px 44px -26px rgba(0, 0, 0, 0.62);
+ --paper-grain: 0;
- /* fumadocs remap → night stakeout */
- --color-fd-background: #1b1610;
- --color-fd-foreground: #ece1cd;
- --color-fd-muted: #2a2116;
- --color-fd-muted-foreground: #c3b094;
- --color-fd-popover: #221b12;
- --color-fd-popover-foreground: #ece1cd;
- --color-fd-card: #2a2116;
- --color-fd-card-foreground: #ece1cd;
- --color-fd-border: #43361f;
- --color-fd-primary: #e0934a;
- --color-fd-primary-foreground: #1b1610;
- --color-fd-secondary: #32281a;
- --color-fd-secondary-foreground: #ece1cd;
- --color-fd-accent: #322619;
- --color-fd-accent-foreground: #fff8ec;
- --color-fd-ring: #e0934a;
+ /* fumadocs remap → cool dark */
+ --color-fd-background: #0b0f17;
+ --color-fd-foreground: #e6ebf3;
+ --color-fd-muted: #151c2b;
+ --color-fd-muted-foreground: #aeb6c4;
+ --color-fd-popover: #121826;
+ --color-fd-popover-foreground: #e6ebf3;
+ --color-fd-card: #151c2b;
+ --color-fd-card-foreground: #e6ebf3;
+ --color-fd-border: #283146;
+ --color-fd-primary: #5a8cff;
+ --color-fd-primary-foreground: #06122a;
+ --color-fd-secondary: #1b2336;
+ --color-fd-secondary-foreground: #e6ebf3;
+ --color-fd-accent: #1c2740;
+ --color-fd-accent-foreground: #ffffff;
+ --color-fd-ring: #5a8cff;
+}
+
+/* ════════════════════════════════════════════════════════════════════════
+ Theme scan — the View Transitions API "develops" the new theme down the page
+ top→bottom, with a glowing cyan scan edge leading the front. Triggered only
+ by the theme toggle (ThemeToggle.tsx); falls back to an instant flip where
+ the API or motion preference says no.
+ ──────────────────────────────────────────────────────────────────────── */
+@media (prefers-reduced-motion: no-preference) {
+ ::view-transition-old(root),
+ ::view-transition-new(root) {
+ /* drop the default cross-fade — we own the reveal */
+ animation: none;
+ mix-blend-mode: normal;
+ }
+ /* old theme stays put underneath; the new theme is clipped in from the top */
+ ::view-transition-new(root) {
+ /* steady, near-even travel so you actually watch the line sweep down */
+ animation: theme-scan 1000ms cubic-bezier(0.45, 0.05, 0.55, 0.95);
+ filter: drop-shadow(0 8px 16px rgba(45, 212, 230, 0.7))
+ drop-shadow(0 2px 4px rgba(45, 212, 230, 0.6));
+ }
+}
+@keyframes theme-scan {
+ from {
+ clip-path: inset(0 0 100% 0);
+ }
+ to {
+ clip-path: inset(0 0 0 0);
+ }
}
diff --git a/website/app/icon.svg b/website/app/icon.svg
index a6ac4a8..f982ffc 100644
--- a/website/app/icon.svg
+++ b/website/app/icon.svg
@@ -1,6 +1,6 @@
-
-
-
-
+
+
+
+
diff --git a/website/components/home/Hero.tsx b/website/components/home/Hero.tsx
index 9e3fa61..f3d4643 100644
--- a/website/components/home/Hero.tsx
+++ b/website/components/home/Hero.tsx
@@ -8,13 +8,13 @@ export function Hero() {
- Chrome Extension · Manifest V3 · v3.0
+ Case No. RL-3.0 · Chrome · Manifest V3
Read code before
- you trust it.
+ you trust it.
diff --git a/website/components/home/SiteMotion.tsx b/website/components/home/SiteMotion.tsx
index 6c65e58..55dc816 100644
--- a/website/components/home/SiteMotion.tsx
+++ b/website/components/home/SiteMotion.tsx
@@ -56,6 +56,27 @@ export function SiteMotion() {
}),
});
+ // 2b) Section titles wipe in left→right — a bolder editorial reveal
+ // layered over the section fade. Bento is excluded (own choreography).
+ const titles = gsap.utils.toArray(
+ '.reveal:not(.feature-section) .section-title',
+ );
+ ScrollTrigger.batch(titles, {
+ start: 'top 88%',
+ onEnter: (batch) =>
+ gsap.fromTo(
+ batch,
+ { clipPath: 'inset(0 100% 0 0)' },
+ {
+ clipPath: 'inset(0 0% 0 0)',
+ duration: 0.7,
+ ease: 'power3.inOut',
+ stagger: 0.12,
+ overwrite: 'auto',
+ },
+ ),
+ });
+
// 3) Feature bento — header in, then tiles stagger.
const bento = document.querySelector('.feature-section');
if (bento) {
diff --git a/website/components/home/VerdictDemo.tsx b/website/components/home/VerdictDemo.tsx
index e539544..c6e2082 100644
--- a/website/components/home/VerdictDemo.tsx
+++ b/website/components/home/VerdictDemo.tsx
@@ -1,10 +1,214 @@
-const TABS = ['Verdict', 'ELI5', 'Technical', 'Use Cases', 'Health', 'Red Flags', 'Tech Stack'];
+'use client';
+
+import { useEffect, useRef, useState, type ReactNode } from 'react';
/**
- * A faithful, fully themeable recreation of the real scan output — so it
- * re-skins with the toggle instead of being a screenshot that breaks in latte.
+ * A faithful, fully themeable recreation of the real scan output — now a LIVE
+ * demo: the tabs are a real tablist (click + arrow-key) that crossfades the
+ * card body, and the "Strong fit" verdict stamp slams in the first time the
+ * card scrolls into view. Everything re-skins with the theme toggle instead of
+ * being a screenshot that breaks in another palette. Motion is CSS-only and
+ * gated behind `prefers-reduced-motion` in home.css.
*/
+
+type Tab = { id: string; label: string; panel: ReactNode };
+
+const HealthBars = () => {
+ const rows: Array<[string, number]> = [
+ ['Maintenance', 82],
+ ['Activity', 71],
+ ['Documentation', 95],
+ ['Adoption', 99],
+ ];
+ return (
+
+ {rows.map(([label, v]) => (
+
+ {label}
+
+
+
+ {v}
+
+ ))}
+
+ );
+};
+
+const TABS: Tab[] = [
+ {
+ id: 'verdict',
+ label: 'Verdict',
+ panel: (
+ <>
+
+ Strong fit
+ Health 88 · 0 flags · 3 pros / 1 cons
+
+
+
AI bottom line
+
The default choice for Node HTTP services; boring in the best way.
+
+
+ JavaScript 99%
+ TypeScript · Shell
+
+ >
+ ),
+ },
+ {
+ id: 'eli5',
+ label: 'ELI5',
+ panel: (
+
+
+ Express is the “hello world” of Node web servers. You hand it routes — “when someone
+ visits /users, run this function” — and it handles the plumbing in between.
+
+
+ Minimal on purpose: no database, no folder structure, no opinions. Just the
+ request-in / response-out basics, and a way to stack little functions in the middle.
+
+
+ ),
+ },
+ {
+ id: 'technical',
+ label: 'Technical',
+ panel: (
+
+
+ Middleware pipeline — every request flows through a stack of
+ (req, res, next) functions you compose yourself.
+
+
+ Thin routing layer over Node’s native http module; ~16k LOC,
+ no framework runtime underneath.
+
+
+ Unbundled — no build step, composes directly with the npm ecosystem
+ rather than replacing it.
+
+
+ ),
+ },
+ {
+ id: 'usecases',
+ label: 'Use Cases',
+ panel: (
+
+ REST / JSON APIs
+ Server-rendered apps
+ Microservices
+ A base other frameworks build on
+ Quick prototypes
+
+ ),
+ },
+ {
+ id: 'health',
+ label: 'Health',
+ panel: ,
+ },
+ {
+ id: 'redflags',
+ label: 'Red Flags',
+ panel: (
+
+
+
+
+
+
+
+
No red flags
+
Permissive MIT license, active maintenance, no critical advisories in the manifest.
+
+
+ ),
+ },
+ {
+ id: 'techstack',
+ label: 'Tech Stack',
+ panel: (
+
+
+
Runtime
+ Node.js
+
+
+
Language
+ JavaScript · TypeScript types shipped separately
+
+
+
Notable deps
+ finalhandler · qs · send · serve-static
+
+
+ ),
+ },
+];
+
export function VerdictDemo() {
+ const [active, setActive] = useState(0);
+ const [inView, setInView] = useState(false);
+ const cardRef = useRef(null);
+ const tabRefs = useRef>([]);
+
+ // Slam the stamp in the first time the card meets the viewport.
+ useEffect(() => {
+ const el = cardRef.current;
+ if (!el) return;
+ const obs = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setInView(true);
+ obs.disconnect();
+ }
+ },
+ { threshold: 0.4 },
+ );
+ obs.observe(el);
+ return () => obs.disconnect();
+ }, []);
+
+ const selectTab = (next: number) => {
+ const idx = (next + TABS.length) % TABS.length;
+ setActive(idx);
+ tabRefs.current[idx]?.focus();
+ };
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'ArrowRight':
+ e.preventDefault();
+ selectTab(active + 1);
+ break;
+ case 'ArrowLeft':
+ e.preventDefault();
+ selectTab(active - 1);
+ break;
+ case 'Home':
+ e.preventDefault();
+ selectTab(0);
+ break;
+ case 'End':
+ e.preventDefault();
+ selectTab(TABS.length - 1);
+ break;
+ default:
+ break;
+ }
+ };
+
+ const current = TABS[active];
+
return (
@@ -34,7 +238,7 @@ export function VerdictDemo() {
-
+
@@ -58,27 +262,36 @@ export function VerdictDemo() {
-
+
{TABS.map((t, idx) => (
-
- {t}
-
+ {
+ tabRefs.current[idx] = el;
+ }}
+ className={`vd-tab${idx === active ? ' is-active' : ''}`}
+ onClick={() => setActive(idx)}
+ >
+ {t.label}
+
))}
-
- Strong fit
- Health 88 · 0 flags · 3 pros / 1 cons
-
-
-
-
AI bottom line
-
The default choice for Node HTTP services; boring in the best way.
-
-
-
-
JavaScript 99%
-
TypeScript · Shell
+
+ {current.panel}
diff --git a/website/components/site/ThemeToggle.tsx b/website/components/site/ThemeToggle.tsx
index 96dcc4c..1f5ab71 100644
--- a/website/components/site/ThemeToggle.tsx
+++ b/website/components/site/ThemeToggle.tsx
@@ -1,8 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
import { useTheme } from 'next-themes';
+type ViewTransitionDocument = Document & {
+ startViewTransition?: (callback: () => void) => unknown;
+};
+
/**
* Sun⇄moon toggle that *morphs*: in dark mode a cutout circle carves the orb
* into a crescent and the rays retract; switching to light slides the cutout
@@ -18,15 +23,28 @@ export function ThemeToggle() {
useEffect(() => setMounted(true), []);
const isLight = mounted && resolvedTheme !== 'dark';
- const label = mounted
- ? `Switch to ${isLight ? 'midnight (dark)' : 'latte (light)'}`
- : 'Toggle theme';
+ const label = mounted ? `Switch to ${isLight ? 'dark' : 'light'} mode` : 'Toggle theme';
+
+ // A scanline "develops" the page into the new theme top→bottom via the View
+ // Transitions API (see ::view-transition-new(root) in global.css). flushSync
+ // makes next-themes apply the class synchronously so the snapshot captures the
+ // new theme. Instant flip where the API is missing or motion is reduced.
+ const runToggle = () => {
+ const next = isLight ? 'dark' : 'light';
+ const doc = document as ViewTransitionDocument;
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ if (reduce || typeof doc.startViewTransition !== 'function') {
+ setTheme(next);
+ return;
+ }
+ doc.startViewTransition(() => flushSync(() => setTheme(next)));
+ };
return (
setTheme(isLight ? 'dark' : 'light')}
+ onClick={runToggle}
aria-label={label}
title={label}
>
From baf1749443a059b9dcca5219ff6ee487acf70cd6 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:01:20 -0700
Subject: [PATCH 02/11] feat(site): holographic verdict card + smoother
theme-scan
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Verdict card now lifts and tilts in 3D toward the cursor, with an iridescent
cyan→blue→silver→coral foil (hard-light blend, vivid on both light and dark)
and a glare that tracks the pointer. Desktop + motion-OK only (pointer:fine);
touch and reduced-motion get the clean static card. Pure CSS+JS, ~zero weight.
- Theme-scan reveal extracted to lib/themeScan.ts (shared by the toggle) and
de-lagged: dropped the full-page animated drop-shadow (the jank source) and
tightened to 720ms — the clip-path wipe alone is GPU-cheap.
---
website/app/(home)/styles/home.css | 58 +++++++++++++++++++++++++
website/app/global.css | 8 ++--
website/components/home/VerdictDemo.tsx | 45 ++++++++++++++++++-
website/components/site/ThemeToggle.tsx | 17 +-------
website/lib/themeScan.ts | 23 ++++++++++
5 files changed, 131 insertions(+), 20 deletions(-)
create mode 100644 website/lib/themeScan.ts
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index 1fdf943..9b95926 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -215,6 +215,64 @@
box-shadow: var(--shadow-lg);
overflow: hidden;
}
+
+/* ── Holographic verdict card — tilts toward the cursor with an iridescent foil
+ sweep + a glare that tracks the pointer. JS adds .is-holo on desktop only
+ (pointer:fine, motion OK). Content stays legible: the foil is a blended
+ sheen, intensifying on hover, not a wash. ──────────────────────────────── */
+.vd-card-stage {
+ perspective: 1300px;
+}
+.vd-card.is-holo {
+ transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
+ transition: transform 0.2s var(--ease-out);
+ will-change: transform;
+}
+.vd-foil,
+.vd-glare {
+ position: absolute;
+ inset: 0;
+ z-index: 6;
+ border-radius: inherit;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.3s var(--ease-out);
+}
+/* The foil: a broad cyan→blue→silver→coral→lavender band that drifts with the
+ pointer (matches the reference holographic sheet). Faint at rest, vivid on tilt. */
+.vd-card.is-holo .vd-foil {
+ opacity: calc(0.14 + var(--active, 0) * 0.54);
+ mix-blend-mode: hard-light;
+ background: linear-gradient(
+ 115deg,
+ transparent 6%,
+ rgba(6, 182, 212, 0.55) 20%,
+ rgba(37, 99, 255, 0.45) 33%,
+ rgba(255, 255, 255, 0.62) 45%,
+ rgba(255, 90, 95, 0.46) 58%,
+ rgba(167, 139, 250, 0.5) 72%,
+ transparent 90%
+ );
+ background-size: 200% 200%;
+ background-position: calc(var(--px, 50%) * 0.7 + 15%) calc(var(--py, 50%) * 0.7 + 15%);
+}
+/* The glare: a soft white spotlight that sits under the cursor. */
+.vd-card.is-holo .vd-glare {
+ opacity: calc(var(--active, 0) * 0.6);
+ mix-blend-mode: soft-light;
+ background: radial-gradient(
+ circle at var(--px, 50%) var(--py, 50%),
+ rgba(255, 255, 255, 0.75),
+ rgba(255, 255, 255, 0) 42%
+ );
+}
+@media (prefers-reduced-motion: reduce) {
+ .vd-card.is-holo {
+ transform: none;
+ transition: none;
+ }
+}
+
.vd-card-bar {
display: flex;
align-items: center;
diff --git a/website/app/global.css b/website/app/global.css
index 65e44c5..d0d4a39 100644
--- a/website/app/global.css
+++ b/website/app/global.css
@@ -167,10 +167,10 @@
}
/* old theme stays put underneath; the new theme is clipped in from the top */
::view-transition-new(root) {
- /* steady, near-even travel so you actually watch the line sweep down */
- animation: theme-scan 1000ms cubic-bezier(0.45, 0.05, 0.55, 0.95);
- filter: drop-shadow(0 8px 16px rgba(45, 212, 230, 0.7))
- drop-shadow(0 2px 4px rgba(45, 212, 230, 0.6));
+ /* Steady, near-even travel so you watch the line sweep down. No drop-shadow
+ glow here on purpose — a blurred shadow of a full-page snapshot animating
+ every frame is the main source of jank. The clip wipe alone is GPU-cheap. */
+ animation: theme-scan 720ms cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
}
@keyframes theme-scan {
diff --git a/website/components/home/VerdictDemo.tsx b/website/components/home/VerdictDemo.tsx
index c6e2082..ba171ae 100644
--- a/website/components/home/VerdictDemo.tsx
+++ b/website/components/home/VerdictDemo.tsx
@@ -207,6 +207,39 @@ export function VerdictDemo() {
}
};
+ const [interactive, setInteractive] = useState(false);
+ useEffect(() => {
+ const fine = window.matchMedia('(pointer: fine)').matches;
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ setInteractive(fine && !reduce);
+ }, []);
+
+ // Holographic tilt — write CSS vars straight to the card so a pointer move
+ // never triggers a React re-render. Desktop + non-reduced-motion only.
+ const onHoloMove = (e: React.PointerEvent) => {
+ const el = cardRef.current;
+ if (!interactive || !el) return;
+ const r = el.getBoundingClientRect();
+ const px = (e.clientX - r.left) / r.width;
+ const py = (e.clientY - r.top) / r.height;
+ const MAX = 9;
+ el.style.setProperty('--rx', `${(-(py - 0.5) * 2 * MAX).toFixed(2)}deg`);
+ el.style.setProperty('--ry', `${((px - 0.5) * 2 * MAX).toFixed(2)}deg`);
+ el.style.setProperty('--px', `${(px * 100).toFixed(1)}%`);
+ el.style.setProperty('--py', `${(py * 100).toFixed(1)}%`);
+ el.style.setProperty('--active', '1');
+ };
+
+ const onHoloLeave = () => {
+ const el = cardRef.current;
+ if (!el) return;
+ el.style.setProperty('--rx', '0deg');
+ el.style.setProperty('--ry', '0deg');
+ el.style.setProperty('--px', '50%');
+ el.style.setProperty('--py', '50%');
+ el.style.setProperty('--active', '0');
+ };
+
const current = TABS[active];
return (
@@ -238,7 +271,13 @@ export function VerdictDemo() {
-
+
+
@@ -293,6 +332,10 @@ export function VerdictDemo() {
>
{current.panel}
+
+
+
+
diff --git a/website/components/site/ThemeToggle.tsx b/website/components/site/ThemeToggle.tsx
index 1f5ab71..56f651b 100644
--- a/website/components/site/ThemeToggle.tsx
+++ b/website/components/site/ThemeToggle.tsx
@@ -1,12 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
-import { flushSync } from 'react-dom';
import { useTheme } from 'next-themes';
-
-type ViewTransitionDocument = Document & {
- startViewTransition?: (callback: () => void) => unknown;
-};
+import { scanToTheme } from '@/lib/themeScan';
/**
* Sun⇄moon toggle that *morphs*: in dark mode a cutout circle carves the orb
@@ -29,16 +25,7 @@ export function ThemeToggle() {
// Transitions API (see ::view-transition-new(root) in global.css). flushSync
// makes next-themes apply the class synchronously so the snapshot captures the
// new theme. Instant flip where the API is missing or motion is reduced.
- const runToggle = () => {
- const next = isLight ? 'dark' : 'light';
- const doc = document as ViewTransitionDocument;
- const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
- if (reduce || typeof doc.startViewTransition !== 'function') {
- setTheme(next);
- return;
- }
- doc.startViewTransition(() => flushSync(() => setTheme(next)));
- };
+ const runToggle = () => scanToTheme(setTheme, isLight ? 'dark' : 'light');
return (
void) => unknown;
+};
+
+/**
+ * Flip the theme with the top→bottom "scan" reveal (see ::view-transition-new
+ * in global.css). `flushSync` makes next-themes apply the class synchronously
+ * so the View Transition snapshots the new theme. Falls back to an instant flip
+ * where the API is missing or motion is reduced.
+ *
+ * Shared by the manual toggle (ThemeToggle) and the scroll watcher (ScrollTheme).
+ */
+export function scanToTheme(setTheme: (theme: string) => void, next: string): void {
+ const doc = document as ViewTransitionDocument;
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ if (reduce || typeof doc.startViewTransition !== 'function') {
+ setTheme(next);
+ return;
+ }
+ doc.startViewTransition(() => flushSync(() => setTheme(next)));
+}
From e6e553ce595498281f6062db87ff234dd94d4451 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:17:49 -0700
Subject: [PATCH 03/11] =?UTF-8?q?feat(site):=20WebGL=203D=20hero=20lens=20?=
=?UTF-8?q?=E2=80=94=20Vee=20refracted=20through=20floating=20glass?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The hero mascot becomes a real WebGL scene on capable desktops: Vee floats
inside a clear refractive glass lens (transmission + chromatic aberration) with
a glowing cyan aperture rim, tilting toward the cursor with a slow idle drift.
Procedural studio lighting via Lightformers — no external HDR, so it keeps the
zero-external-calls promise.
- Lazy-loaded via next/dynamic (ssr:false): three/R3F/drei never touch the
initial bundle — home First Load JS 112 → 114 kB (+2 kB).
- Gated to pointer:fine + motion-OK + WebGL + ≥880px. Touch, reduced-motion,
no-WebGL, narrow, or while-loading all fall back to the existing clip/poster.
- Error boundary around the scene → poster fallback if WebGL drops.
- Adds three, @react-three/fiber, @react-three/drei (+@types/three).
---
website/app/(home)/styles/home.css | 26 +
website/components/home/HeroLens3D.tsx | 104 ++++
website/components/home/HeroMascot.tsx | 100 +++-
website/package-lock.json | 639 ++++++++++++++++++++++++-
website/package.json | 6 +-
5 files changed, 839 insertions(+), 36 deletions(-)
create mode 100644 website/components/home/HeroLens3D.tsx
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index 9b95926..e003e4e 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -108,6 +108,32 @@
max-width: 34ch;
text-align: center;
}
+
+/* Floating 3D glass lens (replaces the framed clip on capable desktops). The
+ canvas is transparent; a soft radial glow gives the lens some atmosphere so
+ it doesn't read as floating in a void. */
+.hero-lens-3d {
+ position: relative;
+ width: clamp(300px, 38vw, 440px);
+ aspect-ratio: 1 / 1;
+ isolation: isolate;
+ cursor: grab;
+}
+.hero-lens-3d::before {
+ content: '';
+ position: absolute;
+ inset: 6%;
+ z-index: -1;
+ border-radius: 50%;
+ background: radial-gradient(circle at 50% 46%, var(--accent-glow), transparent 64%);
+ opacity: 0.5;
+ filter: blur(10px);
+}
+.hero-lens-3d canvas {
+ display: block;
+ width: 100% !important;
+ height: 100% !important;
+}
.hero-title {
font-size: clamp(2.6rem, 0.9rem + 5.4vw, 5rem);
font-weight: 800;
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
new file mode 100644
index 0000000..37df62d
--- /dev/null
+++ b/website/components/home/HeroLens3D.tsx
@@ -0,0 +1,104 @@
+'use client';
+
+import { Suspense, useRef } from 'react';
+import { Canvas, useFrame } from '@react-three/fiber';
+import { Environment, Lightformer, MeshTransmissionMaterial, useTexture } from '@react-three/drei';
+import * as THREE from 'three';
+
+// Static assets aren't auto-prefixed with the GitHub Pages basePath, so prefix by hand.
+const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
+
+/**
+ * Vee on a disc directly behind the lens — sized to sit fully within the lens
+ * silhouette, so he's only ever seen *through* the glass (refracted), never as a
+ * bare rectangle. Swap the poster for a transparent cutout later for an even
+ * cleaner float.
+ */
+function VeeDisc() {
+ const tex = useTexture(`${BASE}/mascot-poster.jpg`);
+ tex.colorSpace = THREE.SRGBColorSpace;
+ return (
+
+
+
+
+ );
+}
+
+/** The glass lens + its glowing cyan aperture rim. Tilts toward the cursor and
+ * idles with a slow drift. */
+function Lens() {
+ const group = useRef(null);
+ useFrame((state, delta) => {
+ const g = group.current;
+ if (!g) return;
+ const t = state.clock.elapsedTime;
+ // cursor tilt + a slow idle sway
+ const targetY = state.pointer.x * 0.5 + Math.sin(t * 0.35) * 0.12;
+ const targetX = -state.pointer.y * 0.4 + Math.cos(t * 0.28) * 0.08;
+ const k = 1 - Math.pow(0.0015, delta); // frame-rate-independent damping
+ g.rotation.y = THREE.MathUtils.lerp(g.rotation.y, targetY, k);
+ g.rotation.x = THREE.MathUtils.lerp(g.rotation.x, targetX, k);
+ g.position.y = Math.sin(t * 0.6) * 0.04;
+ });
+
+ return (
+
+ {/* the lens: a flattened sphere → a thick magnifying disc */}
+
+
+
+
+ {/* glowing aperture rim — a ring around the lens edge, facing the viewer
+ (no rotation: TorusGeometry already lies in the camera-facing plane). */}
+
+
+
+
+
+ );
+}
+
+export default function HeroLens3D() {
+ return (
+
+
+
+
+
+
+
+ {/* Procedural studio env — NO external HDR (keeps zero external calls). */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/website/components/home/HeroMascot.tsx b/website/components/home/HeroMascot.tsx
index 2ed9697..5208a5e 100644
--- a/website/components/home/HeroMascot.tsx
+++ b/website/components/home/HeroMascot.tsx
@@ -1,9 +1,10 @@
'use client';
-import { useEffect, useState } from 'react';
+import { Component, useEffect, useState, type ReactNode } from 'react';
+import dynamic from 'next/dynamic';
-// Static assets live in public/ and are NOT auto-prefixed with the GitHub
-// Pages basePath the way next/link is — so we prefix the / src by hand.
+// Static assets live in public/ and are NOT auto-prefixed with the GitHub Pages
+// basePath the way next/link is — so we prefix the / src by hand.
const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
const CAPTIONS = [
@@ -14,18 +15,58 @@ const CAPTIONS = [
const ALT = 'Vee, the RepoLens mascot, peering through a lens';
+// The WebGL scene is heavy (three.js) — load it lazily and client-only so it
+// never touches the initial bundle or the server render.
+const HeroLens3D = dynamic(() => import('./HeroLens3D'), { ssr: false });
+
+/** If the 3D scene throws (WebGL context lost, chunk load fail, …) fall back to
+ * the poster rather than blanking the hero. */
+class LensBoundary extends Component<
+ { fallback: ReactNode; children: ReactNode },
+ { failed: boolean }
+> {
+ state = { failed: false };
+ static getDerivedStateFromError() {
+ return { failed: true };
+ }
+ render() {
+ return this.state.failed ? this.props.fallback : this.props.children;
+ }
+}
+
+function Poster() {
+ return (
+ // eslint-disable-next-line @next/next/no-img-element -- static export (output: export); next/image adds no value for this decorative poster
+
+ );
+}
+
/**
- * The hero mascot: an autoplaying, muted, looping clip of Vee in a framed
- * stage. Under reduced-motion we show the poster frame instead — no autoplay,
- * no loop — and the caption stops rotating.
+ * The hero mascot. On a capable desktop (fine pointer, motion OK, WebGL, wide
+ * enough) Vee appears inside a floating refractive glass lens that tracks the
+ * cursor. Everywhere else — touch, reduced-motion, no WebGL, narrow, or while
+ * the 3D chunk loads — the original framed clip / poster shows instead.
*/
export function HeroMascot() {
const [reduced, setReduced] = useState(false);
+ const [use3D, setUse3D] = useState(false);
const [i, setI] = useState(0);
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
setReduced(reduce);
+
+ const fine = window.matchMedia('(pointer: fine)').matches;
+ const wide = window.matchMedia('(min-width: 880px)').matches;
+ let webgl = false;
+ try {
+ const c = document.createElement('canvas');
+ webgl = !!(c.getContext('webgl2') || c.getContext('webgl'));
+ } catch {
+ webgl = false;
+ }
+ setUse3D(!reduce && fine && wide && webgl);
+
if (reduce) return;
const id = setInterval(() => setI((n) => (n + 1) % CAPTIONS.length), 3400);
return () => clearInterval(id);
@@ -33,26 +74,33 @@ export function HeroMascot() {
return (
-
- {reduced ? (
- // eslint-disable-next-line @next/next/no-img-element -- static export (output: export) with unoptimized images; next/image adds no value for this decorative poster
-
- ) : (
-
-
-
- )}
-
+ {use3D ? (
+
}>
+
+
+
+ ) : (
+
+ {reduced ? (
+
+ ) : (
+
+
+
+ )}
+
+ )}
{CAPTIONS[reduced ? 0 : i]}
);
diff --git a/website/package-lock.json b/website/package-lock.json
index fc7268f..370b9a9 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -9,24 +9,43 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
+ "@react-three/drei": "^10.7.7",
+ "@react-three/fiber": "^9.6.1",
"fumadocs-core": "^15.0.0",
"fumadocs-mdx": "^11.0.0",
"fumadocs-ui": "^15.0.0",
"gsap": "^3.15.0",
"next": "^15.3.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "three": "^0.184.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+ "@types/three": "^0.184.1",
"eslint": "^9.39.4",
"eslint-config-next": "^15.5.19",
"typescript": "^5.6.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1236,6 +1255,24 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.17",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@monogrid/gainmap-js": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
+ "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -2194,6 +2231,94 @@
"integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
"license": "MIT"
},
+ "node_modules/@react-three/drei": {
+ "version": "10.7.7",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
+ "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^3.1.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.8.3",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.4",
+ "tunnel-rat": "^0.1.2",
+ "use-sync-external-store": "^1.4.0",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^9.0.0",
+ "react": "^19",
+ "react-dom": "^19",
+ "three": ">=0.159"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz",
+ "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^2.0.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.27.0",
+ "suspend-react": "^0.1.3",
+ "use-sync-external-store": "^1.4.0",
+ "zustand": "^5.0.3"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=19 <19.3",
+ "react-dom": ">=19 <19.3",
+ "react-native": ">=0.78",
+ "three": ">=0.156"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2314,6 +2439,12 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -2334,6 +2465,12 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -2403,11 +2540,16 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
"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==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -2423,12 +2565,47 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.184.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
+ "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
@@ -3041,6 +3218,24 @@
"win32"
]
},
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -3362,6 +3557,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -3386,6 +3610,30 @@
"node": ">=8"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -3446,6 +3694,19 @@
"node": ">=6"
}
},
+ "node_modules/camera-controls": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
+ "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.0.0",
+ "npm": ">=10.5.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001797",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
@@ -3628,11 +3889,28 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -3659,7 +3937,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -3805,6 +4082,15 @@
"node": ">=6"
}
},
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3847,6 +4133,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4738,6 +5030,12 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5165,6 +5463,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -5382,6 +5686,12 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/hls.js": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
+ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
+ "license": "Apache-2.0"
+ },
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -5392,6 +5702,26 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5414,6 +5744,12 @@
"node": ">=16.x"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5791,6 +6127,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -5947,7 +6289,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -5968,6 +6309,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/its-fine": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
+ "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.9"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6091,6 +6444,15 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6145,6 +6507,16 @@
"node": "20 || >=22"
}
},
+ "node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
"node_modules/markdown-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
@@ -6474,6 +6846,21 @@
"node": ">= 8"
}
},
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "license": "MIT"
+ },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -7660,7 +8047,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7752,6 +8138,12 @@
"node": ">=4"
}
},
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7762,6 +8154,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/promise-worker-transferable": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "is-promise": "^2.1.0",
+ "lie": "^3.0.2"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7928,6 +8330,21 @@
}
}
},
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -8187,6 +8604,15 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "2.0.0-next.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
@@ -8447,7 +8873,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -8460,7 +8885,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -8593,6 +9017,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -8825,6 +9275,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
"node_modules/tailwind-merge": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
@@ -8835,6 +9294,44 @@
"url": "https://github.com/sponsors/dcastil"
}
},
+ "node_modules/three": {
+ "version": "0.184.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
+ "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
+ "license": "MIT"
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
+ "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
"node_modules/tinyexec": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
@@ -8883,6 +9380,36 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/troika-three-text": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
+ "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.52.4",
+ "troika-worker-utils": "^0.52.0",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
+ "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
+ "license": "MIT"
+ },
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
@@ -8925,6 +9452,43 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9247,12 +9811,30 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -9281,11 +9863,21 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -9418,6 +10010,35 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/website/package.json b/website/package.json
index 0659561..27506cb 100644
--- a/website/package.json
+++ b/website/package.json
@@ -10,19 +10,23 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
+ "@react-three/drei": "^10.7.7",
+ "@react-three/fiber": "^9.6.1",
"fumadocs-core": "^15.0.0",
"fumadocs-mdx": "^11.0.0",
"fumadocs-ui": "^15.0.0",
"gsap": "^3.15.0",
"next": "^15.3.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "three": "^0.184.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+ "@types/three": "^0.184.1",
"eslint": "^9.39.4",
"eslint-config-next": "^15.5.19",
"typescript": "^5.6.0"
From 67552852bd55bd9d2ca421d216f15e2287c1c497 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:25:37 -0700
Subject: [PATCH 04/11] =?UTF-8?q?feat(site):=20glow=20up=20the=20hero=20le?=
=?UTF-8?q?ns=20=E2=80=94=20drifting=20sparkles=20+=20stronger=20halo?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add drei Sparkles: a cluster of cyan motes drifting around the lens for a bit
of magic (especially against the dark theme).
- Brighten the aperture rim and beef up the CSS radial glow behind the floating
lens so it reads luminous without postprocessing.
Tried @react-three/postprocessing Bloom for a real glow, but EffectComposer
broke the transparent canvas (a visible box around the lens), so it's out — the
emissive rim + Sparkles + CSS halo get the look with the float intact. First
Load JS unchanged (114 kB); all 3D still lazy-loaded.
---
website/app/(home)/styles/home.css | 10 ++++++----
website/components/home/HeroLens3D.tsx | 20 ++++++++++++++++++--
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index e003e4e..27db9dc 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -122,12 +122,14 @@
.hero-lens-3d::before {
content: '';
position: absolute;
- inset: 6%;
+ inset: -8%;
z-index: -1;
border-radius: 50%;
- background: radial-gradient(circle at 50% 46%, var(--accent-glow), transparent 64%);
- opacity: 0.5;
- filter: blur(10px);
+ background:
+ radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--accent-3) 45%, transparent), transparent 58%),
+ radial-gradient(circle at 50% 52%, var(--accent-glow), transparent 70%);
+ opacity: 0.7;
+ filter: blur(16px);
}
.hero-lens-3d canvas {
display: block;
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
index 37df62d..5d082e9 100644
--- a/website/components/home/HeroLens3D.tsx
+++ b/website/components/home/HeroLens3D.tsx
@@ -2,7 +2,13 @@
import { Suspense, useRef } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
-import { Environment, Lightformer, MeshTransmissionMaterial, useTexture } from '@react-three/drei';
+import {
+ Environment,
+ Lightformer,
+ MeshTransmissionMaterial,
+ Sparkles,
+ useTexture,
+} from '@react-three/drei';
import * as THREE from 'three';
// Static assets aren't auto-prefixed with the GitHub Pages basePath, so prefix by hand.
@@ -67,7 +73,7 @@ function Lens() {
+ {/* drifting motes of light around the lens */}
+
{/* Procedural studio env — NO external HDR (keeps zero external calls). */}
From d605f222264eef20bebd06b63cde399a64a17665 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:28:23 -0700
Subject: [PATCH 05/11] =?UTF-8?q?fix(site):=20clearer=20hero=20lens=20?=
=?UTF-8?q?=E2=80=94=20dial=20down=20the=20glass=20distortion?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Vee was over-warped through the glass. Cut distortion 0.25→0.03, drop temporal
distortion, ease thickness/ior/aberration/blur so he reads clearly with just a
gentle lens refraction at the rim.
---
website/components/home/HeroLens3D.tsx | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
index 5d082e9..9281052 100644
--- a/website/components/home/HeroLens3D.tsx
+++ b/website/components/home/HeroLens3D.tsx
@@ -55,14 +55,14 @@ function Lens() {
From 789b63c28fe9379fc117b2766f9c5679f477e112 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:34:01 -0700
Subject: [PATCH 06/11] =?UTF-8?q?feat(site):=20hero=20lens=20=E2=80=94=20a?=
=?UTF-8?q?nimate=20Vee=20+=20calm=20the=20rim?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Vee is now the live mascot clip (useVideoTexture on mascot.mp4) refracted
through the glass, so the character actually moves instead of a still frame.
- Rim was too cyan: shift it toward brand blue, dim and thin it; pull cyan out
of the sparkles and the CSS glow halo too.
---
website/app/(home)/styles/home.css | 6 ++--
website/components/home/HeroLens3D.tsx | 40 +++++++++++++++-----------
2 files changed, 26 insertions(+), 20 deletions(-)
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index 27db9dc..f8e7099 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -126,9 +126,9 @@
z-index: -1;
border-radius: 50%;
background:
- radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--accent-3) 45%, transparent), transparent 58%),
- radial-gradient(circle at 50% 52%, var(--accent-glow), transparent 70%);
- opacity: 0.7;
+ radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--accent) 38%, transparent), transparent 60%),
+ radial-gradient(circle at 50% 52%, var(--accent-glow), transparent 72%);
+ opacity: 0.5;
filter: blur(16px);
}
.hero-lens-3d canvas {
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
index 9281052..c0e400c 100644
--- a/website/components/home/HeroLens3D.tsx
+++ b/website/components/home/HeroLens3D.tsx
@@ -7,7 +7,7 @@ import {
Lightformer,
MeshTransmissionMaterial,
Sparkles,
- useTexture,
+ useVideoTexture,
} from '@react-three/drei';
import * as THREE from 'three';
@@ -15,13 +15,19 @@ import * as THREE from 'three';
const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
/**
- * Vee on a disc directly behind the lens — sized to sit fully within the lens
- * silhouette, so he's only ever seen *through* the glass (refracted), never as a
- * bare rectangle. Swap the poster for a transparent cutout later for an even
- * cleaner float.
+ * Vee — the live mascot clip — on a disc directly behind the lens, sized to sit
+ * fully within the lens silhouette so he's only ever seen *through* the glass
+ * (refracted), never as a bare rectangle. The video texture means he actually
+ * moves. Swap mascot.mp4 for a transparent cutout/render later for a cleaner float.
*/
function VeeDisc() {
- const tex = useTexture(`${BASE}/mascot-poster.jpg`);
+ const tex = useVideoTexture(`${BASE}/mascot.mp4`, {
+ muted: true,
+ loop: true,
+ start: true,
+ playsInline: true,
+ crossOrigin: 'anonymous',
+ });
tex.colorSpace = THREE.SRGBColorSpace;
return (
@@ -70,13 +76,13 @@ function Lens() {
{/* glowing aperture rim — a ring around the lens edge, facing the viewer
(no rotation: TorusGeometry already lies in the camera-facing plane). */}
-
+
@@ -100,12 +106,12 @@ export default function HeroLens3D() {
{/* drifting motes of light around the lens */}
{/* Procedural studio env — NO external HDR (keeps zero external calls). */}
From 9846f8dc56959abec58f247f2eaa72fb99e92c80 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:39:04 -0700
Subject: [PATCH 07/11] =?UTF-8?q?feat(site):=20hero=20lens=20=E2=80=94=20s?=
=?UTF-8?q?till=20until=20hovered,=20gentler=20loop,=20clearer=20glass?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- The glass now rests perfectly still and only tilts while hovered (removed the
idle drift/bob); eases back to rest on leave.
- Slow the mascot clip (playbackRate 0.8) so Vee's loop reads gentler.
- Cut the glass distortion further (distortion 0, lower ior/thickness/aberration)
so Vee reads almost directly through near-clear glass.
---
website/components/home/HeroLens3D.tsx | 42 +++++++++++++++++---------
1 file changed, 27 insertions(+), 15 deletions(-)
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
index c0e400c..f74862a 100644
--- a/website/components/home/HeroLens3D.tsx
+++ b/website/components/home/HeroLens3D.tsx
@@ -29,6 +29,9 @@ function VeeDisc() {
crossOrigin: 'anonymous',
});
tex.colorSpace = THREE.SRGBColorSpace;
+ // Ease the playback so the loop reads gentler and more continuous.
+ const video = tex.image as HTMLVideoElement | undefined;
+ if (video) video.playbackRate = 0.8;
return (
@@ -41,33 +44,42 @@ function VeeDisc() {
* idles with a slow drift. */
function Lens() {
const group = useRef(null);
+ const hovered = useRef(false);
useFrame((state, delta) => {
const g = group.current;
if (!g) return;
- const t = state.clock.elapsedTime;
- // cursor tilt + a slow idle sway
- const targetY = state.pointer.x * 0.5 + Math.sin(t * 0.35) * 0.12;
- const targetX = -state.pointer.y * 0.4 + Math.cos(t * 0.28) * 0.08;
- const k = 1 - Math.pow(0.0015, delta); // frame-rate-independent damping
+ // The glass rests perfectly still until hovered; then it tilts toward the
+ // cursor and eases back to rest on leave. No idle motion.
+ const targetY = hovered.current ? state.pointer.x * 0.5 : 0;
+ const targetX = hovered.current ? -state.pointer.y * 0.4 : 0;
+ const k = 1 - Math.pow(0.0016, delta); // frame-rate-independent damping
g.rotation.y = THREE.MathUtils.lerp(g.rotation.y, targetY, k);
g.rotation.x = THREE.MathUtils.lerp(g.rotation.x, targetX, k);
- g.position.y = Math.sin(t * 0.6) * 0.04;
});
return (
-
- {/* the lens: a flattened sphere → a thick magnifying disc */}
+ {
+ hovered.current = true;
+ }}
+ onPointerOut={() => {
+ hovered.current = false;
+ }}
+ >
+ {/* the lens: a flattened sphere → a thick magnifying disc. Near-clear glass
+ (minimal distortion) so Vee reads almost directly through it. */}
Date: Mon, 15 Jun 2026 02:42:07 -0700
Subject: [PATCH 08/11] revert(site): remove the WebGL 3D hero lens
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The 3D glass lens didn't land — restore the original framed video mascot in the
hero and drop three/@react-three/fiber/@react-three/drei. The bright palette,
holographic verdict card, and theme-scan all stay. First Load JS back to 112 kB.
---
website/app/(home)/styles/home.css | 28 --
website/components/home/HeroLens3D.tsx | 138 ------
website/components/home/HeroMascot.tsx | 100 +---
website/package-lock.json | 639 +------------------------
website/package.json | 6 +-
5 files changed, 36 insertions(+), 875 deletions(-)
delete mode 100644 website/components/home/HeroLens3D.tsx
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index f8e7099..9b95926 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -108,34 +108,6 @@
max-width: 34ch;
text-align: center;
}
-
-/* Floating 3D glass lens (replaces the framed clip on capable desktops). The
- canvas is transparent; a soft radial glow gives the lens some atmosphere so
- it doesn't read as floating in a void. */
-.hero-lens-3d {
- position: relative;
- width: clamp(300px, 38vw, 440px);
- aspect-ratio: 1 / 1;
- isolation: isolate;
- cursor: grab;
-}
-.hero-lens-3d::before {
- content: '';
- position: absolute;
- inset: -8%;
- z-index: -1;
- border-radius: 50%;
- background:
- radial-gradient(circle at 50% 48%, color-mix(in srgb, var(--accent) 38%, transparent), transparent 60%),
- radial-gradient(circle at 50% 52%, var(--accent-glow), transparent 72%);
- opacity: 0.5;
- filter: blur(16px);
-}
-.hero-lens-3d canvas {
- display: block;
- width: 100% !important;
- height: 100% !important;
-}
.hero-title {
font-size: clamp(2.6rem, 0.9rem + 5.4vw, 5rem);
font-weight: 800;
diff --git a/website/components/home/HeroLens3D.tsx b/website/components/home/HeroLens3D.tsx
deleted file mode 100644
index f74862a..0000000
--- a/website/components/home/HeroLens3D.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-'use client';
-
-import { Suspense, useRef } from 'react';
-import { Canvas, useFrame } from '@react-three/fiber';
-import {
- Environment,
- Lightformer,
- MeshTransmissionMaterial,
- Sparkles,
- useVideoTexture,
-} from '@react-three/drei';
-import * as THREE from 'three';
-
-// Static assets aren't auto-prefixed with the GitHub Pages basePath, so prefix by hand.
-const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
-
-/**
- * Vee — the live mascot clip — on a disc directly behind the lens, sized to sit
- * fully within the lens silhouette so he's only ever seen *through* the glass
- * (refracted), never as a bare rectangle. The video texture means he actually
- * moves. Swap mascot.mp4 for a transparent cutout/render later for a cleaner float.
- */
-function VeeDisc() {
- const tex = useVideoTexture(`${BASE}/mascot.mp4`, {
- muted: true,
- loop: true,
- start: true,
- playsInline: true,
- crossOrigin: 'anonymous',
- });
- tex.colorSpace = THREE.SRGBColorSpace;
- // Ease the playback so the loop reads gentler and more continuous.
- const video = tex.image as HTMLVideoElement | undefined;
- if (video) video.playbackRate = 0.8;
- return (
-
-
-
-
- );
-}
-
-/** The glass lens + its glowing cyan aperture rim. Tilts toward the cursor and
- * idles with a slow drift. */
-function Lens() {
- const group = useRef(null);
- const hovered = useRef(false);
- useFrame((state, delta) => {
- const g = group.current;
- if (!g) return;
- // The glass rests perfectly still until hovered; then it tilts toward the
- // cursor and eases back to rest on leave. No idle motion.
- const targetY = hovered.current ? state.pointer.x * 0.5 : 0;
- const targetX = hovered.current ? -state.pointer.y * 0.4 : 0;
- const k = 1 - Math.pow(0.0016, delta); // frame-rate-independent damping
- g.rotation.y = THREE.MathUtils.lerp(g.rotation.y, targetY, k);
- g.rotation.x = THREE.MathUtils.lerp(g.rotation.x, targetX, k);
- });
-
- return (
- {
- hovered.current = true;
- }}
- onPointerOut={() => {
- hovered.current = false;
- }}
- >
- {/* the lens: a flattened sphere → a thick magnifying disc. Near-clear glass
- (minimal distortion) so Vee reads almost directly through it. */}
-
-
-
-
- {/* glowing aperture rim — a ring around the lens edge, facing the viewer
- (no rotation: TorusGeometry already lies in the camera-facing plane). */}
-
-
-
-
-
- );
-}
-
-export default function HeroLens3D() {
- return (
-
-
-
-
-
-
-
- {/* drifting motes of light around the lens */}
-
- {/* Procedural studio env — NO external HDR (keeps zero external calls). */}
-
-
-
-
-
-
-
- );
-}
diff --git a/website/components/home/HeroMascot.tsx b/website/components/home/HeroMascot.tsx
index 5208a5e..2ed9697 100644
--- a/website/components/home/HeroMascot.tsx
+++ b/website/components/home/HeroMascot.tsx
@@ -1,10 +1,9 @@
'use client';
-import { Component, useEffect, useState, type ReactNode } from 'react';
-import dynamic from 'next/dynamic';
+import { useEffect, useState } from 'react';
-// Static assets live in public/ and are NOT auto-prefixed with the GitHub Pages
-// basePath the way next/link is — so we prefix the / src by hand.
+// Static assets live in public/ and are NOT auto-prefixed with the GitHub
+// Pages basePath the way next/link is — so we prefix the / src by hand.
const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
const CAPTIONS = [
@@ -15,58 +14,18 @@ const CAPTIONS = [
const ALT = 'Vee, the RepoLens mascot, peering through a lens';
-// The WebGL scene is heavy (three.js) — load it lazily and client-only so it
-// never touches the initial bundle or the server render.
-const HeroLens3D = dynamic(() => import('./HeroLens3D'), { ssr: false });
-
-/** If the 3D scene throws (WebGL context lost, chunk load fail, …) fall back to
- * the poster rather than blanking the hero. */
-class LensBoundary extends Component<
- { fallback: ReactNode; children: ReactNode },
- { failed: boolean }
-> {
- state = { failed: false };
- static getDerivedStateFromError() {
- return { failed: true };
- }
- render() {
- return this.state.failed ? this.props.fallback : this.props.children;
- }
-}
-
-function Poster() {
- return (
- // eslint-disable-next-line @next/next/no-img-element -- static export (output: export); next/image adds no value for this decorative poster
-
- );
-}
-
/**
- * The hero mascot. On a capable desktop (fine pointer, motion OK, WebGL, wide
- * enough) Vee appears inside a floating refractive glass lens that tracks the
- * cursor. Everywhere else — touch, reduced-motion, no WebGL, narrow, or while
- * the 3D chunk loads — the original framed clip / poster shows instead.
+ * The hero mascot: an autoplaying, muted, looping clip of Vee in a framed
+ * stage. Under reduced-motion we show the poster frame instead — no autoplay,
+ * no loop — and the caption stops rotating.
*/
export function HeroMascot() {
const [reduced, setReduced] = useState(false);
- const [use3D, setUse3D] = useState(false);
const [i, setI] = useState(0);
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
setReduced(reduce);
-
- const fine = window.matchMedia('(pointer: fine)').matches;
- const wide = window.matchMedia('(min-width: 880px)').matches;
- let webgl = false;
- try {
- const c = document.createElement('canvas');
- webgl = !!(c.getContext('webgl2') || c.getContext('webgl'));
- } catch {
- webgl = false;
- }
- setUse3D(!reduce && fine && wide && webgl);
-
if (reduce) return;
const id = setInterval(() => setI((n) => (n + 1) % CAPTIONS.length), 3400);
return () => clearInterval(id);
@@ -74,33 +33,26 @@ export function HeroMascot() {
return (
- ) : (
-
- {reduced ? (
-
- ) : (
-
-
-
- )}
-
- )}
+
+ {reduced ? (
+ // eslint-disable-next-line @next/next/no-img-element -- static export (output: export) with unoptimized images; next/image adds no value for this decorative poster
+
+ ) : (
+
+
+
+ )}
+
{CAPTIONS[reduced ? 0 : i]}
);
diff --git a/website/package-lock.json b/website/package-lock.json
index 370b9a9..fc7268f 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -9,43 +9,24 @@
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
- "@react-three/drei": "^10.7.7",
- "@react-three/fiber": "^9.6.1",
"fumadocs-core": "^15.0.0",
"fumadocs-mdx": "^11.0.0",
"fumadocs-ui": "^15.0.0",
"gsap": "^3.15.0",
"next": "^15.3.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0",
- "three": "^0.184.0"
+ "react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
- "@types/three": "^0.184.1",
"eslint": "^9.39.4",
"eslint-config-next": "^15.5.19",
"typescript": "^5.6.0"
}
},
- "node_modules/@babel/runtime": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
- "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@dimforge/rapier3d-compat": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
- "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
- "license": "Apache-2.0"
- },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -1255,24 +1236,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/@mediapipe/tasks-vision": {
- "version": "0.10.17",
- "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
- "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
- "license": "Apache-2.0"
- },
- "node_modules/@monogrid/gainmap-js": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
- "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
- "license": "MIT",
- "dependencies": {
- "promise-worker-transferable": "^1.0.4"
- },
- "peerDependencies": {
- "three": ">= 0.159.0"
- }
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
@@ -2231,94 +2194,6 @@
"integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==",
"license": "MIT"
},
- "node_modules/@react-three/drei": {
- "version": "10.7.7",
- "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
- "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.26.0",
- "@mediapipe/tasks-vision": "0.10.17",
- "@monogrid/gainmap-js": "^3.0.6",
- "@use-gesture/react": "^10.3.1",
- "camera-controls": "^3.1.0",
- "cross-env": "^7.0.3",
- "detect-gpu": "^5.0.56",
- "glsl-noise": "^0.0.0",
- "hls.js": "^1.5.17",
- "maath": "^0.10.8",
- "meshline": "^3.3.1",
- "stats-gl": "^2.2.8",
- "stats.js": "^0.17.0",
- "suspend-react": "^0.1.3",
- "three-mesh-bvh": "^0.8.3",
- "three-stdlib": "^2.35.6",
- "troika-three-text": "^0.52.4",
- "tunnel-rat": "^0.1.2",
- "use-sync-external-store": "^1.4.0",
- "utility-types": "^3.11.0",
- "zustand": "^5.0.1"
- },
- "peerDependencies": {
- "@react-three/fiber": "^9.0.0",
- "react": "^19",
- "react-dom": "^19",
- "three": ">=0.159"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
- "node_modules/@react-three/fiber": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz",
- "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.17.8",
- "@types/webxr": "*",
- "base64-js": "^1.5.1",
- "buffer": "^6.0.3",
- "its-fine": "^2.0.0",
- "react-use-measure": "^2.1.7",
- "scheduler": "^0.27.0",
- "suspend-react": "^0.1.3",
- "use-sync-external-store": "^1.4.0",
- "zustand": "^5.0.3"
- },
- "peerDependencies": {
- "expo": ">=43.0",
- "expo-asset": ">=8.4",
- "expo-file-system": ">=11.0",
- "expo-gl": ">=11.0",
- "react": ">=19 <19.3",
- "react-dom": ">=19 <19.3",
- "react-native": ">=0.78",
- "three": ">=0.156"
- },
- "peerDependenciesMeta": {
- "expo": {
- "optional": true
- },
- "expo-asset": {
- "optional": true
- },
- "expo-file-system": {
- "optional": true
- },
- "expo-gl": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
- }
- },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2439,12 +2314,6 @@
"tslib": "^2.8.0"
}
},
- "node_modules/@tweenjs/tween.js": {
- "version": "23.1.3",
- "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
- "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
- "license": "MIT"
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -2465,12 +2334,6 @@
"@types/ms": "*"
}
},
- "node_modules/@types/draco3d": {
- "version": "1.4.10",
- "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
- "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
- "license": "MIT"
- },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -2540,16 +2403,11 @@
"undici-types": "~6.21.0"
}
},
- "node_modules/@types/offscreencanvas": {
- "version": "2019.7.3",
- "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
- "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
- "license": "MIT"
- },
"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==",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -2565,47 +2423,12 @@
"@types/react": "^19.2.0"
}
},
- "node_modules/@types/react-reconciler": {
- "version": "0.28.9",
- "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
- "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "*"
- }
- },
- "node_modules/@types/stats.js": {
- "version": "0.17.4",
- "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
- "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
- "license": "MIT"
- },
- "node_modules/@types/three": {
- "version": "0.184.1",
- "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
- "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
- "license": "MIT",
- "dependencies": {
- "@dimforge/rapier3d-compat": "~0.12.0",
- "@tweenjs/tween.js": "~23.1.3",
- "@types/stats.js": "*",
- "@types/webxr": ">=0.5.17",
- "fflate": "~0.8.2",
- "meshoptimizer": "~1.1.1"
- }
- },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
- "node_modules/@types/webxr": {
- "version": "0.5.24",
- "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
- "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
- "license": "MIT"
- },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
@@ -3218,24 +3041,6 @@
"win32"
]
},
- "node_modules/@use-gesture/core": {
- "version": "10.3.1",
- "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
- "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
- "license": "MIT"
- },
- "node_modules/@use-gesture/react": {
- "version": "10.3.1",
- "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
- "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
- "license": "MIT",
- "dependencies": {
- "@use-gesture/core": "10.3.1"
- },
- "peerDependencies": {
- "react": ">= 16.8.0"
- }
- },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -3557,35 +3362,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/base64-js": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/bidi-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
- "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
- "license": "MIT",
- "dependencies": {
- "require-from-string": "^2.0.2"
- }
- },
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -3610,30 +3386,6 @@
"node": ">=8"
}
},
- "node_modules/buffer": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
- "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.2.1"
- }
- },
"node_modules/call-bind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
@@ -3694,19 +3446,6 @@
"node": ">=6"
}
},
- "node_modules/camera-controls": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
- "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
- "license": "MIT",
- "engines": {
- "node": ">=22.0.0",
- "npm": ">=10.5.1"
- },
- "peerDependencies": {
- "three": ">=0.126.1"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001797",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz",
@@ -3889,28 +3628,11 @@
"dev": true,
"license": "MIT"
},
- "node_modules/cross-env": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
- "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
- "license": "MIT",
- "dependencies": {
- "cross-spawn": "^7.0.1"
- },
- "bin": {
- "cross-env": "src/bin/cross-env.js",
- "cross-env-shell": "src/bin/cross-env-shell.js"
- },
- "engines": {
- "node": ">=10.14",
- "npm": ">=6",
- "yarn": ">=1"
- }
- },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -3937,6 +3659,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -4082,15 +3805,6 @@
"node": ">=6"
}
},
- "node_modules/detect-gpu": {
- "version": "5.0.70",
- "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
- "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
- "license": "MIT",
- "dependencies": {
- "webgl-constants": "^1.1.1"
- }
- },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4133,12 +3847,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/draco3d": {
- "version": "1.5.7",
- "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
- "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
- "license": "Apache-2.0"
- },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5030,12 +4738,6 @@
}
}
},
- "node_modules/fflate": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
- "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
- "license": "MIT"
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5463,12 +5165,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/glsl-noise": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
- "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
- "license": "MIT"
- },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -5686,12 +5382,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/hls.js": {
- "version": "1.6.16",
- "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
- "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
- "license": "Apache-2.0"
- },
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -5702,26 +5392,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause"
- },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5744,12 +5414,6 @@
"node": ">=16.x"
}
},
- "node_modules/immediate": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
- "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
- "license": "MIT"
- },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -6127,12 +5791,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-promise": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
- "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
- "license": "MIT"
- },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -6289,6 +5947,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -6309,18 +5968,6 @@
"node": ">= 0.4"
}
},
- "node_modules/its-fine": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
- "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
- "license": "MIT",
- "dependencies": {
- "@types/react-reconciler": "^0.28.9"
- },
- "peerDependencies": {
- "react": "^19.0.0"
- }
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6444,15 +6091,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/lie": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
- "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
- "license": "MIT",
- "dependencies": {
- "immediate": "~3.0.5"
- }
- },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -6507,16 +6145,6 @@
"node": "20 || >=22"
}
},
- "node_modules/maath": {
- "version": "0.10.8",
- "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
- "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
- "license": "MIT",
- "peerDependencies": {
- "@types/three": ">=0.134.0",
- "three": ">=0.134.0"
- }
- },
"node_modules/markdown-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
@@ -6846,21 +6474,6 @@
"node": ">= 8"
}
},
- "node_modules/meshline": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
- "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
- "license": "MIT",
- "peerDependencies": {
- "three": ">=0.137"
- }
- },
- "node_modules/meshoptimizer": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
- "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
- "license": "MIT"
- },
"node_modules/micromark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -8047,6 +7660,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -8138,12 +7752,6 @@
"node": ">=4"
}
},
- "node_modules/potpack": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
- "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
- "license": "ISC"
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -8154,16 +7762,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/promise-worker-transferable": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
- "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
- "license": "Apache-2.0",
- "dependencies": {
- "is-promise": "^2.1.0",
- "lie": "^3.0.2"
- }
- },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8330,21 +7928,6 @@
}
}
},
- "node_modules/react-use-measure": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
- "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16.13",
- "react-dom": ">=16.13"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- }
- }
- },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -8604,15 +8187,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/resolve": {
"version": "2.0.0-next.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
@@ -8873,6 +8447,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -8885,6 +8460,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -9017,32 +8593,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/stats-gl": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
- "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
- "license": "MIT",
- "dependencies": {
- "@types/three": "*",
- "three": "^0.170.0"
- },
- "peerDependencies": {
- "@types/three": "*",
- "three": "*"
- }
- },
- "node_modules/stats-gl/node_modules/three": {
- "version": "0.170.0",
- "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
- "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
- "license": "MIT"
- },
- "node_modules/stats.js": {
- "version": "0.17.0",
- "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
- "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
- "license": "MIT"
- },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -9275,15 +8825,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/suspend-react": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
- "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=17.0"
- }
- },
"node_modules/tailwind-merge": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
@@ -9294,44 +8835,6 @@
"url": "https://github.com/sponsors/dcastil"
}
},
- "node_modules/three": {
- "version": "0.184.0",
- "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
- "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
- "license": "MIT"
- },
- "node_modules/three-mesh-bvh": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
- "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
- "license": "MIT",
- "peerDependencies": {
- "three": ">= 0.159.0"
- }
- },
- "node_modules/three-stdlib": {
- "version": "2.36.1",
- "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
- "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
- "license": "MIT",
- "dependencies": {
- "@types/draco3d": "^1.4.0",
- "@types/offscreencanvas": "^2019.6.4",
- "@types/webxr": "^0.5.2",
- "draco3d": "^1.4.1",
- "fflate": "^0.6.9",
- "potpack": "^1.0.1"
- },
- "peerDependencies": {
- "three": ">=0.128.0"
- }
- },
- "node_modules/three-stdlib/node_modules/fflate": {
- "version": "0.6.10",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
- "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
- "license": "MIT"
- },
"node_modules/tinyexec": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
@@ -9380,36 +8883,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/troika-three-text": {
- "version": "0.52.4",
- "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
- "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
- "license": "MIT",
- "dependencies": {
- "bidi-js": "^1.0.2",
- "troika-three-utils": "^0.52.4",
- "troika-worker-utils": "^0.52.0",
- "webgl-sdf-generator": "1.1.1"
- },
- "peerDependencies": {
- "three": ">=0.125.0"
- }
- },
- "node_modules/troika-three-utils": {
- "version": "0.52.4",
- "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
- "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
- "license": "MIT",
- "peerDependencies": {
- "three": ">=0.125.0"
- }
- },
- "node_modules/troika-worker-utils": {
- "version": "0.52.0",
- "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
- "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
- "license": "MIT"
- },
"node_modules/trough": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
@@ -9452,43 +8925,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/tunnel-rat": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
- "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
- "license": "MIT",
- "dependencies": {
- "zustand": "^4.3.2"
- }
- },
- "node_modules/tunnel-rat/node_modules/zustand": {
- "version": "4.5.7",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
- "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
- "license": "MIT",
- "dependencies": {
- "use-sync-external-store": "^1.2.2"
- },
- "engines": {
- "node": ">=12.7.0"
- },
- "peerDependencies": {
- "@types/react": ">=16.8",
- "immer": ">=9.0.6",
- "react": ">=16.8"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "immer": {
- "optional": true
- },
- "react": {
- "optional": true
- }
- }
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -9811,30 +9247,12 @@
}
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
- "node_modules/utility-types": {
- "version": "3.11.0",
- "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
- "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -9863,21 +9281,11 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/webgl-constants": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
- "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
- },
- "node_modules/webgl-sdf-generator": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
- "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
- "license": "MIT"
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -10010,35 +9418,6 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
- "node_modules/zustand": {
- "version": "5.0.14",
- "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
- "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- },
- "peerDependencies": {
- "@types/react": ">=18.0.0",
- "immer": ">=9.0.6",
- "react": ">=18.0.0",
- "use-sync-external-store": ">=1.2.0"
- },
- "peerDependenciesMeta": {
- "@types/react": {
- "optional": true
- },
- "immer": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "use-sync-external-store": {
- "optional": true
- }
- }
- },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/website/package.json b/website/package.json
index 27506cb..0659561 100644
--- a/website/package.json
+++ b/website/package.json
@@ -10,23 +10,19 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
- "@react-three/drei": "^10.7.7",
- "@react-three/fiber": "^9.6.1",
"fumadocs-core": "^15.0.0",
"fumadocs-mdx": "^11.0.0",
"fumadocs-ui": "^15.0.0",
"gsap": "^3.15.0",
"next": "^15.3.0",
"react": "^19.0.0",
- "react-dom": "^19.0.0",
- "three": "^0.184.0"
+ "react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.5",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
- "@types/three": "^0.184.1",
"eslint": "^9.39.4",
"eslint-config-next": "^15.5.19",
"typescript": "^5.6.0"
From a2c164161858e6b7babbca8713dd6e1170201405 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Mon, 15 Jun 2026 03:01:33 -0700
Subject: [PATCH 09/11] feat(site): smoother, seamless hero mascot loop
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The single clip had a hard cut at the loop point. Render a boomerang
(forward → reversed, via ffmpeg, -tune animation) so it loops with no seam, and
ease playback to 0.85x for a gentler feel. Replaces mascot.mp4 (185K) with
mascot-loop.mp4 (403K, 12s). Reduced-motion still shows the static poster.
---
website/components/home/HeroMascot.tsx | 14 +++++++++-----
website/public/mascot-loop.mp4 | Bin 0 -> 413073 bytes
website/public/mascot.mp4 | Bin 188961 -> 0 bytes
3 files changed, 9 insertions(+), 5 deletions(-)
create mode 100644 website/public/mascot-loop.mp4
delete mode 100644 website/public/mascot.mp4
diff --git a/website/components/home/HeroMascot.tsx b/website/components/home/HeroMascot.tsx
index 2ed9697..a4c9339 100644
--- a/website/components/home/HeroMascot.tsx
+++ b/website/components/home/HeroMascot.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
// Static assets live in public/ and are NOT auto-prefixed with the GitHub
// Pages basePath the way next/link is — so we prefix the