Add 3D clamshell phone dialer spike + persistent-board design

A diegetic 90s handset rendered as a small three.js scene in a fixed 1:2
portrait stage (visual spike at /?phone=1, three.js lazy-loaded so the
board bundle is unchanged). Blocky flat-shaded clamshell with glowing
keys, a flip-open animation with an intro camera orbit that settles
head-on before the DOM screen appears, a hinge barrel on the pivot axis,
and a chubby antenna. Pressable 3D keypad (raycast + keyboard) with DTMF
tones drives a dialer: connect / voicemail / SIT "unobtainable", against
a stub number->node directory with flag-gated node enablement.

The screen UI is real DOM positioned in percent of the stage, kept
pixel-exact by the head-on ortho camera.

Also documents the persistent-board flow model (docs/persistent-boards.md):
board_key reuse across level nodes, present-but-hidden flag-gated exhibits
with live arrival reveals, reset/new-game semantics, and A/B/M citation codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 12:14:11 +02:00
co-authored by Claude Opus 4.8
parent 1b3ff1e78c
commit 1893bf23af
6 changed files with 576 additions and 2 deletions
+9 -2
View File
@@ -1,6 +1,13 @@
import { StrictMode } from 'react'
import { StrictMode, Suspense, lazy } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './App'
import './styles.css'
createRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>)
// three.js is lazy-loaded so the board never pays for it up front.
const PhonePreview = lazy(() => import('./phone').then(m => ({ default: m.PhonePreview })))
// Visual spike: /?phone=1 renders the handset standalone, isolated from the board.
const root = new URLSearchParams(window.location.search).has('phone')
? <Suspense fallback={null}><PhonePreview /></Suspense>
: <App />
createRoot(document.getElementById('root')!).render(<StrictMode>{root}</StrictMode>)