"use client"; import { useState, useSyncExternalStore, useCallback } from "react"; import { defineCatalog } from "@json-render/core"; import type { ComputedFunction } from "@json-render/core"; import { schema, defineRegistry } from "@json-render/react"; import { threeComponentDefinitions, threeComponents, ThreeCanvas, } from "@json-render/react-three-fiber"; import { scenes } from "./scenes"; const catalog = defineCatalog(schema, { components: { ...threeComponentDefinitions, }, actions: {}, }); const { registry } = defineRegistry(catalog, { components: { ...threeComponents, }, }); function num(v: unknown, fallback: number): number { return typeof v === "number" ? v : fallback; } const computedFunctions: Record = { halfHeight: (a) => num(a.h, 1) / 2, helixX: (a) => { const angle = (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI + num(a.strand, 0) * Math.PI; return num(a.radius, 1.8) * Math.cos(angle); }, helixY: (a) => num(a.i, 0) * num(a.spacing, 0.3) + num(a.offset, -6), helixZ: (a) => { const angle = (num(a.i, 0) / num(a.count, 40)) * num(a.turns, 3) * 2 * Math.PI + num(a.strand, 0) * Math.PI; return num(a.radius, 1.8) * Math.sin(angle); }, helixHue: (a) => { const t = num(a.i, 0) / num(a.count, 40); return `hsl(${Math.round(t * 270 + 180)}, 85%, 60%)`; }, spiralX: (a) => { const t = num(a.i, 0) / num(a.count, 60); const r = 0.5 + t * num(a.maxRadius, 7); const angle = t * num(a.turns, 4) * 2 * Math.PI; return r * Math.cos(angle); }, spiralZ: (a) => { const t = num(a.i, 0) / num(a.count, 60); const r = 0.5 + t * num(a.maxRadius, 7); const angle = t * num(a.turns, 4) * 2 * Math.PI; return r * Math.sin(angle); }, spiralY: (a) => { const i = num(a.i, 0); return Math.sin(i * 1.7) * 0.4; }, spiralScale: (a) => { const t = num(a.i, 0) / num(a.count, 60); return 0.08 + (1 - t) * 0.25; }, spiralEmissive: (a) => { const t = num(a.i, 0) / num(a.count, 60); return Math.max(0, 1 - t * 1.5); }, circleX: (a) => { const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI; return num(a.radius, 3) * Math.cos(angle); }, circleZ: (a) => { const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI; return num(a.radius, 3) * Math.sin(angle); }, circleY: (a) => { const angle = (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI; return Math.sin(angle * num(a.freq, 3)) * num(a.amp, 0.3); }, circleAngle: (a) => { return (num(a.i, 0) / num(a.count, 8)) * 2 * Math.PI; }, circleHue: (a) => { const t = num(a.i, 0) / num(a.count, 8); return `hsl(${Math.round(t * 360)}, ${num(a.sat, 70)}%, ${num(a.lit, 80)}%)`; }, }; function highlightJson(json: string): string { return json.replace( /("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g, (_, key, colon, str, num, lit, punct) => { if (key) return `${key}${colon}`; if (str) return `${str}`; if (num) return `${num}`; if (lit) return `${lit}`; if (punct) return `${punct}`; return _; }, ); } const MOBILE_BREAKPOINT = 768; function subscribeToResize(cb: () => void) { window.addEventListener("resize", cb); return () => window.removeEventListener("resize", cb); } function getIsMobile() { return typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false; } function useIsMobile() { return useSyncExternalStore(subscribeToResize, getIsMobile, () => false); } const LIST_WIDTH = 220; const JSON_WIDTH = 380; const HEADER_HEIGHT = 40; const MOBILE_HEADER_HEIGHT = 48; const headerStyle: React.CSSProperties = { height: HEADER_HEIGHT, display: "flex", alignItems: "center", padding: "0 16px", fontSize: 11, fontWeight: 600, color: "#555", letterSpacing: "0.08em", textTransform: "uppercase", fontFamily: "ui-monospace, monospace", borderBottom: "1px solid #1e1e1e", flexShrink: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", boxSizing: "border-box", }; function MobileLayout() { const [selectedIndex, setSelectedIndex] = useState(0); const [showJson, setShowJson] = useState(false); const [showScenes, setShowScenes] = useState(false); const selected = scenes[selectedIndex]!; const closePanels = useCallback(() => { setShowJson(false); setShowScenes(false); }, []); return (
{selected.description}
{showScenes && ( <>
Scenes
{scenes.map((scene, i) => ( ))}
)} {showJson && ( <>
Spec JSON
            
)}
); } function DesktopLayout() { const [selectedIndex, setSelectedIndex] = useState(0); const selected = scenes[selectedIndex]!; return (
Scenes
{scenes.map((scene, i) => ( ))}
{selected.description}
Spec JSON
      
); } export default function Page() { const isMobile = useIsMobile(); return isMobile ? : ; }