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: components

Owais Jamil (Jun 8, 2026, 1:52 PM -0500) 454fee52 8c87a646

+825 -21
+5 -16
eslint.config.js
··· 18 18 svelte.configs.prettier, 19 19 { 20 20 languageOptions: { globals: { ...globals.browser, ...globals.node } }, 21 - rules: { 22 - // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. 23 - // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors 24 - 'no-undef': 'off' 25 - } 21 + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. 22 + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors 23 + rules: { 'no-undef': 'off' } 26 24 }, 27 25 { 28 26 files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 29 27 languageOptions: { 30 - parserOptions: { 31 - projectService: true, 32 - extraFileExtensions: ['.svelte'], 33 - parser: ts.parser, 34 - svelteConfig 35 - } 28 + parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], parser: ts.parser, svelteConfig } 36 29 } 37 30 }, 38 - { 39 - // Override or add rule settings here, such as: 40 - // 'svelte/button-has-type': 'error' 41 - rules: {} 42 - } 31 + { rules: {} } 43 32 );
+13
src/app.d.ts
··· 1 + // See https://svelte.dev/docs/kit/types#app.d.ts 2 + // for information about these interfaces 3 + declare global { 4 + namespace App { 5 + // interface Error {} 6 + // interface Locals {} 7 + // interface PageData {} 8 + // interface PageState {} 9 + // interface Platform {} 10 + } 11 + } 12 + 13 + export {};
+12
src/app.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 + <meta name="text-scale" content="scale" /> 7 + %sveltekit.head% 8 + </head> 9 + <body data-sveltekit-preload-data="hover"> 10 + <div style="display: contents">%sveltekit.body%</div> 11 + </body> 12 + </html>
+7
src/lib/assets/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> 2 + <path d="M0 0h24v24H0z" fill="none" /> 3 + <g fill="none" stroke="#e95420" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> 4 + <path d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0" /> 5 + <path d="M16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-5.5 8.28" /> 6 + </g> 7 + </svg>
+175
src/lib/components/AppWindow.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from 'svelte'; 3 + type Props = { title: string; icon: string }; 4 + 5 + let { title, icon, children }: Props & { children: Snippet } = $props(); 6 + </script> 7 + 8 + <section class="app-window" aria-label={title}> 9 + <header class="titlebar"> 10 + <div class="window-title"> 11 + <img src={icon} alt="" width="18" height="18" /> 12 + <h1>{title}</h1> 13 + </div> 14 + <div class="window-controls" aria-hidden="true"> 15 + <span></span> 16 + <span></span> 17 + <span></span> 18 + </div> 19 + </header> 20 + 21 + <nav class="menubar" aria-label="Application menu"> 22 + <button type="button">Collection</button> 23 + <button type="button">Navigate</button> 24 + <button type="button">View</button> 25 + <button type="button">Bookmarks</button> 26 + <button type="button">Help</button> 27 + </nav> 28 + 29 + <div class="toolbar" aria-label="Application toolbar"> 30 + <button type="button"><span aria-hidden="true">◀</span> Back</button> 31 + <button type="button"><span aria-hidden="true">▶</span> Forward</button> 32 + <button type="button" 33 + ><img src="/icons/humanity/actions/mail-send-receive.svg" alt="" width="18" height="18" /> Sync</button> 34 + <label> 35 + <span class="sr-only">Address</span> 36 + <input value="at://ubuntu.example/app.bsky.feed.post" readonly /> 37 + </label> 38 + </div> 39 + 40 + <div class="content"> 41 + {@render children()} 42 + </div> 43 + </section> 44 + 45 + <style> 46 + .app-window { 47 + display: grid; 48 + grid-template-rows: auto auto auto minmax(0, 1fr); 49 + overflow: hidden; 50 + background: var(--window-surface); 51 + border: 1px solid #2a170d; 52 + border-radius: var(--radius-3) var(--radius-3) var(--radius-2) var(--radius-2); 53 + box-shadow: var(--shadow-window); 54 + } 55 + 56 + .titlebar { 57 + display: flex; 58 + align-items: center; 59 + justify-content: space-between; 60 + min-height: 1.85rem; 61 + padding: 3px var(--space-2) 3px var(--space-3); 62 + color: #fff1d7; 63 + background: 64 + linear-gradient(rgb(255 255 255 / 0.14), transparent 45%), 65 + linear-gradient(var(--window-title-start), var(--window-title-end)); 66 + border-bottom: 1px solid #1b0f09; 67 + text-shadow: 0 1px 1px rgb(0 0 0 / 0.9); 68 + } 69 + 70 + .window-title { 71 + display: flex; 72 + align-items: center; 73 + min-width: 0; 74 + gap: var(--space-2); 75 + } 76 + 77 + h1 { 78 + overflow: hidden; 79 + font-size: var(--text-2); 80 + font-weight: 700; 81 + line-height: var(--leading-tight); 82 + text-overflow: ellipsis; 83 + white-space: nowrap; 84 + } 85 + 86 + .window-controls { 87 + display: flex; 88 + gap: 5px; 89 + } 90 + 91 + .window-controls span { 92 + width: 14px; 93 + height: 14px; 94 + border: 1px solid #1e120a; 95 + border-radius: var(--radius-round); 96 + background: radial-gradient(circle at 35% 30%, #fff4d0, #cf7b1f 45%, #6d3412 78%); 97 + box-shadow: 0 1px 0 rgb(255 255 255 / 0.24) inset; 98 + } 99 + 100 + .menubar, 101 + .toolbar { 102 + display: flex; 103 + align-items: center; 104 + background: linear-gradient(#f3e5ca, #d4bb91); 105 + border-bottom: 1px solid #9b805b; 106 + box-shadow: 0 1px 0 rgb(255 255 255 / 0.68) inset; 107 + } 108 + 109 + .menubar { 110 + gap: var(--space-1); 111 + padding: 2px var(--space-2); 112 + font-size: var(--text-1); 113 + } 114 + 115 + .menubar button { 116 + padding: 1px var(--space-2); 117 + border-radius: var(--radius-1); 118 + cursor: default; 119 + } 120 + 121 + .menubar button:hover { 122 + color: white; 123 + background: var(--selection); 124 + } 125 + 126 + .toolbar { 127 + gap: var(--space-2); 128 + padding: var(--space-2); 129 + } 130 + 131 + .toolbar button { 132 + display: inline-flex; 133 + align-items: center; 134 + gap: var(--space-1); 135 + height: 1.75rem; 136 + padding: 0 var(--space-3); 137 + background: linear-gradient(#fff9ec, #ceb48a); 138 + border: 1px solid #9b7d55; 139 + border-radius: var(--radius-2); 140 + box-shadow: var(--shadow-raised); 141 + font-size: var(--text-1); 142 + cursor: default; 143 + } 144 + 145 + .toolbar label { 146 + min-width: 8rem; 147 + flex: 1; 148 + } 149 + 150 + .toolbar input { 151 + width: 100%; 152 + height: 1.75rem; 153 + padding: 0 var(--space-2); 154 + color: var(--text-muted); 155 + background: #fffcf4; 156 + border: 1px solid #8e7654; 157 + border-radius: var(--radius-2); 158 + box-shadow: var(--shadow-sunken); 159 + font-family: var(--font-mono); 160 + font-size: var(--text-1); 161 + } 162 + 163 + .content { 164 + overflow: hidden; 165 + padding: var(--space-3); 166 + background: var(--window-surface); 167 + } 168 + 169 + @media (max-width: 760px) { 170 + .toolbar button:nth-of-type(2), 171 + .toolbar input { 172 + display: none; 173 + } 174 + } 175 + </style>
+268
src/lib/components/CollectionBrowser.svelte
··· 1 + <script lang="ts"> 2 + const collections = [ 3 + { 4 + name: 'app.bsky.feed.post', 5 + count: '18,204', 6 + icon: '/icons/humanity/apps/internet-feed-reader.svg', 7 + active: true 8 + }, 9 + { name: 'app.bsky.actor.profile', count: '1,282', icon: '/icons/humanity/places/user-home.svg' }, 10 + { name: 'app.bsky.graph.follow', count: '6,914', icon: '/icons/humanity/places/folder.svg' }, 11 + { name: 'chat.bsky.convo.defs', count: '126', icon: '/icons/humanity/apps/evolution-mail.svg' }, 12 + { name: 'com.atproto.repo.strongRef', count: '42', icon: '/icons/humanity/mimes/text-x-generic.svg' } 13 + ]; 14 + 15 + const records = [ 16 + { 17 + author: '@intrepid-ibex.test', 18 + time: '10:08', 19 + title: 'Classic GNOME shell mockup', 20 + body: 'Panels, textured chrome, collection folders, and a very 2008 address bar are now static and ready to wire into AT Protocol data.' 21 + }, 22 + { 23 + author: '@desktop.example', 24 + time: '09:44', 25 + title: 'Humanity icon import', 26 + body: 'Using archived Ubuntu Humanity assets for launchers, desktop shortcuts, window icons, and status tray affordances.' 27 + }, 28 + { 29 + author: '@repo.tools', 30 + time: 'Yesterday', 31 + title: 'Collection-first browsing', 32 + body: 'Each application can become a focused browser for a different collection namespace without losing the GNOME desktop metaphor.' 33 + } 34 + ]; 35 + </script> 36 + 37 + <div class="collection-browser"> 38 + <aside class="sidebar" aria-label="Collections"> 39 + <header> 40 + <h2>Collections</h2> 41 + <p>at:// repo folders</p> 42 + </header> 43 + 44 + <ul> 45 + {#each collections as collection (collection.name)} 46 + <li> 47 + <button class:active={collection.active} type="button"> 48 + <img src={collection.icon} alt="" width="24" height="24" /> 49 + <span>{collection.name}</span> 50 + <small>{collection.count}</small> 51 + </button> 52 + </li> 53 + {/each} 54 + </ul> 55 + </aside> 56 + 57 + <section class="record-pane" aria-label="Collection records"> 58 + <div class="summary-card"> 59 + <img src="/icons/humanity/apps/internet-feed-reader.svg" alt="" width="48" height="48" /> 60 + <div> 61 + <p class="eyebrow">Selected collection</p> 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> 67 + </div> 68 + </div> 69 + 70 + <div class="record-list"> 71 + <header> 72 + <span>Name</span> 73 + <span>Author</span> 74 + <span>Modified</span> 75 + </header> 76 + 77 + {#each records as record (record.title)} 78 + <article> 79 + <img src="/icons/humanity/mimes/text-x-generic.svg" alt="" width="32" height="32" /> 80 + <div> 81 + <h3>{record.title}</h3> 82 + <p>{record.body}</p> 83 + </div> 84 + <strong>{record.author}</strong> 85 + <time>{record.time}</time> 86 + </article> 87 + {/each} 88 + </div> 89 + </section> 90 + </div> 91 + 92 + <style> 93 + .collection-browser { 94 + display: grid; 95 + grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr); 96 + height: 100%; 97 + overflow: hidden; 98 + border: 1px solid #a58a63; 99 + background: #fff8ed; 100 + box-shadow: 0 1px 0 rgb(255 255 255 / 0.7) inset; 101 + } 102 + 103 + .sidebar { 104 + display: grid; 105 + grid-template-rows: auto minmax(0, 1fr); 106 + min-height: 0; 107 + background: linear-gradient(90deg, #cdb690, #e6d5b8); 108 + border-right: 1px solid #9e8057; 109 + } 110 + 111 + .sidebar header { 112 + padding: var(--space-3); 113 + border-bottom: 1px solid #a88c62; 114 + box-shadow: 0 1px 0 rgb(255 255 255 / 0.45); 115 + } 116 + 117 + .sidebar h2, 118 + .summary-card h2 { 119 + font-size: var(--text-4); 120 + line-height: var(--leading-tight); 121 + } 122 + 123 + .sidebar p, 124 + .summary-card p, 125 + .record-list p { 126 + color: var(--text-muted); 127 + font-size: var(--text-1); 128 + } 129 + 130 + .sidebar ul { 131 + overflow: auto; 132 + padding: var(--space-2); 133 + } 134 + 135 + .sidebar button { 136 + display: grid; 137 + grid-template-columns: 24px minmax(0, 1fr) auto; 138 + align-items: center; 139 + gap: var(--space-2); 140 + width: 100%; 141 + padding: var(--space-2); 142 + border: 1px solid transparent; 143 + border-radius: var(--radius-2); 144 + font-size: var(--text-1); 145 + text-align: left; 146 + cursor: default; 147 + } 148 + 149 + .sidebar button.active, 150 + .sidebar button:hover { 151 + color: white; 152 + background: linear-gradient(#e48631, #b65312); 153 + border-color: #f4b46b #8d3c0b #713009 #f1a35e; 154 + text-shadow: 0 1px 0 rgb(0 0 0 / 0.55); 155 + } 156 + 157 + .sidebar span { 158 + overflow: hidden; 159 + text-overflow: ellipsis; 160 + white-space: nowrap; 161 + } 162 + 163 + .sidebar small { 164 + font-size: var(--text-0); 165 + opacity: 0.82; 166 + } 167 + 168 + .record-pane { 169 + display: grid; 170 + grid-template-rows: auto minmax(0, 1fr); 171 + min-width: 0; 172 + min-height: 0; 173 + background: #fff9ef; 174 + } 175 + 176 + .summary-card { 177 + display: flex; 178 + gap: var(--space-3); 179 + align-items: center; 180 + padding: var(--space-4); 181 + background: linear-gradient(#fffaf1, #ead8b8); 182 + border-bottom: 1px solid #b29366; 183 + } 184 + 185 + .eyebrow { 186 + font-weight: 700; 187 + letter-spacing: 0.04em; 188 + text-transform: uppercase; 189 + } 190 + 191 + .record-list { 192 + overflow: auto; 193 + padding: var(--space-3); 194 + } 195 + 196 + .record-list > header, 197 + .record-list article { 198 + display: grid; 199 + grid-template-columns: minmax(14rem, 1fr) minmax(10rem, 14rem) 5rem; 200 + gap: var(--space-3); 201 + align-items: center; 202 + } 203 + 204 + .record-list > header { 205 + padding: var(--space-1) var(--space-3); 206 + color: var(--text-muted); 207 + background: linear-gradient(#eadbbf, #cbb18a); 208 + border: 1px solid #a88b63; 209 + border-radius: var(--radius-2) var(--radius-2) 0 0; 210 + font-size: var(--text-0); 211 + font-weight: 700; 212 + text-transform: uppercase; 213 + } 214 + 215 + .record-list article { 216 + grid-template-columns: 32px minmax(14rem, 1fr) minmax(10rem, 14rem) 5rem; 217 + padding: var(--space-3); 218 + background: #fffdf8; 219 + border-right: 1px solid #d7c4a4; 220 + border-bottom: 1px solid #d7c4a4; 221 + border-left: 1px solid #d7c4a4; 222 + } 223 + 224 + .record-list article:nth-child(odd) { 225 + background: #f7eddb; 226 + } 227 + 228 + .record-list article:hover { 229 + background: #f1d09d; 230 + } 231 + 232 + .record-list h3 { 233 + font-size: var(--text-2); 234 + line-height: var(--leading-tight); 235 + } 236 + 237 + .record-list strong, 238 + .record-list time { 239 + color: var(--text-muted); 240 + font-size: var(--text-1); 241 + font-weight: 500; 242 + } 243 + 244 + .record-list time { 245 + font-variant-numeric: tabular-nums; 246 + } 247 + 248 + @media (max-width: 800px) { 249 + .collection-browser { 250 + grid-template-columns: 1fr; 251 + } 252 + 253 + .sidebar { 254 + display: none; 255 + } 256 + 257 + .record-list > header, 258 + .record-list article { 259 + grid-template-columns: 32px minmax(0, 1fr); 260 + } 261 + 262 + .record-list > header, 263 + .record-list strong, 264 + .record-list time { 265 + display: none; 266 + } 267 + } 268 + </style>
+47
src/lib/components/DesktopIcon.svelte
··· 1 + <script lang="ts"> 2 + type Props = { label: string; icon: string; selected?: boolean }; 3 + 4 + let { label, icon, selected = false }: Props = $props(); 5 + </script> 6 + 7 + <button class:selected class="desktop-icon" type="button" aria-label={label}> 8 + <img src={icon} alt="" width="48" height="48" /> 9 + <span>{label}</span> 10 + </button> 11 + 12 + <style> 13 + .desktop-icon { 14 + display: grid; 15 + justify-items: center; 16 + gap: var(--space-1); 17 + width: 5.75rem; 18 + padding: var(--space-2) var(--space-1); 19 + color: var(--desktop-text); 20 + text-align: center; 21 + text-shadow: 0 1px 2px rgb(0 0 0 / 0.95); 22 + border: 1px solid transparent; 23 + border-radius: var(--radius-2); 24 + cursor: default; 25 + } 26 + 27 + .desktop-icon img { 28 + filter: drop-shadow(0 2px 2px rgb(0 0 0 / 0.4)); 29 + } 30 + 31 + .desktop-icon span { 32 + padding: 1px 4px 2px; 33 + font-size: var(--text-1); 34 + line-height: var(--leading-tight); 35 + border-radius: var(--radius-1); 36 + } 37 + 38 + .desktop-icon:hover, 39 + .desktop-icon.selected { 40 + background: rgb(238 168 82 / 0.2); 41 + border-color: rgb(255 246 213 / 0.36); 42 + } 43 + 44 + .desktop-icon.selected span { 45 + background: rgb(177 81 19 / 0.82); 46 + } 47 + </style>
+145
src/lib/components/GnomePanel.svelte
··· 1 + <script lang="ts"> 2 + const launchers = [ 3 + { label: 'Browser', icon: '/icons/humanity/apps/web-browser.svg' }, 4 + { label: 'Mail', icon: '/icons/humanity/apps/evolution-mail.svg' }, 5 + { label: 'Terminal', icon: '/icons/humanity/apps/utilities-terminal.svg' } 6 + ]; 7 + </script> 8 + 9 + <header class="gnome-panel" aria-label="GNOME top panel"> 10 + <nav class="panel-menus" aria-label="System menus"> 11 + <button class="applications-menu" type="button"> 12 + <img src="/icons/humanity/apps/system-file-manager.svg" alt="" width="20" height="20" /> 13 + <span>Applications</span> 14 + </button> 15 + <button type="button">Places</button> 16 + <button type="button">System</button> 17 + </nav> 18 + 19 + <div class="panel-launchers" aria-label="Launchers"> 20 + {#each launchers as launcher (launcher.label)} 21 + <button type="button" title={launcher.label}> 22 + <img src={launcher.icon} alt="" width="22" height="22" /> 23 + </button> 24 + {/each} 25 + </div> 26 + 27 + <div class="panel-spacer" aria-hidden="true"></div> 28 + 29 + <section class="window-list" aria-label="Open windows"> 30 + <button class="active-window" type="button"> 31 + <img src="/icons/humanity/apps/internet-feed-reader.svg" alt="" width="16" height="16" /> 32 + <span>AT Protocol Collections</span> 33 + </button> 34 + </section> 35 + 36 + <aside class="panel-tray" aria-label="Status tray"> 37 + <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="Network connected" width="16" height="16" /> 38 + <img src="/icons/humanity/status/audio-volume-medium.svg" alt="Volume" width="16" height="16" /> 39 + <time datetime="2008-10-30T10:10">Thu Oct 30, 10:10 AM</time> 40 + </aside> 41 + </header> 42 + 43 + <style> 44 + .gnome-panel { 45 + display: flex; 46 + align-items: center; 47 + gap: var(--space-2); 48 + height: 1.75rem; 49 + padding: 2px var(--space-2); 50 + color: #fff5df; 51 + background: 52 + linear-gradient(rgb(255 255 255 / 0.11), transparent 42%), 53 + linear-gradient(90deg, var(--panel-start), var(--panel-end)); 54 + border-top: 1px solid var(--panel-border-light); 55 + border-bottom: 1px solid var(--panel-border-dark); 56 + box-shadow: 0 1px 3px rgb(0 0 0 / 0.5); 57 + font-size: var(--text-1); 58 + text-shadow: 0 1px 0 rgb(0 0 0 / 0.75); 59 + } 60 + 61 + .panel-menus, 62 + .panel-launchers, 63 + .panel-tray, 64 + .window-list { 65 + display: flex; 66 + align-items: center; 67 + } 68 + 69 + .panel-menus { 70 + gap: 1px; 71 + } 72 + 73 + .panel-menus button, 74 + .panel-launchers button, 75 + .active-window { 76 + display: inline-flex; 77 + align-items: center; 78 + gap: var(--space-1); 79 + height: 1.35rem; 80 + padding: 0 var(--space-2); 81 + border: 1px solid transparent; 82 + border-radius: var(--radius-1); 83 + cursor: default; 84 + } 85 + 86 + .panel-menus button:hover, 87 + .panel-launchers button:hover { 88 + background: rgb(255 238 203 / 0.16); 89 + 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); 90 + } 91 + 92 + .applications-menu { 93 + font-weight: 650; 94 + } 95 + 96 + .panel-launchers { 97 + gap: 1px; 98 + padding-left: var(--space-1); 99 + border-left: 1px solid rgb(255 255 255 / 0.13); 100 + } 101 + 102 + .panel-launchers button { 103 + width: 1.55rem; 104 + justify-content: center; 105 + padding: 0; 106 + } 107 + 108 + .panel-spacer { 109 + flex: 1; 110 + } 111 + 112 + .window-list { 113 + min-width: min(28vw, 19rem); 114 + } 115 + 116 + .active-window { 117 + width: 100%; 118 + color: #23170f; 119 + background: linear-gradient(#f5e7cb, #b99d78); 120 + border-color: #fff3dc #755232 #4b2e19 #e8d3b4; 121 + box-shadow: var(--shadow-sunken); 122 + text-shadow: 0 1px 0 rgb(255 255 255 / 0.65); 123 + } 124 + 125 + .active-window span { 126 + overflow: hidden; 127 + text-overflow: ellipsis; 128 + white-space: nowrap; 129 + } 130 + 131 + .panel-tray { 132 + gap: var(--space-2); 133 + } 134 + 135 + .panel-tray time { 136 + font-variant-numeric: tabular-nums; 137 + } 138 + 139 + @media (max-width: 760px) { 140 + .panel-menus button:not(.applications-menu), 141 + .window-list { 142 + display: none; 143 + } 144 + } 145 + </style>
+1
src/lib/index.ts
··· 1 + // place files you want to import through the `$lib` alias in this folder.
+136
src/routes/+layout.svelte
··· 1 + <script lang="ts"> 2 + import favicon from '$lib/assets/favicon.svg'; 3 + import AppWindow from '$lib/components/AppWindow.svelte'; 4 + import DesktopIcon from '$lib/components/DesktopIcon.svelte'; 5 + import GnomePanel from '$lib/components/GnomePanel.svelte'; 6 + import '$lib/styles/style.css'; 7 + 8 + let { children } = $props(); 9 + 10 + const shortcuts = [ 11 + { label: 'ibex Home', icon: '/icons/humanity/places/user-home.svg', selected: true }, 12 + { label: 'Collections', icon: '/icons/humanity/places/folder.svg' }, 13 + { label: 'Computer', icon: '/icons/humanity/devices/computer.svg' }, 14 + { label: 'Trash', icon: '/icons/humanity/places/user-trash.svg' } 15 + ]; 16 + </script> 17 + 18 + <svelte:head> 19 + <link rel="icon" href={favicon} /> 20 + <title>Intrepid Ibex ATProto Browser</title> 21 + <meta name="description" content="An AT Protocol browser styled after Ubuntu 8.10 and classic GNOME." /> 22 + </svelte:head> 23 + 24 + <div class="gnome-desktop"> 25 + <GnomePanel /> 26 + 27 + <main class="desktop-stage" aria-label="Ubuntu 8.10 inspired AT Protocol browser desktop"> 28 + <section class="desktop-icons" aria-label="Desktop shortcuts"> 29 + {#each shortcuts as shortcut (shortcut.label)} 30 + <DesktopIcon {...shortcut} /> 31 + {/each} 32 + </section> 33 + 34 + <div class="primary-window"> 35 + <AppWindow title="AT Protocol Collections - Intrepid Ibex" icon="/icons/humanity/apps/internet-feed-reader.svg"> 36 + {@render children()} 37 + </AppWindow> 38 + </div> 39 + 40 + <aside class="sticky-note" aria-label="Design note"> 41 + <h2>Intrepid Ibex</h2> 42 + <p>Static GNOME recreation: top panel, Humanity icons, tan window chrome, and collection folders.</p> 43 + </aside> 44 + </main> 45 + </div> 46 + 47 + <style> 48 + .gnome-desktop { 49 + display: grid; 50 + grid-template-rows: auto minmax(0, 1fr); 51 + min-height: 100vh; 52 + background: 53 + radial-gradient(circle at 20% 18%, rgb(255 191 92 / 0.22), transparent 22rem), 54 + radial-gradient(circle at 84% 75%, rgb(78 36 18 / 0.56), transparent 26rem), 55 + linear-gradient(rgb(45 23 14 / 0.08), rgb(45 23 14 / 0.12)), 56 + url('/ubuntu_810_wallpaper.jpg') center / cover; 57 + } 58 + 59 + .desktop-stage { 60 + position: relative; 61 + display: grid; 62 + grid-template-columns: 7rem minmax(20rem, 62rem) minmax(0, 1fr); 63 + grid-template-rows: minmax(0, 1fr); 64 + gap: var(--space-5); 65 + min-height: 0; 66 + padding: var(--space-5); 67 + } 68 + 69 + .desktop-icons { 70 + display: grid; 71 + align-content: start; 72 + justify-items: center; 73 + gap: var(--space-4); 74 + } 75 + 76 + .primary-window { 77 + align-self: center; 78 + height: min(42rem, calc(100vh - 6.5rem)); 79 + min-height: 28rem; 80 + } 81 + 82 + .primary-window :global(.app-window) { 83 + height: 100%; 84 + } 85 + 86 + .sticky-note { 87 + align-self: end; 88 + width: min(18rem, 100%); 89 + margin-bottom: var(--space-8); 90 + padding: var(--space-4); 91 + transform: rotate(1.2deg); 92 + color: #3a250e; 93 + background: linear-gradient(135deg, #fff1a9, #e9bf51); 94 + border: 1px solid #946b19; 95 + border-radius: var(--radius-2); 96 + box-shadow: 97 + 0 12px 24px rgb(0 0 0 / 0.25), 98 + 0 1px 0 rgb(255 255 255 / 0.72) inset; 99 + } 100 + 101 + .sticky-note h2 { 102 + font-size: var(--text-4); 103 + line-height: var(--leading-tight); 104 + } 105 + 106 + .sticky-note p { 107 + margin-top: var(--space-2); 108 + font-size: var(--text-2); 109 + } 110 + 111 + @media (max-width: 1000px) { 112 + .desktop-stage { 113 + grid-template-columns: 6.5rem minmax(0, 1fr); 114 + padding: var(--space-3); 115 + } 116 + 117 + .sticky-note { 118 + display: none; 119 + } 120 + } 121 + 122 + @media (max-width: 640px) { 123 + .desktop-stage { 124 + grid-template-columns: 1fr; 125 + } 126 + 127 + .desktop-icons { 128 + display: none; 129 + } 130 + 131 + .primary-window { 132 + height: calc(100vh - 4.25rem); 133 + min-height: 0; 134 + } 135 + } 136 + </style>
+1
src/routes/+layout.ts
··· 1 + export const prerender = true;
+5
src/routes/+page.svelte
··· 1 + <script lang="ts"> 2 + import CollectionBrowser from '$lib/components/CollectionBrowser.svelte'; 3 + </script> 4 + 5 + <CollectionBrowser />
+9
svelte.config.js
··· 1 + import adapter from '@sveltejs/adapter-static'; 2 + 3 + /** @type {import('@sveltejs/kit').Config} */ 4 + const config = { 5 + compilerOptions: { runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, 6 + kit: { adapter: adapter() } 7 + }; 8 + 9 + export default config;
+1 -5
vite.config.ts
··· 11 11 extends: './vite.config.ts', 12 12 test: { 13 13 name: 'client', 14 - browser: { 15 - enabled: true, 16 - provider: playwright(), 17 - instances: [{ browser: 'chromium', headless: true }] 18 - }, 14 + browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium', headless: true }] }, 19 15 include: ['src/**/*.svelte.{test,spec}.{js,ts}'], 20 16 exclude: ['src/lib/server/**'] 21 17 }