import { useState, useEffect } from 'preact/hooks'; import { User, Buildings, Robot, X, CheckCircle, ArrowRight } from '@phosphor-icons/react'; import '../styles/palacejoin.css'; type Phase = 'splash' | 'ring' | 'complete' | 'welcome'; type CircleId = 'you' | 'palace' | 'butler'; type PalaceStyle = 'Classic' | 'Modern' | 'Minimal'; type ButlerTone = 'Sharp' | 'Friendly' | 'Formal'; interface AppData { you: { name: string; tagline: string }; palace: { name: string; style: PalaceStyle }; butler: { name: string; tone: ButlerTone }; } const META: Record = { you: { label: 'You', color: '#C9A84C', glow: 'rgba(201,168,76,0.42)', delay: '0.12s' }, palace: { label: 'Your Palace', color: '#52A884', glow: 'rgba(82,168,132,0.42)', delay: '0.28s' }, butler: { label: 'Your Butler', color: '#9E8CF0', glow: 'rgba(158,140,240,0.42)', delay: '0.18s' }, }; const CIRCLE_ICON: Record = { you: , palace: , butler: , }; const FORM_ICON: Record = { you: , palace: , butler: , }; const PALACE_CREST = ( ); // Circle centers in 360×340 SVG coordinate space const CTR: Record = { you: [180, 50], palace: [72, 237], butler: [288, 237], }; export function JoinApp() { const [phase, setPhase] = useState('splash'); const [splashOut, setSplashOut] = useState(false); const [ringIn, setRingIn] = useState(false); const [editing, setEditing] = useState(null); const [filled, setFilled] = useState>(new Set()); const [welcomeIn, setWelcomeIn] = useState(false); const [data, setData] = useState({ you: { name: '', tagline: '' }, palace: { name: '', style: 'Classic' }, butler: { name: 'Ace', tone: 'Sharp' }, }); function begin() { setSplashOut(true); setTimeout(() => { setPhase('ring'); requestAnimationFrame(() => requestAnimationFrame(() => setRingIn(true))); }, 560); } function save(id: CircleId, d: Partial) { setData(prev => ({ ...prev, [id]: { ...prev[id], ...d } as any })); const next = new Set([...filled, id]); setFilled(next); setEditing(null); if (next.size === 3) setTimeout(() => setPhase('complete'), 420); } function enter() { setPhase('welcome'); requestAnimationFrame(() => requestAnimationFrame(() => setWelcomeIn(true))); } const hintText = filled.size === 0 ? 'Tap each circle to personalize' : filled.size === 3 ? 'Your palace awaits' : `${3 - filled.size} more to go`; return (
{/* ── Splash ── */} {phase === 'splash' && (
{Array.from({ length: 11 }, (_, i) => ( ))}
{PALACE_CREST}

Palace

The home of those who live well

)} {/* ── Ring / Complete ── */} {(phase === 'ring' || phase === 'complete') && (

{hintText}

{/* Connection lines */} {([['you','palace'],['you','butler'],['palace','butler']] as [CircleId,CircleId][]).map(([a,b]) => { const active = filled.has(a) && filled.has(b); const [x1,y1] = CTR[a]; const [x2,y2] = CTR[b]; return ( ); })} {/* Circle nodes */} {(['you','palace','butler'] as CircleId[]).map(id => { const m = META[id]; const isFilled = filled.has(id); const displayName = id === 'you' ? data.you.name : id === 'palace' ? data.palace.name : data.butler.name; return ( ); })}
)} {/* ── Welcome ── */} {phase === 'welcome' && (
{PALACE_CREST}

Welcome to your Palace

{data.you.name &&

{data.you.name}

} {data.palace.name &&

{data.palace.name}

}
{(['you','palace','butler'] as CircleId[]).map(id => { const m = META[id]; const name = id === 'you' ? (data.you.name || 'You') : id === 'palace' ? (data.palace.name || 'Your Palace') : (data.butler.name || 'Ace'); return (
{CIRCLE_ICON[id]} {m.label} {name}
); })}
Enter Palace
)} {/* ── Edit Sheet ── */} {editing && ( save(editing, d)} onDismiss={() => setEditing(null)} /> )}
); } function EditSheet({ id, data, onSave, onDismiss }: { id: CircleId; data: AppData; onSave: (d: Partial) => void; onDismiss: () => void; }) { const [visible, setVisible] = useState(false); const m = META[id]; useEffect(() => { requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true))); }, []); function dismiss() { setVisible(false); setTimeout(onDismiss, 340); } function handleBackdropClick(e: MouseEvent) { if ((e.target as HTMLElement).classList.contains('sheet-backdrop--in')) dismiss(); } return (
{id === 'you' && } {id === 'palace' && } {id === 'butler' && }
); } function YouForm({ data, onSave, color }: { data: AppData['you']; onSave: (d: Partial) => void; color: string; }) { const [name, setName] = useState(data.name); const [tagline, setTagline] = useState(data.tagline); return (

Who are you?

Tell your palace about yourself

setName((e.target as HTMLInputElement).value)} style={`--focus-color:${color}`} autocomplete="off" />
setTagline((e.target as HTMLInputElement).value)} style={`--focus-color:${color}`} autocomplete="off" />
); } function PalaceForm({ data, onSave, color }: { data: AppData['palace']; onSave: (d: Partial) => void; color: string; }) { const [name, setName] = useState(data.name); const [style, setStyle] = useState(data.style); const styleDesc: Record = { Classic: 'Timeless, warm, and refined', Modern: 'Clean, minimal, and forward', Minimal: 'Pure, quiet, and essential', }; return (

Name your palace

Every palace deserves a name

setName((e.target as HTMLInputElement).value)} style={`--focus-color:${color}`} autocomplete="off" />
{(['Classic', 'Modern', 'Minimal'] as PalaceStyle[]).map(s => ( ))}

{styleDesc[style]}

); } function ButlerForm({ data, onSave, color }: { data: AppData['butler']; onSave: (d: Partial) => void; color: string; }) { const [name, setName] = useState(data.name); const [tone, setTone] = useState(data.tone); const toneDesc: Record = { Sharp: 'Direct, efficient, no filler', Friendly: 'Warm, conversational, encouraging', Formal: 'Polished, respectful, precise', }; return (

Meet your butler

Your AI assistant, configured your way

setName((e.target as HTMLInputElement).value)} style={`--focus-color:${color}`} autocomplete="off" />
{(['Sharp', 'Friendly', 'Formal'] as ButlerTone[]).map(t => ( ))}

{toneDesc[tone]}

); }