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