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: maximize windows

* repo link

Owais Jamil (Jun 8, 2026, 2:19 PM -0500) 28e6418d 3f364498

+276 -32
+10 -1
src/lib/components/AboutComputer.svelte
··· 41 41 </dl> 42 42 </div> 43 43 44 - <p class="note">Humanity icons, Ubuntu fonts, tan window chrome, and public ATProto repo browsing.</p> 44 + <p class="note"> 45 + View the project on 46 + <a href="https://tangled.org/desertthunder.dev/ibex" target="_blank" rel="noreferrer">Tangled</a>. 47 + </p> 45 48 </section> 46 49 47 50 <style> ··· 127 130 128 131 .note { 129 132 align-self: end; 133 + } 134 + 135 + .note a { 136 + color: var(--blue-700); 137 + font-weight: 700; 138 + text-decoration: underline; 130 139 } 131 140 132 141 @media (max-width: 640px) {
+55 -4
src/lib/components/AppWindow.svelte
··· 1 1 <script lang="ts"> 2 + import { browser } from '$app/environment'; 3 + import { onMount } from 'svelte'; 2 4 import type { Snippet } from 'svelte'; 3 5 4 6 type Props = { ··· 8 10 movable?: boolean; 9 11 showMenubar?: boolean; 10 12 showToolbar?: boolean; 13 + windowId?: string; 14 + maximized?: boolean; 15 + onfocus?: () => void; 16 + onminimize?: () => void; 17 + onmaximize?: () => void; 11 18 onclose?: () => void; 12 19 }; 13 20 ··· 18 25 movable = true, 19 26 showMenubar = true, 20 27 showToolbar = true, 28 + windowId, 29 + maximized = false, 30 + onfocus, 31 + onminimize, 32 + onmaximize, 21 33 onclose, 22 34 children 23 35 }: Props & { children: Snippet } = $props(); ··· 28 40 null 29 41 ); 30 42 43 + const storageKey = $derived(windowId ? `intrepid-ibex:window-position:${windowId}` : null); 44 + 45 + onMount(() => { 46 + if (!browser || !storageKey) return; 47 + 48 + const storedPosition = localStorage.getItem(storageKey); 49 + if (!storedPosition) return; 50 + 51 + try { 52 + const position = JSON.parse(storedPosition) as { x?: number; y?: number }; 53 + x = typeof position.x === 'number' ? position.x : 0; 54 + y = typeof position.y === 'number' ? position.y : 0; 55 + } catch { 56 + localStorage.removeItem(storageKey); 57 + } 58 + }); 59 + 31 60 function startDrag(event: PointerEvent) { 32 - if (!movable || event.button !== 0 || event.target instanceof HTMLButtonElement) { 61 + onfocus?.(); 62 + 63 + if (!movable || maximized || event.button !== 0 || event.target instanceof HTMLButtonElement) { 33 64 return; 34 65 } 35 66 ··· 54 85 55 86 (event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId); 56 87 drag = null; 88 + 89 + if (browser && storageKey) { 90 + localStorage.setItem(storageKey, JSON.stringify({ x, y })); 91 + } 57 92 } 58 93 </script> 59 94 60 - <section class="app-window" class:dragging={drag} aria-label={title} style:transform={`translate(${x}px, ${y}px)`}> 95 + <section 96 + class="app-window" 97 + class:dragging={drag} 98 + class:maximized 99 + aria-label={title} 100 + role="group" 101 + style:transform={maximized ? undefined : `translate(${x}px, ${y}px)`} 102 + onpointerdown={onfocus}> 61 103 <header 62 104 class="titlebar" 63 105 role="group" ··· 71 113 <h1>{title}</h1> 72 114 </div> 73 115 <div class="window-controls" aria-label="Window controls"> 74 - <button type="button" aria-label="Minimize" tabindex="-1"></button> 75 - <button type="button" aria-label="Maximize" tabindex="-1"></button> 116 + <button type="button" aria-label="Minimize" tabindex={onminimize ? 0 : -1} onclick={onminimize}></button> 117 + <button 118 + type="button" 119 + aria-label={maximized ? 'Restore' : 'Maximize'} 120 + tabindex={onmaximize ? 0 : -1} 121 + onclick={onmaximize}></button> 76 122 <button type="button" aria-label="Close" tabindex={onclose ? 0 : -1} onclick={onclose}></button> 77 123 </div> 78 124 </header> ··· 119 165 120 166 .app-window.dragging { 121 167 user-select: none; 168 + } 169 + 170 + .app-window.maximized { 171 + border-radius: 0; 172 + transform: none; 122 173 } 123 174 124 175 .titlebar {
+19 -6
src/lib/components/GnomePanel.svelte
··· 1 1 <script lang="ts"> 2 2 import { accountSetup } from '$lib/atproto/setup.svelte'; 3 3 import { repoBrowser } from '$lib/atproto/repo.svelte'; 4 + import { windowManager } from '$lib/window-manager.svelte'; 4 5 5 6 const launchers = [ 6 7 { label: 'Browser', icon: '/icons/humanity/apps/web-browser.svg' }, ··· 66 67 <div class="panel-spacer" aria-hidden="true"></div> 67 68 68 69 <section class="window-list" aria-label="Open windows"> 69 - <button class="active-window" type="button"> 70 - <img src="/icons/humanity/apps/internet-feed-reader.svg" alt="" width="16" height="16" /> 71 - <span>{accountSetup.isConfigured ? 'AT Protocol Collections' : 'AT Protocol Account Setup'}</span> 72 - </button> 70 + {#each windowManager.openWindows as window (window.id)} 71 + <button 72 + class="active-window" 73 + class:minimized={window.isMinimized} 74 + type="button" 75 + onclick={() => windowManager.restore(window.id)}> 76 + <img src={window.icon} alt="" width="16" height="16" /> 77 + <span>{window.title}</span> 78 + </button> 79 + {/each} 73 80 </section> 74 81 75 82 <aside class="panel-tray" aria-label="Status tray"> ··· 224 231 } 225 232 226 233 .window-list { 227 - min-width: min(28vw, 19rem); 234 + gap: var(--space-1); 235 + min-width: min(32vw, 24rem); 228 236 } 229 237 230 238 .active-window { 231 - width: 100%; 239 + min-width: 8rem; 240 + max-width: 13rem; 232 241 color: #23170f; 233 242 background: linear-gradient(#f5e7cb, #b99d78); 234 243 border-color: #fff3dc #755232 #4b2e19 #e8d3b4; 235 244 box-shadow: var(--shadow-sunken); 236 245 text-shadow: 0 1px 0 rgb(255 255 255 / 0.65); 246 + } 247 + 248 + .active-window.minimized { 249 + opacity: 0.72; 237 250 } 238 251 239 252 .active-window span {
+105
src/lib/window-manager.svelte.ts
··· 1 + export type WindowId = 'main' | 'about-computer'; 2 + 3 + export type ManagedWindow = { 4 + id: WindowId; 5 + title: string; 6 + icon: string; 7 + isOpen: boolean; 8 + isMinimized: boolean; 9 + isMaximized: boolean; 10 + zIndex: number; 11 + }; 12 + 13 + class WindowManager { 14 + windows = $state<ManagedWindow[]>([ 15 + { 16 + id: 'main', 17 + title: 'AT Protocol Collections', 18 + icon: '/icons/humanity/apps/internet-feed-reader.svg', 19 + isOpen: true, 20 + isMinimized: false, 21 + isMaximized: false, 22 + zIndex: 1 23 + }, 24 + { 25 + id: 'about-computer', 26 + title: 'About This Computer', 27 + icon: '/icons/humanity/devices/computer.svg', 28 + isOpen: false, 29 + isMinimized: false, 30 + isMaximized: false, 31 + zIndex: 2 32 + } 33 + ]); 34 + 35 + private nextZIndex = 3; 36 + 37 + get openWindows() { 38 + return this.windows.filter((window) => window.isOpen); 39 + } 40 + 41 + getWindow(id: WindowId) { 42 + return this.windows.find((window) => window.id === id); 43 + } 44 + 45 + setTitle(id: WindowId, title: string, icon?: string) { 46 + const target = this.getWindow(id); 47 + if (!target) return; 48 + 49 + if (target.title !== title) target.title = title; 50 + if (icon && target.icon !== icon) target.icon = icon; 51 + } 52 + 53 + open(id: WindowId) { 54 + const target = this.getWindow(id); 55 + if (!target) return; 56 + 57 + target.isOpen = true; 58 + target.isMinimized = false; 59 + this.focus(id); 60 + } 61 + 62 + close(id: WindowId) { 63 + const target = this.getWindow(id); 64 + if (!target || id === 'main') return; 65 + 66 + target.isOpen = false; 67 + target.isMinimized = false; 68 + target.isMaximized = false; 69 + } 70 + 71 + focus(id: WindowId) { 72 + const target = this.getWindow(id); 73 + if (!target) return; 74 + 75 + target.zIndex = this.nextZIndex; 76 + this.nextZIndex += 1; 77 + } 78 + 79 + minimize(id: WindowId) { 80 + const target = this.getWindow(id); 81 + if (!target) return; 82 + 83 + target.isMinimized = true; 84 + } 85 + 86 + restore(id: WindowId) { 87 + const target = this.getWindow(id); 88 + if (!target) return; 89 + 90 + target.isOpen = true; 91 + target.isMinimized = false; 92 + this.focus(id); 93 + } 94 + 95 + toggleMaximize(id: WindowId) { 96 + const target = this.getWindow(id); 97 + if (!target) return; 98 + 99 + target.isMaximized = !target.isMaximized; 100 + target.isMinimized = false; 101 + this.focus(id); 102 + } 103 + } 104 + 105 + export const windowManager = new WindowManager();
+87 -21
src/routes/+layout.svelte
··· 2 2 import { onMount } from 'svelte'; 3 3 import { accountSetup } from '$lib/atproto/setup.svelte'; 4 4 import favicon from '$lib/assets/favicon.svg'; 5 + import { windowManager } from '$lib/window-manager.svelte'; 5 6 import AboutComputer from '$lib/components/AboutComputer.svelte'; 6 7 import AppWindow from '$lib/components/AppWindow.svelte'; 7 8 import DesktopIcon from '$lib/components/DesktopIcon.svelte'; ··· 11 12 12 13 let { children } = $props(); 13 14 let showAboutComputer = $state(false); 15 + let showStickyNote = $state(true); 14 16 15 17 const shortcuts = $derived([ 16 - { label: 'ibex Home', icon: '/icons/humanity/places/user-home.svg', selected: true }, 17 - { label: 'Collections', icon: '/icons/humanity/places/folder.svg' }, 18 + { 19 + label: 'ibex Home', 20 + icon: '/icons/humanity/places/user-home.svg', 21 + selected: false, 22 + onactivate: () => windowManager.restore('main') 23 + }, 24 + { 25 + label: 'Collections', 26 + icon: '/icons/humanity/places/folder.svg', 27 + onactivate: () => windowManager.restore('main') 28 + }, 18 29 { 19 30 label: 'Computer', 20 31 icon: '/icons/humanity/devices/computer.svg', 21 32 selected: showAboutComputer, 22 - onactivate: () => (showAboutComputer = true) 33 + onactivate: () => { 34 + showAboutComputer = true; 35 + windowManager.open('about-computer'); 36 + } 23 37 }, 24 - { label: 'Trash', icon: '/icons/humanity/places/user-trash.svg' } 38 + { 39 + label: 'Trash', 40 + icon: '/icons/humanity/places/user-trash.svg', 41 + onactivate: () => window.open('https://tangled.org/desertthunder.dev/ibex', '_blank', 'noopener,noreferrer') 42 + } 25 43 ]); 26 44 27 45 const windowTitle = $derived( ··· 30 48 const windowIcon = $derived( 31 49 accountSetup.isConfigured ? '/icons/humanity/apps/internet-feed-reader.svg' : '/icons/humanity/places/user-home.svg' 32 50 ); 51 + const mainWindow = $derived(windowManager.getWindow('main')); 52 + const aboutWindow = $derived(windowManager.getWindow('about-computer')); 53 + 54 + $effect(() => { 55 + windowManager.setTitle('main', windowTitle, windowIcon); 56 + showAboutComputer = windowManager.getWindow('about-computer')?.isOpen ?? false; 57 + }); 33 58 34 59 onMount(() => { 35 60 accountSetup.load(); ··· 52 77 {/each} 53 78 </section> 54 79 55 - <div class="primary-window"> 56 - <AppWindow title={windowTitle} icon={windowIcon}> 57 - {#if accountSetup.isConfigured} 58 - {@render children()} 59 - {:else} 60 - <SetupDialog /> 61 - {/if} 62 - </AppWindow> 63 - </div> 80 + {#if mainWindow?.isOpen && !mainWindow.isMinimized} 81 + <div class="primary-window" class:maximized={mainWindow.isMaximized} style:z-index={mainWindow.zIndex}> 82 + <AppWindow 83 + windowId="main" 84 + title={windowTitle} 85 + icon={windowIcon} 86 + maximized={mainWindow.isMaximized} 87 + onfocus={() => windowManager.focus('main')} 88 + onminimize={() => windowManager.minimize('main')} 89 + onmaximize={() => windowManager.toggleMaximize('main')}> 90 + {#if accountSetup.isConfigured} 91 + {@render children()} 92 + {:else} 93 + <SetupDialog /> 94 + {/if} 95 + </AppWindow> 96 + </div> 97 + {/if} 64 98 65 - {#if showAboutComputer} 66 - <div class="about-window"> 99 + {#if showAboutComputer && aboutWindow?.isOpen && !aboutWindow.isMinimized} 100 + <div class="about-window" class:maximized={aboutWindow.isMaximized} style:z-index={aboutWindow.zIndex}> 67 101 <AppWindow 102 + windowId="about-computer" 68 103 title="About This Computer" 69 104 icon="/icons/humanity/devices/computer.svg" 70 105 showMenubar={false} 71 106 showToolbar={false} 72 - onclose={() => (showAboutComputer = false)}> 107 + maximized={aboutWindow.isMaximized} 108 + onfocus={() => windowManager.focus('about-computer')} 109 + onminimize={() => windowManager.minimize('about-computer')} 110 + onmaximize={() => windowManager.toggleMaximize('about-computer')} 111 + onclose={() => windowManager.close('about-computer')}> 73 112 <AboutComputer /> 74 113 </AppWindow> 75 114 </div> 76 115 {/if} 77 116 78 - <!-- TODO: make this closeable --> 79 - <aside class="sticky-note" aria-label="Design note"> 80 - <h2>Intrepid Ibex</h2> 81 - <p>This app is a recreation of the spirit of Ubuntu 8.10</p> 82 - </aside> 117 + {#if showStickyNote} 118 + <aside class="sticky-note" aria-label="Design note"> 119 + <button type="button" aria-label="Close note" onclick={() => (showStickyNote = false)}>×</button> 120 + <h2>Intrepid Ibex</h2> 121 + <p>This app is a recreation of the spirit of Ubuntu 8.10</p> 122 + </aside> 123 + {/if} 83 124 </main> 84 125 </div> 85 126 ··· 113 154 } 114 155 115 156 .primary-window { 157 + position: relative; 116 158 align-self: center; 117 159 height: min(42rem, calc(100vh - 6.5rem)); 118 160 min-height: 28rem; ··· 122 164 height: 100%; 123 165 } 124 166 167 + .primary-window.maximized, 168 + .about-window.maximized { 169 + position: fixed; 170 + top: 1.75rem; 171 + right: 0; 172 + bottom: 0; 173 + left: 0; 174 + width: auto; 175 + height: auto; 176 + min-height: 0; 177 + } 178 + 125 179 .about-window { 126 180 position: absolute; 127 181 top: min(8rem, 16vh); ··· 136 190 } 137 191 138 192 .sticky-note { 193 + position: relative; 139 194 align-self: end; 140 195 width: min(18rem, 100%); 141 196 margin-bottom: var(--space-8); ··· 148 203 box-shadow: 149 204 0 12px 24px rgb(0 0 0 / 0.25), 150 205 0 1px 0 rgb(255 255 255 / 0.72) inset; 206 + } 207 + 208 + .sticky-note button { 209 + position: absolute; 210 + top: var(--space-1); 211 + right: var(--space-1); 212 + width: 1.25rem; 213 + height: 1.25rem; 214 + color: #6b4a12; 215 + font-weight: 700; 216 + cursor: default; 151 217 } 152 218 153 219 .sticky-note h2 {