Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default [
},
rules: {
...astro.configs.recommended.rules,
...astro.configs["jsx-a11y-recommended"].rules,
...astro.configs["jsx-a11y-strict"].rules,
"no-undef": "off", // TypeScript handles this
},
},
Expand Down
8 changes: 5 additions & 3 deletions src/components/BackToPrev.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ type Props = {
const { href } = Astro.props;
---

<a href={href} class="relative group w-fit flex pl-7 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
<a id="back-to-prev" href={href} class="relative group w-fit flex pl-7 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current">
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
Expand Down
8 changes: 5 additions & 3 deletions src/components/BackToTop.astro
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<button id="back-to-top" class="relative group w-fit flex pl-8 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
<button id="back-to-top" type="button" class="relative group w-fit flex pl-8 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current rotate-90">
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
Expand Down
11 changes: 10 additions & 1 deletion src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import BackToTop from "@components/BackToTop.astro";
<div>
&copy; 2024 {`|`} {SITE.NAME}
</div>
<div class="flex flex-wrap gap-1 items-center">
<div class="flex flex-wrap gap-1 items-center" role="group" aria-label="Theme selection">
<button
id="light-theme-button"
type="button"
aria-label="Light theme"
aria-pressed="false"
data-theme-button="light"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
Expand Down Expand Up @@ -46,7 +49,10 @@ import BackToTop from "@components/BackToTop.astro";
</button>
<button
id="dark-theme-button"
type="button"
aria-label="Dark theme"
aria-pressed="false"
data-theme-button="dark"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
Expand All @@ -66,7 +72,10 @@ import BackToTop from "@components/BackToTop.astro";
</button>
<button
id="system-theme-button"
type="button"
aria-label="System theme"
aria-pressed="false"
data-theme-button="system"
class="group size-8 flex items-center justify-center rounded-full"
>
<svg
Expand Down
121 changes: 82 additions & 39 deletions src/components/Head.astro
Original file line number Diff line number Diff line change
Expand Up @@ -99,43 +99,40 @@ const defaultOgData: OpenGraphData = ogData || {
</script>

<script is:inline>
const colorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)");

function init() {
preloadTheme();
onScroll();
animate();

const backToTop = document.getElementById("back-to-top");
backToTop?.addEventListener("click", (event) => scrollToTop(event));
backToTop?.removeEventListener("click", handleScrollToTop);
backToTop?.addEventListener("click", handleScrollToTop);

const backToPrev = document.getElementById("back-to-prev");
backToPrev?.addEventListener("click", () => window.history.back());

const lightThemeButton = document.getElementById("light-theme-button");
lightThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "light");
toggleTheme(false);
});

const darkThemeButton = document.getElementById("dark-theme-button");
darkThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "dark");
toggleTheme(true);
});

const systemThemeButton = document.getElementById("system-theme-button");
systemThemeButton?.addEventListener("click", () => {
localStorage.setItem("theme", "system");
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
});
backToPrev?.removeEventListener("click", handleBackToPrev);
backToPrev?.addEventListener("click", handleBackToPrev);

const skipLink = document.querySelector(".skip-link");
skipLink?.removeEventListener("click", focusMainContent);
skipLink?.addEventListener("click", focusMainContent);

document
.querySelectorAll("[data-theme-button]")
.forEach((button) => {
if (button.dataset.initialized === "true") return;
button.dataset.initialized = "true";
button.addEventListener("click", () => {
const preference = button.getAttribute("data-theme-button");
if (preference) {
setThemePreference(preference);
}
});
});

window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", event => {
if (localStorage.theme === "system" || !localStorage.theme) {
toggleTheme(event.matches);
}
}
);
updateThemeButtons(getThemePreference());

document.removeEventListener("scroll", onScroll);
document.addEventListener("scroll", onScroll);
}

Expand All @@ -157,15 +154,31 @@ const defaultOgData: OpenGraphData = ogData || {
}
}

function scrollToTop(event) {
function handleScrollToTop(event) {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth"
});
}

