Personal site staging.colinozanne.co.uk
portfolio astro

feat: improve themes and picker

finxol.io 161f78ef 1c44bb7b

verified
+216 -75
+195 -75
src/components/customise.astro
··· 1 1 --- 2 + import { config } from "@/config"; 3 + import type { Locale } from "@/hooks/useLocale.astro"; 2 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; 3 38 --- 4 39 5 40 <button 6 41 type="button" 7 42 class="customise-trigger container" 8 - popovertarget="customisation-popover" 43 + id="customise-trigger" 9 44 > 10 45 <Icon name="pixel:themes" /> 11 - Customise <wbr />the page 46 + <span> 47 + {content.trigger[locale]} 48 + </span> 12 49 </button> 13 - <aside id="customisation-popover" popover="auto"> 50 + <dialog id="customisation-dialog"> 14 51 <div> 15 - <h2>Customise</h2> 52 + <h2>{content.dialog.title[locale]}</h2> 16 53 <p> 17 - Change the theme, layout, and other settings to personalise your 18 - experience. 54 + {content.dialog.content[locale]} 19 55 </p> 20 56 21 57 <section> 22 58 <button id="light-button"> 23 - <Icon name="pixel:sun" /> 24 - Light 59 + <Icon name="tabler:sun" /> 60 + {content.dialog.themes.light[locale]} 25 61 </button> 26 62 <button id="dark-button"> 27 - <Icon name="pixel:moon" /> 28 - Dark 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]} 29 69 </button> 30 70 </section> 31 71 </div> 32 - </aside> 72 + </dialog> 33 73 34 74 <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; 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")!; 49 83 50 - const v = localStorage.getItem(key) ?? "light"; 51 - store[key] = v; 84 + // Open dialog 85 + trigger.addEventListener("click", () => { 86 + dialog.showModal(); 87 + }); 52 88 53 - return v; 54 - }, 89 + // Light dismiss - close when clicking on backdrop 90 + dialog.addEventListener("click", (e) => { 91 + if (e.target === dialog) { 92 + dialog.close(); 93 + } 55 94 }); 56 95 57 - const lightButton = document.getElementById("light-button")!; 58 - const darkButton = document.getElementById("dark-button")!; 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 + 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 + ]; 72 124 </script> 73 125 74 126 <style> ··· 79 131 flex-direction: column; 80 132 align-items: center; 81 133 justify-content: center; 82 - transition: background-color 0.3s ease; 134 + gap: 1rem; 135 + font-size: var(--size-0); 83 136 border: 0; 84 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; 85 145 86 - &:hover { 87 - animation: rainbow 2s infinite; 146 + @media screen and (max-width: 768px) { 147 + flex-direction: row; 88 148 } 89 149 90 - @keyframes rainbow { 91 - 0% { 92 - background-color: var(--rose-600); 93 - } 94 - 100% { 95 - background-color: var(--fuchsia-600); 96 - } 150 + &:hover { 151 + animation: rotate 2s linear infinite; 97 152 } 153 + } 98 154 99 - & > svg { 100 - margin-block-end: 0.5rem; 155 + @keyframes rotate { 156 + from { 157 + --angle: 0deg; 158 + } 159 + to { 160 + --angle: 360deg; 101 161 } 102 162 } 103 163 104 - aside#customisation-popover { 164 + dialog#customisation-dialog { 105 165 position: fixed; 106 166 top: var(--spacing); 107 167 right: var(--spacing); 108 - bottom: var(--spacing); 168 + bottom: auto; 109 169 left: auto; 110 - max-width: 30rem; 170 + width: min(22rem, calc(100vw - 2 * var(--spacing))); 171 + max-width: unset; 172 + max-height: unset; 111 173 margin: 0; 174 + padding: calc(var(--spacing) * 1.5); 112 175 113 176 opacity: 1; 114 - background-color: var(--background); 177 + background: linear-gradient( 178 + 145deg, 179 + oklch(from var(--background) calc(l + 0.05) c h), 180 + var(--background) 181 + ); 115 182 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); 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); 119 188 120 - transform: translateX(0); 121 - transform-origin: right center; 189 + transform: translateY(0) scale(1); 190 + transform-origin: top right; 122 191 transition: 123 - opacity 0.3s ease, 124 - transform 0.3s ease; 125 - transition-behavior: allow-discrete; 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 + } 126 202 127 203 @starting-style { 128 204 opacity: 0; 129 - transform: translateX(100%); 205 + transform: translateY(-1rem) scale(0.95); 130 206 } 131 207 132 208 &::backdrop { 133 - background: transparent; 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; 134 214 } 135 215 136 216 div { 137 217 display: flex; 138 218 flex-direction: column; 139 - align-items: start; 219 + align-items: stretch; 140 220 justify-content: start; 141 - gap: calc(var(--spacing) * 0.5); 142 - padding: var(--spacing); 221 + gap: calc(var(--spacing) * 1.25); 143 222 144 223 h2 { 145 - margin-block: 0; 224 + margin: 0; 146 225 font-size: var(--size-2); 147 226 font-weight: bold; 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 + text-align: center; 154 235 text-wrap: balance; 236 + opacity: 0.8; 155 237 } 156 238 157 239 section { 158 240 display: grid; 159 241 grid-template-columns: 1fr 1fr; 160 - gap: var(--spacing); 242 + gap: calc(var(--spacing) * 0.75); 243 + margin-top: calc(var(--spacing) * 0.5); 161 244 162 245 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; 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; 169 259 cursor: pointer; 170 260 color: inherit; 171 - transition: background-color 0.3s ease; 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 + } 172 270 173 271 &:hover { 174 - background-color: var(--primary-muted); 175 - animation: spin 0.5s cubic-bezier(0.86, 0, 0.07, 1); 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); 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 + border-color: var(--primary); 181 287 } 182 288 183 289 :where(html[data-theme="dark"]) &#dark-button { 184 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; 185 301 } 186 302 } 187 303 } 188 304 } 189 305 } 190 306 191 - @keyframes spin { 192 - from { 307 + @keyframes wiggle { 308 + 0%, 309 + 100% { 193 310 transform: rotate(0deg); 194 311 } 195 - to { 196 - transform: rotate(360deg); 312 + 25% { 313 + transform: rotate(-15deg); 314 + } 315 + 75% { 316 + transform: rotate(15deg); 197 317 } 198 318 } 199 319 </style>
+21
src/util/store.ts
··· 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 + });