Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
408de04
feat: centralize live simulation policies and density selectors
ummsehun Apr 5, 2026
fe94037
feat:Pr-1 clear
ummsehun Apr 5, 2026
130731c
refactor: centralize camera controller policies in app config
ummsehun Apr 5, 2026
40ac878
feat: add reusable UI primitives for HUD composition
ummsehun Apr 5, 2026
b0ffd82
refactor: adopt UI primitives in playback and scene info HUD
ummsehun Apr 5, 2026
b3584d7
refactor: add semantic surface and text utility classes
ummsehun Apr 5, 2026
68420f5
refactor: map UI primitive tones to semantic token classes
ummsehun Apr 5, 2026
717a2fe
refactor: apply semantic UI tokens across home, loading, and HUD
ummsehun Apr 5, 2026
ba6bf18
feat: add in-scene performance instrumentation and HUD
ummsehun Apr 5, 2026
2f1a605
refactor: reduce per-frame allocations in place scene runtime
ummsehun Apr 5, 2026
08168e0
feat: add trackpad and touch look support for walk camera
ummsehun Apr 5, 2026
70a2616
refactor: polish HUD language and camera mode wording
ummsehun Apr 5, 2026
a8a9170
feat: improve trackpad camera control without modifier lock-in
ummsehun Apr 5, 2026
bda84a0
feat: add trackpad vertical zoom for walk camera
ummsehun Apr 5, 2026
a3830ef
feat: enable two-finger zoom gestures for trackpad
ummsehun Apr 5, 2026
1385539
feat: support both two-finger scroll and pinch zoom gestures
ummsehun Apr 5, 2026
e282f2b
fix: enable spread/pinch zoom path reliably
ummsehun Apr 5, 2026
db5bfcc
feat:pinch out add
ummsehun Apr 5, 2026
b245528
feat:pad UX update
ummsehun Apr 6, 2026
c6c81d1
feat:CameraController module
ummsehun Apr 6, 2026
73b836b
feat:phase02 clear
ummsehun Apr 6, 2026
6271455
feat:scene module
ummsehun Apr 6, 2026
06803a4
feat:i18n add
ummsehun Apr 6, 2026
99c9e90
feat:exploerer UI fixd
ummsehun Apr 6, 2026
41e390c
feat:Cinematic scene delete
ummsehun Apr 6, 2026
6795489
feat:Layer-4 fixd
ummsehun Apr 6, 2026
7c7c2b2
feat:cn utilty add
ummsehun Apr 6, 2026
4f07cf6
feat:3D Optimization
ummsehun Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .bkit/state/memory.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"version": "1.0",
"lastUpdated": "2026-04-04T06:57:03.197Z",
"lastUpdated": "2026-04-05T23:22:41.783Z",
"platform": "gemini",
"data": {
"session": {
"sessionCount": 2,
"lastSessionStarted": "2026-04-04T06:57:03.197Z",
"lastSessionEnded": "2026-04-04T06:57:02.768Z"
"sessionCount": 3,
"lastSessionStarted": "2026-04-05T23:18:25.147Z",
"lastSessionEnded": "2026-04-05T23:22:41.783Z"
},
"pdca": {
"lastFeature": null,
Expand Down
16 changes: 14 additions & 2 deletions .bkit/state/pdca-status.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "3.0",
"lastUpdated": "2026-04-04T06:57:02.805Z",
"lastUpdated": "2026-04-05T23:22:41.783Z",
"activeFeatures": {},
"primaryFeature": null,
"archivedFeatures": {},
Expand All @@ -12,14 +12,26 @@
"session": {
"startedAt": "2026-04-04T06:56:16.748Z",
"onboardingCompleted": false,
"lastActivity": "2026-04-04T06:57:02.767Z"
"lastActivity": "2026-04-05T23:22:41.782Z"
},
"history": [
{
"timestamp": "2026-04-04T06:57:02.768Z",
"action": "session_end",
"feature": null,
"details": "Session ended"
},
{
"timestamp": "2026-04-05T23:22:41.751Z",
"action": "session_end",
"feature": null,
"details": "Session ended"
},
{
"timestamp": "2026-04-05T23:22:41.783Z",
"action": "session_end",
"feature": null,
"details": "Session ended"
}
]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.bkit
20 changes: 13 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
"tailwind-merge": "^3.5.0",
"three": "^0.183.2",
"zod": "^4.3.6",
"zustand": "^5.0.12"
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/live/[slug]/places/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLivePlaceSnapshot } from "../../../../../shared/contracts";
import { normalizeLiveLevel } from "../../../../../shared/selectors";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -26,8 +27,7 @@ export async function GET(
}

const bootstrap = createStaticSceneBootstrap(place);
const raw = request.nextUrl.searchParams.get("density") ?? "medium";
const density = raw === "low" || raw === "high" ? raw : "medium";
const density = normalizeLiveLevel(request.nextUrl.searchParams.get("density"));

const snapshot = validateLivePlaceSnapshot({
geometryId: bootstrap.geometryId,
Expand Down
9 changes: 5 additions & 4 deletions src/app/api/live/[slug]/traffic/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveTrafficSnapshot } from "../../../../../shared/contracts";
import { normalizeHour, toTrafficPolicy } from "../../../../../shared/domains";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -27,14 +28,14 @@ export async function GET(

const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;
const rushHour = (normalizedHour >= 8 && normalizedHour <= 10) || (normalizedHour >= 17 && normalizedHour <= 20);
const normalizedHour = normalizeHour(hour);
const trafficPolicy = toTrafficPolicy(normalizedHour);

const payload = validateLiveTrafficSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "traffic"),
density: rushHour ? "high" : "medium",
speedKph: rushHour ? 18 : 32,
density: trafficPolicy.density,
speedKph: trafficPolicy.speedKph,
capturedAtIso: new Date().toISOString(),
});

