browse the protocol like its 2008 ibex.desertthunder.dev
ubuntu atproto svelte
7

Configure Feed

Select the types of activity you want to include in your feed.

feat: account setup

Owais Jamil (Jun 8, 2026, 2:04 PM -0500) bb932a0f 454fee52

+572 -7
+6
package.json
··· 38 38 "vite": "^8.0.7", 39 39 "vitest": "^4.1.3", 40 40 "vitest-browser-svelte": "^2.1.0" 41 + }, 42 + "dependencies": { 43 + "@atcute/atproto": "^4.0.2", 44 + "@atcute/bluesky": "^4.0.6", 45 + "@atcute/client": "^5.0.0", 46 + "@atcute/lexicons": "^2.0.0" 41 47 } 42 48 }
+95
pnpm-lock.yaml
··· 7 7 importers: 8 8 9 9 .: 10 + dependencies: 11 + '@atcute/atproto': 12 + specifier: ^4.0.2 13 + version: 4.0.2(@atcute/lexicons@2.0.0) 14 + '@atcute/bluesky': 15 + specifier: ^4.0.6 16 + version: 4.0.6(@atcute/lexicons@2.0.0) 17 + '@atcute/client': 18 + specifier: ^5.0.0 19 + version: 5.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) 20 + '@atcute/lexicons': 21 + specifier: ^2.0.0 22 + version: 2.0.0 10 23 devDependencies: 11 24 '@eslint/js': 12 25 specifier: ^10.0.1 ··· 76 89 version: 2.1.1(svelte@5.56.3(@typescript-eslint/types@8.61.0))(vitest@4.1.8) 77 90 78 91 packages: 92 + 93 + '@atcute/atproto@4.0.2': 94 + resolution: {integrity: sha512-hLnvjiIOStpdUm0cEN+R5YydvbV0d6ap17Iv+t7i/nhSCN3TGMya7M0ftCWtCo+xoQ1EU6HK74R8jqXWlyrM0w==} 95 + peerDependencies: 96 + '@atcute/lexicons': ^2.0.0 97 + 98 + '@atcute/bluesky@4.0.6': 99 + resolution: {integrity: sha512-h0q6cSkil85MpISJ958fFbEhMWBaiujEwOSmgF7iz+Y8ZeZGHycOD2mjXaQ1CnpHb3yWul6nn8rBwRRrGE3RsA==} 100 + peerDependencies: 101 + '@atcute/lexicons': ^2.0.0 102 + 103 + '@atcute/client@5.0.0': 104 + resolution: {integrity: sha512-Dtrc1no1oAtpUTmkBxH0xKSgy8qcnk8orPf/PkEATW+MB3k8FPHtPH2fshiKd55rioZkb+xaN+7A29WtqPQHRA==} 105 + peerDependencies: 106 + '@atcute/lexicons': ^2.0.0 107 + 108 + '@atcute/identity@2.0.0': 109 + resolution: {integrity: sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==} 110 + peerDependencies: 111 + '@atcute/lexicons': ^2.0.0 112 + 113 + '@atcute/lexicons@2.0.0': 114 + resolution: {integrity: sha512-fIlwP+TPEAGoF5aU5s+f8N5sOjOu8Mww/sQL1B57Dp2hj3G/EWG9XwOHPokzycBCgXx+UxIIrzZCGy8whsVDZw==} 115 + 116 + '@atcute/uint8array@1.1.2': 117 + resolution: {integrity: sha512-n+lutnbN9mKzSjSVdfsYfzJ40u2971H+iLSL46D6d7zcrA4delxusf/ftGFvj5oGW03OioaFgQOy3Lqa3JmTeA==} 118 + 119 + '@atcute/util-text@1.3.1': 120 + resolution: {integrity: sha512-MRgJXkx67znuBXuoAYCJkBZyd3OApL7zZlNf5kXhuoCXcdiu1nblRDycYTADSkym4epBSQWxh26kmI9sewaq6A==} 79 121 80 122 '@blazediff/core@1.9.1': 81 123 resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} ··· 1059 1101 undici-types@7.18.2: 1060 1102 resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} 1061 1103 1104 + unicode-segmenter@0.14.5: 1105 + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1106 + 1062 1107 uri-js@4.4.1: 1063 1108 resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1064 1109 1065 1110 util-deprecate@1.0.2: 1066 1111 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1067 1112 1113 + valibot@1.4.1: 1114 + resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==} 1115 + peerDependencies: 1116 + typescript: '>=5' 1117 + peerDependenciesMeta: 1118 + typescript: 1119 + optional: true 1120 + 1068 1121 vite@8.0.16: 1069 1122 resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} 1070 1123 engines: {node: ^20.19.0 || >=22.12.0} ··· 1201 1254 resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} 1202 1255 1203 1256 snapshots: 1257 + 1258 + '@atcute/atproto@4.0.2(@atcute/lexicons@2.0.0)': 1259 + dependencies: 1260 + '@atcute/lexicons': 2.0.0 1261 + 1262 + '@atcute/bluesky@4.0.6(@atcute/lexicons@2.0.0)': 1263 + dependencies: 1264 + '@atcute/atproto': 4.0.2(@atcute/lexicons@2.0.0) 1265 + '@atcute/lexicons': 2.0.0 1266 + 1267 + '@atcute/client@5.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3)': 1268 + dependencies: 1269 + '@atcute/identity': 2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3) 1270 + '@atcute/lexicons': 2.0.0 1271 + transitivePeerDependencies: 1272 + - typescript 1273 + 1274 + '@atcute/identity@2.0.0(@atcute/lexicons@2.0.0)(typescript@6.0.3)': 1275 + dependencies: 1276 + '@atcute/lexicons': 2.0.0 1277 + valibot: 1.4.1(typescript@6.0.3) 1278 + transitivePeerDependencies: 1279 + - typescript 1280 + 1281 + '@atcute/lexicons@2.0.0': 1282 + dependencies: 1283 + '@atcute/uint8array': 1.1.2 1284 + '@atcute/util-text': 1.3.1 1285 + '@standard-schema/spec': 1.1.0 1286 + esm-env: 1.2.2 1287 + 1288 + '@atcute/uint8array@1.1.2': {} 1289 + 1290 + '@atcute/util-text@1.3.1': 1291 + dependencies: 1292 + unicode-segmenter: 0.14.5 1204 1293 1205 1294 '@blazediff/core@1.9.1': {} 1206 1295 ··· 2126 2215 2127 2216 undici-types@7.18.2: {} 2128 2217 2218 + unicode-segmenter@0.14.5: {} 2219 + 2129 2220 uri-js@4.4.1: 2130 2221 dependencies: 2131 2222 punycode: 2.3.1 2132 2223 2133 2224 util-deprecate@1.0.2: {} 2225 + 2226 + valibot@1.4.1(typescript@6.0.3): 2227 + optionalDependencies: 2228 + typescript: 6.0.3 2134 2229 2135 2230 vite@8.0.16(@types/node@24.13.1): 2136 2231 dependencies:
+84
src/lib/atproto/identity.ts
··· 1 + import { Client, ok, simpleFetchHandler } from '@atcute/client'; 2 + import type { ActorIdentifier } from '@atcute/lexicons/syntax'; 3 + import type { Handle } from '@atcute/lexicons/syntax'; 4 + import type {} from '@atcute/atproto'; 5 + import type {} from '@atcute/bluesky'; 6 + 7 + export type AccountIdentity = { 8 + handle: string; 9 + did: string; 10 + pds: string | null; 11 + displayName: string | null; 12 + avatar: string | null; 13 + description: string | null; 14 + }; 15 + 16 + export const defaultIdentity = { handle: 'desertthunder.dev', did: 'did:plc:xg2vq45muivyy3xwatcehspu' }; 17 + 18 + const publicApi = new Client({ handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) }); 19 + 20 + type DidDocument = { 21 + service?: Array<{ id?: string; type?: string; serviceEndpoint?: string | string[] | Record<string, unknown> }>; 22 + }; 23 + 24 + export async function resolveAccount(handle: string): Promise<AccountIdentity> { 25 + const normalizedHandle = handle.trim().replace(/^@/, '').toLowerCase(); 26 + 27 + if (!normalizedHandle) { 28 + throw new Error('Enter an AT Protocol handle.'); 29 + } 30 + 31 + if (!normalizedHandle.includes('.')) { 32 + throw new Error('Handles should look like a domain, for example desertthunder.dev.'); 33 + } 34 + 35 + const identity = await ok( 36 + publicApi.get('com.atproto.identity.resolveHandle', { params: { handle: normalizedHandle as Handle } }) 37 + ); 38 + 39 + const [profile, pds] = await Promise.all([getPublicProfile(identity.did), resolvePds(identity.did)]); 40 + 41 + return { 42 + handle: normalizedHandle, 43 + did: identity.did, 44 + pds, 45 + displayName: profile?.displayName ?? null, 46 + avatar: profile?.avatar ?? null, 47 + description: profile?.description ?? null 48 + }; 49 + } 50 + 51 + async function getPublicProfile(actor: string) { 52 + const response = await publicApi.get('app.bsky.actor.getProfile', { params: { actor: actor as ActorIdentifier } }); 53 + 54 + return response.ok ? response.data : null; 55 + } 56 + 57 + async function resolvePds(did: string): Promise<string | null> { 58 + const response = await fetch(resolveDidDocumentUrl(did)); 59 + 60 + if (!response.ok) { 61 + return null; 62 + } 63 + 64 + const document = (await response.json()) as DidDocument; 65 + const pdsService = document.service?.find( 66 + (service) => service.id === '#atproto_pds' || service.type === 'AtprotoPersonalDataServer' 67 + ); 68 + const endpoint = pdsService?.serviceEndpoint; 69 + 70 + return typeof endpoint === 'string' ? endpoint : null; 71 + } 72 + 73 + function resolveDidDocumentUrl(did: string) { 74 + if (did.startsWith('did:plc:')) { 75 + return `https://plc.directory/${encodeURIComponent(did)}`; 76 + } 77 + 78 + if (did.startsWith('did:web:')) { 79 + const domain = did.slice('did:web:'.length).replaceAll(':', '/'); 80 + return `https://${domain}/.well-known/did.json`; 81 + } 82 + 83 + throw new Error(`Unsupported DID method: ${did}`); 84 + }
+50
src/lib/atproto/setup.svelte.ts
··· 1 + import { browser } from '$app/environment'; 2 + import { defaultIdentity, type AccountIdentity } from './identity'; 3 + 4 + const storageKey = 'intrepid-ibex:account-identity'; 5 + 6 + class AccountSetupState { 7 + identity = $state<AccountIdentity | null>(null); 8 + loaded = $state(false); 9 + 10 + get isConfigured() { 11 + return this.identity !== null; 12 + } 13 + 14 + load() { 15 + if (!browser || this.loaded) { 16 + return; 17 + } 18 + 19 + const stored = localStorage.getItem(storageKey); 20 + 21 + if (stored) { 22 + try { 23 + this.identity = JSON.parse(stored) as AccountIdentity; 24 + } catch { 25 + localStorage.removeItem(storageKey); 26 + } 27 + } 28 + 29 + this.loaded = true; 30 + } 31 + 32 + save(identity: AccountIdentity) { 33 + this.identity = identity; 34 + 35 + if (browser) { 36 + localStorage.setItem(storageKey, JSON.stringify(identity)); 37 + } 38 + } 39 + 40 + reset() { 41 + this.identity = null; 42 + 43 + if (browser) { 44 + localStorage.removeItem(storageKey); 45 + } 46 + } 47 + } 48 + 49 + export const accountSetup = new AccountSetupState(); 50 + export const setupDefaults = defaultIdentity;
+1 -4
src/lib/components/CollectionBrowser.svelte
··· 60 60 <div> 61 61 <p class="eyebrow">Selected collection</p> 62 62 <h2>app.bsky.feed.post</h2> 63 - <p> 64 - Browse posts as if they were tidy Nautilus files. This is a static design pass for the ATProto desktop 65 - browser. 66 - </p> 63 + <p>Browse posts as if they were tidy Nautilus files.</p> 67 64 </div> 68 65 </div> 69 66
+315
src/lib/components/SetupDialog.svelte
··· 1 + <script lang="ts"> 2 + import { resolveAccount, type AccountIdentity } from '$lib/atproto/identity'; 3 + import { accountSetup, setupDefaults } from '$lib/atproto/setup.svelte'; 4 + 5 + let handle = $state(setupDefaults.handle); 6 + let resolvedIdentity = $state<AccountIdentity | null>(null); 7 + let error = $state<string | null>(null); 8 + let isResolving = $state(false); 9 + 10 + const didMatchesKnownAccount = $derived(resolvedIdentity?.did === setupDefaults.did); 11 + 12 + async function resolveHandle() { 13 + isResolving = true; 14 + error = null; 15 + resolvedIdentity = null; 16 + 17 + try { 18 + resolvedIdentity = await resolveAccount(handle); 19 + } catch (unknownError) { 20 + error = unknownError instanceof Error ? unknownError.message : 'Could not resolve that handle.'; 21 + } finally { 22 + isResolving = false; 23 + } 24 + } 25 + 26 + function useAccount() { 27 + if (resolvedIdentity) { 28 + accountSetup.save(resolvedIdentity); 29 + } 30 + } 31 + </script> 32 + 33 + <section class="setup-dialog" aria-labelledby="setup-title"> 34 + <div class="intro-panel"> 35 + <div class="identity-icon" aria-hidden="true"> 36 + {#if resolvedIdentity?.avatar} 37 + <img src={resolvedIdentity.avatar} alt="" /> 38 + {:else} 39 + <img src="/icons/humanity/places/user-home.svg" alt="" /> 40 + {/if} 41 + </div> 42 + 43 + <div> 44 + <p class="eyebrow">First run setup</p> 45 + <h2 id="setup-title">Welcome to Intrepid Ibex Browser</h2> 46 + <p>Enter an AT Protocol handle to browse public collections from that repository.</p> 47 + </div> 48 + </div> 49 + 50 + <form class="handle-form" onsubmit={(event) => event.preventDefault()}> 51 + <label for="handle">Handle</label> 52 + <div class="handle-row"> 53 + <input id="handle" bind:value={handle} placeholder="desertthunder.dev" autocomplete="username" /> 54 + <button type="button" onclick={resolveHandle} disabled={isResolving}> 55 + {isResolving ? 'Resolving…' : 'Resolve'} 56 + </button> 57 + </div> 58 + </form> 59 + 60 + <div class="status-box" class:verified={resolvedIdentity} class:error role="status"> 61 + {#if isResolving} 62 + <p>Looking up handle through the public AT Protocol API…</p> 63 + {:else if error} 64 + <p>{error}</p> 65 + {:else if resolvedIdentity} 66 + <div class="resolved-header"> 67 + <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="" width="16" height="16" /> 68 + <strong>Identity found</strong> 69 + {#if didMatchesKnownAccount} 70 + <span>known account</span> 71 + {/if} 72 + </div> 73 + 74 + <dl> 75 + <div> 76 + <dt>Handle</dt> 77 + <dd>{resolvedIdentity.handle}</dd> 78 + </div> 79 + <div> 80 + <dt>DID</dt> 81 + <dd>{resolvedIdentity.did}</dd> 82 + </div> 83 + <div> 84 + <dt>PDS</dt> 85 + <dd>{resolvedIdentity.pds ?? 'Not advertised in DID document'}</dd> 86 + </div> 87 + </dl> 88 + {:else} 89 + <p>Ready to resolve <strong>{handle}</strong>.</p> 90 + {/if} 91 + </div> 92 + 93 + <div class="setup-actions"> 94 + <button type="button" class="secondary" onclick={() => (resolvedIdentity = null)} disabled={!resolvedIdentity}> 95 + Change Handle 96 + </button> 97 + <button 98 + type="button" 99 + class="primary" 100 + onclick={resolvedIdentity ? useAccount : resolveHandle} 101 + disabled={isResolving}> 102 + {resolvedIdentity ? 'Use This Account' : 'Resolve Handle'} 103 + </button> 104 + </div> 105 + </section> 106 + 107 + <style> 108 + .setup-dialog { 109 + display: grid; 110 + align-content: start; 111 + gap: var(--space-4); 112 + height: 100%; 113 + padding: var(--space-5); 114 + background: 115 + linear-gradient(90deg, rgb(255 255 255 / 0.65), rgb(255 255 255 / 0.18)), linear-gradient(#f8edda, #decaac); 116 + } 117 + 118 + .intro-panel { 119 + display: grid; 120 + grid-template-columns: 4rem minmax(0, 1fr); 121 + gap: var(--space-4); 122 + align-items: center; 123 + padding: var(--space-4); 124 + background: linear-gradient(#fffaf0, #ead8ba); 125 + border: 1px solid #b29165; 126 + border-radius: var(--radius-3); 127 + box-shadow: var(--shadow-raised); 128 + } 129 + 130 + .identity-icon { 131 + display: grid; 132 + place-items: center; 133 + width: 4rem; 134 + height: 4rem; 135 + overflow: hidden; 136 + background: linear-gradient(#fff5dd, #cba16c); 137 + border: 1px solid #8f673f; 138 + border-radius: var(--radius-3); 139 + box-shadow: var(--shadow-sunken); 140 + } 141 + 142 + .identity-icon img { 143 + width: 48px; 144 + height: 48px; 145 + object-fit: cover; 146 + } 147 + 148 + .eyebrow { 149 + color: var(--orange-700); 150 + font-size: var(--text-0); 151 + font-weight: 700; 152 + letter-spacing: 0.04em; 153 + text-transform: uppercase; 154 + } 155 + 156 + h2 { 157 + margin-top: var(--space-1); 158 + font-size: var(--text-5); 159 + line-height: var(--leading-tight); 160 + } 161 + 162 + .intro-panel p:last-child { 163 + margin-top: var(--space-2); 164 + color: var(--text-muted); 165 + font-size: var(--text-2); 166 + } 167 + 168 + .handle-form { 169 + display: grid; 170 + gap: var(--space-2); 171 + } 172 + 173 + .handle-form label { 174 + font-size: var(--text-1); 175 + font-weight: 700; 176 + } 177 + 178 + .handle-row { 179 + display: grid; 180 + grid-template-columns: minmax(0, 1fr) auto; 181 + gap: var(--space-2); 182 + } 183 + 184 + input { 185 + height: 2rem; 186 + padding: 0 var(--space-3); 187 + color: var(--text); 188 + background: #fffdf7; 189 + border: 1px solid #8e7654; 190 + border-radius: var(--radius-2); 191 + box-shadow: var(--shadow-sunken); 192 + } 193 + 194 + button { 195 + height: 2rem; 196 + padding: 0 var(--space-4); 197 + border: 1px solid #9b7d55; 198 + border-radius: var(--radius-2); 199 + box-shadow: var(--shadow-raised); 200 + font-size: var(--text-1); 201 + font-weight: 700; 202 + cursor: default; 203 + } 204 + 205 + button:disabled { 206 + cursor: not-allowed; 207 + opacity: 0.56; 208 + } 209 + 210 + button:not(:disabled):active { 211 + box-shadow: var(--shadow-sunken); 212 + } 213 + 214 + .secondary, 215 + .handle-row button { 216 + background: linear-gradient(#fff9ec, #ceb48a); 217 + } 218 + 219 + .primary { 220 + color: white; 221 + background: linear-gradient(#e98b34, #ae4f12); 222 + border-color: #f4b46b #8d3c0b #713009 #f1a35e; 223 + text-shadow: 0 1px 0 rgb(0 0 0 / 0.55); 224 + } 225 + 226 + .status-box { 227 + min-height: 7rem; 228 + padding: var(--space-4); 229 + background: #fffaf0; 230 + border: 1px solid #b29165; 231 + border-radius: var(--radius-2); 232 + box-shadow: var(--shadow-sunken); 233 + font-size: var(--text-2); 234 + } 235 + 236 + .status-box.error { 237 + color: #721c0d; 238 + background: #f7d6c9; 239 + border-color: #b55a38; 240 + } 241 + 242 + .status-box.verified { 243 + background: #fffdf6; 244 + } 245 + 246 + .resolved-header { 247 + display: flex; 248 + align-items: center; 249 + gap: var(--space-2); 250 + margin-bottom: var(--space-3); 251 + } 252 + 253 + .resolved-header span { 254 + padding: 1px var(--space-2); 255 + color: white; 256 + background: var(--green-700); 257 + border-radius: var(--radius-round); 258 + font-size: var(--text-0); 259 + font-weight: 700; 260 + } 261 + 262 + dl { 263 + display: grid; 264 + gap: var(--space-2); 265 + } 266 + 267 + dl div { 268 + display: grid; 269 + grid-template-columns: 4rem minmax(0, 1fr); 270 + gap: var(--space-3); 271 + } 272 + 273 + dt { 274 + color: var(--text-muted); 275 + font-weight: 700; 276 + } 277 + 278 + dd { 279 + overflow-wrap: anywhere; 280 + font-family: var(--font-mono); 281 + font-size: var(--text-1); 282 + } 283 + 284 + .setup-actions { 285 + display: flex; 286 + justify-content: flex-end; 287 + gap: var(--space-2); 288 + padding-top: var(--space-2); 289 + border-top: 1px solid rgb(142 118 84 / 0.45); 290 + } 291 + 292 + @media (max-width: 640px) { 293 + .setup-dialog { 294 + padding: var(--space-3); 295 + } 296 + 297 + .intro-panel, 298 + .handle-row, 299 + dl div { 300 + grid-template-columns: 1fr; 301 + } 302 + 303 + .identity-icon { 304 + display: none; 305 + } 306 + 307 + .setup-actions { 308 + justify-content: stretch; 309 + } 310 + 311 + .setup-actions button { 312 + flex: 1; 313 + } 314 + } 315 + </style>
+21 -3
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { accountSetup } from '$lib/atproto/setup.svelte'; 2 4 import favicon from '$lib/assets/favicon.svg'; 3 5 import AppWindow from '$lib/components/AppWindow.svelte'; 4 6 import DesktopIcon from '$lib/components/DesktopIcon.svelte'; 5 7 import GnomePanel from '$lib/components/GnomePanel.svelte'; 8 + import SetupDialog from '$lib/components/SetupDialog.svelte'; 6 9 import '$lib/styles/style.css'; 7 10 8 11 let { children } = $props(); ··· 13 16 { label: 'Computer', icon: '/icons/humanity/devices/computer.svg' }, 14 17 { label: 'Trash', icon: '/icons/humanity/places/user-trash.svg' } 15 18 ]; 19 + 20 + const windowTitle = $derived( 21 + accountSetup.isConfigured ? 'AT Protocol Collections - Intrepid Ibex' : 'AT Protocol Account Setup' 22 + ); 23 + const windowIcon = $derived( 24 + accountSetup.isConfigured ? '/icons/humanity/apps/internet-feed-reader.svg' : '/icons/humanity/places/user-home.svg' 25 + ); 26 + 27 + onMount(() => { 28 + accountSetup.load(); 29 + }); 16 30 </script> 17 31 18 32 <svelte:head> ··· 32 46 </section> 33 47 34 48 <div class="primary-window"> 35 - <AppWindow title="AT Protocol Collections - Intrepid Ibex" icon="/icons/humanity/apps/internet-feed-reader.svg"> 36 - {@render children()} 49 + <AppWindow title={windowTitle} icon={windowIcon}> 50 + {#if accountSetup.isConfigured} 51 + {@render children()} 52 + {:else} 53 + <SetupDialog /> 54 + {/if} 37 55 </AppWindow> 38 56 </div> 39 57 40 58 <aside class="sticky-note" aria-label="Design note"> 41 59 <h2>Intrepid Ibex</h2> 42 - <p>Static GNOME recreation: top panel, Humanity icons, tan window chrome, and collection folders.</p> 60 + <p>This app is a recreation of the spirit of Ubuntu 8.10</p> 43 61 </aside> 44 62 </main> 45 63 </div>