Skip to content

Commit af3ad9e

Browse files
committed
feat(theme): replace segmented picker with pill toggle and browser-default first load
1 parent 9d210b1 commit af3ad9e

4 files changed

Lines changed: 234 additions & 113 deletions

File tree

_includes/head_custom.html

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script>
22
(function () {
33
var STORAGE_KEY = "feast-theme-preference";
4-
var PREF_SYSTEM = "system";
54
var PREF_LIGHT = "light";
65
var PREF_DARK = "dark";
76
var DEFAULT_THEME = "default";
@@ -14,39 +13,44 @@
1413
if (value === PREF_LIGHT || value === PREF_DARK) {
1514
return value;
1615
}
17-
return PREF_SYSTEM;
16+
return null;
1817
}
1918

2019
function getStoredPreference() {
2120
try {
22-
return window.localStorage.getItem(STORAGE_KEY);
21+
return normalizePreference(window.localStorage.getItem(STORAGE_KEY));
2322
} catch (error) {
2423
return null;
2524
}
2625
}
2726

27+
function clearStoredPreference() {
28+
try {
29+
window.localStorage.removeItem(STORAGE_KEY);
30+
} catch (error) {
31+
// Ignore storage failures (private browsing, blocked storage, etc.)
32+
}
33+
}
34+
2835
function setStoredPreference(preference) {
2936
try {
30-
if (preference === PREF_SYSTEM) {
31-
window.localStorage.removeItem(STORAGE_KEY);
32-
} else {
33-
window.localStorage.setItem(STORAGE_KEY, preference);
34-
}
37+
window.localStorage.setItem(STORAGE_KEY, preference);
3538
} catch (error) {
3639
// Ignore storage failures (private browsing, blocked storage, etc.)
3740
}
3841
}
3942

43+
function getSystemPreference() {
44+
if (mediaQuery && mediaQuery.matches) {
45+
return PREF_DARK;
46+
}
47+
return PREF_LIGHT;
48+
}
49+
4050
function resolveTheme(preference) {
4151
if (preference === PREF_DARK) {
4252
return DARK_THEME;
4353
}
44-
if (preference === PREF_LIGHT) {
45-
return DEFAULT_THEME;
46-
}
47-
if (mediaQuery && mediaQuery.matches) {
48-
return DARK_THEME;
49-
}
5054
return DEFAULT_THEME;
5155
}
5256

@@ -58,7 +62,11 @@
5862

5963
function notifyPreferenceApplied() {
6064
if (typeof feastTheme.onPreferenceApplied === "function") {
61-
feastTheme.onPreferenceApplied(currentPreference, resolveTheme(currentPreference));
65+
feastTheme.onPreferenceApplied(
66+
currentPreference,
67+
resolveTheme(currentPreference),
68+
hasUserPreference
69+
);
6270
}
6371
}
6472

@@ -68,26 +76,59 @@
6876
}
6977

