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: application menu

* improve mobile layout

Owais Jamil (Jun 19, 2026, 1:17 PM -0500) adb5c585 9418bf77

+262 -105
+5
.changeset/sharp-signs-exist.md
··· 1 + --- 2 + 'intrepid-ibex': minor 3 + --- 4 + 5 + applications menu & mobile responsiveness improvements
+5
README.md
··· 24 24 A media viewer for repo blobs and record-attached media. It is used when Nautilus 25 25 finds image or media blob references in a selected record. 26 26 27 + ### Web Browser 28 + 29 + A GNOME-style Lexicon schema browser. It resolves NSIDs through lexdns, shows 30 + readable schema definitions, raw Lexicon JSON, and the DNS/PDS resolution trace. 31 + 27 32 ### Identity Inspector 28 33 29 34 A public identity window for DID documents, aliases, services, verification
+89 -7
src/lib/components/GnomeMenuBar.svelte
··· 4 4 import { repoSession } from '$lib/atproto/session.svelte'; 5 5 import { accountSetup } from '$lib/atproto/setup.svelte'; 6 6 import { repoBrowser } from '$lib/atproto/repo.svelte'; 7 - import { collectionPath } from '$lib/atproto/routes'; 7 + import { blobsPath, collectionPath, identityPath } from '$lib/atproto/routes'; 8 + import { REPO_URL } from '$lib/constants'; 9 + import { desktopLaunchers, type DesktopLauncherId } from '$lib/launcher'; 8 10 import { desktopSession } from '$lib/desktop-session.svelte'; 9 11 import { windowManager } from '$lib/window-manager.svelte'; 10 12 import GnomeTray from '$lib/components/GnomeTray.svelte'; 11 13 12 14 type RepoPathname = `/repos/${string}`; 15 + type AppPathname = '/' | '/browse' | '/docs' | '/lexicons' | RepoPathname; 13 16 const navigateTo = goto as (url: string, options?: Parameters<typeof goto>[1]) => ReturnType<typeof goto>; 14 17 15 18 function changeAccount() { ··· 26 29 27 30 void navigateTo(resolve(collectionPath({ did: identity.did, collection: collectionName }) as RepoPathname)); 28 31 } 32 + 33 + function openPath(path: AppPathname) { 34 + void navigateTo(resolve(path), { keepFocus: true, noScroll: true }); 35 + } 36 + 37 + function openIdentityInspector() { 38 + const identity = repoSession.identity ?? accountSetup.identity; 39 + if (!identity) { 40 + openPath('/browse'); 41 + return; 42 + } 43 + 44 + openPath(identityPath(identity.did) as RepoPathname); 45 + } 46 + 47 + function openImageViewer() { 48 + const identity = repoSession.identity ?? accountSetup.identity; 49 + if (!identity) { 50 + openPath('/browse'); 51 + return; 52 + } 53 + 54 + openPath(blobsPath(identity.did) as RepoPathname); 55 + } 56 + 57 + function openAboutComputer() { 58 + windowManager.open('about-computer'); 59 + } 60 + 61 + function openLauncher(id: DesktopLauncherId) { 62 + if (id === 'home') { 63 + openPath('/'); 64 + return; 65 + } 66 + 67 + if (id === 'collections') { 68 + openPath('/browse'); 69 + return; 70 + } 71 + 72 + if (id === 'identity-inspector') { 73 + openIdentityInspector(); 74 + return; 75 + } 76 + 77 + if (id === 'image-viewer') { 78 + openImageViewer(); 79 + return; 80 + } 81 + 82 + if (id === 'web-browser') { 83 + openPath('/lexicons'); 84 + return; 85 + } 86 + 87 + if (id === 'about-computer') { 88 + openAboutComputer(); 89 + return; 90 + } 91 + 92 + if (id === 'document-viewer') { 93 + openPath('/docs'); 94 + return; 95 + } 96 + 97 + window.open(REPO_URL, '_blank', 'noopener,noreferrer'); 98 + } 29 99 </script> 30 100 31 101 <nav class="panel-menus" aria-label="System menus"> 32 - <button class="applications-menu" type="button" onclick={() => goto(resolve('/docs/atmosphere'))}> 33 - <img src="/icons/humanity/apps/system-file-manager.svg" alt="" width="20" height="20" /> 34 - <span>Applications</span> 35 - </button> 102 + <details class="panel-menu applications-panel"> 103 + <summary class="applications-menu"> 104 + <img src="/icons/humanity/apps/system-file-manager.svg" alt="" width="20" height="20" /> 105 + <span>Applications</span> 106 + </summary> 107 + <div class="menu-popover applications-popover"> 108 + <p class="menu-heading">Applications</p> 109 + {#each desktopLaunchers as launcher (launcher.id)} 110 + <button type="button" onclick={() => openLauncher(launcher.id)}> 111 + <img src={launcher.icon} alt="" width="16" height="16" /> 112 + <span>{launcher.label}</span> 113 + </button> 114 + {/each} 115 + </div> 116 + </details> 36 117 37 118 <details class="panel-menu"> 38 119 <summary>Places</summary> ··· 154 235 font-weight: 700; 155 236 } 156 237 157 - .system-popover { 238 + .system-popover, 239 + .applications-popover { 158 240 min-width: 13rem; 159 241 } 160 242 ··· 163 245 } 164 246 165 247 @media (max-width: 760px) { 166 - .panel-menus .panel-menu { 248 + .panel-menus .panel-menu:not(.applications-panel) { 167 249 display: none; 168 250 } 169 251 }
+19 -1
src/lib/components/GnomeTray.svelte
··· 87 87 </label> 88 88 </div> 89 89 </details> 90 - <a class="tray-button" href="https://time.is" target="_blank"> 90 + <a class="tray-button time-button" href="https://time.is" target="_blank" title={panelTime}> 91 91 <time datetime={now.toISOString()}>{panelTime}</time> 92 92 </a> 93 93 </aside> ··· 197 197 } 198 198 199 199 .panel-tray time { 200 + display: block; 201 + max-width: 16rem; 202 + overflow: hidden; 203 + text-overflow: ellipsis; 204 + white-space: nowrap; 200 205 font-variant-numeric: tabular-nums; 206 + } 207 + 208 + .time-button { 209 + min-width: 0; 201 210 } 202 211 203 212 @media (max-width: 760px) { 204 213 .account-chip span { 205 214 display: none; 215 + } 216 + 217 + .panel-tray { 218 + min-width: 0; 219 + gap: var(--space-1); 220 + } 221 + 222 + .panel-tray time { 223 + max-width: 7.5rem; 206 224 } 207 225 } 208 226 </style>
+15 -4
src/lib/components/StickyNote.svelte
··· 16 16 17 17 <style> 18 18 .sticky-note { 19 - position: relative; 20 - align-self: end; 19 + position: absolute; 20 + right: var(--space-5); 21 + bottom: var(--space-5); 22 + z-index: 2; 21 23 width: min(18rem, 100%); 22 - margin-bottom: var(--space-8); 23 24 padding: var(--space-4); 24 25 transform: rotate(1.2deg); 25 26 color: #3a250e; ··· 77 78 78 79 @media (max-width: 1000px) { 79 80 .sticky-note { 80 - display: none; 81 + right: var(--space-3); 82 + bottom: var(--space-3); 83 + width: min(16rem, calc(100vw - 2rem)); 84 + padding: var(--space-3); 85 + } 86 + } 87 + 88 + @media (max-width: 640px) { 89 + .sticky-note { 90 + width: min(14rem, calc(100vw - 1.5rem)); 91 + font-size: var(--text-1); 81 92 } 82 93 } 83 94 </style>
+34
src/lib/launcher.ts
··· 1 + export type DesktopLauncherId = 2 + | 'home' 3 + | 'collections' 4 + | 'identity-inspector' 5 + | 'image-viewer' 6 + | 'web-browser' 7 + | 'about-computer' 8 + | 'document-viewer' 9 + | 'trash'; 10 + 11 + export type DesktopLauncher = { id: DesktopLauncherId; label: string; icon: string }; 12 + 13 + export const desktopLaunchers: DesktopLauncher[] = [ 14 + { id: 'home', label: 'ibex Home', icon: '/icons/humanity/places/user-home.svg' }, 15 + { id: 'collections', label: 'Collections', icon: '/icons/humanity/places/folder.svg' }, 16 + { id: 'identity-inspector', label: 'Identity Inspector', icon: '/icons/humanity/apps/identity-inspector.svg' }, 17 + { id: 'image-viewer', label: 'Image Viewer', icon: '/icons/humanity/apps/eog.svg' }, 18 + { id: 'web-browser', label: 'Web Browser', icon: '/icons/humanity/apps/web-browser.svg' }, 19 + { id: 'about-computer', label: 'Computer', icon: '/icons/humanity/devices/computer.svg' }, 20 + { id: 'document-viewer', label: 'Document Viewer', icon: '/icons/humanity/mimes/gnome-mime-application-pdf.svg' }, 21 + { id: 'trash', label: 'Trash', icon: '/icons/humanity/places/user-trash.svg' } 22 + ]; 23 + 24 + const leftDesktopLauncherIds = new Set<DesktopLauncherId>(['home', 'collections', 'about-computer', 'trash']); 25 + const rightDesktopLauncherIds = new Set<DesktopLauncherId>([ 26 + 'identity-inspector', 27 + 'image-viewer', 28 + 'web-browser', 29 + 'document-viewer' 30 + ]); 31 + 32 + export const leftDesktopLaunchers = desktopLaunchers.filter((launcher) => leftDesktopLauncherIds.has(launcher.id)); 33 + 34 + export const rightDesktopLaunchers = desktopLaunchers.filter((launcher) => rightDesktopLauncherIds.has(launcher.id));
+95 -93
src/routes/+layout.svelte
··· 9 9 import { collectionPath, repoPath } from '$lib/atproto/routes'; 10 10 import { accountSetup } from '$lib/atproto/setup.svelte'; 11 11 import favicon from '$lib/assets/favicon.svg'; 12 + import { leftDesktopLaunchers, rightDesktopLaunchers, type DesktopLauncherId } from '$lib/launcher'; 12 13 import { desktopSession } from '$lib/desktop-session.svelte'; 13 14 import { startupSound } from '$lib/sounds.svelte'; 14 15 import { errorMessage } from '$lib/utils/errors'; ··· 65 66 const identityInspectorWindow = $derived(windowManager.getWindow('identity-inspector')); 66 67 const eogWindow = $derived(windowManager.getWindow('eog')); 67 68 const repoIdentity = $derived(repoSession.identity ?? accountSetup.identity); 68 - const trashShortcut = { 69 - label: 'Trash', 70 - icon: '/icons/humanity/places/user-trash.svg', 71 - onactivate: () => window.open('https://github.com/desertthunder.dev/ibex', '_blank', 'noopener,noreferrer') 72 - }; 73 - const shortcuts = $derived([ 74 - { 75 - label: 'ibex Home', 76 - icon: '/icons/humanity/places/user-home.svg', 77 - selected: page.route.id === '/', 78 - onactivate: () => { 79 - windowManager.restore('main'); 80 - void goto(resolve('/')); 81 - } 82 - }, 83 - { 84 - label: 'Collections', 85 - icon: '/icons/humanity/places/folder.svg', 86 - selected: page.route.id === '/browse' || page.route.id?.startsWith('/repos'), 87 - onactivate: () => { 88 - windowManager.restore('main'); 89 - void goto(resolve('/browse')); 90 - } 91 - }, 92 - { 93 - label: 'Identity Inspector', 94 - icon: '/icons/humanity/apps/identity-inspector.svg', 95 - selected: identityInspectorWindow?.isOpen && !identityInspectorWindow.isMinimized, 96 - onactivate: () => { 97 - if (repoIdentity) { 98 - void goto(resolve(`/repos/${repoIdentity.did}/identity`), { keepFocus: true, noScroll: true }); 99 - return; 100 - } 101 - 102 - windowManager.restore('main'); 103 - void goto(resolve('/browse')); 104 - } 105 - }, 106 - { 107 - label: 'Image Viewer', 108 - icon: '/icons/humanity/apps/eog.svg', 109 - selected: eogWindow?.isOpen && !eogWindow.isMinimized, 110 - onactivate: () => { 111 - if (repoIdentity) { 112 - void goto(resolve(`/repos/${repoIdentity.did}/blobs`), { keepFocus: true, noScroll: true }); 113 - return; 114 - } 115 - 116 - windowManager.restore('main'); 117 - void goto(resolve('/browse')); 118 - } 119 - }, 120 - { 121 - label: 'Web Browser', 122 - icon: '/icons/humanity/apps/web-browser.svg', 123 - selected: page.route.id?.startsWith('/lexicons'), 124 - onactivate: () => { 125 - windowManager.restore('main'); 126 - void goto(resolve('/lexicons')); 127 - } 128 - }, 129 - { 130 - label: 'Computer', 131 - icon: '/icons/humanity/devices/computer.svg', 132 - selected: showAboutComputer, 133 - onactivate: () => { 134 - windowManager.open('about-computer'); 135 - } 136 - }, 137 - { 138 - label: 'Document Viewer', 139 - icon: '/icons/humanity/mimes/gnome-mime-application-pdf.svg', 140 - selected: 141 - page.route.id?.startsWith('/docs') || (documentViewerWindow?.isOpen && !documentViewerWindow.isMinimized), 142 - onactivate: () => { 143 - windowManager.restore('main'); 144 - void goto(resolve('/docs')); 145 - } 146 - } 147 - ]); 69 + const leftShortcuts = $derived( 70 + leftDesktopLaunchers.map((launcher) => ({ 71 + ...launcher, 72 + selected: isLauncherSelected(launcher.id), 73 + onactivate: () => activateLauncher(launcher.id) 74 + })) 75 + ); 76 + const rightShortcuts = $derived( 77 + rightDesktopLaunchers.map((launcher) => ({ 78 + ...launcher, 79 + selected: isLauncherSelected(launcher.id), 80 + onactivate: () => activateLauncher(launcher.id) 81 + })) 82 + ); 148 83 149 84 afterNavigate(() => { 150 85 setMainWindowTitle(); ··· 261 196 void navigateTo(resolve(path as RepoPathname), { keepFocus: true, noScroll: true }); 262 197 } 263 198 199 + function activateLauncher(id: DesktopLauncherId) { 200 + if (id === 'home') { 201 + windowManager.restore('main'); 202 + void goto(resolve('/')); 203 + return; 204 + } 205 + 206 + if (id === 'collections') { 207 + windowManager.restore('main'); 208 + void goto(resolve('/browse')); 209 + return; 210 + } 211 + 212 + if (id === 'identity-inspector') { 213 + if (repoIdentity) { 214 + void goto(resolve(`/repos/${repoIdentity.did}/identity`), { keepFocus: true, noScroll: true }); 215 + return; 216 + } 217 + 218 + windowManager.restore('main'); 219 + void goto(resolve('/browse')); 220 + return; 221 + } 222 + 223 + if (id === 'image-viewer') { 224 + if (repoIdentity) { 225 + void goto(resolve(`/repos/${repoIdentity.did}/blobs`), { keepFocus: true, noScroll: true }); 226 + return; 227 + } 228 + 229 + windowManager.restore('main'); 230 + void goto(resolve('/browse')); 231 + return; 232 + } 233 + 234 + if (id === 'web-browser') { 235 + windowManager.restore('main'); 236 + void goto(resolve('/lexicons')); 237 + return; 238 + } 239 + 240 + if (id === 'about-computer') { 241 + windowManager.open('about-computer'); 242 + return; 243 + } 244 + 245 + if (id === 'document-viewer') { 246 + windowManager.restore('main'); 247 + void goto(resolve('/docs')); 248 + return; 249 + } 250 + 251 + window.open('https://github.com/desertthunder.dev/ibex', '_blank', 'noopener,noreferrer'); 252 + } 253 + 254 + function isLauncherSelected(id: DesktopLauncherId) { 255 + if (id === 'home') return page.route.id === '/'; 256 + if (id === 'collections') return page.route.id === '/browse' || page.route.id?.startsWith('/repos'); 257 + if (id === 'identity-inspector') return identityInspectorWindow?.isOpen && !identityInspectorWindow.isMinimized; 258 + if (id === 'image-viewer') return eogWindow?.isOpen && !eogWindow.isMinimized; 259 + if (id === 'web-browser') return page.route.id?.startsWith('/lexicons'); 260 + if (id === 'about-computer') return showAboutComputer; 261 + if (id === 'document-viewer') { 262 + return page.route.id?.startsWith('/docs') || (documentViewerWindow?.isOpen && !documentViewerWindow.isMinimized); 263 + } 264 + 265 + return false; 266 + } 267 + 264 268 function setMainWindowTitle() { 265 269 windowManager.setTitle('main', windowTitle, windowIcon); 266 270 } ··· 284 288 285 289 <main class="desktop-stage" aria-label="Ubuntu 8.10 inspired AT Protocol browser desktop"> 286 290 <section class="desktop-icons" aria-label="Desktop shortcuts"> 287 - {#each shortcuts as shortcut (shortcut.label)} 291 + {#each leftShortcuts as shortcut (shortcut.label)} 288 292 <DesktopIcon {...shortcut} /> 289 293 {/each} 290 294 </section> 291 295 292 - <section class="desktop-trash" aria-label="Trash"> 293 - <DesktopIcon {...trashShortcut} /> 296 + <section class="desktop-app-icons" aria-label="Application shortcuts"> 297 + {#each rightShortcuts as shortcut (shortcut.label)} 298 + <DesktopIcon {...shortcut} /> 299 + {/each} 294 300 </section> 295 301 296 302 {#if mainWindow?.isOpen && !mainWindow.isMinimized} ··· 459 465 padding: var(--space-5); 460 466 } 461 467 462 - .desktop-icons { 468 + .desktop-icons, 469 + .desktop-app-icons { 463 470 display: grid; 464 471 align-content: start; 465 472 justify-items: center; 466 473 gap: var(--space-4); 467 474 } 468 475 469 - .desktop-trash { 476 + .desktop-app-icons { 470 477 position: absolute; 471 478 top: var(--space-5); 472 479 right: var(--space-5); 473 - display: grid; 474 - justify-items: center; 475 480 } 476 481 477 482 .primary-window { ··· 559 564 padding: var(--space-3); 560 565 } 561 566 562 - .desktop-trash { 567 + .desktop-app-icons { 563 568 top: var(--space-3); 564 569 right: var(--space-3); 565 570 } ··· 578 583 grid-template-columns: 1fr; 579 584 } 580 585 581 - .desktop-icons { 582 - display: none; 583 - } 584 - 585 - .desktop-trash { 586 + .desktop-icons, 587 + .desktop-app-icons { 586 588 display: none; 587 589 } 588 590