function toggleTheme(dark) {
function handleBackToPrev(event) {
if (window.history.length > 1) {
event.preventDefault();
window.history.back();
}
}

function focusMainContent() {
requestAnimationFrame(() => {
const main = document.getElementById("main-content");
if (main) {
main.focus();
}
});
}

function toggleTheme(dark) {
const css = document.createElement("style");

css.appendChild(
Expand All @@ -189,21 +202,51 @@ function toggleTheme(dark) {
document.documentElement.classList.remove("dark");
}

window.getComputedStyle(css).opacity;
window.getComputedStyle(css).opacity;
document.head.removeChild(css);
}

function getThemePreference() {
const stored = localStorage.getItem("theme");
return stored === "light" || stored === "dark" || stored === "system"
? stored
: "system";
}

function resolveDarkMode(preference) {
if (preference === "dark") return true;
if (preference === "light") return false;
return colorSchemeMedia.matches;
}

function updateThemeButtons(activePreference) {
document
.querySelectorAll("[data-theme-button]")
.forEach((button) => {
const value = button.getAttribute("data-theme-button");
button.setAttribute("aria-pressed", value === activePreference ? "true" : "false");
});
}

function setThemePreference(preference) {
localStorage.setItem("theme", preference);
toggleTheme(resolveDarkMode(preference));
updateThemeButtons(preference);
}

function preloadTheme() {
const userTheme = localStorage.theme;
const preference = getThemePreference();
toggleTheme(resolveDarkMode(preference));
}

if (userTheme === "light" || userTheme === "dark") {
toggleTheme(userTheme === "dark");
} else {
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
colorSchemeMedia.addEventListener("change", (event) => {
if (getThemePreference() === "system") {
toggleTheme(event.matches);
updateThemeButtons("system");
}
}
});

document.addEventListener("DOMContentLoaded", () => init());
document.addEventListener("astro:after-swap", () => init());
document.addEventListener("DOMContentLoaded", init);
document.addEventListener("astro:after-swap", init);
preloadTheme();
</script>
51 changes: 35 additions & 16 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import Container from "@components/Container.astro";
import Link from "@components/Link.astro";
import { SITE } from "@consts";

const currentPath = Astro.url?.pathname ?? "/";
---

<header>
Expand All @@ -12,22 +14,39 @@ import { SITE } from "@consts";
{SITE.NAME}
</div>
</Link>
<nav class="flex gap-1">
<Link href="/blog">
blog
</Link>
<span>
{`/`}
</span>
<Link href="/briefs">
briefs
</Link>
<span>
{`/`}
</span>
<Link href="/projects">
projects
</Link>
<nav aria-label="Primary" class="flex">
<ul class="flex items-center gap-3 list-none">
<li>
<Link
href="/blog"
aria-current={currentPath.startsWith("/blog") ? "page" : undefined}
>
blog
</Link>
</li>
<li aria-hidden="true" class="text-black/40 dark:text-white/40">
/
</li>
<li>
<Link
href="/briefs"
aria-current={currentPath.startsWith("/briefs") ? "page" : undefined}
>
briefs
</Link>
</li>
<li aria-hidden="true" class="text-black/40 dark:text-white/40">
/
</li>
<li>
<Link
href="/projects"
aria-current={currentPath.startsWith("/projects") ? "page" : undefined}
>
projects
</Link>
</li>
</ul>
</nav>
</div>
</Container>
Expand Down
3 changes: 2 additions & 1 deletion src/components/Link.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const { href, external, underline = true, ...rest } = Astro.props;

<a
href={href}
target={ external ? "_blank" : "_self" }
target={external ? "_blank" : undefined}
rel={external ? "noopener noreferrer" : undefined}
class={cn("inline-block decoration-black/15 dark:decoration-white/30 hover:decoration-black/25 hover:dark:decoration-white/50 text-current hover:text-black hover:dark:text-white transition-colors duration-300 ease-in-out", underline && "underline underline-offset-2")}
{...rest}>
<slot/>
Expand Down
3 changes: 2 additions & 1 deletion src/layouts/PageLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ const plainTitle = stripMarkdown(title);
<Head title={`${plainTitle} | ${SITE.NAME}`} description={description} ogData={ogData} />
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<Header />
<main>
<main id="main-content" tabindex="-1">
<slot />
</main>
<Footer />
Expand Down
30 changes: 30 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ body {
@apply text-black/50 dark:text-white/75;
}

.skip-link {
position: absolute;
left: 1rem;
top: -4rem;
z-index: 100;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: #f5f5f5;
color: #111827;
text-decoration: none;
transition: top 150ms ease-in-out;
}

.dark .skip-link {
background-color: #1f2937;
color: #f5f5f5;
}

.skip-link:focus,
.skip-link:hover {
top: 1rem;
outline: 2px solid #2563eb;
outline-offset: 2px;
}

header {
@apply fixed top-0 left-0 right-0 z-50 py-5;
@apply bg-stone-100/75 dark:bg-stone-900/25;
Expand All @@ -33,6 +58,11 @@ main {
@apply flex-1 py-32;
}

main:focus {
outline: 2px solid #2563eb;
outline-offset: 0.5rem;
}

footer {
@apply py-5 text-sm;
}
Expand Down