// Home screen + app shell with routing, Tweaks integration.
const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useMemo: useMemoA } = React;
// Live countdown hook — returns ms remaining, re-renders each second
function useCountdown() {
const [ms, setMs] = useStateA(window.PLANETALIN.msUntilNextWindow());
useEffectA(() => {
const iv = setInterval(() => setMs(window.PLANETALIN.msUntilNextWindow()), 1000);
return () => clearInterval(iv);
}, []);
return ms;
}
function formatCountdown(ms) {
if (ms <= 0) return '00:00:00';
const s = Math.floor(ms / 1000);
const h = String(Math.floor(s / 3600)).padStart(2, '0');
const m = String(Math.floor((s % 3600) / 60)).padStart(2, '0');
const sec = String(s % 60).padStart(2, '0');
return `${h}:${m}:${sec}`;
}
function poeticWindow(ms) {
const h = ms / 3600000;
if (h > 20) return 'The sky has just closed.';
if (h > 16) return 'Stars are still settling.';
if (h > 10) return 'The cosmos is breathing.';
if (h > 5) return 'The window is narrowing to open.';
if (h > 1) return 'Soon. The light is gathering.';
if (h > 0.1) return 'Moments now. Listen for the bell.';
return 'The window opens.';
}
// ============ Home ============
function HomeHero({ starfieldDensity, animIntensity }) {
const canvasRef = useRefA(null);
const cooldownMs = useCountdown();
const canCreate = cooldownMs === 0;
const seal = useMemoA(() => window.PLANETALIN.getSeal(), []);
const myWorlds = window.PLANETALIN.allPlanets().length;
useEffectA(() => {
if (!canvasRef.current) return;
const seeds = window.PLANETALIN_HERO_SEEDS;
const spec = seeds[Math.floor(Math.random()*seeds.length)];
const scene = window.PLANETALIN.createPlanetScene(canvasRef.current, {
cameraDistance: 3.4,
starfieldDensity,
animIntensity,
});
scene.setSpec(spec);
return () => scene.dispose();
}, []);
return (
PL·01 · constellation lab
PLANETALIN
Compose an atmosphere from its elements.
Seal a world that time will carry forward.
canCreate && window.PLANETALIN.go('create')}
title={canCreate ? '' : poeticWindow(cooldownMs)}
>
{canCreate ? 'Begin a world' : 'The sky is closed'}
window.PLANETALIN.go('constellation')}>
{myWorlds > 0 ? `View your constellation (${myWorlds})` : 'View the cosmos'}
{!canCreate && (
{poeticWindow(cooldownMs)}
Next window opens in
{formatCountdown(cooldownMs)}
You may shape one world per day. The rest is rest.
)}
{seal.glyph}
your seal
{seal.name}
worlds shaped
{myWorlds}
);
}
// ============ Catalogue import/export ============
function CatalogueActions() {
const fileRef = useRefA(null);
const [flash, setFlash] = useStateA('');
function toast(msg) { setFlash(msg); setTimeout(() => setFlash(''), 2000); }
function exportAll() {
const data = window.PLANETALIN.exportCatalogueJson();
if (!data.planets.length) { toast('Nothing to export yet'); return; }
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `planetalin-catalogue-${data.seal?.name||'anon'}-${Date.now()}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
toast(`Exported ${data.planets.length} world${data.planets.length===1?'':'s'}`);
}
function triggerImport() { fileRef.current?.click(); }
async function onFile(e) {
const file = e.target.files?.[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
if (data.format !== 'planetalin.catalogue.v1') throw new Error('Not a PLANETALIN catalogue');
let n = 0;
for (const p of (data.planets||[])) {
if (window.PLANETALIN.importCataloguePlanet(p)) n++;
}
toast(`Imported ${n} world${n===1?'':'s'}`);
setTimeout(() => window.location.reload(), 900);
} catch(err) {
toast('Import failed: ' + err.message);
} finally {
e.target.value = '';
}
}
function seedSolarSystem() {
const n = window.PLANETALIN.seedSolarSystem();
if (n === 0) { toast('Solar system already catalogued'); return; }
toast(`Catalogued ${n} real planet${n===1?'':'s'} from our solar system`);
setTimeout(() => window.location.reload(), 900);
}
return (
↓ Export catalogue
·
↑ Import
·
☉ Seed solar system
{flash && {flash} }
);
}
// ============ Passkey Gate ============
// Renders before the wizard on every create attempt.
// Checks for a valid JWT first — if present, calls onAuthorized immediately.
// If not, prompts the user to register (first time) or sign in (returning).
//
// The passkey identifies every world created in this session and lets the
// user's constellation persist across devices and time.
function PasskeyGate({ onAuthorized }) {
const seal = useMemoA(() => window.PLANETALIN.getSeal(), []);
const isReturning = window.PLANETALIN.hasRegisteredPasskey?.() || false;
const supported = window.PLANETALIN.webAuthnSupported();
// 'check' | 'register' | 'login' | 'busy' | 'error'
const [phase, setPhase] = useStateA('check');
const [error, setError] = useStateA('');
useEffectA(() => {
const token = window.PLANETALIN.getToken?.();
if (window.PLANETALIN.isTokenValid?.(token)) {
onAuthorized(token);
return;
}
setPhase(isReturning ? 'login' : 'register');
}, []);
async function doRegister() {
setPhase('busy'); setError('');
try {
const result = await window.PLANETALIN.performRegistration(seal);
onAuthorized(result.token);
} catch(e) {
if (e.name === 'NotAllowedError') {
setError('Passkey was dismissed. Try again when you\'re ready.');
} else {
setError(e.message || 'Passkey creation failed');
}
setPhase('register');
}
}
async function doLogin() {
setPhase('busy'); setError('');
try {
const result = await window.PLANETALIN.performLogin(null); // discoverable
onAuthorized(result.token);
} catch(e) {
if (e.name === 'NotAllowedError') {
setError('Sign-in was dismissed.');
} else {
setError(e.message || 'Sign in failed');
}
setPhase('login');
}
}
if (phase === 'check') return null;
return (
{seal.glyph}
{phase === 'login' ? 'Welcome back, creator' : 'Identify your creation'}
{phase === 'register' || phase === 'busy' && !isReturning ? (
<>
A passkey saves your progress and signs every world you create —
so your constellation stays yours, across devices and across time.
Your private key never leaves your device.
>
) : (
<>
Use your passkey to continue. Your worlds and seal will be ready.
>
)}
{!supported && (
This browser does not support passkeys. Try Safari, Chrome, or Firefox on a modern device.
)}
{error &&
{error}
}
{phase === 'busy' && (
authorizing…
)}
{phase === 'register' && supported && (
Create a passkey → begin
{isReturning && (
{ setError(''); setPhase('login'); }}>
Already have one? Sign in
)}
)}
{phase === 'login' && supported && (
Continue with passkey
{ setError(''); setPhase('register'); }}>
Register a new passkey instead
)}
Sealed with {seal.name}
);
}
// ============ Create wizard host ============
function CreateFlow({ forkFromId, starfieldDensity, animIntensity }) {
const { defaultSpec, WIZ_STEPS, StepAtmosphere, StepSurface, StepMoons, StepRings, StepStar, StepSeal } = window.PLANETALINWizard;
// Token state — null means gate not yet passed
const [token, setToken] = useStateA(() => {
const t = window.PLANETALIN.getToken?.();
return window.PLANETALIN.isTokenValid?.(t) ? t : null;
});
const initial = useMemoA(() => {
if (forkFromId) {
const src = window.PLANETALIN.findById(forkFromId);
if (src) {
return { ...JSON.parse(JSON.stringify(src)), name: '', discoverer: '', id: undefined, createdAt: undefined };
}
}
return defaultSpec();
}, [forkFromId]);
const [spec, setSpec] = useStateA(initial);
const [stepIdx, setStepIdx] = useStateA(0);
const [sealState, setSealState] = useStateA('idle'); // idle | sealing | done | blocked | error
const canvasRef = useRefA(null);
const sceneRef = useRefA(null);
useEffectA(() => {
if (!canvasRef.current || !token) return; // don't init canvas until past gate
const scene = window.PLANETALIN.createPlanetScene(canvasRef.current, {
cameraDistance: 3.3,
starfieldDensity,
animIntensity,
});
sceneRef.current = scene;
scene.setSpec(spec);
return () => scene.dispose();
}, [token]); // re-run when token is set (gate cleared)
useEffectA(() => {
if (sceneRef.current) sceneRef.current.setSpec(spec);
}, [spec]);
useEffectA(() => {
if (sceneRef.current) sceneRef.current.update({ starfieldDensity, animIntensity });
}, [starfieldDensity, animIntensity]);
// ── Gate: show PasskeyGate until token is obtained ──────────────────────
if (!token) {
return setToken(tok)} />;
}
// ── Seal: call server to persist planet ──────────────────────────────────
async function onSeal() {
if (sealState === 'sealing' || sealState === 'done') return;
setSealState('sealing');
try {
const seal = window.PLANETALIN.getSeal();
const specWithMeta = {
...spec,
createdAt: new Date().toISOString(),
seal,
};
const result = await window.PLANETALIN.createPlanet(specWithMeta);
// result.spec has the server-stamped canonical PL-X##-YYY id
const savedSpec = typeof result.spec === 'string' ? JSON.parse(result.spec) : result.spec;
window.PLANETALIN.saveOne(savedSpec); // mirror locally for offline viewer access
window.PLANETALIN.markCreated(); // also update local cooldown for UI
setSealState('done');
setTimeout(() => window.PLANETALIN.go(`planet/${result.id}`), 1200);
} catch(e) {
if (e.code === 'COOLDOWN_ACTIVE') {
setSealState('blocked');
setTimeout(() => setSealState('idle'), 2600);
} else if (e.status === 401) {
// JWT expired mid-session — send back through the gate
window.PLANETALIN.clearToken?.();
setToken(null);
setSealState('idle');
} else {
setSealState('error');
setTimeout(() => setSealState('idle'), 2600);
}
}
}
const step = WIZ_STEPS[stepIdx];
const palette = window.PLANETALIN.derivePalette(spec.atmosphere || []);
return (
{(sealState === 'sealing' || sealState === 'done') && (
SEALED · IMMUTABLE · CATALOGUED · ETERNAL ·
{sealState==='sealing' && 'sealing the specification…'}
{sealState==='done' && 'world catalogued.'}
)}
Atmosphere {spec.atmosphere.length} elements
Surface {spec.surface}
Moons {spec.moons.length}
Rings {spec.rings.enabled?`${spec.rings.count} bands`:'none'}
Star {spec.star}-type
{spec.atmosphere.map(a => {
const el = window.PLANETALIN_ELEMENTS.find(e => e.sym === a.sym);
return
;
})}
{step.id === 'atmosphere' && }
{step.id === 'surface' && }
{step.id === 'moons' && }
{step.id === 'rings' && }
{step.id === 'star' && }
{step.id === 'seal' && }
);
}
// ============ Tweaks panel ============
function TweaksPanel({ tweaks, setTweaks, onSave, visible }) {
if (!visible) return null;
return (
);
}
// ============ Dev panel (?dev=1) ============
function DevPanel() {
const [open, setOpen] = useStateA(true);
const [flash, setFlash] = useStateA('');
function toast(msg) { setFlash(msg); setTimeout(() => setFlash(''), 1800); }
return (
setOpen(!open)}>
dev
{open && (
dev tools
?dev=1
Cooldown
{ window.PLANETALIN.devResetCooldown(); toast('Cooldown cleared'); }}>Reset now
{ window.PLANETALIN.devSetCooldownHours(12); toast('12h remaining'); }}>12h
{ window.PLANETALIN.devSetCooldownHours(1); toast('1h remaining'); }}>1h
{ window.PLANETALIN.devSetCooldownHours(0.05); toast('~3min remaining'); }}>3m
Storage
{ window.PLANETALIN.devClearWorlds(); toast('Worlds wiped'); location.reload(); }}>Clear worlds
{ window.PLANETALIN.devClearSeal(); window.PLANETALIN.clearToken?.(); toast('Seal + token reset'); location.reload(); }}>New seal
{ if (confirm('Wipe everything?')) { window.PLANETALIN.devNukeAll(); window.PLANETALIN.clearToken?.(); location.reload(); } }}>Nuke all
Auth
{
const t = window.PLANETALIN.getToken?.();
toast(t ? (window.PLANETALIN.isTokenValid?.(t) ? 'Token valid' : 'Token expired') : 'No token');
}}>Check token
{ window.PLANETALIN.clearToken?.(); toast('Token cleared'); }}>Clear token
Seed
{
const seed = window.PLANETALIN_HERO_SEEDS[Math.floor(Math.random()*window.PLANETALIN_HERO_SEEDS.length)];
const id = window.PLANETALIN.importCataloguePlanet({ ...seed, id: window.PLANETALIN.genId(), seal: window.PLANETALIN.getSeal(), createdAt: new Date().toISOString() });
toast('Seeded ' + id);
}}>+1 sample world
{
for (let i=0;i<5;i++){
const seed = window.PLANETALIN_HERO_SEEDS[Math.floor(Math.random()*window.PLANETALIN_HERO_SEEDS.length)];
window.PLANETALIN.importCataloguePlanet({ ...seed, id: window.PLANETALIN.genId(), seal: window.PLANETALIN.getSeal(), createdAt: new Date().toISOString() });
}
toast('Seeded 5 worlds');
}}>+5
{
const n = window.PLANETALIN.seedSolarSystem();
toast(n ? `Seeded ${n} solar system` : 'Already seeded');
}}>☉ Solar system
{flash &&
{flash}
}
)}
);
}
// ============ Root ============
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"starfieldDensity": 1.0,
"animIntensity": 1.0
}/*EDITMODE-END*/;
function App() {
const [route, setRoute] = useStateA(window.PLANETALIN.parseHash());
const [tweaks, setTweaks] = useStateA(TWEAK_DEFAULTS);
const [tweaksVisible, setTweaksVisible] = useStateA(false);
useEffectA(() => {
const onHash = () => setRoute(window.PLANETALIN.parseHash());
window.addEventListener('hashchange', onHash);
const onMsg = (e) => {
if (e.data?.type === '__activate_edit_mode') setTweaksVisible(true);
if (e.data?.type === '__deactivate_edit_mode') setTweaksVisible(false);
};
window.addEventListener('message', onMsg);
try { window.parent.postMessage({type:'__edit_mode_available'}, '*'); } catch(e){}
return () => {
window.removeEventListener('hashchange', onHash);
window.removeEventListener('message', onMsg);
};
}, []);
function saveTweaks(edits) {
try { window.parent.postMessage({type:'__edit_mode_set_keys', edits}, '*'); } catch(e){}
}
const common = { starfieldDensity: tweaks.starfieldDensity, animIntensity: tweaks.animIntensity };
return (
<>
{route.route === 'home' && }
{route.route === 'create' && }
{route.route === 'planet' && (() => {
const p = window.PLANETALIN.findById(route.id);
if (!p) return ;
return ;
})()}
{route.route === 'permalink' && (() => {
try {
const p = window.PLANETALIN.decodeSpec(route.encoded);
return ;
} catch(e) {
return ;
}
})()}
{route.route === 'constellation' && }
{window.PLANETALIN.isDevMode() && }
>
);
}
function NotFound() {
return (
404 · uncharted
No such world
This planet is not in the catalogue.
window.PLANETALIN.go('')}>Return home
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );