serve a static website from your pds

Improve UI

Changed files
+208 -26
src
components
routes
styles
static
+3
src/app.html
··· 1 1 <!doctype html> 2 2 <html lang="en"> 3 3 <head> 4 + <script> 5 + if (localStorage.theme) document.documentElement.dataset.theme; 6 + </script> 4 7 <meta charset="utf-8" /> 5 8 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 9 %sveltekit.head%
+14
src/components/Icon.svelte
··· 1 + <script lang="ts"> 2 + interface Props { 3 + name: string; 4 + label?: string; 5 + size?: number; 6 + } 7 + 8 + let { name, label, size = 20 }: Props = $props(); 9 + </script> 10 + 11 + <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} aria-hidden={!label}> 12 + <use href={`/icons.svg#${name}`}></use> 13 + {#if label}<title>{label}</title>{/if} 14 + </svg>
+1
src/routes/+layout.svelte
··· 20 20 21 21 :global(html) { 22 22 font-family: var(--font-body); 23 + background-color: var(--font-background); 23 24 } 24 25 25 26 .app {
+52 -7
src/routes/~/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import { goto } from "$app/navigation"; 3 3 import { OAuthUserAgent, deleteStoredSession } from "@atcute/oauth-browser-client"; 4 + import Icon from "~/components/Icon.svelte"; 4 5 5 6 let { children, data } = $props(); 7 + 8 + let theme = $state(localStorage.theme || "system"); 9 + $effect(() => { 10 + localStorage.theme = theme; 11 + document.documentElement.dataset.theme = theme; 12 + }); 6 13 7 14 async function logOut() { 8 15 try { ··· 20 27 <header class="header"> 21 28 <h1><a href="/~/">athost</a></h1> 22 29 <button class="menubutton" popovertarget="menu"> 23 - <img class="avatar" src={data.profile.avatar} alt="" width={24} /> 30 + <img class="avatar" src={data.profile.avatar} alt="" width={20} /> 24 31 <span>@{data.profile.displayName}</span> 25 32 </button> 26 33 <div id="menu" class="menu" popover> 27 - <ul> 28 - <li><label><input type="radio" name="theme" value="system" />system</label></li> 29 - <li><label><input type="radio" name="theme" value="light" />light</label></li> 30 - <li><label><input type="radio" name="theme" value="dark" />dark</label></li> 34 + <ul class="themes"> 35 + <li> 36 + <label><input type="radio" name="theme" value="system" bind:group={theme} />system</label> 37 + </li> 38 + <li> 39 + <label><input type="radio" name="theme" value="light" bind:group={theme} />light</label> 40 + </li> 41 + <li> 42 + <label><input type="radio" name="theme" value="dark" bind:group={theme} />dark</label> 43 + </li> 31 44 </ul> 32 - <button onclick={logOut}>log out</button> 45 + <button onclick={logOut}> 46 + <Icon name="logout" /> 47 + <span>log out</span> 48 + </button> 33 49 </div> 34 50 </header> 35 51 ··· 48 64 .menubutton { 49 65 display: flex; 50 66 align-items: center; 67 + gap: var(--size-3); 68 + padding: var(--size-2) var(--size-3); 69 + padding-right: var(--size-4); 70 + background-color: transparent; 71 + border: none; 72 + border-radius: var(--radius-md); 73 + transition: 74 + 0.1s ease background-color, 75 + 0.1s ease box-shadow; 76 + 77 + &:hover, 78 + &:has(+ .menu:popover-open) { 79 + background-color: light-dark(#eeeeee, #ffffff22); 80 + } 81 + 82 + &:active, 83 + &:active:has(+ .menu:popover-open) { 84 + background-color: light-dark(#dddddd, #ffffff33); 85 + box-shadow: inset 0 2px 4px light-dark(#00000066, #00000099); 86 + } 51 87 } 52 88 53 89 .menu { 90 + background-color: light-dark(#eeeeee, #3c3c3c); 91 + border: none; 92 + padding: var(--size-3); 93 + border-radius: var(--radius-md); 94 + 54 95 &:popover-open { 55 96 position: absolute; 56 97 inset: unset; 57 98 right: calc(50% - var(--app-max-width) / 2 + var(--app-inline-padding)); 58 - top: 3rem; 99 + top: 2.75rem; 59 100 } 101 + } 102 + 103 + .themes { 104 + display: flex; 60 105 } 61 106 62 107 .avatar {
+95 -19
src/routes/~/sites/[name]/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { goto, invalidate } from "$app/navigation"; 3 - import { getRelativeTime } from "~/lib/date"; 4 3 4 + import Icon from "~/components/Icon.svelte"; 5 5 import { client } from "~/lib/oauth"; 6 + import { getRelativeTime } from "~/lib/date"; 6 7 7 8 let { params, data } = $props(); 8 9 9 10 const atp = client(data.session); 10 11 12 + const defaultDescription = "Uploaded from website"; 13 + 11 14 async function deleteBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) { 12 15 e.preventDefault(); 13 16 const form = e.currentTarget; ··· 29 32 if (typeof rkey !== "string") throw new Error("invalid rkey"); 30 33 31 34 let description = formdata.get("description"); 32 - if (typeof description !== "string" || !description) description = "Uploaded on website"; 35 + if (typeof description !== "string" || !description) description = defaultDescription; 33 36 34 37 const files = formdata.getAll("files").filter(entry => entry instanceof File); 35 38 await atp.updateBundle(rkey, description, files); ··· 44 47 </header> 45 48 46 49 <details class="deploy"> 47 - <summary class="summary"> 48 - <span>{data.record.value.description || data.record.cid}</span> 49 - <time datetime={data.record.value.createdAt}> 50 + <summary class="deploy-summary"> 51 + <div class="deploy-label"> 52 + <Icon name="expand" /> 53 + <span class="deploy-description">{data.record.value.description || data.record.cid}</span> 54 + </div> 55 + <time class="deploy-created" datetime={data.record.value.createdAt}> 50 56 {getRelativeTime(new Date(data.record.value.createdAt))} 51 57 </time> 52 58 </summary> 53 - <ul> 59 + <ul class="deploy-files"> 54 60 {#each Object.entries(data.record.value.assets) as [path, file]} 55 61 <li> 56 62 <span>{path}</span> ··· 64 70 </ul> 65 71 </details> 66 72 67 - <section> 68 - <h3>Upload</h3> 73 + <section class="section"> 74 + <h3 class="heading"> 75 + <Icon name="upload" /> 76 + <span>Upload</span> 77 + </h3> 69 78 <form onsubmit={deployBundle}> 70 79 <input type="hidden" name="rkey" value={params.name} /> 80 + <div class="dropzone"> 81 + <input type="file" name="files" webkitdirectory /> 82 + </div> 71 83 <label> 72 84 <span>description</span> 73 - <input name="description" /> 85 + <input name="description" placeholder={defaultDescription} /> 74 86 </label> 75 - <input type="file" name="files" webkitdirectory /> 76 87 <button>upload</button> 77 88 </form> 78 89 </section> 79 90 80 - <section> 81 - <h3>Settings</h3> 91 + <section class="section"> 92 + <h3 class="heading"> 93 + <Icon name="toggle" /> 94 + <span>Settings</span> 95 + </h3> 82 96 <form> 83 - <label> 84 - <span>not found</span> 85 - <input name="notfound" /> 86 - </label> 97 + <fieldset> 98 + <legend>Fallback</legend> 99 + <label> 100 + <span>path</span> 101 + <input name="fallback_path" /> 102 + </label> 103 + <label> 104 + <span>200</span> 105 + <input type="radio" name="fallback_status" value="200" /> 106 + </label> 107 + <label> 108 + <span>404</span> 109 + <input type="radio" name="fallback_status" value="404" /> 110 + </label> 111 + </fieldset> 87 112 <button>save</button> 88 113 </form> 89 114 </section> 90 115 91 - <section> 92 - <h3>Danger Zone</h3> 116 + <section class="section"> 117 + <h3 class="heading"> 118 + <Icon name="warning" /> 119 + <span>Danger Zone</span> 120 + </h3> 93 121 <form onsubmit={deleteBundle}> 94 122 <label> 95 123 <span>site name</span> ··· 107 135 gap: var(--size-10); 108 136 } 109 137 138 + .section { 139 + display: flex; 140 + flex-direction: column; 141 + gap: var(--size-3); 142 + } 143 + 144 + .heading { 145 + display: flex; 146 + align-items: center; 147 + gap: var(--size-3); 148 + } 149 + 110 150 .deploy { 111 151 border: var(--size-border) solid black; 112 152 border-radius: var(--radius-md); 113 153 } 114 154 115 - .summary { 155 + .deploy-summary { 116 156 display: flex; 117 157 justify-content: space-between; 118 158 align-items: center; 159 + gap: var(--size-4); 119 160 padding: var(--size-3) var(--size-4); 161 + cursor: pointer; 162 + 163 + .deploy[open] & { 164 + box-shadow: 0 1px 0 black; 165 + } 166 + } 167 + 168 + .deploy-label { 169 + display: flex; 170 + align-items: center; 171 + gap: var(--size-2); 172 + min-width: 0; 173 + } 174 + 175 + .deploy-description { 176 + display: block; 177 + white-space: nowrap; 178 + overflow: hidden; 179 + text-overflow: ellipsis; 180 + } 181 + 182 + .deploy-created { 183 + opacity: 0.5; 184 + white-space: nowrap; 185 + } 186 + 187 + .deploy-files { 188 + padding: var(--size-3) var(--size-4); 189 + } 190 + 191 + .dropzone { 192 + background-color: light-dark(#eeeeee, #3c3c3c); 193 + padding: var(--size-6); 194 + border-radius: var(--radius-lg); 195 + border: 2px dashed light-dark(#cccccc, #555555); 120 196 } 121 197 </style>
+16
src/styles/theme.css
··· 8 8 /* fonts */ 9 9 --font-body: "Bricolage Grotesque", sans-serif; 10 10 11 + /* colors */ 12 + --color-background: light-dark(white, #1e1e1e); 13 + --color-text: light-dark(black, white); 14 + 11 15 /* sizes */ 12 16 --size-border: 1px; 13 17 --size-outline: 3px; ··· 41 45 --radius-xl: 24px; 42 46 } 43 47 } 48 + 49 + :root { 50 + color-scheme: light dark; 51 + 52 + &[data-theme="light"] { 53 + color-scheme: light; 54 + } 55 + 56 + &[data-theme="dark"] { 57 + color-scheme: dark; 58 + } 59 + }
+27
static/icons.svg
··· 1 + <symbol xmlns="http://www.w3.org/2000/svg"> 2 + <symbol id="logout" viewBox="0 0 20 20"> 3 + <g fill="none"> 4 + <path fill="currentColor" d="M17 6a2.99 2.99 0 0 0-1.714-2.7l-2.993 2.993A1 1 0 0 0 12 7v6c0 .265.105.52.293.707l2.993 2.993A2.99 2.99 0 0 0 17 14z"/> 5 + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h6" /> 6 + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.44 15.544A2.99 2.99 0 0 0 10 17h4a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3h-4a2.99 2.99 0 0 0-2.558 1.453"/> 7 + <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.75 7.5 3 10l2.75 2.5" /> 8 + </g> 9 + </symbol> 10 + 11 + <symbol id="upload" viewBox="0 0 20 20"> 12 + <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m7 7 3-3 3 3M10 4v7"/> 13 + <path fill="currentColor" d="M14 2a1 1 0 0 0 0 2 2 2 0 0 1 2 2v7a1 1 0 0 1-1 1h-1.719a1 1 0 0 0-.97.757l-.121.485a1 1 0 0 1-.97.757H8.781a1 1 0 0 1-.97-.757l-.121-.485A1 1 0 0 0 6.72 14H5.001a1 1 0 0 1-1-1V6a2 2 0 0 1 2-2 1 1 0 0 0 0-2 4 4 0 0 0-4 4v8a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4V6a4 4 0 0 0-4-4Z"/> 14 + </symbol> 15 + 16 + <symbol id="warning" viewBox="0 0 20 20"> 17 + <path fill="currentColor" d="m17.794 12.5-5.196-9C12.056 2.561 11.084 2 10 2s-2.056.561-2.598 1.5l-5.196 9a2.97 2.97 0 0 0 0 3A2.97 2.97 0 0 0 4.804 17h10.393a2.97 2.97 0 0 0 2.598-1.5 2.97 2.97 0 0 0 0-3ZM9 7a1 1 0 1 1 2 0v3.5a1 1 0 1 1-2 0zm1 8c-.689 0-1.25-.561-1.25-1.25S9.311 12.5 10 12.5s1.25.561 1.25 1.25S10.689 15 10 15"/> 18 + </symbol> 19 + 20 + <symbol id="toggle" viewBox="0 0 20 20"> 21 + <path fill="currentColor" d="M12.5 4h-5c-3.309 0-6 2.691-6 6s2.691 6 6 6h5c3.309 0 6-2.691 6-6s-2.691-6-6-6m-5 10a4 4 0 1 1 0-8 4 4 0 0 1 0 8"/> 22 + <symbol> 23 + 24 + <symbol id="expand" viewBox="0 0 20 20"> 25 + <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m13 7-3-3-3 3M13 13l-3 3-3-3"/> 26 + </symbol> 27 + </svg>