"use client";
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from "react";
import { GaussianSplatViewer } from "./GaussianSplatViewer";
import { scenes } from "./scenes";
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 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 SplatViewer({ sceneIndex }: { sceneIndex: number }) {
const scene = scenes[sceneIndex]!;
const v = scene.viewer;
return (
);
}
function SceneListItem({
scene,
index,
isSelected,
onSelect,
}: {
scene: (typeof scenes)[number];
index: number;
isSelected: boolean;
onSelect: (i: number) => void;
}) {
return (
);
}
function DesktopLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = scenes[selectedIndex]!;
return (
{selected.description}
);
}
function MobileLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const [showJson, setShowJson] = useState(false);
const [showScenes, setShowScenes] = useState(false);
const selected = scenes[selectedIndex]!;
const scenePanelRef = useRef(null);
const jsonPanelRef = useRef(null);
const closeScenes = useCallback(() => setShowScenes(false), []);
const closeJson = useCallback(() => setShowJson(false), []);
// Close overlays on Escape
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (showScenes) closeScenes();
if (showJson) closeJson();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [showScenes, showJson, closeScenes, closeJson]);
// Focus the panel when it opens
useEffect(() => {
if (showScenes) scenePanelRef.current?.focus();
}, [showScenes]);
useEffect(() => {
if (showJson) jsonPanelRef.current?.focus();
}, [showJson]);
return (
{selected.description}
{showScenes && (
<>
Splat Scenes
{scenes.map((scene, i) => (
{
setSelectedIndex(idx);
closeScenes();
}}
/>
))}
>
)}
{showJson && (
<>
>
)}
);
}
export default function Page() {
const isMobile = useIsMobile();
return isMobile ? : ;
}