interactive intro to open social

fix: scope issue with apps variable and soften messaging

- move identity click handler after apps is populated
- change messaging from ban-focused to lock-in/distribution focused
- emphasize 'you build their network, they own distribution'
- softer language: locked in vs can't export, starting over vs gone

Changed files
+30 -31
src
static
+3 -3
src/templates.rs
··· 955 955 <div class="overlay" id="overlay"></div> 956 956 <div class="info-modal" id="infoModal"> 957 957 <h2>@me - your repository</h2> 958 - <p>on instagram, facebook, or twitter: the platform owns your content. if they ban you, it's all gone. you can't move it, export it, or control who accesses it.</p> 959 - <p>on atproto: you own everything. your data lives in your personal server. apps like bluesky, whitewind, and frontpage just write to YOUR repository. you can switch apps, move servers, or revoke access anytime.</p> 960 - <p>click your @ in the center to see what you've built. click any app to see what it's stored in your space.</p> 958 + <p>on traditional social platforms, your content is locked in. want to switch? you start from zero. you build their network, they control the distribution.</p> 959 + <p>on atproto, you own everything. your data lives in your personal server. apps like bluesky, whitewind, and frontpage just write to YOUR space. switch apps anytime, take it all with you.</p> 960 + <p>click your @ in the center to see what you've built. click any app to see what it's stored in your repository.</p> 961 961 <button id="closeInfo">got it</button> 962 962 <button id="restartTour" onclick="window.restartOnboarding()" style="margin-left: 0.5rem; background: var(--surface-hover);">restart tour</button> 963 963 </div>
+27 -28
static/app.js
··· 93 93 // User may not have an avatar set 94 94 }); 95 95 96 - // Store collections for later use 96 + // Store collections and apps for later use 97 97 let allCollections = []; 98 + let apps = {}; 98 99 99 - // Add identity click handler to show PDS info 100 + // Get all collections from PDS 101 + return fetch(`${pds}/xrpc/com.atproto.repo.describeRepo?repo=${did}`); 102 + }) 103 + .then(r => r.json()) 104 + .then(repo => { 105 + const collections = repo.collections || []; 106 + allCollections = collections; 107 + 108 + // Group by app namespace (first two parts of lexicon) 109 + apps = {}; 110 + collections.forEach(collection => { 111 + const parts = collection.split('.'); 112 + if (parts.length >= 2) { 113 + const namespace = `${parts[0]}.${parts[1]}`; 114 + if (!apps[namespace]) apps[namespace] = []; 115 + apps[namespace].push(collection); 116 + } 117 + }); 118 + 119 + // Add identity click handler now that we have the data 120 + const pdsHost = globalPds.replace('https://', '').replace('http://', ''); 100 121 document.querySelector('.identity').addEventListener('click', () => { 101 122 const detail = document.getElementById('detail'); 102 - const pdsHost = pds.replace('https://', '').replace('http://', ''); 103 - 104 - // Count total apps 105 123 const appCount = Object.keys(apps).length; 106 124 107 125 detail.innerHTML = ` ··· 121 139 </div> 122 140 123 141 <div class="ownership-box"> 124 - <div class="ownership-header">on walled gardens</div> 125 - <div class="ownership-text">platform owns your content. account ban = everything gone. no export, no control.</div> 142 + <div class="ownership-header">on traditional platforms</div> 143 + <div class="ownership-text">your content is locked in. switching platforms means starting over. you build their network, they own the distribution.</div> 126 144 </div> 127 145 128 146 <div class="ownership-box yours"> 129 147 <div class="ownership-header">on atproto</div> 130 - <div class="ownership-text">you own it. lives at <strong>${pdsHost}</strong>. move servers, switch apps, export anytime. can't be taken away.</div> 148 + <div class="ownership-text">your content, your server. apps just read and write to <strong>${pdsHost}</strong>. switch apps anytime, take your data anywhere.</div> 131 149 </div> 132 150 133 151 <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);"> ··· 141 159 <div class="tree-item"> 142 160 <div class="tree-item-header"> 143 161 <span style="color: var(--text-light);">handle</span> 144 - <span style="font-size: 0.6rem; color: var(--text);">@${handle}</span> 162 + <span style="font-size: 0.6rem; color: var(--text);">@${globalHandle}</span> 145 163 </div> 146 164 </div> 147 165 </div> ··· 153 171 e.stopPropagation(); 154 172 detail.classList.remove('visible'); 155 173 }); 156 - }); 157 - 158 - // Get all collections from PDS 159 - return fetch(`${pds}/xrpc/com.atproto.repo.describeRepo?repo=${did}`); 160 - }) 161 - .then(r => r.json()) 162 - .then(repo => { 163 - const collections = repo.collections || []; 164 - allCollections = collections; 165 - 166 - // Group by app namespace (first two parts of lexicon) 167 - const apps = {}; 168 - collections.forEach(collection => { 169 - const parts = collection.split('.'); 170 - if (parts.length >= 2) { 171 - const namespace = `${parts[0]}.${parts[1]}`; 172 - if (!apps[namespace]) apps[namespace] = []; 173 - apps[namespace].push(collection); 174 - } 175 174 }); 176 175 177 176 const field = document.getElementById('field');