interactive intro to open social

feat: add ownership visualization and silo comparison

- show stats (record types, apps) when clicking identity
- contrast walled gardens vs atproto ownership
- update info modal to emphasize ownership
- add visual styling for ownership boxes

Changed files
+114 -22
src
static
+72 -4
src/templates.rs
··· 878 878 .onboarding-progress span.done {{ 879 879 background: var(--text-light); 880 880 }} 881 + 882 + .stats-box {{ 883 + display: flex; 884 + gap: 1.5rem; 885 + margin: 1.5rem 0; 886 + padding: 1rem; 887 + background: var(--bg); 888 + border-radius: 4px; 889 + border: 1px solid var(--border); 890 + }} 891 + 892 + .stat {{ 893 + flex: 1; 894 + text-align: center; 895 + }} 896 + 897 + .stat-value {{ 898 + font-size: 1.8rem; 899 + font-weight: 600; 900 + color: var(--text); 901 + margin-bottom: 0.25rem; 902 + }} 903 + 904 + .stat-label {{ 905 + font-size: 0.65rem; 906 + color: var(--text-light); 907 + text-transform: uppercase; 908 + letter-spacing: 0.05em; 909 + }} 910 + 911 + .ownership-box {{ 912 + margin: 1rem 0; 913 + padding: 1rem; 914 + background: var(--bg); 915 + border-radius: 4px; 916 + border: 1px solid var(--border); 917 + }} 918 + 919 + .ownership-box.yours {{ 920 + background: rgba(76, 175, 80, 0.05); 921 + border-color: rgba(76, 175, 80, 0.3); 922 + }} 923 + 924 + @media (prefers-color-scheme: dark) {{ 925 + .ownership-box.yours {{ 926 + background: rgba(76, 175, 80, 0.08); 927 + border-color: rgba(76, 175, 80, 0.4); 928 + }} 929 + }} 930 + 931 + .ownership-header {{ 932 + font-size: 0.7rem; 933 + font-weight: 600; 934 + color: var(--text); 935 + margin-bottom: 0.5rem; 936 + text-transform: uppercase; 937 + letter-spacing: 0.05em; 938 + }} 939 + 940 + .ownership-text {{ 941 + font-size: 0.7rem; 942 + color: var(--text-lighter); 943 + line-height: 1.5; 944 + }} 945 + 946 + .ownership-text strong {{ 947 + color: var(--text); 948 + }} 881 949 </style> 882 950 </head> 883 951 <body> ··· 886 954 887 955 <div class="overlay" id="overlay"></div> 888 956 <div class="info-modal" id="infoModal"> 889 - <h2>@me - your at protocol identity</h2> 890 - <p>in decentralized social networks, you own your identity and your data lives in your personal data server (pds).</p> 891 - <p>third-party applications create records in your repository using different lexicons (data schemas). for example, bluesky creates posts, white wind stores blog entries, tangled.org hosts code repositories, and frontpage aggregates links - all in the same place.</p> 892 - <p>this visualization shows your identity at the center, surrounded by the third-party apps that have created data for you. click an app to see what types of records it stores, then click a record type to see the actual data.</p> 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> 893 961 <button id="closeInfo">got it</button> 894 962 <button id="restartTour" onclick="window.restartOnboarding()" style="margin-left: 0.5rem; background: var(--surface-hover);">restart tour</button> 895 963 </div>
+42 -18
static/app.js
··· 93 93 // User may not have an avatar set 94 94 }); 95 95 96 + // Store collections for later use 97 + let allCollections = []; 98 + 96 99 // Add identity click handler to show PDS info 97 100 document.querySelector('.identity').addEventListener('click', () => { 98 101 const detail = document.getElementById('detail'); 99 102 const pdsHost = pds.replace('https://', '').replace('http://', ''); 103 + 104 + // Count total apps 105 + const appCount = Object.keys(apps).length; 106 + 100 107 detail.innerHTML = ` 101 108 <button class="detail-close" id="detailClose">×</button> 102 - <h3>your identity</h3> 103 - <div class="subtitle">decentralized identifier & storage</div> 104 - <div class="tree-item"> 105 - <div class="tree-item-header"> 106 - <span style="color: var(--text-light);">did</span> 107 - <span style="font-size: 0.6rem; color: var(--text);">${did}</span> 109 + <h3>your repository</h3> 110 + <div class="subtitle">what you've built</div> 111 + 112 + <div class="stats-box"> 113 + <div class="stat"> 114 + <div class="stat-value">${allCollections.length}</div> 115 + <div class="stat-label">record types</div> 108 116 </div> 109 - </div> 110 - <div class="tree-item"> 111 - <div class="tree-item-header"> 112 - <span style="color: var(--text-light);">handle</span> 113 - <span style="font-size: 0.6rem; color: var(--text);">@${handle}</span> 117 + <div class="stat"> 118 + <div class="stat-value">${appCount}</div> 119 + <div class="stat-label">apps</div> 114 120 </div> 115 121 </div> 116 - <div class="tree-item"> 117 - <div class="tree-item-header"> 118 - <span style="color: var(--text-light);">personal data server</span> 119 - <span style="font-size: 0.6rem; color: var(--text);">${pds}</span> 120 - </div> 122 + 123 + <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> 121 126 </div> 122 - <div style="margin-top: 1rem; padding: 0.6rem; background: var(--bg); border-radius: 4px; font-size: 0.65rem; line-height: 1.5; color: var(--text-lighter);"> 123 - your data lives at <strong style="color: var(--text);">${pdsHost}</strong>. apps like bluesky write to and read from this server. you control @<strong style="color: var(--text);">${handle}</strong> and can move it to a different server anytime. 127 + 128 + <div class="ownership-box yours"> 129 + <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> 131 + </div> 132 + 133 + <div style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);"> 134 + <div style="font-size: 0.65rem; color: var(--text-light); margin-bottom: 0.5rem;">technical details</div> 135 + <div class="tree-item"> 136 + <div class="tree-item-header"> 137 + <span style="color: var(--text-light);">did</span> 138 + <span style="font-size: 0.55rem; color: var(--text);">${did}</span> 139 + </div> 140 + </div> 141 + <div class="tree-item"> 142 + <div class="tree-item-header"> 143 + <span style="color: var(--text-light);">handle</span> 144 + <span style="font-size: 0.6rem; color: var(--text);">@${handle}</span> 145 + </div> 146 + </div> 124 147 </div> 125 148 `; 126 149 detail.classList.add('visible'); ··· 138 161 .then(r => r.json()) 139 162 .then(repo => { 140 163 const collections = repo.collections || []; 164 + allCollections = collections; 141 165 142 166 // Group by app namespace (first two parts of lexicon) 143 167 const apps = {};