code quality changes
Build and Push Docker Image / build-and-push (pull_request) Successful in 1m3s
Python Code Quality / python-code-quality (pull_request) Successful in 10s
Python Test / python-test (pull_request) Successful in 17s

This commit is contained in:
Brian Bjarke Jensen
2026-01-27 21:21:55 +01:00
parent b2aa71b95b
commit ec0e1f6140
11 changed files with 664 additions and 506 deletions
+2 -1
View File
@@ -31,6 +31,7 @@ def readiness_check() -> dict[str, str]:
if redis_uri: if redis_uri:
try: try:
import redis import redis
redis_client = redis.from_url(redis_uri) redis_client = redis.from_url(redis_uri)
redis_client.ping() redis_client.ping()
except Exception as exc: except Exception as exc:
@@ -51,6 +52,7 @@ def readiness_check() -> dict[str, str]:
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy import text from sqlalchemy import text
engine = create_engine(postgres_uri) engine = create_engine(postgres_uri)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
with SessionLocal() as db: with SessionLocal() as db:
@@ -105,7 +107,6 @@ def service_info() -> dict:
if not features["postgresql"]: if not features["postgresql"]:
config["data_dir"] = os.getenv("DATA_DIR", "/data") config["data_dir"] = os.getenv("DATA_DIR", "/data")
return { return {
"service": "baby-monitor", "service": "baby-monitor",
"version": "0.1.0", "version": "0.1.0",
+66 -62
View File
@@ -4,70 +4,74 @@
*/ */
(function () { (function () {
'use strict'; "use strict";
// Helper to get effective theme // Helper to get effective theme
function getEffectiveTheme() { function getEffectiveTheme() {
return window.getEffectiveTheme ? window.getEffectiveTheme() : 'light'; return window.getEffectiveTheme ? window.getEffectiveTheme() : "light";
} }
// Get theme-appropriate colors for Chart.js // Get theme-appropriate colors for Chart.js
window.getChartColors = function () { window.getChartColors = function () {
const isDark = getEffectiveTheme() === 'dark'; const isDark = getEffectiveTheme() === "dark";
return { return {
// Grid lines // Grid lines
grid: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)', grid: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
gridMajor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)', gridMajor: isDark ? "rgba(255, 255, 255, 0.15)" : "rgba(0, 0, 0, 0.1)",
// Text colors // Text colors
text: isDark ? '#b0b0b0' : '#666666', text: isDark ? "#b0b0b0" : "#666666",
textPrimary: isDark ? '#e0e0e0' : '#333333', textPrimary: isDark ? "#e0e0e0" : "#333333",
// Data series colors (optimized for background) // Data series colors (optimized for background)
series: isDark ? [ series: isDark
'rgba(139, 158, 255, 0.8)', // Brighter purple ? [
'rgba(110, 197, 255, 0.8)', // Brighter blue "rgba(139, 158, 255, 0.8)", // Brighter purple
'rgba(255, 179, 255, 0.8)', // Brighter pink "rgba(110, 197, 255, 0.8)", // Brighter blue
'rgba(129, 199, 132, 0.8)', // Brighter green "rgba(255, 179, 255, 0.8)", // Brighter pink
'rgba(255, 183, 77, 0.8)', // Brighter orange "rgba(129, 199, 132, 0.8)", // Brighter green
'rgba(156, 107, 184, 0.8)', // Brighter deep purple "rgba(255, 183, 77, 0.8)", // Brighter orange
'rgba(77, 255, 255, 0.8)', // Brighter cyan "rgba(156, 107, 184, 0.8)", // Brighter deep purple
'rgba(255, 138, 155, 0.8)', // Brighter red "rgba(77, 255, 255, 0.8)", // Brighter cyan
] : [ "rgba(255, 138, 155, 0.8)", // Brighter red
'rgba(102, 126, 234, 0.8)', // Purple ]
'rgba(118, 75, 162, 0.8)', // Deep purple : [
'rgba(255, 159, 64, 0.8)', // Orange "rgba(102, 126, 234, 0.8)", // Purple
'rgba(76, 175, 80, 0.8)', // Green "rgba(118, 75, 162, 0.8)", // Deep purple
'rgba(244, 67, 54, 0.8)', // Red "rgba(255, 159, 64, 0.8)", // Orange
'rgba(156, 39, 176, 0.8)', // Violet "rgba(76, 175, 80, 0.8)", // Green
'rgba(102, 126, 234, 0.6)', // Light purple "rgba(244, 67, 54, 0.8)", // Red
'rgba(118, 75, 162, 0.6)', // Light deep purple "rgba(156, 39, 176, 0.8)", // Violet
"rgba(102, 126, 234, 0.6)", // Light purple
"rgba(118, 75, 162, 0.6)", // Light deep purple
], ],
// Solid data colors (for pie charts, etc.) // Solid data colors (for pie charts, etc.)
solidSeries: isDark ? [ solidSeries: isDark
'#8b9eff', ? [
'#6ec5ff', "#8b9eff",
'#ffb3ff', "#6ec5ff",
'#81c784', "#ffb3ff",
'#ffb74d', "#81c784",
'#9c6bb8', "#ffb74d",
'#4dffff', "#9c6bb8",
'#ff8a9b', "#4dffff",
] : [ "#ff8a9b",
'#667eea', ]
'#764ba2', : [
'#ff9f40', "#667eea",
'#4caf50', "#764ba2",
'#f44336', "#ff9f40",
'#9c27b0', "#4caf50",
'#667eea', "#f44336",
'#764ba2', "#9c27b0",
"#667eea",
"#764ba2",
], ],
// Background colors // Background colors
background: isDark ? '#2d2d2d' : '#f8f9fa', background: isDark ? "#2d2d2d" : "#f8f9fa",
}; };
}; };
@@ -83,19 +87,20 @@
labels: { labels: {
color: colors.text, color: colors.text,
font: { font: {
size: 12 size: 12,
} },
} },
}, },
tooltip: { tooltip: {
backgroundColor: getEffectiveTheme() === 'dark' ? '#3a3a3a' : '#ffffff', backgroundColor:
getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff",
titleColor: colors.textPrimary, titleColor: colors.textPrimary,
bodyColor: colors.text, bodyColor: colors.text,
borderColor: colors.grid, borderColor: colors.grid,
borderWidth: 1, borderWidth: 1,
}
}, },
scales: {} },
scales: {},
}; };
// Deep merge customOptions with defaults // Deep merge customOptions with defaults
@@ -103,7 +108,7 @@
}; };
// Get theme-appropriate scale options // Get theme-appropriate scale options
window.getChartScaleOptions = function(axis = 'x', customOptions = {}) { window.getChartScaleOptions = function (axis = "x", customOptions = {}) {
const colors = window.getChartColors(); const colors = window.getChartColors();
const defaults = { const defaults = {
@@ -114,16 +119,16 @@
ticks: { ticks: {
color: colors.text, color: colors.text,
font: { font: {
size: 12 size: 12,
} },
}, },
title: { title: {
color: colors.textPrimary, color: colors.textPrimary,
font: { font: {
size: 13, size: 13,
weight: '600' weight: "600",
} },
} },
}; };
return mergeDeep(defaults, customOptions); return mergeDeep(defaults, customOptions);
@@ -133,7 +138,7 @@
function mergeDeep(target, source) { function mergeDeep(target, source) {
const output = Object.assign({}, target); const output = Object.assign({}, target);
if (isObject(target) && isObject(source)) { if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => { Object.keys(source).forEach((key) => {
if (isObject(source[key])) { if (isObject(source[key])) {
if (!(key in target)) { if (!(key in target)) {
Object.assign(output, { [key]: source[key] }); Object.assign(output, { [key]: source[key] });
@@ -149,7 +154,7 @@
} }
function isObject(item) { function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item); return item && typeof item === "object" && !Array.isArray(item);
} }
// Function to update all charts on theme change // Function to update all charts on theme change
@@ -160,12 +165,12 @@
const colors = window.getChartColors(); const colors = window.getChartColors();
chartInstances.forEach(chart => { chartInstances.forEach((chart) => {
if (!chart) return; if (!chart) return;
// Update grid colors // Update grid colors
if (chart.options.scales) { if (chart.options.scales) {
Object.keys(chart.options.scales).forEach(axis => { Object.keys(chart.options.scales).forEach((axis) => {
if (chart.options.scales[axis].grid) { if (chart.options.scales[axis].grid) {
chart.options.scales[axis].grid.color = colors.grid; chart.options.scales[axis].grid.color = colors.grid;
chart.options.scales[axis].grid.borderColor = colors.gridMajor; chart.options.scales[axis].grid.borderColor = colors.gridMajor;
@@ -189,7 +194,7 @@
// Update tooltip colors // Update tooltip colors
if (chart.options.plugins && chart.options.plugins.tooltip) { if (chart.options.plugins && chart.options.plugins.tooltip) {
chart.options.plugins.tooltip.backgroundColor = chart.options.plugins.tooltip.backgroundColor =
getEffectiveTheme() === 'dark' ? '#3a3a3a' : '#ffffff'; getEffectiveTheme() === "dark" ? "#3a3a3a" : "#ffffff";
chart.options.plugins.tooltip.titleColor = colors.textPrimary; chart.options.plugins.tooltip.titleColor = colors.textPrimary;
chart.options.plugins.tooltip.bodyColor = colors.text; chart.options.plugins.tooltip.bodyColor = colors.text;
chart.options.plugins.tooltip.borderColor = colors.grid; chart.options.plugins.tooltip.borderColor = colors.grid;
@@ -199,5 +204,4 @@
chart.update(); chart.update();
}); });
}; };
})(); })();
+7 -3
View File
@@ -857,7 +857,7 @@
}, },
}, },
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
@@ -1098,8 +1098,12 @@
loadDiaperChanges(); loadDiaperChanges();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, poopPieChartInstance, peePieChartInstance].filter(c => c); const charts = [
barChartInstance,
poopPieChartInstance,
peePieChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+9 -5
View File
@@ -940,7 +940,7 @@
display: false, display: false,
}, },
}, },
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
stacked: true, stacked: true,
beginAtZero: true, beginAtZero: true,
title: { title: {
@@ -1127,7 +1127,7 @@
}, },
}, },
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
title: { title: {
display: true, display: true,
@@ -1144,7 +1144,7 @@
}, },
}, },
}), }),
x: getChartScaleOptions('x', { x: getChartScaleOptions("x", {
title: { title: {
display: true, display: true,
text: "Duration (minutes)", text: "Duration (minutes)",
@@ -1202,8 +1202,12 @@
loadFeedings(); loadFeedings();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, pieChartInstance, durationChartInstance].filter(c => c); const charts = [
barChartInstance,
pieChartInstance,
durationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+4 -2
View File
@@ -3,9 +3,11 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Baby Monitor</title> <link rel="stylesheet" href="/dark-mode.css" /> <title>Baby Monitor</title>
<link rel="stylesheet" href="/dark-mode.css" />
<script src="/theme-init.js"></script> <script src="/theme-init.js"></script>
<script src="/chart-theme.js"></script> <link rel="stylesheet" href="/menu.css" /> <script src="/chart-theme.js"></script>
<link rel="stylesheet" href="/menu.css" />
<style> <style>
* { * {
margin: 0; margin: 0;
+10 -3
View File
@@ -9,7 +9,9 @@
padding: 0.5rem; padding: 0.5rem;
border-radius: 4px; border-radius: 4px;
box-shadow: var(--shadow-md, 0 2px 10px rgba(0, 0, 0, 0.1)); box-shadow: var(--shadow-md, 0 2px 10px rgba(0, 0, 0, 0.1));
transition: background 0.3s, box-shadow 0.3s; transition:
background 0.3s,
box-shadow 0.3s;
} }
.burger-menu div { .burger-menu div {
@@ -43,7 +45,9 @@
height: 100vh; height: 100vh;
background: var(--bg-secondary, #ffffff); background: var(--bg-secondary, #ffffff);
box-shadow: var(--shadow-md, 2px 0 10px rgba(0, 0, 0, 0.1)); box-shadow: var(--shadow-md, 2px 0 10px rgba(0, 0, 0, 0.1));
transition: left 0.3s, background 0.3s; transition:
left 0.3s,
background 0.3s;
z-index: 999; z-index: 999;
padding: 4rem 1rem 1rem 1rem; padding: 4rem 1rem 1rem 1rem;
overflow: hidden; overflow: hidden;
@@ -57,7 +61,10 @@
padding: 1rem; padding: 1rem;
border-bottom: 1px solid var(--border-color, #eeeeee); border-bottom: 1px solid var(--border-color, #eeeeee);
cursor: pointer; cursor: pointer;
transition: background 0.2s, color 0.3s, border-color 0.3s; transition:
background 0.2s,
color 0.3s,
border-color 0.3s;
color: var(--text-primary, #333333); color: var(--text-primary, #333333);
} }
+151 -30
View File
@@ -240,30 +240,132 @@
<div class="section"> <div class="section">
<h2>🌓 Theme Preference</h2> <h2>🌓 Theme Preference</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px; font-size: 14px"> <p
style="
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 14px;
"
>
Choose how Baby Monitor looks to you. Choose how Baby Monitor looks to you.
</p> </p>
<div class="form-group"> <div class="form-group">
<div style="display: flex; flex-direction: column; gap: 15px"> <div style="display: flex; flex-direction: column; gap: 15px">
<label style="display: flex; align-items: center; cursor: pointer; padding: 12px; border: 2px solid var(--border-light); border-radius: 8px; transition: all 0.2s;" class="theme-option" data-theme="light"> <label
<input type="radio" name="theme" value="light" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="light"
>
<input
type="radio"
name="theme"
value="light"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div> <div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">☀️ Light Mode</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Always use light theme</div> style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
☀️ Light Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use light theme
</div>
</div> </div>
</label> </label>
<label style="display: flex; align-items: center; cursor: pointer; padding: 12px; border: 2px solid var(--border-light); border-radius: 8px; transition: all 0.2s;" class="theme-option" data-theme="dark"> <label
<input type="radio" name="theme" value="dark" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="dark"
>
<input
type="radio"
name="theme"
value="dark"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div> <div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">🌙 Dark Mode</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Always use dark theme</div> style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🌙 Dark Mode
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Always use dark theme
</div>
</div> </div>
</label> </label>
<label style="display: flex; align-items: center; cursor: pointer; padding: 12px; border: 2px solid var(--border-light); border-radius: 8px; transition: all 0.2s;" class="theme-option" data-theme="auto"> <label
<input type="radio" name="theme" value="auto" style="margin-right: 12px; width: 20px; height: 20px; cursor: pointer;" /> style="
display: flex;
align-items: center;
cursor: pointer;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
transition: all 0.2s;
"
class="theme-option"
data-theme="auto"
>
<input
type="radio"
name="theme"
value="auto"
style="
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
"
/>
<div> <div>
<div style="font-weight: 600; color: var(--text-primary); margin-bottom: 4px;">🔄 Auto</div> <div
<div style="font-size: 12px; color: var(--text-secondary);">Match your browser settings</div> style="
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
"
>
🔄 Auto
</div>
<div style="font-size: 12px; color: var(--text-secondary)">
Match your browser settings
</div>
</div> </div>
</label> </label>
</div> </div>
@@ -298,7 +400,13 @@
" "
> >
<h3 style="color: #dc3545; margin-bottom: 15px">⚠️ Danger Zone</h3> <h3 style="color: #dc3545; margin-bottom: 15px">⚠️ Danger Zone</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px"> <p
style="
color: var(--text-secondary);
margin-bottom: 15px;
font-size: 14px;
"
>
Deleting your account is permanent and cannot be undone. This will: Deleting your account is permanent and cannot be undone. This will:
</p> </p>
<ul <ul
@@ -322,9 +430,7 @@
</ul> </ul>
<button <button
class="button" class="button"
style=" style="background: var(--gradient-error)"
background: var(--gradient-error);
"
onclick="confirmDeleteAccount()" onclick="confirmDeleteAccount()"
> >
🗑️ Delete Account 🗑️ Delete Account
@@ -382,7 +488,9 @@
width: 90%; width: 90%;
" "
> >
<h2 style="margin-top: 0; color: var(--text-primary);">🔗 Redeem Share Code</h2> <h2 style="margin-top: 0; color: var(--text-primary)">
🔗 Redeem Share Code
</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px"> <p style="color: var(--text-secondary); margin-bottom: 20px">
Enter the share code to get access to a child. Enter the share code to get access to a child.
</p> </p>
@@ -448,8 +556,11 @@
width: 90%; width: 90%;
" "
> >
<h2 style="margin-top: 0; color: var(--text-primary);">🔗 Share Code</h2> <h2 style="margin-top: 0; color: var(--text-primary)">🔗 Share Code</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px" id="shareCodeChildName"></p> <p
style="color: var(--text-secondary); margin-bottom: 20px"
id="shareCodeChildName"
></p>
<div <div
style=" style="
background: var(--bg-tertiary); background: var(--bg-tertiary);
@@ -490,7 +601,13 @@
</button> </button>
</div> </div>
</div> </div>
<p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 20px"> <p
style="
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 20px;
"
>
Share this code with another user. They can use it in Settings > Share this code with another user. They can use it in Settings >
Redeem Share Code to get access to this child. Redeem Share Code to get access to this child.
<br /><br /> <br /><br />
@@ -520,7 +637,9 @@
// Initialize theme selector // Initialize theme selector
function initThemeSelector() { function initThemeSelector() {
const currentTheme = window.getThemePreference(); const currentTheme = window.getThemePreference();
const themeRadio = document.querySelector(`input[name="theme"][value="${currentTheme}"]`); const themeRadio = document.querySelector(
`input[name="theme"][value="${currentTheme}"]`,
);
if (themeRadio) { if (themeRadio) {
themeRadio.checked = true; themeRadio.checked = true;
} }
@@ -529,8 +648,8 @@
updateThemeOptionStyles(); updateThemeOptionStyles();
// Listen for theme changes // Listen for theme changes
document.querySelectorAll('input[name="theme"]').forEach(radio => { document.querySelectorAll('input[name="theme"]').forEach((radio) => {
radio.addEventListener('change', (e) => { radio.addEventListener("change", (e) => {
window.setTheme(e.target.value); window.setTheme(e.target.value);
updateThemeOptionStyles(); updateThemeOptionStyles();
}); });
@@ -538,15 +657,17 @@
} }
function updateThemeOptionStyles() { function updateThemeOptionStyles() {
const selectedValue = document.querySelector('input[name="theme"]:checked')?.value; const selectedValue = document.querySelector(
document.querySelectorAll('.theme-option').forEach(option => { 'input[name="theme"]:checked',
)?.value;
document.querySelectorAll(".theme-option").forEach((option) => {
const isSelected = option.dataset.theme === selectedValue; const isSelected = option.dataset.theme === selectedValue;
if (isSelected) { if (isSelected) {
option.style.borderColor = '#667eea'; option.style.borderColor = "#667eea";
option.style.background = 'rgba(102, 126, 234, 0.05)'; option.style.background = "rgba(102, 126, 234, 0.05)";
} else { } else {
option.style.borderColor = '#e0e0e0'; option.style.borderColor = "#e0e0e0";
option.style.background = 'transparent'; option.style.background = "transparent";
} }
}); });
} }
+8 -4
View File
@@ -895,7 +895,7 @@
x: { x: {
stacked: true, stacked: true,
}, },
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
stacked: true, stacked: true,
beginAtZero: true, beginAtZero: true,
title: { title: {
@@ -1056,7 +1056,7 @@
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
scales: { scales: {
y: getChartScaleOptions('y', { y: getChartScaleOptions("y", {
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
callback: function (value) { callback: function (value) {
@@ -1135,8 +1135,12 @@
loadData(); loadData();
// Listen for theme changes and update charts // Listen for theme changes and update charts
window.addEventListener('themechange', () => { window.addEventListener("themechange", () => {
const charts = [barChartInstance, durationChartInstance, avgDurationChartInstance].filter(c => c); const charts = [
barChartInstance,
durationChartInstance,
avgDurationChartInstance,
].filter((c) => c);
updateChartsForTheme(charts); updateChartsForTheme(charts);
}); });
</script> </script>
+36 -25
View File
@@ -4,72 +4,83 @@
*/ */
(function () { (function () {
'use strict'; "use strict";
// Get saved theme preference from localStorage // Get saved theme preference from localStorage
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
// Apply theme immediately to prevent FOUC (Flash of Unstyled Content) // Apply theme immediately to prevent FOUC (Flash of Unstyled Content)
function applyTheme(theme) { function applyTheme(theme) {
const body = document.body || document.documentElement; const body = document.body || document.documentElement;
if (theme === 'dark') { if (theme === "dark") {
body.classList.remove('light-mode'); body.classList.remove("light-mode");
body.classList.add('dark-mode'); body.classList.add("dark-mode");
} else if (theme === 'light') { } else if (theme === "light") {
body.classList.remove('dark-mode'); body.classList.remove("dark-mode");
body.classList.add('light-mode'); body.classList.add("light-mode");
} else { } else {
// 'auto' mode - remove manual override classes // 'auto' mode - remove manual override classes
body.classList.remove('dark-mode', 'light-mode'); body.classList.remove("dark-mode", "light-mode");
} }
} }
// Apply theme based on preference // Apply theme based on preference
if (savedTheme === 'dark' || savedTheme === 'light' || savedTheme === 'auto') { if (
savedTheme === "dark" ||
savedTheme === "light" ||
savedTheme === "auto"
) {
applyTheme(savedTheme); applyTheme(savedTheme);
} }
// If no preference saved, default to 'auto' (browser preference) // If no preference saved, default to 'auto' (browser preference)
// Make theme switching function available globally // Make theme switching function available globally
window.setTheme = function (theme) { window.setTheme = function (theme) {
if (theme === 'light' || theme === 'dark' || theme === 'auto') { if (theme === "light" || theme === "dark" || theme === "auto") {
localStorage.setItem('theme-preference', theme); localStorage.setItem("theme-preference", theme);
applyTheme(theme); applyTheme(theme);
// Trigger custom event for components that need to update (e.g., charts) // Trigger custom event for components that need to update (e.g., charts)
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } })); window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme } }),
);
} }
}; };
// Get current effective theme (resolves 'auto' to actual theme) // Get current effective theme (resolves 'auto' to actual theme)
window.getEffectiveTheme = function () { window.getEffectiveTheme = function () {
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
if (savedTheme === 'dark') return 'dark'; if (savedTheme === "dark") return "dark";
if (savedTheme === 'light') return 'light'; if (savedTheme === "light") return "light";
// 'auto' or no preference - check system preference // 'auto' or no preference - check system preference
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { if (
return 'dark'; window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
return "dark";
} }
return 'light'; return "light";
}; };
// Get saved theme preference (returns 'light', 'dark', 'auto', or null) // Get saved theme preference (returns 'light', 'dark', 'auto', or null)
window.getThemePreference = function () { window.getThemePreference = function () {
return localStorage.getItem('theme-preference') || 'auto'; return localStorage.getItem("theme-preference") || "auto";
}; };
// Listen for system theme changes when in auto mode // Listen for system theme changes when in auto mode
if (window.matchMedia) { if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener('change', function(e) { mediaQuery.addEventListener("change", function (e) {
const savedTheme = localStorage.getItem('theme-preference'); const savedTheme = localStorage.getItem("theme-preference");
// Only react to system changes if in auto mode // Only react to system changes if in auto mode
if (!savedTheme || savedTheme === 'auto') { if (!savedTheme || savedTheme === "auto") {
// Trigger theme change event for components that need to update // Trigger theme change event for components that need to update
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme: 'auto' } })); window.dispatchEvent(
new CustomEvent("themechange", { detail: { theme: "auto" } }),
);
} }
}); });
} }