From 95aec8350a348a5ba4fae4ea670460c3253f428d Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Fri, 10 Apr 2026 22:31:22 +0200 Subject: [PATCH] fix(settings): stabilize timezone edit UX and tooltip visibility (#535) --- frontend/src/i18n/de.json | 4 +- frontend/src/i18n/en.json | 4 +- frontend/src/pages/SettingsPage.tsx | 67 ++++++++++++++++++++--- frontend/src/styles/foundation.css | 2 +- frontend/src/styles/settings-surfaces.css | 20 ++++++- 5 files changed, 82 insertions(+), 15 deletions(-) diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index bf76a4a..a559e78 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -393,7 +393,9 @@ "select": "Zeitzone", "hint": "IANA-Zeitzone wählen. Wenn gesetzt, überschreibt sie die Server-TZ für deine Reminder-Zeitpunkte.", "useServerDefault": "Server-Standard nutzen", - "currentServerTz": "Server-Standardzeitzone: {{timezone}}" + "currentServerTz": "Server-Standardzeitzone: {{timezone}}", + "saving": "Zeitzone wird gespeichert...", + "saved": "Zeitzone gespeichert" }, "apiKey": { "title": "API-Zugriff", diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3011082..f526db3 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -393,7 +393,9 @@ "select": "Timezone", "hint": "Select an IANA timezone. When set, this overrides server TZ for your reminder timing.", "useServerDefault": "Use server default", - "currentServerTz": "Server default timezone: {{timezone}}" + "currentServerTz": "Server default timezone: {{timezone}}", + "saving": "Saving timezone...", + "saved": "Timezone saved" }, "apiKey": { "title": "API Access", diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 7992857..4626a47 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,5 +1,5 @@ /* biome-ignore-all lint/a11y/noLabelWithoutControl: settings rows use label-styled text with adjacent custom toggle controls */ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { ConfirmModal, ExportModal } from "../components"; import { useAppContext } from "../context"; @@ -13,8 +13,11 @@ export function SettingsPage() { const [apiKeyError, setApiKeyError] = useState(null); const { settings, + savedSettings, setSettings, settingsLoading, + settingsSaving, + settingsSaved, settingsLoadError, // Email testing testEmail, @@ -39,6 +42,8 @@ export function SettingsPage() { setImportResult, meds, } = useAppContext(); + const [timezoneTouched, setTimezoneTouched] = useState(false); + const [timezoneDraft, setTimezoneDraft] = useState(""); const hasExistingData = meds.length > 0; let emailUnavailableReason: string | null = null; @@ -116,9 +121,35 @@ export function SettingsPage() { const automaticStockCalculationId = "settings-stock-calculation-automatic"; const manualStockCalculationId = "settings-stock-calculation-manual"; + + useEffect(() => { + setTimezoneDraft(settings.timezone); + }, [settings.timezone]); + + const commitTimezoneDraft = () => { + if (timezoneDraft === settings.timezone) { + return; + } + + setTimezoneTouched(true); + setSettings((prev) => ({ ...prev, timezone: timezoneDraft })); + }; + + const savedTimezone = savedSettings?.timezone ?? settings.timezone; + const timezoneChanged = settings.timezone !== savedTimezone; + const showTimezoneSaving = timezoneTouched && timezoneChanged && settingsSaving; + const showTimezoneSaved = timezoneTouched && !timezoneChanged && settingsSaved; + let timezoneStatusText = ""; + if (showTimezoneSaving) { + timezoneStatusText = t("settings.timezone.saving"); + } else if (showTimezoneSaved) { + timezoneStatusText = t("settings.timezone.saved"); + } + const timezoneStatusClassName = showTimezoneSaved ? "timezone-status timezone-status-saved" : "timezone-status"; + const availableTimezones = Array.isArray(settings.availableTimezones) ? settings.availableTimezones : []; const timezoneSuggestions = - settings.availableTimezones.length > 0 - ? settings.availableTimezones + availableTimezones.length > 0 + ? availableTimezones : (() => { try { type IntlWithSupportedValuesOf = typeof Intl & { @@ -177,19 +208,28 @@ export function SettingsPage() { -
+
{t("settings.timezone.select")} - +
-
+
setSettings({ ...settings, timezone: e.target.value })} + value={timezoneDraft} + onChange={(e) => { + setTimezoneDraft(e.target.value); + }} + onBlur={commitTimezoneDraft} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + (e.currentTarget as HTMLInputElement).blur(); + } + }} list="settings-timezone-suggestions" placeholder={settings.serverTimezone || "UTC"} /> @@ -198,11 +238,20 @@ export function SettingsPage() {
+

{timezoneStatusText || " "}

{t("settings.timezone.currentServerTz", { timezone: settings.serverTimezone || "UTC" })}

diff --git a/frontend/src/styles/foundation.css b/frontend/src/styles/foundation.css index 5e379d1..3df8136 100644 --- a/frontend/src/styles/foundation.css +++ b/frontend/src/styles/foundation.css @@ -613,7 +613,7 @@ body.modal-open { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); margin-bottom: 1rem; max-width: 100%; - overflow: hidden; + overflow: visible; } .card { diff --git a/frontend/src/styles/settings-surfaces.css b/frontend/src/styles/settings-surfaces.css index 9c38e2e..a7f2561 100644 --- a/frontend/src/styles/settings-surfaces.css +++ b/frontend/src/styles/settings-surfaces.css @@ -10,7 +10,7 @@ flex-direction: column; gap: 1.5rem; max-width: 100%; - overflow: hidden; + overflow: visible; } .setting-row { @@ -311,7 +311,7 @@ transition: opacity 0.15s, visibility 0.15s; - z-index: 1100; + z-index: 12000; pointer-events: none; } @@ -329,7 +329,7 @@ transition: opacity 0.15s, visibility 0.15s; - z-index: 1101; + z-index: 12001; } /* Tooltip aligned to left edge of icon (prevents clipping inside modals) */ @@ -507,6 +507,20 @@ border-radius: 6px; } +.timezone-status { + min-height: 1.25rem; + margin-top: 8px; + margin-bottom: 0; + padding: 0; + font-size: 0.85rem; + color: transparent; + background: transparent; +} + +.timezone-status-saved { + color: var(--success); +} + /* Notification Matrix Mobile */ @media (max-width: 480px) { .notification-matrix {