+3
src/app.html
+3
src/app.html
+14
src/components/Icon.svelte
+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
+1
src/routes/+layout.svelte
+52
-7
src/routes/~/+layout.svelte
+52
-7
src/routes/~/+layout.svelte
···
1
<script lang="ts">
2
import { goto } from "$app/navigation";
3
import { OAuthUserAgent, deleteStoredSession } from "@atcute/oauth-browser-client";
4
5
let { children, data } = $props();
6
7
async function logOut() {
8
try {
···
20
<header class="header">
21
<h1><a href="/~/">athost</a></h1>
22
<button class="menubutton" popovertarget="menu">
23
-
<img class="avatar" src={data.profile.avatar} alt="" width={24} />
24
<span>@{data.profile.displayName}</span>
25
</button>
26
<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>
31
</ul>
32
-
<button onclick={logOut}>log out</button>
33
</div>
34
</header>
35
···
48
.menubutton {
49
display: flex;
50
align-items: center;
51
}
52
53
.menu {
54
&:popover-open {
55
position: absolute;
56
inset: unset;
57
right: calc(50% - var(--app-max-width) / 2 + var(--app-inline-padding));
58
-
top: 3rem;
59
}
60
}
61
62
.avatar {
···
1
<script lang="ts">
2
import { goto } from "$app/navigation";
3
import { OAuthUserAgent, deleteStoredSession } from "@atcute/oauth-browser-client";
4
+
import Icon from "~/components/Icon.svelte";
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
+
});
13
14
async function logOut() {
15
try {
···
27
<header class="header">
28
<h1><a href="/~/">athost</a></h1>
29
<button class="menubutton" popovertarget="menu">
30
+
<img class="avatar" src={data.profile.avatar} alt="" width={20} />
31
<span>@{data.profile.displayName}</span>
32
</button>
33
<div id="menu" class="menu" popover>
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>
44
</ul>
45
+
<button onclick={logOut}>
46
+
<Icon name="logout" />
47
+
<span>log out</span>
48
+
</button>
49
</div>
50
</header>
51
···
64
.menubutton {
65
display: flex;
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
+
}
87
}
88
89
.menu {
90
+
background-color: light-dark(#eeeeee, #3c3c3c);
91
+
border: none;
92
+
padding: var(--size-3);
93
+
border-radius: var(--radius-md);
94
+
95
&:popover-open {
96
position: absolute;
97
inset: unset;
98
right: calc(50% - var(--app-max-width) / 2 + var(--app-inline-padding));
99
+
top: 2.75rem;
100
}
101
+
}
102
+
103
+
.themes {
104
+
display: flex;
105
}
106
107
.avatar {
+95
-19
src/routes/~/sites/[name]/+page.svelte
+95
-19
src/routes/~/sites/[name]/+page.svelte
···
1
<script lang="ts">
2
import { goto, invalidate } from "$app/navigation";
3
-
import { getRelativeTime } from "~/lib/date";
4
5
import { client } from "~/lib/oauth";
6
7
let { params, data } = $props();
8
9
const atp = client(data.session);
10
11
async function deleteBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
12
e.preventDefault();
13
const form = e.currentTarget;
···
29
if (typeof rkey !== "string") throw new Error("invalid rkey");
30
31
let description = formdata.get("description");
32
-
if (typeof description !== "string" || !description) description = "Uploaded on website";
33
34
const files = formdata.getAll("files").filter(entry => entry instanceof File);
35
await atp.updateBundle(rkey, description, files);
···
44
</header>
45
46
<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
{getRelativeTime(new Date(data.record.value.createdAt))}
51
</time>
52
</summary>
53
-
<ul>
54
{#each Object.entries(data.record.value.assets) as [path, file]}
55
<li>
56
<span>{path}</span>
···
64
</ul>
65
</details>
66
67
-
<section>
68
-
<h3>Upload</h3>
69
<form onsubmit={deployBundle}>
70
<input type="hidden" name="rkey" value={params.name} />
71
<label>
72
<span>description</span>
73
-
<input name="description" />
74
</label>
75
-
<input type="file" name="files" webkitdirectory />
76
<button>upload</button>
77
</form>
78
</section>
79
80
-
<section>
81
-
<h3>Settings</h3>
82
<form>
83
-
<label>
84
-
<span>not found</span>
85
-
<input name="notfound" />
86
-
</label>
87
<button>save</button>
88
</form>
89
</section>
90
91
-
<section>
92
-
<h3>Danger Zone</h3>
93
<form onsubmit={deleteBundle}>
94
<label>
95
<span>site name</span>
···
107
gap: var(--size-10);
108
}
109
110
.deploy {
111
border: var(--size-border) solid black;
112
border-radius: var(--radius-md);
113
}
114
115
-
.summary {
116
display: flex;
117
justify-content: space-between;
118
align-items: center;
119
padding: var(--size-3) var(--size-4);
120
}
121
</style>
···
1
<script lang="ts">
2
import { goto, invalidate } from "$app/navigation";
3
4
+
import Icon from "~/components/Icon.svelte";
5
import { client } from "~/lib/oauth";
6
+
import { getRelativeTime } from "~/lib/date";
7
8
let { params, data } = $props();
9
10
const atp = client(data.session);
11
12
+
const defaultDescription = "Uploaded from website";
13
+
14
async function deleteBundle(e: SubmitEvent & { currentTarget: HTMLFormElement }) {
15
e.preventDefault();
16
const form = e.currentTarget;
···
32
if (typeof rkey !== "string") throw new Error("invalid rkey");
33
34
let description = formdata.get("description");
35
+
if (typeof description !== "string" || !description) description = defaultDescription;
36
37
const files = formdata.getAll("files").filter(entry => entry instanceof File);
38
await atp.updateBundle(rkey, description, files);
···
47
</header>
48
49
<details class="deploy">
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}>
56
{getRelativeTime(new Date(data.record.value.createdAt))}
57
</time>
58
</summary>
59
+
<ul class="deploy-files">
60
{#each Object.entries(data.record.value.assets) as [path, file]}
61
<li>
62
<span>{path}</span>
···
70
</ul>
71
</details>
72
73
+
<section class="section">
74
+
<h3 class="heading">
75
+
<Icon name="upload" />
76
+
<span>Upload</span>
77
+
</h3>
78
<form onsubmit={deployBundle}>
79
<input type="hidden" name="rkey" value={params.name} />
80
+
<div class="dropzone">
81
+
<input type="file" name="files" webkitdirectory />
82
+
</div>
83
<label>
84
<span>description</span>
85
+
<input name="description" placeholder={defaultDescription} />
86
</label>
87
<button>upload</button>
88
</form>
89
</section>
90
91
+
<section class="section">
92
+
<h3 class="heading">
93
+
<Icon name="toggle" />
94
+
<span>Settings</span>
95
+
</h3>
96
<form>
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>
112
<button>save</button>
113
</form>
114
</section>
115
116
+
<section class="section">
117
+
<h3 class="heading">
118
+
<Icon name="warning" />
119
+
<span>Danger Zone</span>
120
+
</h3>
121
<form onsubmit={deleteBundle}>
122
<label>
123
<span>site name</span>
···
135
gap: var(--size-10);
136
}
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
+
150
.deploy {
151
border: var(--size-border) solid black;
152
border-radius: var(--radius-md);
153
}
154
155
+
.deploy-summary {
156
display: flex;
157
justify-content: space-between;
158
align-items: center;
159
+
gap: var(--size-4);
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);
196
}
197
</style>
+16
src/styles/theme.css
+16
src/styles/theme.css
···
8
/* fonts */
9
--font-body: "Bricolage Grotesque", sans-serif;
10
11
+
/* colors */
12
+
--color-background: light-dark(white, #1e1e1e);
13
+
--color-text: light-dark(black, white);
14
+
15
/* sizes */
16
--size-border: 1px;
17
--size-outline: 3px;
···
45
--radius-xl: 24px;
46
}
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
+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>