/* Pyramid · main app */ const ACCENT_PRESETS = [ { name: 'amber', h: 42, s: 95, l: 58 }, { name: 'coral', h: 12, s: 85, l: 62 }, { name: 'mint', h: 160, s: 65, l: 50 }, { name: 'violet', h: 268, s: 78, l: 66 }, { name: 'cobalt', h: 220, s: 85, l: 62 }, { name: 'lime', h: 80, s: 70, l: 55 }, ]; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accentIdx": 0, "theme": "dark", "density": 1, "motion": 1, "radius": 12, "view": "chat" }/*EDITMODE-END*/; const loadTweak = (k, d) => { try { const v = localStorage.getItem('pyr_' + k); return v === null ? d : JSON.parse(v); } catch { return d; } }; const saveTweak = (k, v) => { try { localStorage.setItem('pyr_' + k, JSON.stringify(v)); } catch {} }; const DEFAULT_CALL_PARTICIPANTS = [ { name: 'juno', avatar: 'J', color: '#06b6d4', speaking: true, muted: false, cam: false }, { name: 'sachi', avatar: 'S', color: '#a855f7', speaking: false, muted: false, cam: false }, { name: 'mira (you)', avatar: 'M', color: '#ff6b9d', speaking: false, muted: false, cam: false, self: true }, { name: 'ren', avatar: 'R', color: '#10b981', speaking: false, muted: true, cam: false }, ]; const App = () => { const [theme, setTheme] = React.useState(() => loadTweak('theme', TWEAK_DEFAULTS.theme)); const [accentIdx, setAccentIdx] = React.useState(() => loadTweak('accentIdx', TWEAK_DEFAULTS.accentIdx)); const [density, setDensity] = React.useState(() => loadTweak('density', TWEAK_DEFAULTS.density)); const [motion, setMotion] = React.useState(() => loadTweak('motion', TWEAK_DEFAULTS.motion)); const [radius, setRadius] = React.useState(() => loadTweak('radius', TWEAK_DEFAULTS.radius)); const [loggedIn, setLoggedIn] = React.useState(() => loadTweak('loggedIn', true)); const [view, setView] = React.useState(() => { // never land on voice without an active call const v = loadTweak('view', 'chat'); return v === 'voice' ? 'chat' : v; }); const [activeSpace, setActiveSpace] = React.useState(() => loadTweak('space', 'atelier')); const [activeRoom, setActiveRoom] = React.useState(() => loadTweak('room', 'general')); const [railCollapsed, setRailCollapsed] = React.useState(false); const [membersOpen, setMembersOpen] = React.useState(true); const [tweaksMode, setTweaksMode] = React.useState(false); const [showSettings, setShowSettings] = React.useState(false); const [showSearch, setShowSearch] = React.useState(false); const [profilePopover, setProfilePopover] = React.useState(null); const [emojiPickerAt, setEmojiPickerAt] = React.useState(null); const [messageActions, setMessageActions] = React.useState(null); const [toast, setToast] = React.useState(null); const [messagesState, setMessagesState] = React.useState(() => window.PYRAMID_DATA.messagesByRoom); /* call state — persists across navigation */ const [call, setCall] = React.useState(null); const [callReturnRoom, setCallReturnRoom] = React.useState(null); React.useEffect(() => { saveTweak('theme', theme); }, [theme]); React.useEffect(() => { saveTweak('accentIdx', accentIdx); }, [accentIdx]); React.useEffect(() => { saveTweak('density', density); }, [density]); React.useEffect(() => { saveTweak('motion', motion); }, [motion]); React.useEffect(() => { saveTweak('radius', radius); }, [radius]); React.useEffect(() => { saveTweak('view', view); }, [view]); React.useEffect(() => { saveTweak('space', activeSpace); }, [activeSpace]); React.useEffect(() => { saveTweak('room', activeRoom); }, [activeRoom]); React.useEffect(() => { saveTweak('loggedIn', loggedIn); }, [loggedIn]); React.useEffect(() => { const a = ACCENT_PRESETS[accentIdx]; const r = document.documentElement; r.setAttribute('data-theme', theme); r.style.setProperty('--accent-h', a.h); r.style.setProperty('--accent-s', a.s + '%'); r.style.setProperty('--accent-l', a.l + '%'); r.style.setProperty('--density', density); r.style.setProperty('--motion', motion); r.style.setProperty('--r-base', radius + 'px'); }, [theme, accentIdx, density, motion, radius]); React.useEffect(() => { const handler = (e) => { if (e.data?.type === '__activate_edit_mode') setTweaksMode(true); if (e.data?.type === '__deactivate_edit_mode') setTweaksMode(false); }; window.addEventListener('message', handler); window.parent?.postMessage({ type: '__edit_mode_available' }, '*'); return () => window.removeEventListener('message', handler); }, []); const pushEdits = (edits) => { try { window.parent?.postMessage({ type: '__edit_mode_set_keys', edits }, '*'); } catch {} }; React.useEffect(() => { const h = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); setShowSearch(true); } if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'b') { e.preventDefault(); setRailCollapsed(v => !v); } if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'u') { e.preventDefault(); setMembersOpen(v => !v); } if (e.key === 'Escape') { setShowSearch(false); setShowSettings(false); setProfilePopover(null); setEmojiPickerAt(null); setMessageActions(null); } }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, []); const showToast = (msg, icon = 'check') => { setToast({ msg, icon }); setTimeout(() => setToast(null), 2400); }; const handleSelectSpace = (spaceId) => { setActiveSpace(spaceId); setView('chat'); const data = window.PYRAMID_DATA.rooms[spaceId]; if (data) { const firstActive = data.sections.flatMap(s => s.rooms).find(r => r.active) || data.sections[0].rooms[0]; setActiveRoom(firstActive.id); } }; const handleSelectRoom = (roomId) => { setActiveRoom(roomId); const data = window.PYRAMID_DATA.rooms[activeSpace]; const roomDef = data?.sections.flatMap(s => s.rooms).find(r => r.id === roomId); if (roomDef?.type === 'voice') { // joining a voice room starts a call for that room startCall({ roomName: roomDef.name, kind: 'voice', room: roomDef }); setView('voice'); } else { setView('chat'); } }; const startCall = ({ roomName, kind, room }) => { setCall({ active: true, roomName, kind, participants: DEFAULT_CALL_PARTICIPANTS, mic: true, cam: kind === 'video', deaf: false, streaming: false, streamCollapsed: false, streamer: 'mira (you)', roomId: room?.id || null, }); setCallReturnRoom({ space: activeSpace, room: activeRoom }); }; /* called from DM header */ React.useEffect(() => { window.__startDMCall = (roomDef, kind) => { startCall({ roomName: roomDef?.name || 'DM', kind, room: roomDef }); setView('voice'); }; return () => { window.__startDMCall = null; }; }, [activeSpace, activeRoom]); const handleLeaveCall = () => { setCall(null); if (view === 'voice') { if (callReturnRoom) { setActiveSpace(callReturnRoom.space); setActiveRoom(callReturnRoom.room); } setView('chat'); } setCallReturnRoom(null); showToast('left the call', 'phoneOff'); }; const updateCall = (patch) => setCall(c => c ? { ...c, ...patch } : c); const handleJumpToCall = () => { setView('voice'); }; const handleSend = (text) => { const newMsg = { id: 'm' + Date.now(), author: 'mira', avatar: 'M', color: '#ff6b9d', time: new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), text, }; setMessagesState(prev => ({ ...prev, [activeRoom]: [...(prev[activeRoom] || []), newMsg], })); }; const handleReact = (msgId, emoji) => { setMessagesState(prev => { const list = [...(prev[activeRoom] || [])]; const idx = list.findIndex(m => m.id === msgId); if (idx < 0) return prev; const msg = { ...list[idx] }; const reactions = [...(msg.reactions || [])]; const existing = reactions.findIndex(r => r.emoji === emoji); if (existing >= 0) { const r = { ...reactions[existing] }; if (r.mine) { r.mine = false; r.count--; } else { r.mine = true; r.count++; } if (r.count <= 0) reactions.splice(existing, 1); else reactions[existing] = r; } else { reactions.push({ emoji, count: 1, mine: true }); } msg.reactions = reactions; list[idx] = msg; return { ...prev, [activeRoom]: list }; }); }; const handleOpenProfile = (name, e) => { const rect = e?.currentTarget?.getBoundingClientRect(); const profiles = { juno: { name: 'juno', handle: '@juno:pyramid.chat', avatar: 'J', color: '#06b6d4', status: 'online', about: 'risograph, zines, tea. teaches at the atelier on tuesdays.', roles: [{ name: 'founder', color: '#f59e0b' }, { name: 'print-nerd', color: '#06b6d4' }] }, sachi: { name: 'sachi', handle: '@sachi:pyramid.chat', avatar: 'S', color: '#a855f7', status: 'online', about: 'illustration, collage, usually reading', roles: [{ name: 'mod', color: '#a855f7' }] }, ren: { name: 'ren', handle: '@ren:pyramid.chat', avatar: 'R', color: '#10b981', status: 'busy', about: 'graphic design studio of one', roles: [] }, kai: { name: 'kai', handle: '@kai:pyramid.chat', avatar: 'K', color: '#f43f5e', status: 'away', about: '3d + motion. fancy renders upon request.', roles: [] }, ezra: { name: 'ezra', handle: '@ezra:pyramid.chat', avatar: 'E', color: '#3b82f6', status: 'online', about: 'keeps the lights on. dev-lab lurker.', roles: [{ name: 'dev', color: '#3b82f6' }] }, mira: { name: 'mira', handle: '@mira:pyramid.chat', avatar: 'M', color: '#ff6b9d', status: 'online', about: 'painter · printmaker · pastry enthusiast', roles: [{ name: 'founder', color: '#f59e0b' }] }, }; setProfilePopover({ anchor: rect, profile: profiles[name] || profiles.juno }); }; const handleOpenEmoji = (target, e) => { const rect = e?.currentTarget?.getBoundingClientRect(); setEmojiPickerAt({ target, rect }); }; const handlePickEmoji = (emoji) => { if (emojiPickerAt?.target === 'composer') { showToast(`${emoji} added`, 'check'); } else if (emojiPickerAt?.target) { handleReact(emojiPickerAt.target, emoji); } }; const handleOpenActions = (msgId, e) => { const rect = e?.currentTarget?.getBoundingClientRect(); setMessageActions({ msgId, rect }); }; const handleMsgAction = (action) => { const labels = { reply: 'replying…', thread: 'thread opened', edit: 'editing message', pin: 'pinned to room', bookmark: 'saved for later', copy: 'copied', link: 'message link copied', delete: 'message deleted', }; showToast(labels[action] || action); }; if (!loggedIn) { return setLoggedIn(true)} />; } const spaces = window.PYRAMID_DATA.spaces; const roomsData = window.PYRAMID_DATA.rooms[activeSpace] || window.PYRAMID_DATA.rooms.atelier; const messages = messagesState[activeRoom] || []; const members = window.PYRAMID_DATA.membersByRoom[activeRoom] || window.PYRAMID_DATA.membersByRoom.general; const showMembers = membersOpen && view !== 'voice'; const appClasses = [ 'app', railCollapsed && 'rail-collapsed', showMembers && 'right-open', ].filter(Boolean).join(' '); return ( <>
handleOpenProfile('mira', e)} onOpenSettings={() => setShowSettings(true)} call={call} onLeaveCall={handleLeaveCall} onToggleStreamCollapse={() => call && updateCall({ streamCollapsed: !call.streamCollapsed })} onToggleMic={() => call && updateCall({ mic: !call.mic })} onToggleDeaf={() => call && updateCall({ deaf: !call.deaf })} onToggleScreen={() => call && updateCall({ streaming: !call.streaming, streamCollapsed: false })} onToggleCam={() => call && updateCall({ cam: !call.cam })} onJumpToCall={handleJumpToCall} /> {view === 'voice' ? ( setRailCollapsed(v => !v)} onOpenProfile={handleOpenProfile} onToggleMic={() => call && updateCall({ mic: !call.mic })} onToggleDeaf={() => call && updateCall({ deaf: !call.deaf })} onToggleCam={() => call && updateCall({ cam: !call.cam })} onToggleScreen={() => call && updateCall({ streaming: !call.streaming, streamCollapsed: false })} /> ) : ( setMembersOpen(v => !v)} membersOpen={membersOpen} onOpenSearch={() => setShowSearch(true)} onToggleRail={() => setRailCollapsed(v => !v)} railCollapsed={railCollapsed} onSend={handleSend} onReact={handleReact} /> )} {showMembers && }
{ if (v === 'voice') { if (!call) startCall({ roomName: 'The Lounge', kind: 'voice', room: { id: 'lounge', name: 'The Lounge' } }); setView('voice'); } else if (v === 'dm') { setActiveSpace('friends'); setActiveRoom('dm-juno'); setView('chat'); } else if (v === 'chat') { setActiveSpace('atelier'); setActiveRoom('general'); setView('chat'); } }} onLogin={() => setLoggedIn(false)} onSearch={() => setShowSearch(true)} onSettings={() => setShowSettings(true)} onProfile={(e) => handleOpenProfile('juno', e)} onEmoji={(e) => handleOpenEmoji('composer', e)} onTheme={() => setTheme(t => t === 'dark' ? 'light' : 'dark')} theme={theme} /> {showSettings && ( setShowSettings(false)} theme={theme} onTheme={setTheme} accentPresets={ACCENT_PRESETS} accent={ACCENT_PRESETS[accentIdx].h} onAccent={(p) => setAccentIdx(ACCENT_PRESETS.findIndex(a => a.name === p.name))} density={density} onDensity={setDensity} motion={motion} onMotion={setMotion} radius={radius} onRadius={setRadius} /> )} {showSearch && setShowSearch(false)} onJump={() => setShowSearch(false)} />} {profilePopover && ( setProfilePopover(null)} /> )} {emojiPickerAt && ( setEmojiPickerAt(null)} /> )} {messageActions && ( setMessageActions(null)} /> )} {toast && (
{toast.msg}
)} {tweaksMode && ( { setTheme(v); pushEdits({ theme: v }); }} accentIdx={accentIdx} setAccentIdx={(v) => { setAccentIdx(v); pushEdits({ accentIdx: v }); }} density={density} setDensity={(v) => { setDensity(v); pushEdits({ density: v }); }} motion={motion} setMotion={(v) => { setMotion(v); pushEdits({ motion: v }); }} radius={radius} setRadius={(v) => { setRadius(v); pushEdits({ radius: v }); }} /> )} ); }; const DemoSwitcher = ({ view, setView, onLogin, onSearch, onSettings, onProfile, onEmoji, onTheme, theme }) => { return (
); }; const TweaksPanel = ({ theme, setTheme, accentIdx, setAccentIdx, density, setDensity, motion, setMotion, radius, setRadius }) => (
Tweaks
pyramid.sys
Theme{theme}
Accent{ACCENT_PRESETS[accentIdx].name}
{ACCENT_PRESETS.map((p, i) => (
setAccentIdx(i)} title={p.name} /> ))}
Density{density === 0.85 ? 'compact' : density === 1.2 ? 'cozy' : 'default'}
Corner radius{radius}px
setRadius(+e.target.value)} />
Motion{Math.round(motion*100)}%
setMotion(+e.target.value/100)} />
); ReactDOM.createRoot(document.getElementById('root')).render();