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: lock screen

Owais Jamil (Jun 9, 2026, 1:55 PM -0500) 0b31680e 08ebc67b

+454 -168
+155
src/lib/components/GnomeMenuBar.svelte
··· 1 + <script lang="ts"> 2 + import { accountSetup } from '$lib/atproto/setup.svelte'; 3 + import { repoBrowser } from '$lib/atproto/repo.svelte'; 4 + import { desktopSession } from '$lib/desktop-session.svelte'; 5 + import { windowManager } from '$lib/window-manager.svelte'; 6 + import GnomeTry from '$lib/components/GnomeTry.svelte'; 7 + 8 + function changeAccount() { 9 + windowManager.close('gedit'); 10 + repoBrowser.reset(); 11 + accountSetup.reset(); 12 + } 13 + </script> 14 + 15 + <nav class="panel-menus" aria-label="System menus"> 16 + <button class="applications-menu" type="button"> 17 + <img src="/icons/humanity/apps/system-file-manager.svg" alt="" width="20" height="20" /> 18 + <span>Applications</span> 19 + </button> 20 + 21 + <details class="panel-menu"> 22 + <summary>Places</summary> 23 + <div class="menu-popover places-popover"> 24 + <p class="menu-heading">Collections</p> 25 + {#if repoBrowser.collections.length > 0} 26 + {#each repoBrowser.collections.slice(0, 10) as collection (collection.name)} 27 + <button 28 + type="button" 29 + onclick={() => 30 + accountSetup.identity && repoBrowser.selectCollection(accountSetup.identity, collection.name)}> 31 + <img src={collection.icon} alt="" width="16" height="16" /> 32 + <span>{collection.name}</span> 33 + </button> 34 + {/each} 35 + {:else} 36 + <span class="menu-empty">Collections appear here after setup.</span> 37 + {/if} 38 + </div> 39 + </details> 40 + 41 + <details class="panel-menu"> 42 + <summary>System</summary> 43 + <div class="menu-popover system-popover"> 44 + {#if accountSetup.identity} 45 + <p class="menu-heading">Signed in as @{accountSetup.identity.handle}</p> 46 + {/if} 47 + <button type="button" onclick={() => desktopSession.lock()}> 48 + <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="" width="16" height="16" /> 49 + <span>Lock Screen</span> 50 + </button> 51 + <button type="button" onclick={changeAccount}> 52 + <img src="/icons/humanity/places/user-home.svg" alt="" width="16" height="16" /> 53 + <span>Change Account…</span> 54 + </button> 55 + </div> 56 + </details> 57 + </nav> 58 + 59 + <GnomeTry /> 60 + 61 + <style> 62 + .panel-menus { 63 + display: flex; 64 + align-items: center; 65 + gap: 1px; 66 + } 67 + 68 + .panel-menus button, 69 + .panel-menus summary { 70 + display: inline-flex; 71 + align-items: center; 72 + gap: var(--space-1); 73 + height: 1.35rem; 74 + padding: 0 var(--space-2); 75 + border: 1px solid transparent; 76 + border-radius: var(--radius-1); 77 + cursor: default; 78 + } 79 + 80 + .panel-menus button:hover, 81 + .panel-menus summary:hover, 82 + .panel-menu[open] summary { 83 + background: rgb(255 238 203 / 0.16); 84 + border-color: rgb(255 255 255 / 0.18) rgb(0 0 0 / 0.35) rgb(0 0 0 / 0.45) rgb(255 255 255 / 0.16); 85 + } 86 + 87 + .panel-menu { 88 + position: relative; 89 + } 90 + 91 + .panel-menu summary { 92 + list-style: none; 93 + } 94 + 95 + .panel-menu summary::-webkit-details-marker { 96 + display: none; 97 + } 98 + 99 + .menu-popover { 100 + position: absolute; 101 + top: calc(100% + 3px); 102 + left: 0; 103 + display: grid; 104 + gap: 1px; 105 + min-width: 17rem; 106 + padding: var(--space-1); 107 + color: var(--text); 108 + background: linear-gradient(#f4e6cc, #cfb58c); 109 + border: 1px solid #4d2c17; 110 + z-index: 1; 111 + box-shadow: 0 10px 22px rgb(0 0 0 / 0.42); 112 + text-shadow: none; 113 + } 114 + 115 + .menu-popover button { 116 + justify-content: flex-start; 117 + width: 100%; 118 + height: 1.6rem; 119 + color: var(--text); 120 + } 121 + 122 + .menu-popover button:hover { 123 + color: white; 124 + background: var(--selection); 125 + border-color: transparent; 126 + } 127 + 128 + .menu-popover span { 129 + overflow: hidden; 130 + text-overflow: ellipsis; 131 + white-space: nowrap; 132 + } 133 + 134 + .menu-heading, 135 + .menu-empty { 136 + padding: var(--space-1) var(--space-2); 137 + color: var(--text-muted); 138 + font-size: var(--text-0); 139 + font-weight: 700; 140 + } 141 + 142 + .system-popover { 143 + min-width: 13rem; 144 + } 145 + 146 + .applications-menu { 147 + font-weight: 650; 148 + } 149 + 150 + @media (max-width: 760px) { 151 + .panel-menus .panel-menu { 152 + display: none; 153 + } 154 + } 155 + </style>
+6 -167
src/lib/components/GnomePanel.svelte
··· 1 1 <script lang="ts"> 2 - import { accountSetup } from '$lib/atproto/setup.svelte'; 3 - import { repoBrowser } from '$lib/atproto/repo.svelte'; 4 2 import { windowManager } from '$lib/window-manager.svelte'; 3 + import GnomeMenuBar from '$lib/components/GnomeMenuBar.svelte'; 5 4 6 5 const launchers = [ 7 6 { label: 'Browser', icon: '/icons/humanity/apps/web-browser.svg' }, 8 7 { label: 'Mail', icon: '/icons/humanity/apps/evolution-mail.svg' }, 9 8 { label: 'Terminal', icon: '/icons/humanity/apps/utilities-terminal.svg' } 10 9 ]; 11 - 12 - function changeAccount() { 13 - windowManager.close('gedit'); 14 - repoBrowser.reset(); 15 - accountSetup.reset(); 16 - } 17 10 </script> 18 11 19 12 <header class="gnome-panel" aria-label="GNOME top panel"> 20 - <nav class="panel-menus" aria-label="System menus"> 21 - <button class="applications-menu" type="button"> 22 - <img src="/icons/humanity/apps/system-file-manager.svg" alt="" width="20" height="20" /> 23 - <span>Applications</span> 24 - </button> 25 - 26 - <details class="panel-menu"> 27 - <summary>Places</summary> 28 - <div class="menu-popover places-popover"> 29 - <p class="menu-heading">Collections</p> 30 - {#if repoBrowser.collections.length > 0} 31 - {#each repoBrowser.collections.slice(0, 10) as collection (collection.name)} 32 - <button 33 - type="button" 34 - onclick={() => 35 - accountSetup.identity && repoBrowser.selectCollection(accountSetup.identity, collection.name)}> 36 - <img src={collection.icon} alt="" width="16" height="16" /> 37 - <span>{collection.name}</span> 38 - </button> 39 - {/each} 40 - {:else} 41 - <span class="menu-empty">Collections appear here after setup.</span> 42 - {/if} 43 - </div> 44 - </details> 45 - 46 - <details class="panel-menu"> 47 - <summary>System</summary> 48 - <div class="menu-popover system-popover"> 49 - {#if accountSetup.identity} 50 - <p class="menu-heading">Signed in as @{accountSetup.identity.handle}</p> 51 - {/if} 52 - <button type="button" onclick={changeAccount}> 53 - <img src="/icons/humanity/places/user-home.svg" alt="" width="16" height="16" /> 54 - <span>Change Account…</span> 55 - </button> 56 - </div> 57 - </details> 58 - </nav> 13 + <GnomeMenuBar /> 59 14 60 15 <div class="panel-launchers" aria-label="Launchers"> 61 16 {#each launchers as launcher (launcher.label)} ··· 79 34 </button> 80 35 {/each} 81 36 </section> 82 - 83 - <aside class="panel-tray" aria-label="Status tray"> 84 - {#if accountSetup.identity} 85 - <button class="account-chip" type="button" onclick={changeAccount} title="Change account"> 86 - {#if accountSetup.identity.avatar} 87 - <img src={accountSetup.identity.avatar} alt="" width="16" height="16" /> 88 - {:else} 89 - <img src="/icons/humanity/places/user-home.svg" alt="" width="16" height="16" /> 90 - {/if} 91 - <span>@{accountSetup.identity.handle}</span> 92 - </button> 93 - {/if} 94 - <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="Network connected" width="16" height="16" /> 95 - <img src="/icons/humanity/status/audio-volume-medium.svg" alt="Volume" width="16" height="16" /> 96 - <time datetime="2008-10-30T10:10">Thu Oct 30, 10:10 AM</time> 97 - </aside> 98 37 </header> 99 38 100 39 <style> 101 40 .gnome-panel { 102 41 position: relative; 103 - z-index: 10; 42 + z-index: 9000; 104 43 display: flex; 105 44 align-items: center; 106 45 gap: var(--space-2); ··· 117 56 text-shadow: 0 1px 0 rgb(0 0 0 / 0.75); 118 57 } 119 58 120 - .panel-menus, 121 59 .panel-launchers, 122 - .panel-tray, 123 60 .window-list { 124 61 display: flex; 125 62 align-items: center; 126 63 } 127 64 128 - .panel-menus { 129 - gap: 1px; 130 - } 131 - 132 - .panel-menus button, 133 - .panel-menus summary, 134 65 .panel-launchers button, 135 - .active-window, 136 - .account-chip { 66 + .active-window { 137 67 display: inline-flex; 138 68 align-items: center; 139 69 gap: var(--space-1); ··· 144 74 cursor: default; 145 75 } 146 76 147 - .panel-menus button:hover, 148 - .panel-menus summary:hover, 149 - .panel-menu[open] summary, 150 - .panel-launchers button:hover, 151 - .account-chip:hover { 77 + .panel-launchers button:hover { 152 78 background: rgb(255 238 203 / 0.16); 153 79 border-color: rgb(255 255 255 / 0.18) rgb(0 0 0 / 0.35) rgb(0 0 0 / 0.45) rgb(255 255 255 / 0.16); 154 80 } 155 81 156 - .panel-menu { 157 - position: relative; 158 - } 159 - 160 - .panel-menu summary { 161 - list-style: none; 162 - } 163 - 164 - .panel-menu summary::-webkit-details-marker { 165 - display: none; 166 - } 167 - 168 - .menu-popover { 169 - position: absolute; 170 - top: calc(100% + 3px); 171 - left: 0; 172 - display: grid; 173 - gap: 1px; 174 - min-width: 17rem; 175 - padding: var(--space-1); 176 - color: var(--text); 177 - background: linear-gradient(#f4e6cc, #cfb58c); 178 - border: 1px solid #4d2c17; 179 - box-shadow: 0 10px 22px rgb(0 0 0 / 0.42); 180 - text-shadow: none; 181 - } 182 - 183 - .menu-popover button { 184 - justify-content: flex-start; 185 - width: 100%; 186 - height: 1.6rem; 187 - color: var(--text); 188 - } 189 - 190 - .menu-popover button:hover { 191 - color: white; 192 - background: var(--selection); 193 - border-color: transparent; 194 - } 195 - 196 - .menu-popover span { 197 - overflow: hidden; 198 - text-overflow: ellipsis; 199 - white-space: nowrap; 200 - } 201 - 202 - .menu-heading, 203 - .menu-empty { 204 - padding: var(--space-1) var(--space-2); 205 - color: var(--text-muted); 206 - font-size: var(--text-0); 207 - font-weight: 700; 208 - } 209 - 210 - .system-popover { 211 - min-width: 13rem; 212 - } 213 - 214 - .applications-menu { 215 - font-weight: 650; 216 - } 217 - 218 82 .panel-launchers { 219 83 gap: 1px; 220 84 padding-left: var(--space-1); ··· 256 120 white-space: nowrap; 257 121 } 258 122 259 - .panel-tray { 260 - gap: var(--space-2); 261 - } 262 - 263 - .account-chip { 264 - max-width: 12rem; 265 - padding: 0 var(--space-1); 266 - } 267 - 268 - .account-chip img { 269 - border-radius: var(--radius-1); 270 - } 271 - 272 - .account-chip span { 273 - overflow: hidden; 274 - text-overflow: ellipsis; 275 - white-space: nowrap; 276 - } 277 - 278 - .panel-tray time { 279 - font-variant-numeric: tabular-nums; 280 - } 281 - 282 123 @media (max-width: 760px) { 283 - .panel-menus .panel-menu, 284 - .window-list, 285 - .account-chip span { 124 + .window-list { 286 125 display: none; 287 126 } 288 127 }
+113
src/lib/components/GnomeTry.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { accountSetup } from '$lib/atproto/setup.svelte'; 4 + import { repoBrowser } from '$lib/atproto/repo.svelte'; 5 + import { desktopSession } from '$lib/desktop-session.svelte'; 6 + import { windowManager } from '$lib/window-manager.svelte'; 7 + 8 + let now = $state(new Date()); 9 + 10 + const panelTime = $derived( 11 + now.toLocaleString([], { 12 + weekday: 'short', 13 + month: 'short', 14 + day: 'numeric', 15 + hour: 'numeric', 16 + minute: '2-digit', 17 + second: '2-digit', 18 + timeZoneName: 'short' 19 + }) 20 + ); 21 + 22 + onMount(() => { 23 + const interval = window.setInterval(() => { 24 + now = new Date(); 25 + }, 1000); 26 + 27 + return () => window.clearInterval(interval); 28 + }); 29 + 30 + function changeAccount() { 31 + windowManager.close('gedit'); 32 + repoBrowser.reset(); 33 + accountSetup.reset(); 34 + } 35 + </script> 36 + 37 + <aside class="panel-tray" aria-label="Status tray"> 38 + {#if accountSetup.identity} 39 + <button class="account-chip" type="button" onclick={changeAccount} title="Change account"> 40 + {#if accountSetup.identity.avatar} 41 + <img src={accountSetup.identity.avatar} alt="" width="16" height="16" /> 42 + {:else} 43 + <img src="/icons/humanity/places/user-home.svg" alt="" width="16" height="16" /> 44 + {/if} 45 + <span>@{accountSetup.identity.handle}</span> 46 + </button> 47 + {/if} 48 + <button class="tray-button" type="button" onclick={() => desktopSession.lock()} title="Lock screen"> 49 + <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="Lock screen" width="16" height="16" /> 50 + </button> 51 + <a class="tray-button" href="https://plyr.fm" target="_blank" rel="noopener noreferrer" title="Open plyr.fm"> 52 + <img src="/icons/humanity/status/audio-volume-medium.svg" alt="Open plyr.fm" width="16" height="16" /> 53 + </a> 54 + <a class="tray-button" href="https://time.is" target="_blank"> 55 + <time datetime={now.toISOString()}>{panelTime}</time> 56 + </a> 57 + </aside> 58 + 59 + <style> 60 + .panel-tray { 61 + order: 100; 62 + display: flex; 63 + align-items: center; 64 + gap: var(--space-2); 65 + } 66 + 67 + .account-chip, 68 + .tray-button { 69 + display: inline-flex; 70 + align-items: center; 71 + height: 1.35rem; 72 + border: 1px solid transparent; 73 + border-radius: var(--radius-1); 74 + cursor: default; 75 + } 76 + 77 + .account-chip { 78 + gap: var(--space-1); 79 + max-width: 12rem; 80 + padding: 0 var(--space-1); 81 + } 82 + 83 + .tray-button { 84 + justify-content: center; 85 + min-width: 1.35rem; 86 + } 87 + 88 + .account-chip:hover, 89 + .tray-button:hover { 90 + background: rgb(255 238 203 / 0.16); 91 + border-color: rgb(255 255 255 / 0.18) rgb(0 0 0 / 0.35) rgb(0 0 0 / 0.45) rgb(255 255 255 / 0.16); 92 + } 93 + 94 + .account-chip img { 95 + border-radius: var(--radius-1); 96 + } 97 + 98 + .account-chip span { 99 + overflow: hidden; 100 + text-overflow: ellipsis; 101 + white-space: nowrap; 102 + } 103 + 104 + .panel-tray time { 105 + font-variant-numeric: tabular-nums; 106 + } 107 + 108 + @media (max-width: 760px) { 109 + .account-chip span { 110 + display: none; 111 + } 112 + } 113 + </style>
+154
src/lib/components/LockScreen.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import favicon from '$lib/assets/favicon.svg'; 4 + import type { AccountIdentity } from '$lib/atproto/types'; 5 + 6 + type Props = { identity: AccountIdentity | null; onunlock: () => void }; 7 + 8 + let { identity, onunlock }: Props = $props(); 9 + let now = $state(new Date()); 10 + 11 + const displayName = $derived(identity ? `@${identity.handle}` : 'Intrepid Ibex'); 12 + const time = $derived(now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })); 13 + const date = $derived(now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })); 14 + 15 + onMount(() => { 16 + const interval = window.setInterval(() => { 17 + now = new Date(); 18 + }, 1000); 19 + 20 + return () => window.clearInterval(interval); 21 + }); 22 + </script> 23 + 24 + <section class="lock-screen" aria-label="Screen locked"> 25 + <div class="lock-card"> 26 + <div class="lock-clock" aria-live="polite"> 27 + <time datetime={now.toISOString()}>{time}</time> 28 + <p>{date}</p> 29 + </div> 30 + 31 + <div class="lock-user"> 32 + {#if identity?.avatar} 33 + <img class="lock-avatar" src={identity.avatar} alt="" width="56" height="56" /> 34 + {:else} 35 + <img class="lock-avatar fallback" src={favicon} alt="" width="56" height="56" /> 36 + {/if} 37 + <div> 38 + <p class="lock-kicker">Locked</p> 39 + <h1>{displayName}</h1> 40 + </div> 41 + </div> 42 + 43 + <button type="button" onclick={onunlock}>Unlock</button> 44 + </div> 45 + </section> 46 + 47 + <style> 48 + .lock-screen { 49 + position: fixed; 50 + inset: 0; 51 + z-index: 9998; 52 + display: grid; 53 + place-items: center; 54 + color: #eeeeec; 55 + background: 56 + radial-gradient(circle at 50% 24%, rgb(233 84 32 / 0.2), transparent 18rem), 57 + linear-gradient(rgb(48 10 36 / 0.9), rgb(48 10 36 / 0.96)); 58 + backdrop-filter: blur(5px); 59 + } 60 + 61 + .lock-card { 62 + display: grid; 63 + justify-items: center; 64 + width: min(24rem, calc(100vw - 2rem)); 65 + padding: var(--space-8) var(--space-6); 66 + text-align: center; 67 + } 68 + 69 + .lock-clock time { 70 + display: block; 71 + color: #eeeeec; 72 + font-size: clamp(3rem, 16vw, 5.5rem); 73 + font-weight: 300; 74 + line-height: 0.95; 75 + letter-spacing: -0.06em; 76 + text-shadow: 0 0.25rem 1rem rgb(0 0 0 / 0.32); 77 + } 78 + 79 + .lock-clock p { 80 + margin-top: var(--space-2); 81 + color: rgb(238 238 236 / 0.76); 82 + font-size: var(--text-2); 83 + } 84 + 85 + .lock-user { 86 + display: inline-flex; 87 + align-items: center; 88 + gap: var(--space-3); 89 + margin-top: var(--space-7); 90 + padding: var(--space-3) var(--space-4); 91 + text-align: left; 92 + background: rgb(238 238 236 / 0.08); 93 + border: 1px solid rgb(238 238 236 / 0.16); 94 + border-radius: var(--radius-3); 95 + box-shadow: 96 + 0 1px 0 rgb(255 255 255 / 0.12) inset, 97 + 0 1rem 2.2rem rgb(0 0 0 / 0.22); 98 + } 99 + 100 + .lock-avatar { 101 + width: 3.5rem; 102 + height: 3.5rem; 103 + object-fit: cover; 104 + border: 2px solid rgb(238 238 236 / 0.8); 105 + border-radius: 0.8rem; 106 + box-shadow: 0 0.25rem 0.8rem rgb(0 0 0 / 0.35); 107 + } 108 + 109 + .lock-avatar.fallback { 110 + padding: 0.45rem; 111 + object-fit: contain; 112 + background: #eeeeec; 113 + } 114 + 115 + .lock-kicker, 116 + .lock-user h1 { 117 + color: #e95420; 118 + } 119 + 120 + .lock-kicker { 121 + font-size: var(--text-0); 122 + font-weight: 700; 123 + letter-spacing: 0.08em; 124 + text-transform: uppercase; 125 + } 126 + 127 + .lock-user h1 { 128 + max-width: 14rem; 129 + overflow: hidden; 130 + font-size: var(--text-4); 131 + line-height: var(--leading-tight); 132 + text-overflow: ellipsis; 133 + white-space: nowrap; 134 + } 135 + 136 + .lock-card button { 137 + margin-top: var(--space-5); 138 + padding: 0.45rem 1.15rem; 139 + color: #eeeeec; 140 + font: inherit; 141 + font-size: var(--text-1); 142 + font-weight: 700; 143 + background: #e95420; 144 + border: 1px solid #e95420; 145 + border-radius: var(--radius-2); 146 + box-shadow: 0 0.25rem 0.8rem rgb(0 0 0 / 0.25); 147 + } 148 + 149 + .lock-card button:hover, 150 + .lock-card button:focus-visible { 151 + background: #f06a35; 152 + outline: none; 153 + } 154 + </style>
+13
src/lib/desktop-session.svelte.ts
··· 1 + class DesktopSessionState { 2 + isLocked = $state(false); 3 + 4 + lock() { 5 + this.isLocked = true; 6 + } 7 + 8 + unlock() { 9 + this.isLocked = false; 10 + } 11 + } 12 + 13 + export const desktopSession = new DesktopSessionState();
+13 -1
src/routes/+layout.svelte
··· 4 4 import { repoBrowser } from '$lib/atproto/repo.svelte'; 5 5 import { accountSetup } from '$lib/atproto/setup.svelte'; 6 6 import favicon from '$lib/assets/favicon.svg'; 7 + import { desktopSession } from '$lib/desktop-session.svelte'; 7 8 import { getDatabase, resetLocalDatabase, runMigrations } from '$lib/db'; 8 9 import { errorMessage } from '$lib/utils/errors'; 9 10 import { windowManager } from '$lib/window-manager.svelte'; ··· 13 14 import DesktopIcon from '$lib/components/DesktopIcon.svelte'; 14 15 import Gedit from '$lib/components/Gedit.svelte'; 15 16 import GnomePanel from '$lib/components/GnomePanel.svelte'; 17 + import LockScreen from '$lib/components/LockScreen.svelte'; 16 18 import NativeWindow from '$lib/components/NativeWindow.svelte'; 17 19 import SetupDialog from '$lib/components/SetupDialog.svelte'; 18 20 import '$lib/styles/style.css'; ··· 68 70 showAboutComputer = windowManager.getWindow('about-computer')?.isOpen ?? false; 69 71 70 72 if (repoBrowser.selectedRecord) { 71 - windowManager.setTitle('gedit', `${repoBrowser.selectedRecord.rkey}.json - gedit`, repoBrowser.selectedRecord.icon); 73 + windowManager.setTitle( 74 + 'gedit', 75 + `${repoBrowser.selectedRecord.rkey}.json - gedit`, 76 + repoBrowser.selectedRecord.icon 77 + ); 72 78 } 73 79 }); 74 80 ··· 117 123 } 118 124 119 125 async function waitForMinimumBootTime(startedAt: number) { 126 + // TODO: this should be configurable/off after migrations are done 127 + // We probably need to track app version 120 128 const remaining = 2500 - (performance.now() - startedAt); 121 129 122 130 if (remaining > 0) { ··· 212 220 </aside> 213 221 {/if} 214 222 </main> 223 + 224 + {#if desktopSession.isLocked} 225 + <LockScreen identity={accountSetup.identity} onunlock={() => desktopSession.unlock()} /> 226 + {/if} 215 227 </div> 216 228 {/if} 217 229