Attic is a cozy space with lofty ambitions. attic.social
11
fork

Configure Feed

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

base template + styles

+176 -14
+3 -3
src/app.html
··· 1 - <!doctype html> 2 - <html lang="en"> 1 + <!DOCTYPE html> 2 + <html dir="ltr" lang="en"> 3 3 <head> 4 4 <meta charset="utf-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 6 %sveltekit.head% 7 7 </head> 8 8 <body data-sveltekit-preload-data="hover"> 9 - <div style="display: contents">%sveltekit.body%</div> 9 + <attic-app>%sveltekit.body%</attic-app> 10 10 </body> 11 11 </html>
+58
src/css/base/global.css
··· 1 + html { 2 + background: wheat; 3 + color: black; 4 + } 5 + 6 + body { 7 + font-family: sans-serif; 8 + font-size: 1.125rem; 9 + font-weight: 400; 10 + font-synthesis: none; 11 + line-height: 1.5; 12 + min-block-size: 100svb; 13 + } 14 + 15 + attic-app { 16 + display: flex; 17 + flex-direction: column; 18 + justify-content: start; 19 + min-block-size: 100svb; 20 + 21 + & > :is(header, footer, main) { 22 + display: grid; 23 + grid-template-columns: 24 + [full-start] 25 + 1fr 26 + [margin-start] 27 + minmax(20px, min(5vi, 100px)) 28 + [main-start] 29 + minmax(260px, 800px) 30 + [main-end] 31 + minmax(20px, min(5vi, 100px)) 32 + [margin-end] 33 + 1fr 34 + [full-end]; 35 + 36 + & > * { 37 + grid-column: main; 38 + } 39 + } 40 + 41 + & > header { 42 + padding-block: 30px; 43 + } 44 + 45 + & > footer { 46 + padding-block: 30px; 47 + margin-block-start: auto; 48 + } 49 + 50 + & > main { 51 + row-gap: 30px; 52 + } 53 + } 54 + 55 + :focus-visible { 56 + outline: 2px solid magenta; 57 + outline-offset: 2px; 58 + }
+13
src/css/base/typography.css
··· 1 + h1 { 2 + font-size: 2rem; 3 + font-weight: 700; 4 + } 5 + 6 + :is(h2, h3, h4, h5, h6) { 7 + font-size: 1.5rem; 8 + font-weight: 700; 9 + } 10 + 11 + p:has(small:read-only) { 12 + font-size: 1rem; 13 + }
+7
src/css/components/button.css
··· 1 + button { 2 + border: 2px solid currentColor; 3 + font-weight: 700; 4 + inline-size: fit-content; 5 + line-height: 1.5; 6 + padding: 5px 10px; 7 + }
+15
src/css/components/form.css
··· 1 + form { 2 + background: white; 3 + display: grid; 4 + gap: 10px; 5 + justify-items: start; 6 + padding: 20px; 7 + 8 + & label { 9 + display: block; 10 + font-size: 1rem; 11 + font-weight: 700; 12 + inline-size: fit-content; 13 + line-height: 1.5; 14 + } 15 + }
+8
src/css/components/input.css
··· 1 + input[type="text"] { 2 + background: white; 3 + border: 2px solid currentColor; 4 + font-weight: 400; 5 + inline-size: min(100%, 300px); 6 + line-height: 1.5; 7 + padding: 5px 10px; 8 + }
+28
src/css/main.css
··· 1 1 @layer base, components, utility; 2 2 3 3 @import "base/reset.css" layer(base); 4 + @import "base/global.css" layer(base); 5 + @import "base/typography.css" layer(base); 6 + 7 + @import "components/button.css" layer(base); 8 + @import "components/input.css" layer(base); 9 + @import "components/form.css" layer(base); 10 + 11 + .error { 12 + color: #dd2244; 13 + font-weight: 700; 14 + } 15 + 16 + .avatar { 17 + align-items: center; 18 + display: grid; 19 + grid-template-columns: 50px auto; 20 + gap: 10px; 21 + 22 + & img { 23 + border-radius: calc(1px * infinity); 24 + block-size: 50px; 25 + inline-size: 50px; 26 + } 27 + 28 + & p { 29 + font-weight: 700; 30 + } 31 + }
+19 -1
src/routes/+layout.svelte
··· 4 4 let { children } = $props(); 5 5 </script> 6 6 7 - {@render children()} 7 + <header> 8 + <hgroup> 9 + <h1>attic.social</h1> 10 + <p>Attic is a cozy space with lofty ambitions.</p> 11 + </hgroup> 12 + </header> 13 + 14 + <main> 15 + {@render children()} 16 + </main> 17 + 18 + <footer> 19 + <p> 20 + <small> 21 + &copy; Copyright 2026 22 + <a href="https://dbushell.com">David Bushell</a> 23 + </small> 24 + </p> 25 + </footer>
+1 -1
src/routes/+page.server.ts
··· 15 15 url = await startSession(event, String(handle ?? "")); 16 16 } catch (err) { 17 17 console.log(err); 18 - return fail(400, { handle, invalid: true }); 18 + return fail(400, { handle, action: "login", error: "Invalid handle." }); 19 19 } 20 20 redirect(303, url); 21 21 },
+24 -9
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import type { PageProps } from "./$types.d.ts"; 3 3 let { data, form }: PageProps = $props(); 4 - 5 - let handle = $derived(form?.handle ?? ""); 6 4 </script> 7 5 8 6 {#if data.user} 9 - <img alt="avatar" src="/avatar/{data.user.did}" width="50" height="50" /> 10 - <h2>Hello, {data.user.displayName}!</h2> 7 + <h2>Signed in as:</h2> 8 + <div class="avatar"> 9 + <img alt="avatar" src="/avatar/{data.user.did}" width="50" height="50" /> 10 + <p>{data.user.displayName}</p> 11 + </div> 12 + 13 + <form method="POST" action="?/displayName"> 14 + <h2>Attic settings</h2> 15 + <label for="displayName">Display name</label> 16 + <input 17 + type="text" 18 + id="displayName" 19 + name="displayName" 20 + value={data.user.displayName} 21 + /> 22 + <button type="submit">Update</button> 23 + </form> 24 + 11 25 <form method="POST" action="?/logout"> 12 - <button type="submit">Logout</button> 26 + <h2>Bye!</h2> 27 + <button type="submit">Sign out</button> 13 28 </form> 14 29 {:else} 15 30 <form method="POST" action="?/login"> 16 31 <h2>Connect</h2> 17 32 <p>Connect with your Bluesky / Atmosphere account.</p> 18 - {#if form?.invalid} 19 - <p><strong>Invalid handle</strong></p> 33 + {#if form?.action === "login" && form?.error} 34 + <p class="error">{form.error}</p> 20 35 {/if} 21 36 <label for="handle">Handle</label> 22 - <input type="text" id="handle" name="handle" bind:value={handle} /> 23 - <button type="submit">Login</button> 37 + <input type="text" id="handle" name="handle" value={form?.handle} /> 38 + <button type="submit">Sign in</button> 24 39 </form> 25 40 {/if}