Skip to content
Draft
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: 2 additions & 0 deletions astro/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export default defineConfig({
'facebook', 'instagram', 'linkedin', 'rss-fill', 'tiktok', 'globe', 'mastodon', 'twitter',
// Descriptive
'gift-fill', 'pencil-fill', 'people-fill', 'person-fill', 'puzzle-fill', 'stopwatch-fill', 'tools',
// Theme toggle
'sun-fill', 'moon-fill', 'circle-half', 'check-lg',
],
// CoreUI Brands
cib: [
Expand Down
2 changes: 1 addition & 1 deletion astro/src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const breadCrumbs: Breadcrumbs =
</header>
<slot />

<style lang="scss">
<style is:global lang="scss">
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: Without this, the nav bar in the top header was not styled correctly and the menu items were not visible.

/* first declaration that is commented out below is for debugging purposes. */
// * {
// outline: 1px solid white;
Expand Down
206 changes: 206 additions & 0 deletions astro/src/components/ThemeToggle.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
---
import { Icon } from "astro-icon/components";

interface Props {
class?: string;
}

const { class: classes = "" } = Astro.props;
---

<div class:list={["theme-toggle dropdown", classes]}>
<button
type="button"
id="theme-toggle"
class="theme-toggle-btn btn btn-outline-primary display-65 dropdown-toggle"
data-bs-toggle="dropdown"
data-theme="system"
aria-expanded="false"
aria-label="Color theme: Auto. Activate to change."
>
<span class="theme-toggle-icon" data-theme-icon="light" aria-hidden="true">
<Icon name="bi:sun-fill" class="bi me-1" role="img" />
</span>
<span class="theme-toggle-icon" data-theme-icon="dark" aria-hidden="true">
<Icon name="bi:moon-fill" class="bi me-1" role="img" />
</span>
<span class="theme-toggle-icon" data-theme-icon="system" aria-hidden="true">
<Icon name="bi:circle-half" class="bi me-1" role="img" />
</span>
<span data-theme-label>Auto</span>
</button>
<ul class="dropdown-menu dropdown-menu-end px-15 pb-1" aria-labelledby="theme-toggle">
<li>
<button
type="button"
class="dropdown-item theme-option"
data-theme-option="light"
aria-pressed="false"
>
<Icon name="bi:sun-fill" class="bi me-2" role="img" aria-hidden="true" />
<span class="theme-option-label">Light</span>
<Icon name="bi:check-lg" class="bi ms-2 theme-option-check" role="img" aria-hidden="true" />
</button>
</li>
<li>
<button
type="button"
class="dropdown-item theme-option"
data-theme-option="dark"
aria-pressed="false"
>
<Icon name="bi:moon-fill" class="bi me-2" role="img" aria-hidden="true" />
<span class="theme-option-label">Dark</span>
<Icon name="bi:check-lg" class="bi ms-2 theme-option-check" role="img" aria-hidden="true" />
</button>
</li>
<li>
<button
type="button"
class="dropdown-item theme-option"
data-theme-option="system"
aria-pressed="false"
>
<Icon name="bi:circle-half" class="bi me-2" role="img" aria-hidden="true" />
<span class="theme-option-label">Auto</span>
<Icon name="bi:check-lg" class="bi ms-2 theme-option-check" role="img" aria-hidden="true" />
</button>
</li>
</ul>
</div>

<style is:global lang="scss">
@import "../styles/dark-mode.scss";

.theme-toggle .theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 7em;
}

.theme-toggle .theme-toggle-icon {
display: none;
}
.theme-toggle .theme-toggle-btn[data-theme="light"] [data-theme-icon="light"],
.theme-toggle .theme-toggle-btn[data-theme="dark"] [data-theme-icon="dark"],
.theme-toggle .theme-toggle-btn[data-theme="system"] [data-theme-icon="system"] {
display: inline-flex;
align-items: center;
}

.theme-toggle .theme-option {
display: flex;
align-items: center;
}

.theme-toggle .theme-option-label {
flex: 1 1 auto;
}

.theme-toggle .theme-option-check {
visibility: hidden;
}
.theme-toggle .theme-option[aria-pressed="true"] .theme-option-check {
visibility: visible;
}

@include color-mode(dark) {
.theme-toggle .theme-toggle-btn.btn-outline-primary {
--bs-btn-color: var(--bs-white);
--bs-btn-border-color: var(--bs-white);
--bs-btn-hover-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-white);
--bs-btn-hover-border-color: var(--bs-white);
--bs-btn-active-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-white);
--bs-btn-active-border-color: var(--bs-white);
}
}
</style>