Expand Down
15 changes: 5 additions & 10 deletions src/app/api/live/[slug]/weather/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveWeatherSnapshot } from "../../../../../shared/contracts";
import { buildWeatherPolicy, normalizeHour } from "../../../../../shared/domains";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
Expand All @@ -27,20 +28,14 @@ export async function GET(

const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;

const condition =
normalizedHour >= 6 && normalizedHour < 17
? "clear"
: normalizedHour >= 17 && normalizedHour < 21
? "cloudy"
: "rain";
const normalizedHour = normalizeHour(hour);
const weatherPolicy = buildWeatherPolicy(normalizedHour);

const snapshot = validateLiveWeatherSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "weather"),
condition,
temperatureCelsius: condition === "rain" ? 10 : 18,
condition: weatherPolicy.condition,
temperatureCelsius: weatherPolicy.temperatureCelsius,
capturedAtIso: new Date().toISOString(),
});

Expand Down
98 changes: 98 additions & 0 deletions src/app/explorer/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";

import GlobeScene from "@/src/globe/GlobeScene";
import { MVP_PLACES } from "@/src/data/places";
import type { Place } from "@/src/types/place";
import { Globe, MapPin, Search, ArrowLeft, Map } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "@/src/stores/useI18nStore";

export default function ExplorerHome() {
const { t } = useTranslation();

return (
<main className="relative flex h-screen w-full bg-[#101214] overflow-hidden font-sans">
<GlobeScene places={MVP_PLACES} />

{/* Top Header - Redesigned to match the dark theme */}
<div className="pointer-events-none absolute left-8 top-8 z-10 flex flex-col gap-4 animate-in fade-in slide-in-from-top-4 duration-1000">

{/* Back Button */}
<Link href="/" className="pointer-events-auto flex w-fit items-center gap-2 px-4 py-2 bg-[#16181A]/90 border border-zinc-800 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors backdrop-blur-md shadow-lg">
<ArrowLeft size={16} />
<span className="text-sm font-medium">Dashboard</span>
</Link>

<div className="bg-[#16181A]/90 border border-zinc-800 rounded-2xl px-6 py-5 w-80 backdrop-blur-md shadow-2xl">
<div className="flex items-center gap-3">
<div className="bg-blue-500/10 text-blue-500 border border-blue-500/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl">
<Globe size={20} className="animate-spin-slow" />
</div>
<div>
<p className="text-blue-500 text-[10px] font-bold uppercase tracking-[0.2em]">{t('explorer.engine')}</p>
<h1 className="text-zinc-100 text-lg font-bold tracking-tight leading-tight">{t('explorer.title')}</h1>
</div>
</div>

<div className="mt-5 flex h-10 cursor-text items-center gap-3 px-4 text-xs font-medium text-zinc-400 border border-zinc-700/50 bg-zinc-900/50 rounded-lg pointer-events-auto hover:border-zinc-500 transition-colors">
<Search size={14} className="opacity-70" />
<span>{t('explorer.search')}</span>
</div>

<div className="mt-5 space-y-2">
<div className="flex items-center justify-between text-[11px] font-semibold uppercase tracking-wider text-zinc-500">
<span>{t('explorer.systemStatus')}</span>
<div className="flex items-center gap-1.5 opacity-90">
<span className="w-1.5 h-1.5 rounded-full bg-green-500 ring-2 ring-green-500/20 animate-pulse" />
<span className="text-green-500">{t('explorer.online')}</span>
</div>
</div>
</div>
</div>
</div>

{/* Bottom Place Selector */}
<div className="pointer-events-none absolute inset-x-0 bottom-12 z-10 flex justify-center px-4 md:px-8 animate-in fade-in slide-in-from-bottom-8 duration-700 delay-300">
<div className="flex flex-wrap items-end justify-center gap-4 md:gap-5">
{MVP_PLACES.map((place: Place) => (
<Link
key={place.id}
href={`/place/${place.slug}`}
className="pointer-events-auto group relative flex flex-col"
>
<div className="absolute -top-10 left-1/2 -translate-x-1/2 opacity-0 scale-95 group-hover:scale-100 group-hover:opacity-100 transition-all duration-300 pointer-events-none z-20">
<div className="bg-[#1e2024] border border-zinc-700 shadow-xl whitespace-nowrap px-4 py-2 rounded-md text-xs font-semibold tracking-wide text-zinc-200 flex items-center gap-2">
<MapPin size={12} className="text-blue-400" />
{t('explorer.travelTo', { name: place.name })}
</div>
{/* Tooltip triangle */}
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-[#1e2024] border-b border-r border-zinc-700 rotate-45"></div>
</div>

<div className="w-40 md:w-48 overflow-hidden rounded-xl border border-zinc-800 bg-[#16181A]/90 backdrop-blur-md shadow-lg transition-all duration-300 group-hover:-translate-y-2 group-hover:border-blue-500/50 group-hover:shadow-[0_8px_30px_rgb(59,130,246,0.2)]">
<div className="aspect-[16/10] bg-zinc-900 overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent z-10" />
<div className="absolute inset-0 flex items-center justify-center opacity-30 group-hover:opacity-60 group-hover:scale-110 transition-all duration-700">
<MapPin size={40} className="text-blue-500 drop-shadow-[0_0_15px_rgba(59,130,246,0.5)]" />
</div>
<div className="absolute bottom-3 left-4 z-20">
<p className="text-blue-400 text-[9px] md:text-[10px] font-bold uppercase tracking-widest mb-0.5">{place.city}</p>
<h3 className="text-zinc-100 text-xs md:text-sm font-semibold leading-tight">{place.name}</h3>
</div>
</div>
</div>
</Link>
))}
</div>
</div>

{/* Info Badge */}
<div className="pointer-events-none absolute right-4 bottom-4 md:right-8 md:bottom-8 z-10 bg-[#16181A]/90 border border-zinc-800 shadow-lg px-4 py-2.5 rounded-lg flex items-center gap-3 animate-in fade-in duration-1000 backdrop-blur-md hidden sm:flex">
<Map size={14} className="text-zinc-500" />
<p className="text-zinc-400 text-[10px] md:text-[11px] font-medium tracking-wide">
{t('explorer.coordinates')}: <span className="text-zinc-200 font-mono tracking-tight ml-1">37.5665° N, 126.9780° E</span>
</p>
</div>
</main>
);
}
Loading