11---
2- import LogoIcon from " @layout/body/logo-icon.astro" ;
3- import ModeIcon from " @layout/body/mode-icon.astro" ;
2+ import " @styles/header.scss" ;
43
54import {
5+ headerId ,
66 logoTitleId ,
7+ menuButtonId ,
8+ menuTemplateId ,
9+ menuTitleId ,
710 modeButtonId ,
8- modeTitleId
11+ modeMoonId ,
12+ modeSunId ,
13+ modeTemplateId ,
14+ modeTitleId ,
15+ navId ,
16+ navListId
917} from " @layout/body/header.ids.ts" ;
1018---
1119
12- <header >
13- <a class =" logo no-decor" href =" /" aria-labelledby ={ logoTitleId } >
14- <LogoIcon >
15- <title id ={ logoTitleId } slot =" title" >mce.codes logo</title >
16- </LogoIcon >
20+ <header id ={ headerId } class =" header no-decor" >
21+ <a class =" logo" href =" /" aria-labelledby ={ logoTitleId } >
22+ <svg
23+ class =" logo-svg"
24+ width =" 124"
25+ height =" 104"
26+ viewBox =" 0 0 124 104"
27+ fill =" none"
28+ >
29+ <title id ={ logoTitleId } >mce.codes logo</title >
30+ <path
31+ d =" M121.5 104V4M25.232 94.156l91.924-91.924M59.164 92c-.5 2.5 0 4 3.5 3.874 2.5-.374 2.439-.799 2-3.874"
32+ ></path >
33+ <path d =" M6.768 2.232l91.924 91.924M2.5 104V4" ></path >
34+ </svg >
1735 </a >
18- <a class =" about" href =" /" >About</a >
19- <a class =" projects" href =" /projects" >Projects</a >
20- <button
21- id ={ modeButtonId }
22- class =" mode no-button"
23- aria-labelledby ={ modeTitleId }
24- >
25- <ModeIcon >
26- <title id ={ modeTitleId } slot =" title" ></title >
27- </ModeIcon >
28- </button >
36+ <nav id ={ navId } >
37+ <template id ={ menuTemplateId } >
38+ <button
39+ id ={ menuButtonId }
40+ class =" menu no-button"
41+ aria-labelledby ={ menuTitleId }
42+ >
43+ <svg
44+ class =" menu-svg"
45+ width =" 39"
46+ height =" 34"
47+ viewBox =" 0 0 39 34"
48+ fill =" none"
49+ >
50+ <title id ={ menuTitleId } >Menu</title >
51+ <path
52+ d =" M25.864 6.737H2m35 12.632H2M37 32H2m35-20.526l-4.773-4.737L37 2"
53+ ></path >
54+ </svg >
55+ </button >
56+ </template >
57+ <ul id ={ navListId } class =" nav-list" >
58+ <li class =" nav-item" ><a href =" /" >About</a ></li >
59+ <li class =" nav-item" ><a href =" /projects" >Projects</a ></li >
60+ <li class =" nav-item" ><a href =" /notes" >Notes</a ></li >
61+ </ul >
62+ </nav >
63+ <template id ={ modeTemplateId } >
64+ <button
65+ id ={ modeButtonId }
66+ class =" mode no-button"
67+ aria-labelledby ={ modeTitleId }
68+ >
69+ <svg
70+ class =" mode-svg"
71+ width =" 56"
72+ height =" 56"
73+ viewBox =" 0 0 56 56"
74+ fill =" none"
75+ >
76+ <title id ={ modeTitleId } ></title >
77+ <g id ={ modeMoonId } class =" mode-moon" >
78+ <path
79+ d =" M3 28c0 13.8 11.2 25 25 25 13.64-.012 25-12 25-24-4 2-16 6-24-2S25 7 27 3C15 3 3 14.36 3 28z"
80+ ></path >
81+ </g >
82+ <g id ={ modeSunId } class =" mode-sun" >
83+ <path
84+ d =" M28 40.037A12.04 12.04 0 0 0 40.037 28 12.04 12.04 0 0 0 28 15.963 12.04 12.04 0 0 0 15.963 28 12.04 12.04 0 0 0 28 40.037z"
85+ ></path >
86+ <path
87+ d =" M8.556 47.444l1.852-1.852m35.185 0l1.852 1.852m-1.852-37.037l1.852-1.852m-37.037 1.852L8.556 8.556M6.704 28H3m50 0h-3.704M28 49.296V53m0-50v3.704"
88+ ></path >
89+ </g >
90+ </svg >
91+ </button >
92+ </template >
2993</header >
3094
31- <style lang =" scss" >
32- header {
33- display: grid;
34- grid-template-areas: "mode mode mode" "about logo projects";
35- grid-template-columns: repeat(3, 1fr);
36- row-gap: v-size(2);
37- margin-bottom: v-size(10);
38- }
39- a {
40- place-self: center;
41- }
42- .logo {
43- grid-area: logo;
44- }
45- .about {
46- grid-area: about;
47- }
48- .projects {
49- grid-area: projects;
50- }
51- .mode {
52- grid-area: mode;
53- width: fit-content;
54- }
95+ <script >
96+ import {
97+ headerId,
98+ menuButtonId,
99+ menuTemplateId,
100+ modeButtonId,
101+ modeMoonId,
102+ modeSunId,
103+ modeTemplateId,
104+ modeTitleId,
105+ navId,
106+ navListId
107+ } from "@layout/body/header.ids.ts";
108+ import { colors } from "@styles/data/colors.json";
55109
56- @include no-js {
57- .mode {
58- display: none;
59- }
60- }
110+ const header = document.getElementById(headerId) as HTMLElement;
61111
62- @include m-small {
63- header {
64- grid-template-areas: "logo about projects mode";
65- grid-template-columns: repeat(4, auto);
66- column-gap: v-size(5);
67- }
68- a,
69- button {
70- align-self: center;
71- }
72- .logo,
73- .projects {
74- justify-self: start;
75- }
76- .about,
77- .mode {
78- justify-self: end;
79- }
112+ header.append(
113+ (document.getElementById(modeTemplateId) as HTMLTemplateElement).content
114+ );
115+ (document.getElementById(navId) as HTMLElement).prepend(
116+ (document.getElementById(menuTemplateId) as HTMLTemplateElement).content
117+ );
80118
81- @include no-js {
82- header {
83- grid-template-areas: "logo about projects";
84- grid-template-columns: 1fr auto auto;
85- }
86- .projects {
87- justify-self: end;
88- }
89- }
90- }
91- </style >
119+ const navList = document.getElementById(navListId) as HTMLUListElement;
120+ const menuButton = document.getElementById(menuButtonId) as HTMLButtonElement;
121+ const modeButton = document.getElementById(modeButtonId) as HTMLButtonElement;
92122
93- <script >
94- import { modeButtonId, modeTitleId } from "@layout/body/header.ids.ts";
95- import { modeChange } from "@lib/events.ts";
96- import { colors } from "@styles/data/colors.json";
123+ menuButton.addEventListener("click", () => {
124+ document.body.classList.toggle("open-nav");
125+ header.classList.toggle("open-nav");
126+ navList.classList.toggle("open-nav");
127+ modeButton.classList.toggle("open-nav");
128+ });
129+
130+ type Mode = "light" | "dark";
97131
98- const rootStyle = document.documentElement.style;
99- const modeButton = document.getElementById(modeButtonId) as HTMLButtonElement;
100- const modeTitle = document.getElementById(modeTitleId) as HTMLTitleElement;
101132 const darkModeMedia = matchMedia("(prefers-color-scheme: dark)");
102- const storedMode = localStorage.getItem("mode");
133+ const rootStyle = document.documentElement.style;
134+ const modeTitle = document.getElementById(modeTitleId) as HTMLTitleElement & {
135+ textContent: "Light mode" | "Dark mode" | null;
136+ };
137+ const modeMoon = document.getElementById(
138+ modeMoonId
139+ ) as unknown as SVGGElement;
140+ const modeSun = document.getElementById(modeSunId) as unknown as SVGGElement;
103141
104142 function setModeTitleBasedOnMedia() {
105143 if (darkModeMedia.matches) {
@@ -110,7 +148,7 @@ import {
110148 modeTitle.textContent = "Dark mode";
111149 }
112150
113- function setMode(mode: string ) {
151+ function setMode(mode: Mode, animateModeSvg = true ) {
114152 // Once the user has set a mode preference, follow that instead of the
115153 // system default.
116154 darkModeMedia.removeEventListener("change", setModeTitleBasedOnMedia);
@@ -121,8 +159,12 @@ import {
121159 rootStyle.setProperty("--c-background", colors.white);
122160 rootStyle.setProperty("--c-accent", colors.grayDark);
123161
124- modeTitle.textContent = "Dark mode";
162+ if (animateModeSvg) {
163+ modeMoon.classList.add("rotate-appear");
164+ modeSun.classList.add("rotate-vanish");
165+ }
125166
167+ modeTitle.textContent = "Dark mode";
126168 localStorage.setItem("mode", "light");
127169 break;
128170 }
@@ -131,26 +173,62 @@ import {
131173 rootStyle.setProperty("--c-background", colors.black);
132174 rootStyle.setProperty("--c-accent", colors.grayLight);
133175
134- modeTitle.textContent = "Light mode";
176+ if (animateModeSvg) {
177+ modeMoon.classList.add("rotate-vanish");
178+ modeSun.classList.add("rotate-appear");
179+ }
135180
181+ modeTitle.textContent = "Light mode";
136182 localStorage.setItem("mode", "dark");
137183 break;
138184 }
139185 }
186+ }
187+
188+ function resetModeSvgState(mode: Mode) {
189+ switch (mode) {
190+ case "light": {
191+ modeMoon.style.opacity = "1";
192+ modeSun.style.opacity = "0";
193+ break;
194+ }
195+ case "dark": {
196+ modeMoon.style.opacity = "0";
197+ modeSun.style.opacity = "1";
198+ break;
199+ }
200+ }
140201
141- modeChange.dispatch(mode);
202+ modeMoon.classList.remove("rotate-appear");
203+ modeMoon.classList.remove("rotate-vanish");
204+ modeSun.classList.remove("rotate-appear");
205+ modeSun.classList.remove("rotate-vanish");
142206 }
143207
208+ darkModeMedia.addEventListener("change", setModeTitleBasedOnMedia);
144209 modeButton.addEventListener("click", () => {
145210 setMode(modeTitle.textContent === "Light mode" ? "light" : "dark");
146211 });
147- darkModeMedia.addEventListener("change", setModeTitleBasedOnMedia);
212+ // Mode moon and sun animations end at the same time so put 'animationend'
213+ // handling on only one of them.
214+ modeMoon.addEventListener("animationend", () => {
215+ const storedMode = localStorage.getItem("mode") as Mode | undefined;
216+
217+ if (typeof storedMode !== "undefined") {
218+ resetModeSvgState(storedMode);
219+ }
220+ });
148221
149222 addEventListener("DOMContentLoaded", () => {
150- if (typeof storedMode === "string ") {
151- setMode(storedMode);
152- } else {
223+ const storedMode = localStorage.getItem("mode ") as Mode | undefined;
224+
225+ if (typeof storedMode === "undefined") {
153226 setModeTitleBasedOnMedia();
227+ return;
154228 }
229+
230+ // Do not animate mode SVG on the first run, it is already in the correct
231+ // state.
232+ setMode(storedMode, false);
155233 });
156234</script >
0 commit comments