<script>
type Mode = "light" | "dark" | "system";
const modes: Mode[] = ["light", "dark", "system"];
const labels: Record<Mode, string> = {
light: "Light",
dark: "Dark",
system: "Auto",
};

const retrieveTheme = (): Mode => {
try {
const stored = localStorage.getItem("theme");
return stored === "light" || stored === "dark" ? stored : "system";
} catch {
return "system";
}
};

const storeTheme = (mode: Mode) => {
try {
if (mode === "system") {
localStorage.removeItem("theme");
} else {
localStorage.setItem("theme", mode);
}
} catch {
// localStorage blocked — theme applies for this session but won't persist.
}
};

const init = () => {
const root = document.querySelector(".theme-toggle") as HTMLElement | null;
const button = document.getElementById(
"theme-toggle",
) as HTMLButtonElement | null;
if (!root || !button) return;

const applyTheme = (mode: Mode) => {
let themeToApply = mode;
if (themeToApply === "system") {
themeToApply = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
document.documentElement.setAttribute("data-bs-theme", themeToApply);
};

const updateButton = (mode: Mode) => {
const label = button.querySelector(
"[data-theme-label]",
) as HTMLElement | null;
button.setAttribute("data-theme", mode);
if (label) label.textContent = labels[mode];
button.setAttribute(
"aria-label",
`Color theme: ${labels[mode]}. Activate to change.`,
);
root.querySelectorAll<HTMLButtonElement>(".theme-option").forEach((el) => {
const isCurrent = el.dataset.themeOption === mode;
el.setAttribute("aria-pressed", isCurrent ? "true" : "false");
});
};

const apply = (mode: Mode) => {
storeTheme(mode);
applyTheme(mode);
updateButton(mode);
};

apply(retrieveTheme());

root.querySelectorAll<HTMLButtonElement>(".theme-option").forEach((el) => {
el.addEventListener("click", () => {
const next = el.dataset.themeOption as Mode | undefined;
if (next && modes.includes(next)) apply(next);
});
});
};

if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", init);
} else {
init();
}
</script>
19 changes: 12 additions & 7 deletions astro/src/components/navigation/NavSiteList.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
import NavDropdownMenu from "./NavDropdownMenu.astro";
import ThemeToggle from "../ThemeToggle.astro";

interface Props {
main?: boolean;
Expand Down Expand Up @@ -42,13 +43,17 @@ const { main = false, class: classes = "" } = Astro.props;
</ul>
{
main && (
<a
class="btn btn-info btn-contrast-dark display-65 mx-1 my-1"
href="/donate"
role="button"
>
Donate
</a>
<Fragment>
<ThemeToggle class="ms-1 me-4 my-1" />
<div class="w-100 d-lg-none" aria-hidden="true"></div>
<a
class="btn btn-info btn-contrast-dark display-65 mx-1 my-1"
href="/donate"
role="button"
>
Donate
</a>
</Fragment>
)
}
</Fragment>
Expand Down
33 changes: 33 additions & 0 deletions astro/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,39 @@ const { title, brandedTitle = false, crumbs, heading, metadata } = Astro.props;
<!doctype html>
<html lang="en">
<head>
<script is:inline>
// Set data-bs-theme before paint to avoid a flash of the wrong theme.
// Reads localStorage; falls back to OS preference when no stored choice
// or when localStorage is blocked.
(function () {
function getStoredTheme() {
try {
var stored = localStorage.getItem("theme");
return stored === "light" || stored === "dark" ? stored : "system";
} catch (_) {
return "system";
}
}

function applyTheme() {
var theme = getStoredTheme();
if (theme === "system") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
document.documentElement.setAttribute("data-bs-theme", theme);
}

applyTheme();

// While in "system" mode (no stored choice), follow live OS changes.
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", applyTheme);
})();
</script>

<script>
import { init } from '@plausible-analytics/tracker';
init({
Expand Down
2 changes: 1 addition & 1 deletion astro/src/styles/colors-and-themes.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@charset "UTF-8";

$color-mode-type: media-query;
$color-mode-type: data-attribute;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: See Bootstrap docs - Building with SASS for description

$ac-navy: #041058;

//*******************
Expand Down
5 changes: 3 additions & 2 deletions astro/src/styles/dark-mode.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// To be included in components and pages that need additional dark mode rules.
$color-mode-type: media-query;
$color-mode-type: data-attribute;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: See Bootstrap docs - Building with SASS for description


@import "bootstrap/scss/mixins/_color-mode.scss";

@import "bootstrap/scss/mixins/_color-mode.scss";