7078
function handleSystemThemeChange() {
71-
if (currentPreference === PREF_SYSTEM) {
79+
if (!hasUserPreference) {
80+
currentPreference = getSystemPreference();
7281
applyCurrentPreference();
7382
}
7483
}
7584

76-
var currentPreference = normalizePreference(getStoredPreference());
85+
var storedPreference = getStoredPreference();
86+
var hasUserPreference = storedPreference !== null;
87+
var currentPreference = hasUserPreference
88+
? storedPreference
89+
: getSystemPreference();
7790

7891
feastTheme.getPreference = function () {
7992
return currentPreference;
8093
};
8194

95+
feastTheme.hasUserPreference = function () {
96+
return hasUserPreference;
97+
};
98+
8299
feastTheme.setPreference = function (preference) {
83-
currentPreference = normalizePreference(preference);
100+
var normalizedPreference = normalizePreference(preference);
101+
if (!normalizedPreference) {
102+
return currentPreference;
103+
}
104+
currentPreference = normalizedPreference;
105+
hasUserPreference = true;
84106
setStoredPreference(currentPreference);
85107
applyCurrentPreference();
86108
return currentPreference;
87109
};
88110

111+
feastTheme.togglePreference = function () {
112+
if (currentPreference === PREF_DARK) {
113+
return feastTheme.setPreference(PREF_LIGHT);
114+
}
115+
return feastTheme.setPreference(PREF_DARK);
116+
};
117+
118+
feastTheme.clearPreference = function () {
119+
hasUserPreference = false;
120+
clearStoredPreference();
121+
currentPreference = getSystemPreference();
122+
applyCurrentPreference();
123+
return currentPreference;
124+
};
125+
89126
feastTheme.resolveTheme = function (preference) {
90-
return resolveTheme(normalizePreference(preference));
127+
var normalizedPreference = normalizePreference(preference);
128+
if (!normalizedPreference) {
129+
normalizedPreference = getSystemPreference();
130+
}
131+
return resolveTheme(normalizedPreference);
91132
};
92133

93134
window.feastTheme = feastTheme;

_includes/header_custom.html

Lines changed: 68 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,91 @@
1-
<div id="feast-theme-switcher" class="feast-theme-switcher" role="group" aria-label="Theme mode switcher">
1+
<div id="feast-theme-switcher" class="feast-theme-switcher">
22
<button
3+
id="feast-theme-toggle"
34
type="button"
4-
class="feast-theme-switcher__button js-feast-theme-option"
5-
data-theme-option="system"
6-
aria-pressed="false"
7-
aria-label="Use system theme setting"
8-
title="System"
5+
class="feast-theme-switcher__toggle"
6+
role="switch"
7+
aria-checked="false"
8+
aria-label="Switch to dark theme"
9+
title="Switch to dark theme"
910
>
10-
Auto
11-
</button>
12-
<button
13-
type="button"
14-
class="feast-theme-switcher__button js-feast-theme-option"
15-
data-theme-option="light"
16-
aria-pressed="false"
17-
aria-label="Use light theme"
18-
title="Light"
19-
>
20-
Light
21-
</button>
22-
<button
23-
type="button"
24-
class="feast-theme-switcher__button js-feast-theme-option"
25-
data-theme-option="dark"
26-
aria-pressed="false"
27-
aria-label="Use dark theme"
28-
title="Dark"
29-
>
30-
Dark
11+
<span class="feast-theme-switcher__glyph feast-theme-switcher__glyph--sun" aria-hidden="true">
12+
<svg viewBox="0 0 24 24" fill="none">
13+
<circle cx="12" cy="12" r="4"></circle>
14+
<path d="M12 2V4"></path>
15+
<path d="M12 20V22"></path>
16+
<path d="M4.93 4.93L6.34 6.34"></path>
17+
<path d="M17.66 17.66L19.07 19.07"></path>
18+
<path d="M2 12H4"></path>
19+
<path d="M20 12H22"></path>
20+
<path d="M4.93 19.07L6.34 17.66"></path>
21+
<path d="M17.66 6.34L19.07 4.93"></path>
22+
</svg>
23+
</span>
24+
<span class="feast-theme-switcher__glyph feast-theme-switcher__glyph--moon" aria-hidden="true">
25+
<svg viewBox="0 0 24 24" fill="none">
26+
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
27+
</svg>
28+
</span>
29+
<span class="feast-theme-switcher__thumb" aria-hidden="true">
30+
<svg class="feast-theme-switcher__thumb-icon feast-theme-switcher__thumb-icon--sun" viewBox="0 0 24 24" fill="none">
31+
<circle cx="12" cy="12" r="4"></circle>
32+
<path d="M12 2V4"></path>
33+
<path d="M12 20V22"></path>
34+
<path d="M4.93 4.93L6.34 6.34"></path>
35+
<path d="M17.66 17.66L19.07 19.07"></path>
36+
<path d="M2 12H4"></path>
37+
<path d="M20 12H22"></path>
38+
<path d="M4.93 19.07L6.34 17.66"></path>
39+
<path d="M17.66 6.34L19.07 4.93"></path>
40+
</svg>
41+
<svg class="feast-theme-switcher__thumb-icon feast-theme-switcher__thumb-icon--moon" viewBox="0 0 24 24" fill="none">
42+
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"></path>
43+
</svg>
44+
</span>
3145
</button>
3246
</div>
3347

3448
<script>
3549
(function () {
36-
var switcher = document.getElementById("feast-theme-switcher");
37-
if (!switcher || !window.feastTheme || typeof window.feastTheme.getPreference !== "function") {
50+
var toggle = document.getElementById("feast-theme-toggle");
51+
if (
52+
!toggle ||
53+
!window.feastTheme ||
54+
typeof window.feastTheme.getPreference !== "function" ||
55+
typeof window.feastTheme.togglePreference !== "function"
56+
) {
3857
return;
3958
}
4059

41-
var buttons = switcher.querySelectorAll(".js-feast-theme-option");
42-
43-
function setButtonState(button, isActive) {
44-
button.setAttribute("aria-pressed", isActive ? "true" : "false");
45-
if (button.classList && typeof button.classList.toggle === "function") {
46-
button.classList.toggle("is-active", isActive);
60+
function syncToggleState() {
61+
var isDark = window.feastTheme.getPreference() === "dark";
62+
toggle.setAttribute("aria-checked", isDark ? "true" : "false");
63+
if (isDark) {
64+
toggle.setAttribute("aria-label", "Switch to light theme");
65+
toggle.setAttribute("title", "Switch to light theme");
66+
} else {
67+
toggle.setAttribute("aria-label", "Switch to dark theme");
68+
toggle.setAttribute("title", "Switch to dark theme");
4769
}
48-
}
49-
50-
function syncSwitcherState() {
51-
var activePreference = window.feastTheme.getPreference();
52-
for (var index = 0; index < buttons.length; index += 1) {
53-
var button = buttons[index];
54-
var themeOption = button.getAttribute("data-theme-option");
55-
setButtonState(button, themeOption === activePreference);
70+
if (toggle.classList && typeof toggle.classList.toggle === "function") {
71+
toggle.classList.toggle("is-dark", isDark);
5672
}
5773
}
5874

59-
function bindClick(button) {
75+
function bindToggleClick() {
6076
var handler = function () {
61-
var preference = button.getAttribute("data-theme-option");
62-
window.feastTheme.setPreference(preference);
63-
syncSwitcherState();
77+
window.feastTheme.togglePreference();
78+
syncToggleState();
6479
};
65-
6680
if (window.jtd && typeof window.jtd.addEvent === "function") {
67-
window.jtd.addEvent(button, "click", handler);
68-
} else if (button.addEventListener) {
69-
button.addEventListener("click", handler);
81+
window.jtd.addEvent(toggle, "click", handler);
82+
} else if (toggle.addEventListener) {
83+
toggle.addEventListener("click", handler);
7084
}
7185
}
7286

73-
for (var index = 0; index < buttons.length; index += 1) {
74-
bindClick(buttons[index]);
75-
}
76-
77-
window.feastTheme.onPreferenceApplied = syncSwitcherState;
78-
syncSwitcherState();
87+
bindToggleClick();
88+
window.feastTheme.onPreferenceApplied = syncToggleState;
89+
syncToggleState();
7990
})();
8091
</script>

_pages/site-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ These are managed via `_data/linked_projects.json` and automatically generate re
4949
- Growth greens (automation/nature)
5050
- Neutral grays (surfaces)
5151

52-
**Theme behavior**: A custom header switcher (`_includes/header_custom.html`) supports `System`, `Light`, and `Dark` modes. The site defaults to browser preference when no user override is saved and persists user selections in local storage via `_includes/head_custom.html`.
52+
**Theme behavior**: A custom header pill toggle (`_includes/header_custom.html`) starts from browser preference on first visit and then lets users switch between light and dark mode with a persisted preference via `_includes/head_custom.html`.
5353

5454
## Build Pipeline
5555

0 commit comments

Comments
 (0)