tangled
alpha
login
or
join now
finxol.io
/
portfolio
0
fork
atom
Personal site
staging.colinozanne.co.uk
portfolio
astro
0
fork
atom
overview
issues
pulls
pipelines
feat: improve themes and picker
finxol.io
1 month ago
161f78ef
1c44bb7b
verified
This commit was signed with the committer's
known signature
.
finxol.io
SSH Key Fingerprint:
SHA256:olFE3asYdoBMScuJOt60UxXdJ0RFdGv5kVKrdOtIcPI=
+216
-75
2 changed files
expand all
collapse all
unified
split
src
components
customise.astro
util
store.ts
+195
-75
src/components/customise.astro
···
1
1
---
2
2
+
import { config } from "@/config";
3
3
+
import type { Locale } from "@/hooks/useLocale.astro";
2
4
import { Icon } from "astro-icon/components";
5
5
+
6
6
+
const content = {
7
7
+
trigger: {
8
8
+
en: "Customise the page",
9
9
+
fr: "Personnaliser la page",
10
10
+
},
11
11
+
dialog: {
12
12
+
title: {
13
13
+
en: "Customise",
14
14
+
fr: "Personnaliser",
15
15
+
},
16
16
+
content: {
17
17
+
en: "Change the colours to personalise your experience.",
18
18
+
fr: "Changez les couleurs pour personnaliser votre expérience.",
19
19
+
},
20
20
+
themes: {
21
21
+
light: {
22
22
+
en: "Light",
23
23
+
fr: "Clair",
24
24
+
},
25
25
+
dark: {
26
26
+
en: "Dark",
27
27
+
fr: "Sombre",
28
28
+
},
29
29
+
random: {
30
30
+
en: "Random",
31
31
+
fr: "Aléatoire",
32
32
+
},
33
33
+
},
34
34
+
},
35
35
+
};
36
36
+
37
37
+
const locale = (Astro.currentLocale as Locale) ?? config.defaultLocale;
3
38
---
4
39
5
40
<button
6
41
type="button"
7
42
class="customise-trigger container"
8
8
-
popovertarget="customisation-popover"
43
43
+
id="customise-trigger"
9
44
>
10
45
<Icon name="pixel:themes" />
11
11
-
Customise <wbr />the page
46
46
+
<span>
47
47
+
{content.trigger[locale]}
48
48
+
</span>
12
49
</button>
13
13
-
<aside id="customisation-popover" popover="auto">
50
50
+
<dialog id="customisation-dialog">
14
51
<div>
15
15
-
<h2>Customise</h2>
52
52
+
<h2>{content.dialog.title[locale]}</h2>
16
53
<p>
17
17
-
Change the theme, layout, and other settings to personalise your
18
18
-
experience.
54
54
+
{content.dialog.content[locale]}
19
55
</p>
20
56
21
57
<section>
22
58
<button id="light-button">
23
23
-
<Icon name="pixel:sun" />
24
24
-
Light
59
59
+
<Icon name="tabler:sun" />
60
60
+
{content.dialog.themes.light[locale]}
25
61
</button>
26
62
<button id="dark-button">
27
27
-
<Icon name="pixel:moon" />
28
28
-
Dark
63
63
+
<Icon name="tabler:moon-stars" />
64
64
+
{content.dialog.themes.dark[locale]}
65
65
+
</button>
66
66
+
<button id="random-button">
67
67
+
<Icon name="tabler:arrows-shuffle" />
68
68
+
{content.dialog.themes.random[locale]}
29
69
</button>
30
70
</section>
31
71
</div>
32
32
-
</aside>
72
72
+
</dialog>
33
73
34
74
<script>
35
35
-
const store = new Proxy(document.documentElement.dataset, {
36
36
-
set(target, key: string, value: string | null) {
37
37
-
if (value === null) {
38
38
-
delete target[key];
39
39
-
localStorage.removeItem(key);
40
40
-
} else {
41
41
-
target[key] = value;
42
42
-
localStorage.setItem(key, value);
43
43
-
}
44
44
-
return true;
45
45
-
},
46
46
-
get(target, key: string) {
47
47
-
const item = target[key];
48
48
-
if (item) return item;
75
75
+
import { store } from "@/util/store";
76
76
+
77
77
+
const trigger = document.getElementById("customise-trigger")!;
78
78
+
const dialog = document.getElementById(
79
79
+
"customisation-dialog",
80
80
+
) as HTMLDialogElement;
81
81
+
const lightButton = document.getElementById("light-button")!;
82
82
+
const darkButton = document.getElementById("dark-button")!;
49
83
50
50
-
const v = localStorage.getItem(key) ?? "light";
51
51
-
store[key] = v;
84
84
+
// Open dialog
85
85
+
trigger.addEventListener("click", () => {
86
86
+
dialog.showModal();
87
87
+
});
52
88
53
53
-
return v;
54
54
-
},
89
89
+
// Light dismiss - close when clicking on backdrop
90
90
+
dialog.addEventListener("click", (e) => {
91
91
+
if (e.target === dialog) {
92
92
+
dialog.close();
93
93
+
}
55
94
});
56
95
57
57
-
const lightButton = document.getElementById("light-button")!;
58
58
-
const darkButton = document.getElementById("dark-button")!;
96
96
+
// Close on Escape is built-in for dialog
59
97
60
98
lightButton.addEventListener("click", () => {
61
99
store.theme = "light";
···
69
107
// to trigger the theme change
70
108
store.theme;
71
109
};
110
110
+
111
111
+
const colours = [
112
112
+
"amber",
113
113
+
"yellow",
114
114
+
"lime",
115
115
+
"emerald",
116
116
+
"teal",
117
117
+
"sky",
118
118
+
"indigo",
119
119
+
"fuchsia",
120
120
+
"rose",
121
121
+
"gray",
122
122
+
"sand",
123
123
+
];
72
124
</script>
73
125
74
126
<style>
···
79
131
flex-direction: column;
80
132
align-items: center;
81
133
justify-content: center;
82
82
-
transition: background-color 0.3s ease;
134
134
+
gap: 1rem;
135
135
+
font-size: var(--size-0);
83
136
border: 0;
84
137
cursor: pointer;
138
138
+
background: conic-gradient(
139
139
+
from var(--angle) at 50% 50%,
140
140
+
var(--rose-600),
141
141
+
var(--fuchsia-600),
142
142
+
var(--rose-600)
143
143
+
);
144
144
+
--angle: 0deg;
85
145
86
86
-
&:hover {
87
87
-
animation: rainbow 2s infinite;
146
146
+
@media screen and (max-width: 768px) {
147
147
+
flex-direction: row;
88
148
}
89
149
90
90
-
@keyframes rainbow {
91
91
-
0% {
92
92
-
background-color: var(--rose-600);
93
93
-
}
94
94
-
100% {
95
95
-
background-color: var(--fuchsia-600);
96
96
-
}
150
150
+
&:hover {
151
151
+
animation: rotate 2s linear infinite;
97
152
}
153
153
+
}
98
154
99
99
-
& > svg {
100
100
-
margin-block-end: 0.5rem;
155
155
+
@keyframes rotate {
156
156
+
from {
157
157
+
--angle: 0deg;
158
158
+
}
159
159
+
to {
160
160
+
--angle: 360deg;
101
161
}
102
162
}
103
163
104
104
-
aside#customisation-popover {
164
164
+
dialog#customisation-dialog {
105
165
position: fixed;
106
166
top: var(--spacing);
107
167
right: var(--spacing);
108
108
-
bottom: var(--spacing);
168
168
+
bottom: auto;
109
169
left: auto;
110
110
-
max-width: 30rem;
170
170
+
width: min(22rem, calc(100vw - 2 * var(--spacing)));
171
171
+
max-width: unset;
172
172
+
max-height: unset;
111
173
margin: 0;
174
174
+
padding: calc(var(--spacing) * 1.5);
112
175
113
176
opacity: 1;
114
114
-
background-color: var(--background);
177
177
+
background: linear-gradient(
178
178
+
145deg,
179
179
+
oklch(from var(--background) calc(l + 0.05) c h),
180
180
+
var(--background)
181
181
+
);
115
182
color: var(--foreground);
116
116
-
border: 1px solid var(--primary-muted);
117
117
-
border-radius: 0.5rem;
118
118
-
box-shadow: 0 0 10px oklch(from var(--fuchsia-900) l c h / 0.1);
183
183
+
border: 2px solid var(--primary-muted);
184
184
+
border-radius: 1.5rem;
185
185
+
box-shadow:
186
186
+
0 8px 32px oklch(from var(--fuchsia-900) l c h / 0.15),
187
187
+
0 2px 8px oklch(from var(--fuchsia-900) l c h / 0.1);
119
188
120
120
-
transform: translateX(0);
121
121
-
transform-origin: right center;
189
189
+
transform: translateY(0) scale(1);
190
190
+
transform-origin: top right;
122
191
transition:
123
123
-
opacity 0.3s ease,
124
124
-
transform 0.3s ease;
125
125
-
transition-behavior: allow-discrete;
192
192
+
opacity 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
193
193
+
transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
194
194
+
display 0.3s allow-discrete,
195
195
+
overlay 0.3s allow-discrete;
196
196
+
197
197
+
&:not([open]) {
198
198
+
opacity: 0;
199
199
+
transform: translateY(-1rem) scale(0.95);
200
200
+
pointer-events: none;
201
201
+
}
126
202
127
203
@starting-style {
128
204
opacity: 0;
129
129
-
transform: translateX(100%);
205
205
+
transform: translateY(-1rem) scale(0.95);
130
206
}
131
207
132
208
&::backdrop {
133
133
-
background: transparent;
209
209
+
background: oklch(from var(--background) l c h / 0.5);
210
210
+
backdrop-filter: blur(4px);
211
211
+
transition:
212
212
+
background 0.3s ease,
213
213
+
backdrop-filter 0.3s ease;
134
214
}
135
215
136
216
div {
137
217
display: flex;
138
218
flex-direction: column;
139
139
-
align-items: start;
219
219
+
align-items: stretch;
140
220
justify-content: start;
141
141
-
gap: calc(var(--spacing) * 0.5);
142
142
-
padding: var(--spacing);
221
221
+
gap: calc(var(--spacing) * 1.25);
143
222
144
223
h2 {
145
145
-
margin-block: 0;
224
224
+
margin: 0;
146
225
font-size: var(--size-2);
147
226
font-weight: bold;
227
227
+
text-align: center;
148
228
}
149
229
150
230
p {
151
231
margin: 0;
152
232
font-size: var(--size--1);
153
233
font-weight: normal;
234
234
+
text-align: center;
154
235
text-wrap: balance;
236
236
+
opacity: 0.8;
155
237
}
156
238
157
239
section {
158
240
display: grid;
159
241
grid-template-columns: 1fr 1fr;
160
160
-
gap: var(--spacing);
242
242
+
gap: calc(var(--spacing) * 0.75);
243
243
+
margin-top: calc(var(--spacing) * 0.5);
161
244
162
245
button {
163
163
-
background-color: var(--background);
164
164
-
border: 1px solid var(--primary-muted);
165
165
-
border-radius: 0.5rem;
166
166
-
padding: calc(var(--spacing) * 0.3);
167
167
-
font-size: var(--size--1);
168
168
-
font-weight: bold;
246
246
+
display: flex;
247
247
+
flex-direction: column;
248
248
+
align-items: center;
249
249
+
justify-content: center;
250
250
+
gap: calc(var(--spacing) * 0.5);
251
251
+
padding: calc(var(--spacing) * 1);
252
252
+
border: 2px solid var(--primary-muted);
253
253
+
border-radius: 1rem;
254
254
+
background-color: oklch(
255
255
+
from var(--background) calc(l + 0.02) c h
256
256
+
);
257
257
+
font-size: var(--size-0);
258
258
+
font-weight: 600;
169
259
cursor: pointer;
170
260
color: inherit;
171
171
-
transition: background-color 0.3s ease;
261
261
+
transition:
262
262
+
background-color 0.2s ease,
263
263
+
border-color 0.2s ease,
264
264
+
transform 0.2s ease;
265
265
+
266
266
+
svg {
267
267
+
width: 1.75rem;
268
268
+
height: 1.75rem;
269
269
+
}
172
270
173
271
&:hover {
174
174
-
background-color: var(--primary-muted);
175
175
-
animation: spin 0.5s cubic-bezier(0.86, 0, 0.07, 1);
272
272
+
background-color: oklch(
273
273
+
from var(--primary-muted) l c h / 0.4
274
274
+
);
275
275
+
border-color: var(--primary);
276
276
+
transform: translateY(-2px);
277
277
+
}
278
278
+
279
279
+
&:active {
280
280
+
transform: translateY(0);
176
281
}
177
282
178
283
:where(html:not([data-theme]), html[data-theme="light"])
179
284
&#light-button {
180
285
background-color: var(--primary);
286
286
+
border-color: var(--primary);
181
287
}
182
288
183
289
:where(html[data-theme="dark"]) &#dark-button {
184
290
background-color: var(--primary);
291
291
+
border-color: var(--primary);
292
292
+
}
293
293
+
294
294
+
&:hover svg {
295
295
+
animation: wiggle 0.4s ease;
296
296
+
}
297
297
+
298
298
+
&#random-button {
299
299
+
grid-column: 1 / 3;
300
300
+
flex-direction: row;
185
301
}
186
302
}
187
303
}
188
304
}
189
305
}
190
306
191
191
-
@keyframes spin {
192
192
-
from {
307
307
+
@keyframes wiggle {
308
308
+
0%,
309
309
+
100% {
193
310
transform: rotate(0deg);
194
311
}
195
195
-
to {
196
196
-
transform: rotate(360deg);
312
312
+
25% {
313
313
+
transform: rotate(-15deg);
314
314
+
}
315
315
+
75% {
316
316
+
transform: rotate(15deg);
197
317
}
198
318
}
199
319
</style>
+21
src/util/store.ts
···
1
1
+
export const store = new Proxy(document.documentElement.dataset, {
2
2
+
set(target, key: string, value: string | null) {
3
3
+
if (value === null) {
4
4
+
delete target[key];
5
5
+
localStorage.removeItem(key);
6
6
+
} else {
7
7
+
target[key] = value;
8
8
+
localStorage.setItem(key, value);
9
9
+
}
10
10
+
return true;
11
11
+
},
12
12
+
get(target, key: string) {
13
13
+
const item = target[key];
14
14
+
if (item) return item;
15
15
+
16
16
+
const v = localStorage.getItem(key) ?? "light";
17
17
+
store[key] = v;
18
18
+
19
19
+
return v;
20
20
+
},
21
21
+
});