● Zion Boggan zionboggan.com ↗

Add WebGL stomp-shockwave hero and The Yard community directory

324ed59   Zion Boggan committed on Jun 25, 2026 (1 month ago)
apps/web/package.json +3 -0
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
+ "@react-three/fiber": "^9.1.0",
"@stepping/api": "workspace:*",
"@stepping/config": "workspace:*",
"@stepping/db": "workspace:*",
@@ -25,6 +26,7 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"superjson": "^2.2.2",
+ "three": "^0.171.0",
"zod": "^3.24.1"
},
"devDependencies": {
@@ -32,6 +34,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "@types/three": "^0.171.0",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"tailwindcss": "^4",
apps/web/src/app/city/[slug]/page.tsx +7 -0
@@ -66,6 +66,13 @@ export default async function CityPage({
</p>
<h1 className="display mt-3 text-7xl sm:text-8xl">{city.name}</h1>
+ <Link
+ href={`/city/${slug}/yard`}
+ className="kicker mt-6 inline-block text-gold hover:text-gold-bright"
+ >
+ Enter The Yard →
+ </Link>
+
{/* Add an event */}
<section className="mt-14 border-t border-[var(--line)] pt-10">
<div className="flex items-end justify-between">
apps/web/src/app/city/[slug]/yard/page.tsx +496 -0
@@ -0,0 +1,496 @@
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { revalidatePath } from "next/cache";
+import { serverCaller } from "@/lib/trpc/server";
+
+export const dynamic = "force-dynamic";
+
+export default async function YardPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+ const caller = serverCaller();
+ const city = await caller.city.bySlug({ slug });
+ if (!city) notFound();
+
+ const cityId = city.id;
+ const [stepTeams, chapters, djs, promoters, classes, organizations] =
+ await Promise.all([
+ caller.stepTeam.list({ cityId }),
+ caller.chapter.list({ cityId }),
+ caller.dj.list({ cityId }),
+ caller.promoter.list({ cityId }),
+ caller.class.list({ cityId }),
+ caller.organization.list(),
+ ]);
+
+ const orgById = new Map(organizations.map((o) => [o.id, o]));
+
+ async function addStepTeam(formData: FormData) {
+ "use server";
+ const name = String(formData.get("name") ?? "").trim();
+ const affiliationOrgId = String(formData.get("affiliationOrgId") ?? "");
+ const bio = String(formData.get("bio") ?? "").trim();
+ if (!name) return;
+ await serverCaller().stepTeam.create({
+ cityId: city!.id,
+ name,
+ affiliationOrgId: affiliationOrgId || undefined,
+ bio: bio || undefined,
+ });
+ revalidatePath(`/city/${slug}/yard`);
+ }
+
+ async function addChapter(formData: FormData) {
+ "use server";
+ const organizationId = String(formData.get("organizationId") ?? "");
+ const name = String(formData.get("name") ?? "").trim();
+ const school = String(formData.get("school") ?? "").trim();
+ const bio = String(formData.get("bio") ?? "").trim();
+ if (!name || !organizationId) return;
+ await serverCaller().chapter.create({
+ organizationId,
+ cityId: city!.id,
+ name,
+ school: school || undefined,
+ bio: bio || undefined,
+ });
+ revalidatePath(`/city/${slug}/yard`);
+ }
+
+ async function addDj(formData: FormData) {
+ "use server";
+ const name = String(formData.get("name") ?? "").trim();
+ const bio = String(formData.get("bio") ?? "").trim();
+ if (!name) return;
+ await serverCaller().dj.create({
+ cityId: city!.id,
+ name,
+ bio: bio || undefined,
+ });
+ revalidatePath(`/city/${slug}/yard`);
+ }
+
+ async function addPromoter(formData: FormData) {
+ "use server";
+ const name = String(formData.get("name") ?? "").trim();
+ const bio = String(formData.get("bio") ?? "").trim();
+ if (!name) return;
+ await serverCaller().promoter.create({
+ cityId: city!.id,
+ name,
+ bio: bio || undefined,
+ });
+ revalidatePath(`/city/${slug}/yard`);
+ }
+
+ async function addClass(formData: FormData) {
+ "use server";
+ const title = String(formData.get("title") ?? "").trim();
+ const instructor = String(formData.get("instructor") ?? "").trim();
+ const schedule = String(formData.get("schedule") ?? "").trim();
+ const isFree = formData.get("isFree") != null;
+ if (!title) return;
+ await serverCaller().class.create({
+ cityId: city!.id,
+ title,
+ instructor: instructor || undefined,
+ schedule: schedule || undefined,
+ isFree,
+ });
+ revalidatePath(`/city/${slug}/yard`);
+ }
+
+ return (
+ <main className="mx-auto w-full max-w-3xl px-5 py-16 sm:px-8">
+ <Link href={`/city/${slug}`} className="kicker hover:text-gold">
+ ← {city.name}
+ </Link>
+
+ <p className="kicker mt-10 text-ash">
+ The Yard · {city.metroLabel}
+ </p>
+ <h1 className="display mt-3 text-7xl sm:text-8xl">The Yard</h1>
+ <p className="serif mt-5 max-w-xl text-xl text-ash">
+ The community behind the show - step teams, chapters, DJs, promoters, and
+ classes keeping {city.name} on beat.
+ </p>
+
+ {/* Step Teams */}
+ <section className="mt-16 border-t border-[var(--line)] pt-10">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="kicker text-gold">Section 01</p>
+ <h2 className="display mt-2 text-4xl">Step Teams</h2>
+ </div>
+ <span className="kicker text-ash">
+ {String(stepTeams.length).padStart(2, "0")} teams
+ </span>
+ </div>
+
+ {stepTeams.length === 0 ? (
+ <p className="serif mt-8 text-xl text-ash">
+ No step teams yet. Put the first line on the yard below.
+ </p>
+ ) : (
+ <ul className="mt-6">
+ {stepTeams.map((t, i) => {
+ const org = t.affiliationOrgId
+ ? orgById.get(t.affiliationOrgId)
+ : null;
+ return (
+ <li
+ key={t.id}
+ className="flex items-center gap-5 border-b border-[var(--line)] py-6"
+ >
+ <span className="kicker w-8 text-gold">
+ {String(i + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1">
+ <p className="display text-2xl sm:text-3xl">{t.name}</p>
+ {t.bio ? (
+ <p className="serif mt-1 text-lg text-ash">{t.bio}</p>
+ ) : null}
+ </div>
+ {org ? (
+ <span className="kicker border border-[var(--line)] px-3 py-1">
+ {org.letters ?? org.name}
+ </span>
+ ) : null}
+ </li>
+ );
+ })}
+ </ul>
+ )}
+
+ <form action={addStepTeam} className="mt-8 flex flex-col gap-3">
+ <input
+ name="name"
+ required
+ placeholder="Team name (e.g. Vanguard Steppers)"
+ className="serif border-b border-[var(--line)] bg-transparent py-3 text-2xl text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <input
+ name="bio"
+ placeholder="One line about the team (optional)"
+ className="serif border-b border-[var(--line)] bg-transparent py-2 text-lg text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <div className="mt-2 flex flex-wrap gap-4">
+ <select
+ name="affiliationOrgId"
+ defaultValue=""
+ className="kicker border border-[var(--line)] bg-stage px-4 py-3 text-bone outline-none focus:border-gold"
+ >
+ <option value="" className="bg-stage">
+ NO AFFILIATION
+ </option>
+ {organizations.map((o) => (
+ <option key={o.id} value={o.id} className="bg-stage">
+ {((o.letters ? `${o.letters} ` : "") + o.name).toUpperCase()}
+ </option>
+ ))}
+ </select>
+ <button
+ type="submit"
+ className="kicker bg-gold px-6 py-3 text-stage hover:bg-gold-bright"
+ >
+ Add team
+ </button>
+ </div>
+ </form>
+ </section>
+
+ {/* Chapters */}
+ <section className="mt-16 border-t border-[var(--line)] pt-10">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="kicker text-gold">Section 02</p>
+ <h2 className="display mt-2 text-4xl">Chapters</h2>
+ </div>
+ <span className="kicker text-ash">
+ {String(chapters.length).padStart(2, "0")} chapters
+ </span>
+ </div>
+
+ {chapters.length === 0 ? (
+ <p className="serif mt-8 text-xl text-ash">
+ No chapters yet. Register the first Divine Nine chapter below.
+ </p>
+ ) : (
+ <ul className="mt-6">
+ {chapters.map((c, i) => {
+ const org = orgById.get(c.organizationId);
+ return (
+ <li
+ key={c.id}
+ className="flex items-center gap-5 border-b border-[var(--line)] py-6"
+ >
+ <span className="kicker w-8 text-gold">
+ {String(i + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1">
+ <p className="display text-2xl sm:text-3xl">{c.name}</p>
+ <p className="kicker mt-1 text-ash">
+ {c.school ? c.school.toUpperCase() : "CHAPTER"}
+ </p>
+ </div>
+ {org ? (
+ <span className="kicker border border-[var(--line)] px-3 py-1">
+ {org.letters ?? org.name}
+ </span>
+ ) : null}
+ </li>
+ );
+ })}
+ </ul>
+ )}
+
+ <form action={addChapter} className="mt-8 flex flex-col gap-3">
+ <input
+ name="name"
+ required
+ placeholder="Chapter name (e.g. Alpha Beta Chapter)"
+ className="serif border-b border-[var(--line)] bg-transparent py-3 text-2xl text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <input
+ name="school"
+ placeholder="School or post (optional)"
+ className="serif border-b border-[var(--line)] bg-transparent py-2 text-lg text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <div className="mt-2 flex flex-wrap gap-4">
+ <select
+ name="organizationId"
+ required
+ defaultValue=""
+ className="kicker border border-[var(--line)] bg-stage px-4 py-3 text-bone outline-none focus:border-gold"
+ >
+ <option value="" disabled className="bg-stage">
+ SELECT ORGANIZATION
+ </option>
+ {organizations.map((o) => (
+ <option key={o.id} value={o.id} className="bg-stage">
+ {((o.letters ? `${o.letters} ` : "") + o.name).toUpperCase()}
+ </option>
+ ))}
+ </select>
+ <button
+ type="submit"
+ className="kicker bg-gold px-6 py-3 text-stage hover:bg-gold-bright"
+ >
+ Add chapter
+ </button>
+ </div>
+ </form>
+ </section>
+
+ {/* DJs */}
+ <section className="mt-16 border-t border-[var(--line)] pt-10">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="kicker text-gold">Section 03</p>
+ <h2 className="display mt-2 text-4xl">DJs</h2>
+ </div>
+ <span className="kicker text-ash">
+ {String(djs.length).padStart(2, "0")} djs
+ </span>
+ </div>
+
+ {djs.length === 0 ? (
+ <p className="serif mt-8 text-xl text-ash">
+ No DJs yet. Spin up the first one below.
+ </p>
+ ) : (
+ <ul className="mt-6">
+ {djs.map((d, i) => (
+ <li
+ key={d.id}
+ className="flex items-center gap-5 border-b border-[var(--line)] py-6"
+ >
+ <span className="kicker w-8 text-gold">
+ {String(i + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1">
+ <p className="display text-2xl sm:text-3xl">{d.name}</p>
+ {d.bio ? (
+ <p className="serif mt-1 text-lg text-ash">{d.bio}</p>
+ ) : null}
+ </div>
+ <span className="kicker border border-[var(--line)] px-3 py-1">
+ DJ
+ </span>
+ </li>
+ ))}
+ </ul>
+ )}
+
+ <form action={addDj} className="mt-8 flex flex-col gap-3">
+ <input
+ name="name"
+ required
+ placeholder="DJ name (e.g. DJ Cadence)"
+ className="serif border-b border-[var(--line)] bg-transparent py-3 text-2xl text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <input
+ name="bio"
+ placeholder="One line (optional)"
+ className="serif border-b border-[var(--line)] bg-transparent py-2 text-lg text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <div className="mt-2">
+ <button
+ type="submit"
+ className="kicker bg-gold px-6 py-3 text-stage hover:bg-gold-bright"
+ >
+ Add DJ
+ </button>
+ </div>
+ </form>
+ </section>
+
+ {/* Promoters */}
+ <section className="mt-16 border-t border-[var(--line)] pt-10">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="kicker text-gold">Section 04</p>
+ <h2 className="display mt-2 text-4xl">Promoters</h2>
+ </div>
+ <span className="kicker text-ash">
+ {String(promoters.length).padStart(2, "0")} promoters
+ </span>
+ </div>
+
+ {promoters.length === 0 ? (
+ <p className="serif mt-8 text-xl text-ash">
+ No promoters yet. Name the first one putting on the show below.
+ </p>
+ ) : (
+ <ul className="mt-6">
+ {promoters.map((p, i) => (
+ <li
+ key={p.id}
+ className="flex items-center gap-5 border-b border-[var(--line)] py-6"
+ >
+ <span className="kicker w-8 text-gold">
+ {String(i + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1">
+ <p className="display text-2xl sm:text-3xl">{p.name}</p>
+ {p.bio ? (
+ <p className="serif mt-1 text-lg text-ash">{p.bio}</p>
+ ) : null}
+ </div>
+ <span className="kicker border border-[var(--line)] px-3 py-1">
+ PROMOTER
+ </span>
+ </li>
+ ))}
+ </ul>
+ )}
+
+ <form action={addPromoter} className="mt-8 flex flex-col gap-3">
+ <input
+ name="name"
+ required
+ placeholder="Promoter or brand (e.g. Yardline Events)"
+ className="serif border-b border-[var(--line)] bg-transparent py-3 text-2xl text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <input
+ name="bio"
+ placeholder="One line (optional)"
+ className="serif border-b border-[var(--line)] bg-transparent py-2 text-lg text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <div className="mt-2">
+ <button
+ type="submit"
+ className="kicker bg-gold px-6 py-3 text-stage hover:bg-gold-bright"
+ >
+ Add promoter
+ </button>
+ </div>
+ </form>
+ </section>
+
+ {/* Classes */}
+ <section className="mt-16 border-t border-[var(--line)] pt-10">
+ <div className="flex items-end justify-between">
+ <div>
+ <p className="kicker text-gold">Section 05</p>
+ <h2 className="display mt-2 text-4xl">Classes</h2>
+ </div>
+ <span className="kicker text-ash">
+ {String(classes.length).padStart(2, "0")} classes
+ </span>
+ </div>
+
+ {classes.length === 0 ? (
+ <p className="serif mt-8 text-xl text-ash">
+ No classes yet. Teach the first line below.
+ </p>
+ ) : (
+ <ul className="mt-6">
+ {classes.map((c, i) => (
+ <li
+ key={c.id}
+ className="flex items-center gap-5 border-b border-[var(--line)] py-6"
+ >
+ <span className="kicker w-8 text-gold">
+ {String(i + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1">
+ <p className="display text-2xl sm:text-3xl">{c.title}</p>
+ <p className="kicker mt-1 text-ash">
+ {[
+ c.instructor ? `WITH ${c.instructor.toUpperCase()}` : null,
+ c.schedule ? c.schedule.toUpperCase() : null,
+ ]
+ .filter(Boolean)
+ .join(" · ") || "SCHEDULE TBD"}
+ </p>
+ </div>
+ <span className="kicker border border-[var(--line)] px-3 py-1">
+ {c.isFree ? "FREE" : "CLASS"}
+ </span>
+ </li>
+ ))}
+ </ul>
+ )}
+
+ <form action={addClass} className="mt-8 flex flex-col gap-3">
+ <input
+ name="title"
+ required
+ placeholder="Class title (e.g. Intro to Stepping)"
+ className="serif border-b border-[var(--line)] bg-transparent py-3 text-2xl text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <div className="mt-2 flex flex-wrap items-center gap-4">
+ <input
+ name="instructor"
+ placeholder="Instructor"
+ className="kicker border border-[var(--line)] bg-stage px-4 py-3 text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <input
+ name="schedule"
+ placeholder="Schedule (e.g. Tuesdays 7pm)"
+ className="kicker border border-[var(--line)] bg-stage px-4 py-3 text-bone outline-none placeholder:text-ash focus:border-gold"
+ />
+ <label className="kicker flex items-center gap-2 text-ash">
+ <input
+ type="checkbox"
+ name="isFree"
+ className="h-4 w-4 accent-gold"
+ />
+ Free
+ </label>
+ <button
+ type="submit"
+ className="kicker bg-gold px-6 py-3 text-stage hover:bg-gold-bright"
+ >
+ Add class
+ </button>
+ </div>
+ </form>
+ </section>
+ </main>
+ );
+}
apps/web/src/app/page.tsx +5 -1
@@ -1,6 +1,7 @@
import Link from "next/link";
import { serverCaller } from "@/lib/trpc/server";
import { Marquee } from "@/components/marquee";
+import { HeroCanvas } from "@/components/hero-canvas";
export const dynamic = "force-dynamic";
@@ -62,7 +63,9 @@ export default async function Home() {
</header>
{/* STAGE */}
- <section className="flex min-h-[100svh] flex-col justify-center px-5 pt-24 sm:px-8">
+ <section className="relative flex min-h-[100svh] flex-col justify-center overflow-hidden px-5 pt-24 sm:px-8">
+ <HeroCanvas />
+ <div className="relative z-10">
<p className="kicker beat-in b1">On Beat / Divine Nine Stepping</p>
<h1 className="display mt-6 text-[20vw] leading-[0.82] sm:text-[16vw]">
<span className="block beat-in b1">STOMP.</span>
@@ -85,6 +88,7 @@ export default async function Home() {
))}
<span className="kicker text-ash">More cities loading</span>
</div>
+ </div>
</section>
<Marquee />
apps/web/src/components/hero-canvas.tsx +22 -0
@@ -0,0 +1,22 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
+
+const HeroScene = dynamic(() => import("./hero-scene"), { ssr: false });
+
+export function HeroCanvas() {
+ const [show, setShow] = useState(false);
+
+ useEffect(() => {
+ const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ if (!reduce) setShow(true);
+ }, []);
+
+ if (!show) return null;
+ return (
+ <div className="pointer-events-none absolute inset-0 z-0 opacity-90">
+ <HeroScene />
+ </div>
+ );
+}
apps/web/src/components/hero-scene.tsx +115 -0
@@ -0,0 +1,115 @@
+"use client";
+
+import { Canvas, useFrame, useThree } from "@react-three/fiber";
+import { useEffect, useMemo, useRef } from "react";
+import * as THREE from "three";
+
+const vertex = /* glsl */ `
+ varying vec2 vUv;
+ void main() {
+ vUv = uv;
+ gl_Position = vec4(position.x, position.y, 0.0, 1.0);
+ }
+`;
+
+// Stomp shockwave stage-floor. Concentric gold rings emanate on a beat and
+// react to the cursor, on the warm-black stage. One signature object, subtle.
+const fragment = /* glsl */ `
+ precision highp float;
+ varying vec2 vUv;
+ uniform float uTime;
+ uniform vec2 uMouse;
+ uniform float uAspect;
+
+ const vec3 STAGE = vec3(0.059, 0.055, 0.047);
+ const vec3 GOLD = vec3(0.784, 0.635, 0.298);
+
+ float hash(vec2 p) {
+ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
+ }
+
+ void main() {
+ vec2 p = vUv - 0.5;
+ p.x *= uAspect;
+ vec2 m = uMouse - 0.5;
+ m.x *= uAspect;
+
+ float d = length(p);
+ float beat = 0.82 + 0.18 * sin(uTime * 100.0 / 60.0 * 3.14159);
+
+ float rings = 0.0;
+ float t = uTime * 0.42;
+ for (int i = 0; i < 4; i++) {
+ float phase = fract(t + float(i) * 0.25);
+ float radius = phase * 1.05;
+ float band = smoothstep(0.018, 0.0, abs(d - radius));
+ rings += band * (1.0 - phase);
+ }
+ rings *= beat;
+
+ float md = length(p - m);
+ float cursor = smoothstep(0.32, 0.0, md);
+ float cursorRing = smoothstep(0.01, 0.0, abs(md - 0.14)) * 0.6;
+
+ float energy = rings * 0.55 + cursor * 0.28 + cursorRing;
+ vec3 col = STAGE + GOLD * energy * 0.6;
+
+ col *= smoothstep(1.25, 0.15, d);
+ col += (hash(vUv * 900.0 + uTime) - 0.5) * 0.012;
+
+ gl_FragColor = vec4(col, 1.0);
+ }
+`;
+
+function Floor() {
+ const mat = useRef<THREE.ShaderMaterial>(null);
+ const target = useRef(new THREE.Vector2(0.5, 0.5));
+ const { size } = useThree();
+
+ const uniforms = useMemo(
+ () => ({
+ uTime: { value: 0 },
+ uMouse: { value: new THREE.Vector2(0.5, 0.5) },
+ uAspect: { value: 1 },
+ }),
+ [],
+ );
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ target.current.set(e.clientX / window.innerWidth, 1 - e.clientY / window.innerHeight);
+ };
+ window.addEventListener("mousemove", onMove);
+ return () => window.removeEventListener("mousemove", onMove);
+ }, []);
+
+ useFrame((_, dt) => {
+ const u = mat.current?.uniforms;
+ if (!u) return;
+ u.uTime.value += dt;
+ u.uAspect.value = size.width / size.height;
+ const m = u.uMouse.value as THREE.Vector2;
+ m.x += (target.current.x - m.x) * 0.06;
+ m.y += (target.current.y - m.y) * 0.06;
+ });
+
+ return (
+ <mesh>
+ <planeGeometry args={[2, 2]} />
+ <shaderMaterial
+ ref={mat}
+ uniforms={uniforms}
+ vertexShader={vertex}
+ fragmentShader={fragment}
+ />
+ </mesh>
+ );
+}
+
+export default function HeroScene() {
+ return (
+ <Canvas dpr={[1, 2]} gl={{ antialias: true }} style={{ width: "100%", height: "100%" }}>
+ <Floor />
+ </Canvas>
+ );
+}
packages/api/src/root.ts +8 -0
@@ -8,6 +8,7 @@ import {
djs,
events,
flags,
+ organizations,
promoters,
stepTeams,
videos,
@@ -46,6 +47,13 @@ export const appRouter = router({
),
}),
+ // ---- Organizations (Divine Nine reference set) ---------------------------
+ organization: router({
+ list: publicProcedure.query(({ ctx }) =>
+ ctx.db.select().from(organizations),
+ ),
+ }),
+
// ---- Events --------------------------------------------------------------
event: router({
list: publicProcedure
pnpm-lock.yaml +203 -0
@@ -20,6 +20,9 @@ importers:
apps/web:
dependencies:
+ '@react-three/fiber':
+ specifier: ^9.1.0
+ version: 9.6.1(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.171.0)
'@stepping/api':
specifier: workspace:*
version: link:../../packages/api
@@ -68,6 +71,9 @@ importers:
superjson:
specifier: ^2.2.2
version: 2.2.6
+ three:
+ specifier: ^0.171.0
+ version: 0.171.0
zod:
specifier: ^3.24.1
version: 3.25.76
@@ -84,6 +90,9 @@ importers:
'@types/react-dom':
specifier: ^19
version: 19.2.3(@types/react@19.2.17)
+ '@types/three':
+ specifier: ^0.171.0
+ version: 0.171.0
eslint:
specifier: ^9
version: 9.39.4(jiti@2.7.0)
@@ -241,6 +250,10 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/runtime@7.29.7':
+ resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'}
@@ -992,6 +1005,31 @@ packages:
'@petamoriken/float16@3.9.3':
resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==}
+ '@react-three/fiber@9.6.1':
+ resolution: {integrity: sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==}
+ 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
+
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -1178,6 +1216,9 @@ packages:
cpu: [arm64]
os: [win32]
+ '@tweenjs/tween.js@23.1.3':
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
+
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
@@ -1201,9 +1242,23 @@ packages:
peerDependencies:
'@types/react': ^19.2.0
+ '@types/react-reconciler@0.28.9':
+ resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
+ peerDependencies:
+ '@types/react': '*'
+
'@types/react@19.2.17':
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
+ '@types/stats.js@0.17.4':
+ resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
+
+ '@types/three@0.171.0':
+ resolution: {integrity: sha512-oLuT1SAsT+CUg/wxUTFHo0K3NtJLnx9sJhZWQJp/0uXqFpzSk1hRHmvWvpaAWSfvx2db0lVKZ5/wV0I0isD2mQ==}
+
+ '@types/webxr@0.5.24':
+ resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
+
'@typescript-eslint/eslint-plugin@8.62.0':
resolution: {integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1373,6 +1428,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@webgpu/types@0.1.71':
+ resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==}
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1455,6 +1513,9 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
baseline-browser-mapping@2.10.38:
resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==}
engines: {node: '>=6.0.0'}
@@ -1479,6 +1540,9 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+ buffer@6.0.3:
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
+
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1911,6 +1975,9 @@ packages:
picomatch:
optional: true
+ fflate@0.8.3:
+ resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -2041,6 +2108,9 @@ packages:
resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==}
engines: {node: '>=20.0.0'}
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2186,6 +2256,11 @@ packages:
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
engines: {node: '>= 0.4'}
+ its-fine@2.0.0:
+ resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
+ peerDependencies:
+ react: ^19.0.0
+
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
@@ -2347,6 +2422,9 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
+ meshoptimizer@0.18.1:
+ resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
+
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
@@ -2523,6 +2601,15 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-use-measure@2.1.7:
+ resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
+ peerDependencies:
+ react: '>=16.13'
+ react-dom: '>=16.13'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -2696,6 +2783,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
+ suspend-react@0.1.3:
+ resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
+ peerDependencies:
+ react: '>=17.0'
+
tailwindcss@4.3.1:
resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==}
@@ -2703,6 +2795,9 @@ packages:
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
engines: {node: '>=6'}
+ three@0.171.0:
+ resolution: {integrity: sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==}
+
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
@@ -2783,6 +2878,11 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -2829,6 +2929,24 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+ zustand@5.0.14:
+ resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==}
+ 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
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -2910,6 +3028,8 @@ snapshots:
dependencies:
'@babel/types': 7.29.7
+ '@babel/runtime@7.29.7': {}
+
'@babel/template@7.29.7':
dependencies:
'@babel/code-frame': 7.29.7
@@ -3410,6 +3530,26 @@ snapshots:
'@petamoriken/float16@3.9.3': {}
+ '@react-three/fiber@9.6.1(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.171.0)':
+ dependencies:
+ '@babel/runtime': 7.29.7
+ '@types/webxr': 0.5.24
+ base64-js: 1.5.1
+ buffer: 6.0.3
+ its-fine: 2.0.0(@types/react@19.2.17)(react@19.2.4)
+ react: 19.2.4
+ react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ scheduler: 0.27.0
+ suspend-react: 0.1.3(react@19.2.4)
+ three: 0.171.0
+ use-sync-external-store: 1.6.0(react@19.2.4)
+ zustand: 5.0.14(@types/react@19.2.17)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
+ optionalDependencies:
+ react-dom: 19.2.4(react@19.2.4)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+
'@rtsao/scc@1.1.0': {}
'@supabase/auth-js@2.108.2':
@@ -3565,6 +3705,8 @@ snapshots:
'@turbo/windows-arm64@2.10.0':
optional: true
+ '@tweenjs/tween.js@23.1.3': {}
+
'@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
@@ -3586,10 +3728,27 @@ snapshots:
dependencies:
'@types/react': 19.2.17
+ '@types/react-reconciler@0.28.9(@types/react@19.2.17)':
+ dependencies:
+ '@types/react': 19.2.17
+
'@types/react@19.2.17':
dependencies:
csstype: 3.2.3
+ '@types/stats.js@0.17.4': {}
+
+ '@types/three@0.171.0':
+ dependencies:
+ '@tweenjs/tween.js': 23.1.3
+ '@types/stats.js': 0.17.4
+ '@types/webxr': 0.5.24
+ '@webgpu/types': 0.1.71
+ fflate: 0.8.3
+ meshoptimizer: 0.18.1
+
+ '@types/webxr@0.5.24': {}
+
'@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -3751,6 +3910,8 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true
+ '@webgpu/types@0.1.71': {}
+
acorn-jsx@5.3.2(acorn@8.17.0):
dependencies:
acorn: 8.17.0
@@ -3855,6 +4016,8 @@ snapshots:
balanced-match@4.0.4: {}
+ base64-js@1.5.1: {}
+
baseline-browser-mapping@2.10.38: {}
brace-expansion@1.1.15:
@@ -3880,6 +4043,11 @@ snapshots:
buffer-from@1.1.2: {}
+ buffer@6.0.3:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -4443,6 +4611,8 @@ snapshots:
optionalDependencies:
picomatch: 4.0.4
+ fflate@0.8.3: {}
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -4580,6 +4750,8 @@ snapshots:
iceberg-js@0.8.1: {}
+ ieee754@1.2.1: {}
+
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -4730,6 +4902,13 @@ snapshots:
has-symbols: 1.1.0
set-function-name: 2.0.2
+ its-fine@2.0.0(@types/react@19.2.17)(react@19.2.4):
+ dependencies:
+ '@types/react-reconciler': 0.28.9(@types/react@19.2.17)
+ react: 19.2.4
+ transitivePeerDependencies:
+ - '@types/react'
+
jiti@2.7.0: {}
js-tokens@4.0.0: {}
@@ -4849,6 +5028,8 @@ snapshots:
merge2@1.4.1: {}
+ meshoptimizer@0.18.1: {}
+
micromatch@4.0.8:
dependencies:
braces: 3.0.3
@@ -5023,6 +5204,12 @@ snapshots:
react-is@16.13.1: {}
+ react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ optionalDependencies:
+ react-dom: 19.2.4(react@19.2.4)
+
react@19.2.4: {}
reflect.getprototypeof@1.0.10:
@@ -5267,10 +5454,16 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ suspend-react@0.1.3(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
tailwindcss@4.3.1: {}
tapable@2.3.3: {}
+ three@0.171.0: {}
+
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
@@ -5404,6 +5597,10 @@ snapshots:
dependencies:
punycode: 2.3.1
+ use-sync-external-store@1.6.0(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -5464,3 +5661,9 @@ snapshots:
zod: 3.25.76
zod@3.25.76: {}
+
+ zustand@5.0.14(@types/react@19.2.17)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
+ optionalDependencies:
+ '@types/react': 19.2.17
+ react: 19.2.4
+ use-sync-external-store: 1.6.0(react@19.2.4)