+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
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
+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
+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
+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>