interactive intro to open social at-me.zzstoatzz.io

feat: restore intro walkthrough for first-time users

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+295
+3
src/view/main.js
··· 3 // ============================================================================ 4 5 import './styles.css'; 6 import { state, urlParams, paramDid, paramHandle } from './state.js'; 7 import { 8 resolveHandle, ··· 17 import { initGuestbookUI, updateAuthUI } from './guestbook-ui.js'; 18 import { initFirehoseUI } from './firehose.js'; 19 import { initOAuth, handleOAuthCallback, tryResumeSession } from './oauth.js'; 20 21 // ============================================================================ 22 // INITIALIZATION ··· 133 // Initialize UI components 134 initGuestbookUI(); 135 initFirehoseUI(); 136 137 // Check guestbook state 138 await checkGuestbookState();
··· 3 // ============================================================================ 4 5 import './styles.css'; 6 + import './onboarding.css'; 7 import { state, urlParams, paramDid, paramHandle } from './state.js'; 8 import { 9 resolveHandle, ··· 18 import { initGuestbookUI, updateAuthUI } from './guestbook-ui.js'; 19 import { initFirehoseUI } from './firehose.js'; 20 import { initOAuth, handleOAuthCallback, tryResumeSession } from './oauth.js'; 21 + import { initOnboarding } from './onboarding.js'; 22 23 // ============================================================================ 24 // INITIALIZATION ··· 135 // Initialize UI components 136 initGuestbookUI(); 137 initFirehoseUI(); 138 + initOnboarding(); 139 140 // Check guestbook state 141 await checkGuestbookState();
+101
src/view/onboarding.css
···
··· 1 + .onboarding-overlay { 2 + position: fixed; 3 + inset: 0; 4 + background: transparent; 5 + z-index: 3000; 6 + display: none; 7 + opacity: 0; 8 + transition: opacity 0.3s ease; 9 + pointer-events: none; 10 + } 11 + 12 + .onboarding-overlay.active { 13 + display: block; 14 + opacity: 1; 15 + } 16 + 17 + .onboarding-spotlight { 18 + position: absolute; 19 + border: 2px solid rgba(255, 255, 255, 0.9); 20 + border-radius: 50%; 21 + box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.75), 0 0 40px rgba(255, 255, 255, 0.5); 22 + pointer-events: none; 23 + transition: all 0.5s ease; 24 + } 25 + 26 + .onboarding-content { 27 + position: fixed; 28 + background: var(--surface); 29 + border: 2px solid var(--border); 30 + padding: clamp(1rem, 3vmin, 2rem); 31 + max-width: min(400px, 90vw); 32 + z-index: 3001; 33 + border-radius: 4px; 34 + transition: all 0.3s ease; 35 + pointer-events: auto; 36 + } 37 + 38 + .onboarding-content h3 { 39 + font-size: clamp(0.9rem, 2vmin, 1.1rem); 40 + margin-bottom: clamp(0.5rem, 1.5vmin, 0.75rem); 41 + color: var(--text); 42 + font-weight: 500; 43 + } 44 + 45 + .onboarding-content p { 46 + font-size: clamp(0.7rem, 1.5vmin, 0.85rem); 47 + color: var(--text-light); 48 + line-height: 1.5; 49 + margin-bottom: clamp(1rem, 2vmin, 1.25rem); 50 + } 51 + 52 + .onboarding-actions { 53 + display: flex; 54 + gap: clamp(0.5rem, 1.5vmin, 0.75rem); 55 + justify-content: flex-end; 56 + } 57 + 58 + .onboarding-actions button { 59 + font-family: inherit; 60 + font-size: clamp(0.7rem, 1.5vmin, 0.8rem); 61 + padding: clamp(0.4rem, 1vmin, 0.5rem) clamp(0.8rem, 2vmin, 1rem); 62 + background: transparent; 63 + border: 1px solid var(--border); 64 + color: var(--text); 65 + cursor: pointer; 66 + transition: all 0.2s ease; 67 + border-radius: 2px; 68 + } 69 + 70 + .onboarding-actions button:hover { 71 + background: var(--surface-hover); 72 + border-color: var(--text-light); 73 + } 74 + 75 + .onboarding-actions button.primary { 76 + background: var(--surface-hover); 77 + border-color: var(--text-light); 78 + } 79 + 80 + .onboarding-progress { 81 + display: flex; 82 + gap: clamp(0.4rem, 1vmin, 0.5rem); 83 + justify-content: center; 84 + margin-top: clamp(0.75rem, 2vmin, 1rem); 85 + } 86 + 87 + .onboarding-progress span { 88 + width: clamp(6px, 1.5vmin, 8px); 89 + height: clamp(6px, 1.5vmin, 8px); 90 + border-radius: 50%; 91 + background: var(--border); 92 + transition: background 0.3s ease; 93 + } 94 + 95 + .onboarding-progress span.active { 96 + background: var(--text); 97 + } 98 + 99 + .onboarding-progress span.done { 100 + background: var(--text-light); 101 + }
+186
src/view/onboarding.js
···
··· 1 + // Onboarding overlay for first-time users 2 + const ONBOARDING_KEY = 'atme_onboarding_seen'; 3 + 4 + const steps = [ 5 + { 6 + target: '.identity', 7 + title: 'this is you', 8 + description: 'your global identity and handle. your data is hosted at your <a href="https://atproto.com/guides/overview" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">Personal Data Server (PDS)</a>.', 9 + position: 'bottom' 10 + }, 11 + { 12 + target: '.canvas', 13 + title: 'atproto applications', 14 + description: 'these apps use your global identity to write public records to your PDS. they can also read records you\'ve created.', 15 + position: 'center' 16 + }, 17 + { 18 + target: '.app-view', 19 + title: 'explore your records', 20 + description: 'click any app to see what records it has written to your PDS.', 21 + position: 'bottom' 22 + } 23 + ]; 24 + 25 + let currentStep = 0; 26 + 27 + function showOnboarding() { 28 + const overlay = document.getElementById('onboardingOverlay'); 29 + if (!overlay) return; 30 + 31 + overlay.style.display = 'block'; 32 + setTimeout(() => { 33 + overlay.style.opacity = '1'; 34 + showStep(0); 35 + }, 50); 36 + } 37 + 38 + function hideOnboarding() { 39 + const overlay = document.getElementById('onboardingOverlay'); 40 + const spotlight = document.getElementById('onboardingSpotlight'); 41 + const content = document.getElementById('onboardingContent'); 42 + 43 + if (overlay) { 44 + overlay.style.opacity = '0'; 45 + setTimeout(() => { 46 + overlay.style.display = 'none'; 47 + }, 300); 48 + } 49 + 50 + if (spotlight) spotlight.classList.remove('active'); 51 + if (content) content.classList.remove('active'); 52 + 53 + localStorage.setItem(ONBOARDING_KEY, 'true'); 54 + } 55 + 56 + function showStep(stepIndex) { 57 + if (stepIndex >= steps.length) { 58 + hideOnboarding(); 59 + return; 60 + } 61 + 62 + currentStep = stepIndex; 63 + const step = steps[stepIndex]; 64 + const target = document.querySelector(step.target); 65 + 66 + if (!target) { 67 + console.warn('Onboarding target not found:', step.target); 68 + showStep(stepIndex + 1); 69 + return; 70 + } 71 + 72 + const spotlight = document.getElementById('onboardingSpotlight'); 73 + const content = document.getElementById('onboardingContent'); 74 + 75 + // Position spotlight on target 76 + const rect = target.getBoundingClientRect(); 77 + const padding = step.target === '.canvas' ? 100 : 20; 78 + 79 + spotlight.style.left = `${rect.left - padding}px`; 80 + spotlight.style.top = `${rect.top - padding}px`; 81 + spotlight.style.width = `${rect.width + padding * 2}px`; 82 + spotlight.style.height = `${rect.height + padding * 2}px`; 83 + spotlight.classList.add('active'); 84 + 85 + // Position content 86 + const isLastStep = stepIndex === steps.length - 1; 87 + content.innerHTML = ` 88 + <h3>${step.title}</h3> 89 + <p>${step.description}</p> 90 + <div class="onboarding-actions"> 91 + ${!isLastStep ? '<button id="skipOnboarding" class="onboarding-skip">skip</button>' : ''} 92 + <button id="nextOnboarding" class="onboarding-next"> 93 + ${isLastStep ? 'got it' : 'next'} 94 + </button> 95 + </div> 96 + <div class="onboarding-progress"> 97 + ${steps.map((_, i) => `<span class="${i === stepIndex ? 'active' : i < stepIndex ? 'done' : ''}"></span>`).join('')} 98 + </div> 99 + `; 100 + 101 + // Position content relative to spotlight 102 + let contentTop, contentLeft; 103 + const contentMaxWidth = Math.min(400, window.innerWidth * 0.9); 104 + const contentHeight = 250; 105 + const margin = Math.max(20, window.innerWidth * 0.05); 106 + 107 + if (step.position === 'bottom') { 108 + contentTop = rect.bottom + padding + margin; 109 + contentLeft = rect.left + rect.width / 2; 110 + 111 + if (contentTop + contentHeight > window.innerHeight) { 112 + contentTop = rect.top - padding - contentHeight - margin; 113 + } 114 + } else if (step.position === 'center') { 115 + contentTop = window.innerHeight / 2 - contentHeight / 2; 116 + contentLeft = window.innerWidth / 2; 117 + } else { 118 + contentTop = rect.top - padding - contentHeight - margin; 119 + contentLeft = rect.left + rect.width / 2; 120 + 121 + if (contentTop < margin) { 122 + contentTop = rect.bottom + padding + margin; 123 + } 124 + } 125 + 126 + // Ensure content stays on screen horizontally 127 + const halfWidth = contentMaxWidth / 2; 128 + if (contentLeft - halfWidth < margin) { 129 + contentLeft = halfWidth + margin; 130 + } else if (contentLeft + halfWidth > window.innerWidth - margin) { 131 + contentLeft = window.innerWidth - halfWidth - margin; 132 + } 133 + 134 + // Ensure content stays on screen vertically 135 + if (contentTop < margin) { 136 + contentTop = margin; 137 + } else if (contentTop + contentHeight > window.innerHeight - margin) { 138 + contentTop = window.innerHeight - contentHeight - margin; 139 + } 140 + 141 + content.style.top = `${contentTop}px`; 142 + content.style.left = `${contentLeft}px`; 143 + content.style.transform = 'translate(-50%, 0)'; 144 + content.classList.add('active'); 145 + 146 + // Add event listeners 147 + const skipBtn = document.getElementById('skipOnboarding'); 148 + if (skipBtn) { 149 + skipBtn.addEventListener('click', hideOnboarding); 150 + } 151 + document.getElementById('nextOnboarding').addEventListener('click', () => { 152 + showStep(stepIndex + 1); 153 + }); 154 + } 155 + 156 + export function initOnboarding() { 157 + const seen = localStorage.getItem(ONBOARDING_KEY); 158 + 159 + if (!seen) { 160 + // Wait for app circles to render 161 + setTimeout(() => { 162 + showOnboarding(); 163 + }, 1000); 164 + } 165 + } 166 + 167 + export function restartOnboarding() { 168 + localStorage.removeItem(ONBOARDING_KEY); 169 + const infoModal = document.getElementById('infoModal'); 170 + const overlay = document.getElementById('overlay'); 171 + if (infoModal) infoModal.classList.remove('visible'); 172 + if (overlay) overlay.classList.remove('visible'); 173 + setTimeout(() => { 174 + showOnboarding(); 175 + }, 300); 176 + } 177 + 178 + // ESC key handler 179 + document.addEventListener('keydown', (e) => { 180 + if (e.key === 'Escape') { 181 + const overlay = document.getElementById('onboardingOverlay'); 182 + if (overlay && overlay.style.display === 'block') { 183 + hideOnboarding(); 184 + } 185 + } 186 + });
+5
view.html
··· 134 <div id="guestbookContent"></div> 135 </div> 136 137 <div class="canvas"> 138 <div class="identity"> 139 <img class="identity-avatar" id="identityAvatar" />
··· 134 <div id="guestbookContent"></div> 135 </div> 136 137 + <div class="onboarding-overlay" id="onboardingOverlay"> 138 + <div class="onboarding-spotlight" id="onboardingSpotlight"></div> 139 + <div class="onboarding-content" id="onboardingContent"></div> 140 + </div> 141 + 142 <div class="canvas"> 143 <div class="identity"> 144 <img class="identity-avatar" id="identityAvatar" />