Add configurable sidebar contrast settings

This commit is contained in:
2026-08-05 15:59:16 +02:00
parent a1e3f537e3
commit bf2d86329b
6 changed files with 95 additions and 4 deletions
+24
View File
@@ -48,6 +48,8 @@ const localDbFields = document.querySelector("#localDbFields");
const localDbInputs = document.querySelectorAll("#localDbFields input"); const localDbInputs = document.querySelectorAll("#localDbFields input");
const testLocalDbButton = document.querySelector("#testLocalDbButton"); const testLocalDbButton = document.querySelector("#testLocalDbButton");
const localDbTestResult = document.querySelector("#localDbTestResult"); const localDbTestResult = document.querySelector("#localDbTestResult");
const lightContrastSelect = document.querySelector("#lightContrastSelect");
const darkContrastSelect = document.querySelector("#darkContrastSelect");
const nativeFetch = window.fetch.bind(window); const nativeFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => { window.fetch = (input, init = {}) => {
const url = typeof input === "string" ? input : input.url; const url = typeof input === "string" ? input : input.url;
@@ -128,6 +130,7 @@ const productFieldMap = {
}; };
await loadStatus(); await loadStatus();
loadTheme(); loadTheme();
loadAppearance();
renderFields(); renderFields();
renderTableHeader(); renderTableHeader();
setTableWidth(); setTableWidth();
@@ -210,6 +213,8 @@ for (const input of transactionModeInputs)
for (const input of localDbInputs) for (const input of localDbInputs)
input.addEventListener("change", saveLocalDbSettings); input.addEventListener("change", saveLocalDbSettings);
testLocalDbButton.addEventListener("click", testLocalDbConnection); testLocalDbButton.addEventListener("click", testLocalDbConnection);
lightContrastSelect.addEventListener("change", saveAppearance);
darkContrastSelect.addEventListener("change", saveAppearance);
loadDatabaseMode(); loadDatabaseMode();
syncSidebarForViewport(); syncSidebarForViewport();
window.matchMedia("(min-width: 981px)").addEventListener("change", syncSidebarForViewport); window.matchMedia("(min-width: 981px)").addEventListener("change", syncSidebarForViewport);
@@ -864,6 +869,25 @@ function loadTheme() {
themeToggle.querySelector("span").textContent = isDark ? "Light theme" : "Dark theme"; themeToggle.querySelector("span").textContent = isDark ? "Light theme" : "Dark theme";
themeToggle.setAttribute("aria-pressed", String(isDark)); themeToggle.setAttribute("aria-pressed", String(isDark));
} }
function loadAppearance() {
const settings = JSON.parse(localStorage.getItem("catalog-maker:appearance") || "{}");
lightContrastSelect.value = settings.lightContrast || "medium";
darkContrastSelect.value = settings.darkContrast || "medium";
applyAppearance();
}
function saveAppearance() {
localStorage.setItem("catalog-maker:appearance", JSON.stringify({
lightContrast: lightContrastSelect.value,
darkContrast: darkContrastSelect.value,
}));
applyAppearance();
}
function applyAppearance() {
const root = document.documentElement;
root.classList.remove("contrast-light-low", "contrast-light-medium", "contrast-light-high", "contrast-dark-low", "contrast-dark-medium", "contrast-dark-high");
root.classList.add(`contrast-light-${lightContrastSelect.value || "medium"}`);
root.classList.add(`contrast-dark-${darkContrastSelect.value || "medium"}`);
}
function toggleTheme() { function toggleTheme() {
const isDark = !document.documentElement.classList.contains("dark"); const isDark = !document.documentElement.classList.contains("dark");
localStorage.setItem("catalog-maker:theme", isDark ? "dark" : "light"); localStorage.setItem("catalog-maker:theme", isDark ? "dark" : "light");
+27
View File
@@ -48,6 +48,8 @@ const localDbFields = document.querySelector("#localDbFields");
const localDbInputs = document.querySelectorAll("#localDbFields input"); const localDbInputs = document.querySelectorAll("#localDbFields input");
const testLocalDbButton = document.querySelector("#testLocalDbButton"); const testLocalDbButton = document.querySelector("#testLocalDbButton");
const localDbTestResult = document.querySelector("#localDbTestResult"); const localDbTestResult = document.querySelector("#localDbTestResult");
const lightContrastSelect = document.querySelector("#lightContrastSelect");
const darkContrastSelect = document.querySelector("#darkContrastSelect");
const nativeFetch = window.fetch.bind(window); const nativeFetch = window.fetch.bind(window);
window.fetch = (input, init = {}) => { window.fetch = (input, init = {}) => {
@@ -134,6 +136,7 @@ const productFieldMap = {
await loadStatus(); await loadStatus();
loadTheme(); loadTheme();
loadAppearance();
renderFields(); renderFields();
renderTableHeader(); renderTableHeader();
setTableWidth(); setTableWidth();
@@ -209,6 +212,8 @@ for (const input of permissionInputs) input.addEventListener("change", saveSetti
for (const input of transactionModeInputs) input.addEventListener("change", saveSettings); for (const input of transactionModeInputs) input.addEventListener("change", saveSettings);
for (const input of localDbInputs) input.addEventListener("change", saveLocalDbSettings); for (const input of localDbInputs) input.addEventListener("change", saveLocalDbSettings);
testLocalDbButton.addEventListener("click", testLocalDbConnection); testLocalDbButton.addEventListener("click", testLocalDbConnection);
lightContrastSelect.addEventListener("change", saveAppearance);
darkContrastSelect.addEventListener("change", saveAppearance);
loadDatabaseMode(); loadDatabaseMode();
syncSidebarForViewport(); syncSidebarForViewport();
@@ -954,6 +959,28 @@ function loadTheme() {
themeToggle.setAttribute("aria-pressed", String(isDark)); themeToggle.setAttribute("aria-pressed", String(isDark));
} }
function loadAppearance() {
const settings = JSON.parse(localStorage.getItem("catalog-maker:appearance") || "{}");
lightContrastSelect.value = settings.lightContrast || "medium";
darkContrastSelect.value = settings.darkContrast || "medium";
applyAppearance();
}
function saveAppearance() {
localStorage.setItem("catalog-maker:appearance", JSON.stringify({
lightContrast: lightContrastSelect.value,
darkContrast: darkContrastSelect.value,
}));
applyAppearance();
}
function applyAppearance() {
const root = document.documentElement;
root.classList.remove("contrast-light-low", "contrast-light-medium", "contrast-light-high", "contrast-dark-low", "contrast-dark-medium", "contrast-dark-high");
root.classList.add(`contrast-light-${lightContrastSelect.value || "medium"}`);
root.classList.add(`contrast-dark-${darkContrastSelect.value || "medium"}`);
}
function toggleTheme() { function toggleTheme() {
const isDark = !document.documentElement.classList.contains("dark"); const isDark = !document.documentElement.classList.contains("dark");
localStorage.setItem("catalog-maker:theme", isDark ? "dark" : "light"); localStorage.setItem("catalog-maker:theme", isDark ? "dark" : "light");
+21
View File
@@ -176,6 +176,27 @@
<p class="settings-note" id="databaseModeNote">Local test database selected.</p> <p class="settings-note" id="databaseModeNote">Local test database selected.</p>
</fieldset> </fieldset>
<fieldset class="settings-group">
<legend>Appearance</legend>
<label class="settings-field">
<span>Light contrast</span>
<select class="settings-select" id="lightContrastSelect">
<option value="low">Soft</option>
<option value="medium" selected>Balanced</option>
<option value="high">High</option>
</select>
</label>
<label class="settings-field">
<span>Dark contrast</span>
<select class="settings-select" id="darkContrastSelect">
<option value="low">Soft</option>
<option value="medium" selected>Balanced</option>
<option value="high">High</option>
</select>
</label>
<p class="settings-note">Controls the contrast of the left sidebar and its controls.</p>
</fieldset>
<fieldset class="settings-group"> <fieldset class="settings-group">
<legend>Access permissions</legend> <legend>Access permissions</legend>
<label class="settings-option"> <label class="settings-option">
+1 -1
View File
File diff suppressed because one or more lines are too long
+13
View File
@@ -150,6 +150,7 @@
.local-db-fields[hidden] { display: none; } .local-db-fields[hidden] { display: none; }
.settings-field { @apply grid grid-cols-[72px_minmax(0,1fr)] items-center gap-2 text-[11px] text-slate-700; } .settings-field { @apply grid grid-cols-[72px_minmax(0,1fr)] items-center gap-2 text-[11px] text-slate-700; }
.settings-field input { @apply min-w-0 rounded border border-slate-300 bg-white px-1 py-1 text-[11px]; } .settings-field input { @apply min-w-0 rounded border border-slate-300 bg-white px-1 py-1 text-[11px]; }
.settings-select { @apply min-w-0 rounded border border-slate-300 bg-white px-1 py-1 text-[11px] text-slate-800; }
.settings-note, .settings-permissions { @apply m-0 text-[10px] leading-tight text-slate-500; } .settings-note, .settings-permissions { @apply m-0 text-[10px] leading-tight text-slate-500; }
.settings-permissions { @apply grid gap-0.5; } .settings-permissions { @apply grid gap-0.5; }
.connection-ok { @apply text-emerald-700; } .connection-ok { @apply text-emerald-700; }
@@ -221,6 +222,12 @@
html:not(.dark) .app-status { @apply border-slate-300 bg-white text-slate-700; } html:not(.dark) .app-status { @apply border-slate-300 bg-white text-slate-700; }
html:not(.dark) .manufacturer-trigger { @apply border-slate-300 bg-white text-slate-700; } html:not(.dark) .manufacturer-trigger { @apply border-slate-300 bg-white text-slate-700; }
html:not(.dark) .manufacturer-trigger:hover { @apply border-slate-400 bg-slate-200; } html:not(.dark) .manufacturer-trigger:hover { @apply border-slate-400 bg-slate-200; }
html.contrast-light-high:not(.dark) .control-panel { @apply bg-slate-200 text-slate-900; }
html.contrast-light-high:not(.dark) .action-button,
html.contrast-light-high:not(.dark) .arrow-button,
html.contrast-light-high:not(.dark) .input-like,
html.contrast-light-high:not(.dark) .manufacturer-trigger { @apply border-slate-500 text-slate-900; }
html.contrast-light-high:not(.dark) .sidebar-icon { @apply text-slate-700; }
html.dark body { @apply bg-slate-950 text-slate-100; } html.dark body { @apply bg-slate-950 text-slate-100; }
html.dark .control-panel { @apply bg-slate-950; } html.dark .control-panel { @apply bg-slate-950; }
@@ -243,6 +250,12 @@
html.dark .manufacturer-search { @apply border-slate-600 bg-slate-800 text-slate-100; } html.dark .manufacturer-search { @apply border-slate-600 bg-slate-800 text-slate-100; }
html.dark .manufacturer-option { @apply text-slate-200 hover:bg-slate-800; } html.dark .manufacturer-option { @apply text-slate-200 hover:bg-slate-800; }
html.dark .manufacturer-option[aria-selected="true"] { @apply bg-slate-700 text-white; } html.dark .manufacturer-option[aria-selected="true"] { @apply bg-slate-700 text-white; }
html.contrast-dark-high.dark .control-panel { @apply bg-slate-950 text-white; }
html.contrast-dark-high.dark .action-button,
html.contrast-dark-high.dark .arrow-button,
html.contrast-dark-high.dark .input-like,
html.contrast-dark-high.dark .manufacturer-trigger { @apply border-slate-500 bg-slate-700 text-white; }
html.contrast-dark-high.dark .sidebar-icon { @apply text-slate-100; }
html.dark .settings-dialog, html.dark .settings-dialog,
html.dark .suggested-dialog, html.dark .suggested-dialog,
html.dark .source-dialog { @apply border-slate-700 bg-slate-900; } html.dark .source-dialog { @apply border-slate-700 bg-slate-900; }
+9 -3
View File
@@ -59,15 +59,21 @@ test("left controls open in a responsive sidebar drawer", async ({ page }) => {
test("theme toggle switches and persists dark mode", async ({ page }) => { test("theme toggle switches and persists dark mode", async ({ page }) => {
await page.goto("/"); await page.goto("/");
await expect(page.locator("html")).not.toHaveClass(/dark/); await page.evaluate(() => localStorage.clear());
await page.reload();
await expect(page.locator("html")).not.toHaveClass(/(?:^| )dark(?: |$)/);
const lightPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor); const lightPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor);
await page.locator("#themeToggle").click(); await page.locator("#themeToggle").click();
await expect(page.locator("html")).toHaveClass(/dark/); await expect(page.locator("html")).toHaveClass(/(?:^| )dark(?: |$)/);
await expect(page.locator("#themeToggle")).toHaveText("Light theme"); await expect(page.locator("#themeToggle")).toHaveText("Light theme");
const darkPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor); const darkPanelBackground = await page.locator("#sidebarDrawer").evaluate((element) => getComputedStyle(element).backgroundColor);
expect(darkPanelBackground).not.toBe(lightPanelBackground); expect(darkPanelBackground).not.toBe(lightPanelBackground);
await page.locator("#settingsButton").click();
await page.locator("#darkContrastSelect").selectOption("high");
await expect(page.locator("html")).toHaveClass(/contrast-dark-high/);
await page.reload(); await page.reload();
await expect(page.locator("html")).toHaveClass(/dark/); await expect(page.locator("html")).toHaveClass(/(?:^| )dark(?: |$)/);
}); });