From 831deaf5181dc60ed9aa46920560a1407742dcab Mon Sep 17 00:00:00 2001 From: HDash <16350928+HDash@users.noreply.github.com> Date: Fri, 1 May 2026 13:35:52 +0100 Subject: [PATCH 1/4] feat(settings): add time format mode storage Adds TimeFormatMode ("auto" | "12h" | "24h") with load/save helpers, default "auto" so existing locale-based behavior is preserved. --- src/lib/settings.test.ts | 22 ++++++++++++++++++++++ src/lib/settings.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/lib/settings.test.ts b/src/lib/settings.test.ts index 1c686863..4e496a03 100644 --- a/src/lib/settings.test.ts +++ b/src/lib/settings.test.ts @@ -8,6 +8,7 @@ import { DEFAULT_RESET_TIMER_DISPLAY_MODE, DEFAULT_START_ON_LOGIN, DEFAULT_THEME_MODE, + DEFAULT_TIME_FORMAT_MODE, arePluginSettingsEqual, getEnabledPluginIds, loadAutoUpdateInterval, @@ -17,6 +18,7 @@ import { loadPluginSettings, loadResetTimerDisplayMode, loadStartOnLogin, + loadTimeFormatMode, migrateLegacyTraySettings, loadThemeMode, normalizePluginSettings, @@ -28,6 +30,7 @@ import { saveResetTimerDisplayMode, saveStartOnLogin, saveThemeMode, + saveTimeFormatMode, } from "@/lib/settings" import type { PluginMeta } from "@/lib/plugin-types" @@ -185,6 +188,25 @@ describe("settings", () => { await expect(loadResetTimerDisplayMode()).resolves.toBe(DEFAULT_RESET_TIMER_DISPLAY_MODE) }) + it("loads default time format mode when missing", async () => { + await expect(loadTimeFormatMode()).resolves.toBe(DEFAULT_TIME_FORMAT_MODE) + }) + + it("loads stored time format mode", async () => { + storeState.set("timeFormat", "24h") + await expect(loadTimeFormatMode()).resolves.toBe("24h") + }) + + it("saves time format mode", async () => { + await saveTimeFormatMode("12h") + await expect(loadTimeFormatMode()).resolves.toBe("12h") + }) + + it("falls back to default for invalid time format mode", async () => { + storeState.set("timeFormat", "invalid") + await expect(loadTimeFormatMode()).resolves.toBe(DEFAULT_TIME_FORMAT_MODE) + }) + it("migrates and removes legacy tray settings keys", async () => { storeState.set("trayIconStyle", "provider") storeState.set("trayShowPercentage", false) diff --git a/src/lib/settings.ts b/src/lib/settings.ts index a94d0a7c..253e52ae 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -18,6 +18,8 @@ export type DisplayMode = "used" | "left"; export type ResetTimerDisplayMode = "relative" | "absolute"; +export type TimeFormatMode = "auto" | "12h" | "24h"; + export type MenubarIconStyle = "provider" | "bars" | "donut"; export type GlobalShortcut = string | null; @@ -28,6 +30,7 @@ const AUTO_UPDATE_SETTINGS_KEY = "autoUpdateInterval"; const THEME_MODE_KEY = "themeMode"; const DISPLAY_MODE_KEY = "displayMode"; const RESET_TIMER_DISPLAY_MODE_KEY = "resetTimerDisplayMode"; +const TIME_FORMAT_KEY = "timeFormat"; const MENUBAR_ICON_STYLE_KEY = "menubarIconStyle"; const LEGACY_TRAY_ICON_STYLE_KEY = "trayIconStyle"; const LEGACY_TRAY_SHOW_PERCENTAGE_KEY = "trayShowPercentage"; @@ -38,6 +41,7 @@ export const DEFAULT_AUTO_UPDATE_INTERVAL: AutoUpdateIntervalMinutes = 15; export const DEFAULT_THEME_MODE: ThemeMode = "system"; export const DEFAULT_DISPLAY_MODE: DisplayMode = "left"; export const DEFAULT_RESET_TIMER_DISPLAY_MODE: ResetTimerDisplayMode = "relative"; +export const DEFAULT_TIME_FORMAT_MODE: TimeFormatMode = "auto"; export const DEFAULT_MENUBAR_ICON_STYLE: MenubarIconStyle = "provider"; export const DEFAULT_GLOBAL_SHORTCUT: GlobalShortcut = null; export const DEFAULT_START_ON_LOGIN = false; @@ -46,6 +50,7 @@ const AUTO_UPDATE_INTERVALS: AutoUpdateIntervalMinutes[] = [5, 15, 30, 60]; const THEME_MODES: ThemeMode[] = ["system", "light", "dark"]; const DISPLAY_MODES: DisplayMode[] = ["used", "left"]; const RESET_TIMER_DISPLAY_MODES: ResetTimerDisplayMode[] = ["relative", "absolute"]; +const TIME_FORMAT_MODES: TimeFormatMode[] = ["auto", "12h", "24h"]; const MENUBAR_ICON_STYLES: MenubarIconStyle[] = ["provider", "donut", "bars"]; export const MENUBAR_ICON_STYLE_OPTIONS: { value: MenubarIconStyle; label: string }[] = [ @@ -76,6 +81,12 @@ export const RESET_TIMER_DISPLAY_OPTIONS: { value: ResetTimerDisplayMode; label: { value: "absolute", label: "Absolute" }, ]; +export const TIME_FORMAT_OPTIONS: { value: TimeFormatMode; label: string }[] = [ + { value: "auto", label: "Auto" }, + { value: "12h", label: "12-hour" }, + { value: "24h", label: "24-hour" }, +]; + const store = new LazyStore(SETTINGS_STORE_PATH); const DEFAULT_ENABLED_PLUGINS = new Set(["claude", "codex", "cursor"]); @@ -214,6 +225,24 @@ export async function saveResetTimerDisplayMode(mode: ResetTimerDisplayMode): Pr await store.save(); } +function isTimeFormatMode(value: unknown): value is TimeFormatMode { + return ( + typeof value === "string" && + TIME_FORMAT_MODES.includes(value as TimeFormatMode) + ); +} + +export async function loadTimeFormatMode(): Promise { + const stored = await store.get(TIME_FORMAT_KEY); + if (isTimeFormatMode(stored)) return stored; + return DEFAULT_TIME_FORMAT_MODE; +} + +export async function saveTimeFormatMode(mode: TimeFormatMode): Promise { + await store.set(TIME_FORMAT_KEY, mode); + await store.save(); +} + function isMenubarIconStyle(value: unknown): value is MenubarIconStyle { return ( typeof value === "string" && From 67bd062858626c64b63d649b9b811098ca08cfa8 Mon Sep 17 00:00:00 2001 From: HDash <16350928+HDash@users.noreply.github.com> Date: Fri, 1 May 2026 13:35:59 +0100 Subject: [PATCH 2/4] refactor(reset-tooltip): accept time format mode in formatters Replaces the module-level RESET_TIME_FORMATTER with a cached getTimeFormatter(mode). formatResetAbsoluteLabel and formatResetTooltipText accept an optional timeFormat param (default "auto") so existing call sites stay valid. --- src/lib/reset-tooltip.test.ts | 31 +++++++++++++++++++++++++++++++ src/lib/reset-tooltip.ts | 31 +++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/lib/reset-tooltip.test.ts b/src/lib/reset-tooltip.test.ts index b3f2b438..b236df3f 100644 --- a/src/lib/reset-tooltip.test.ts +++ b/src/lib/reset-tooltip.test.ts @@ -92,4 +92,35 @@ describe("reset-tooltip", () => { }) ).toBe("Resets in 1h 5m") }) + + it("formats absolute reset labels with 12-hour time format", () => { + const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() + const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() + const label = formatResetAbsoluteLabel(nowMs, resetsAtIso, "12h") + expect(label).toBeTruthy() + expect(label).toMatch(/AM|PM/i) + expect(label).toContain("2:05") + }) + + it("formats absolute reset labels with 24-hour time format", () => { + const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() + const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() + const label = formatResetAbsoluteLabel(nowMs, resetsAtIso, "24h") + expect(label).toBeTruthy() + expect(label).not.toMatch(/AM|PM/i) + expect(label).toContain("14:05") + }) + + it("threads timeFormat through formatResetTooltipText", () => { + const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() + const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() + const label = formatResetTooltipText({ + nowMs, + resetsAtIso, + visibleMode: "relative", + timeFormat: "24h", + }) + expect(label).toContain("14:05") + expect(label).not.toMatch(/AM|PM/i) + }) }) diff --git a/src/lib/reset-tooltip.ts b/src/lib/reset-tooltip.ts index 25ae6701..39e0b59d 100644 --- a/src/lib/reset-tooltip.ts +++ b/src/lib/reset-tooltip.ts @@ -1,10 +1,19 @@ -import type { ResetTimerDisplayMode } from "@/lib/settings" +import type { ResetTimerDisplayMode, TimeFormatMode } from "@/lib/settings" import { formatCompactDuration } from "@/lib/pace-tooltip" -const RESET_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { - hour: "numeric", - minute: "2-digit", -}) +const timeFormatterCache = new Map() + +export function getTimeFormatter(mode: TimeFormatMode): Intl.DateTimeFormat { + const cached = timeFormatterCache.get(mode) + if (cached) return cached + const opts: Intl.DateTimeFormatOptions = { hour: "numeric", minute: "2-digit" } + if (mode === "12h") opts.hour12 = true + else if (mode === "24h") opts.hour12 = false + // "auto" leaves hour12 unset so the user's locale decides. + const formatter = new Intl.DateTimeFormat(undefined, opts) + timeFormatterCache.set(mode, formatter) + return formatter +} const RESET_MONTH_DAY_FORMATTER = new Intl.DateTimeFormat(undefined, { month: "short", @@ -36,12 +45,16 @@ export function formatResetRelativeLabel(nowMs: number, resetsAtIso: string): st return durationText ? `Resets in ${durationText}` : null } -export function formatResetAbsoluteLabel(nowMs: number, resetsAtIso: string): string | null { +export function formatResetAbsoluteLabel( + nowMs: number, + resetsAtIso: string, + timeFormat: TimeFormatMode = "auto", +): string | null { const resetsAtMs = parseResetTimestamp(resetsAtIso) if (resetsAtMs === null) return null if (resetsAtMs - nowMs <= 0) return "Resets soon" const dayDiff = getLocalDayIndex(resetsAtMs) - getLocalDayIndex(nowMs) - const timeText = RESET_TIME_FORMATTER.format(resetsAtMs) + const timeText = getTimeFormatter(timeFormat).format(resetsAtMs) if (dayDiff <= 0) return `Resets today at ${timeText}` if (dayDiff === 1) return `Resets tomorrow at ${timeText}` const dateText = formatMonthDay(resetsAtMs) @@ -52,12 +65,14 @@ export function formatResetTooltipText({ nowMs, resetsAtIso, visibleMode, + timeFormat = "auto", }: { nowMs: number resetsAtIso: string visibleMode: ResetTimerDisplayMode + timeFormat?: TimeFormatMode }): string | null { return visibleMode === "absolute" ? formatResetRelativeLabel(nowMs, resetsAtIso) - : formatResetAbsoluteLabel(nowMs, resetsAtIso) + : formatResetAbsoluteLabel(nowMs, resetsAtIso, timeFormat) } From 577e3d4f9959943ed8bc0ddf2e2e2d811de61699 Mon Sep 17 00:00:00 2001 From: HDash <16350928+HDash@users.noreply.github.com> Date: Fri, 1 May 2026 13:36:08 +0100 Subject: [PATCH 3/4] feat(settings): add 12h/24h/auto time format setting Adds a Time Format section to Settings (below Reset Timers) with Auto / 12-hour / 24-hour radios and live previews. Default "Auto" follows OS locale; explicit choices override. The chosen mode flows from the preferences store through app-content -> overview/provider-detail -> provider-card to the reset-tooltip formatter, so reset times in cards and tooltips render in the user's preferred clock. --- src/App.tsx | 6 +++ src/components/app/app-content.tsx | 9 ++++ src/components/provider-card.tsx | 11 ++++- src/hooks/app/use-settings-bootstrap.test.ts | 7 +++ src/hooks/app/use-settings-bootstrap.ts | 14 ++++++ .../app/use-settings-display-actions.test.ts | 21 ++++++++ src/hooks/app/use-settings-display-actions.ts | 13 +++++ src/pages/overview.tsx | 5 +- src/pages/provider-detail.tsx | 5 +- src/pages/settings.test.tsx | 32 ++++++++++++ src/pages/settings.tsx | 49 +++++++++++++++++-- src/stores/app-preferences-store.ts | 6 +++ 12 files changed, 170 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d5edc031..8018ad0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,6 +56,7 @@ function App() { setMenubarIconStyle, resetTimerDisplayMode, setResetTimerDisplayMode, + setTimeFormatMode, setGlobalShortcut, setStartOnLogin, } = useAppPreferencesStore( @@ -70,6 +71,7 @@ function App() { setMenubarIconStyle: state.setMenubarIconStyle, resetTimerDisplayMode: state.resetTimerDisplayMode, setResetTimerDisplayMode: state.setResetTimerDisplayMode, + setTimeFormatMode: state.setTimeFormatMode, setGlobalShortcut: state.setGlobalShortcut, setStartOnLogin: state.setStartOnLogin, })) @@ -118,6 +120,7 @@ function App() { setDisplayMode, setMenubarIconStyle, setResetTimerDisplayMode, + setTimeFormatMode, setGlobalShortcut, setStartOnLogin, setLoadingForPlugins, @@ -132,12 +135,14 @@ function App() { handleDisplayModeChange, handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, + handleTimeFormatModeChange, handleMenubarIconStyleChange, } = useSettingsDisplayActions({ setThemeMode, setDisplayMode, resetTimerDisplayMode, setResetTimerDisplayMode, + setTimeFormatMode, setMenubarIconStyle, scheduleTrayIconUpdate, }) @@ -246,6 +251,7 @@ function App() { onDisplayModeChange: handleDisplayModeChange, onResetTimerDisplayModeChange: handleResetTimerDisplayModeChange, onResetTimerDisplayModeToggle: handleResetTimerDisplayModeToggle, + onTimeFormatModeChange: handleTimeFormatModeChange, onMenubarIconStyleChange: handleMenubarIconStyleChange, traySettingsPreview, onGlobalShortcutChange: handleGlobalShortcutChange, diff --git a/src/components/app/app-content.tsx b/src/components/app/app-content.tsx index e362fa76..c6afaec2 100644 --- a/src/components/app/app-content.tsx +++ b/src/components/app/app-content.tsx @@ -14,6 +14,7 @@ import type { MenubarIconStyle, ResetTimerDisplayMode, ThemeMode, + TimeFormatMode, } from "@/lib/settings" type AppContentDerivedProps = { @@ -31,6 +32,7 @@ export type AppContentActionProps = { onDisplayModeChange: (mode: DisplayMode) => void onResetTimerDisplayModeChange: (mode: ResetTimerDisplayMode) => void onResetTimerDisplayModeToggle: () => void + onTimeFormatModeChange: (mode: TimeFormatMode) => void onMenubarIconStyleChange: (value: MenubarIconStyle) => void traySettingsPreview: TraySettingsPreview onGlobalShortcutChange: (value: GlobalShortcut) => void @@ -51,6 +53,7 @@ export function AppContent({ onDisplayModeChange, onResetTimerDisplayModeChange, onResetTimerDisplayModeToggle, + onTimeFormatModeChange, onMenubarIconStyleChange, traySettingsPreview, onGlobalShortcutChange, @@ -65,6 +68,7 @@ export function AppContent({ const { displayMode, resetTimerDisplayMode, + timeFormatMode, menubarIconStyle, autoUpdateInterval, globalShortcut, @@ -74,6 +78,7 @@ export function AppContent({ useShallow((state) => ({ displayMode: state.displayMode, resetTimerDisplayMode: state.resetTimerDisplayMode, + timeFormatMode: state.timeFormatMode, menubarIconStyle: state.menubarIconStyle, autoUpdateInterval: state.autoUpdateInterval, globalShortcut: state.globalShortcut, @@ -89,6 +94,7 @@ export function AppContent({ onRetryPlugin={onRetryPlugin} displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} /> ) @@ -108,6 +114,8 @@ export function AppContent({ onDisplayModeChange={onDisplayModeChange} resetTimerDisplayMode={resetTimerDisplayMode} onResetTimerDisplayModeChange={onResetTimerDisplayModeChange} + timeFormatMode={timeFormatMode} + onTimeFormatModeChange={onTimeFormatModeChange} menubarIconStyle={menubarIconStyle} onMenubarIconStyleChange={onMenubarIconStyleChange} traySettingsPreview={traySettingsPreview} @@ -129,6 +137,7 @@ export function AppContent({ onRetry={handleRetry} displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} /> ) diff --git a/src/components/provider-card.tsx b/src/components/provider-card.tsx index cc5b6c24..2ac173f0 100644 --- a/src/components/provider-card.tsx +++ b/src/components/provider-card.tsx @@ -9,7 +9,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip import { SkeletonLines } from "@/components/skeleton-lines" import { PluginError } from "@/components/plugin-error" import { useNowTicker } from "@/hooks/use-now-ticker" -import { REFRESH_COOLDOWN_MS, type DisplayMode, type ResetTimerDisplayMode } from "@/lib/settings" +import { REFRESH_COOLDOWN_MS, type DisplayMode, type ResetTimerDisplayMode, type TimeFormatMode } from "@/lib/settings" import type { ManifestLine, MetricLine, PluginLink } from "@/lib/plugin-types" import { groupLinesByType } from "@/lib/group-lines-by-type" import { clamp01, formatCountNumber, formatFixedPrecisionNumber } from "@/lib/utils" @@ -32,6 +32,7 @@ interface ProviderCardProps { scopeFilter?: "overview" | "all" displayMode: DisplayMode resetTimerDisplayMode?: ResetTimerDisplayMode + timeFormatMode?: TimeFormatMode onResetTimerDisplayModeToggle?: () => void } @@ -106,6 +107,7 @@ export function ProviderCard({ scopeFilter = "all", displayMode, resetTimerDisplayMode = "relative", + timeFormatMode = "auto", onResetTimerDisplayModeToggle, }: ProviderCardProps) { const cooldownRemainingMs = useMemo(() => { @@ -311,6 +313,7 @@ export function ProviderCard({ line={line} displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} now={now} refreshing={isRefreshingWithData} @@ -325,6 +328,7 @@ export function ProviderCard({ line={line} displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} now={now} refreshing={isRefreshingWithData} @@ -346,6 +350,7 @@ function MetricLineRenderer({ line, displayMode, resetTimerDisplayMode, + timeFormatMode, onResetTimerDisplayModeToggle, now, refreshing, @@ -353,6 +358,7 @@ function MetricLineRenderer({ line: MetricLine displayMode: DisplayMode resetTimerDisplayMode: ResetTimerDisplayMode + timeFormatMode: TimeFormatMode onResetTimerDisplayModeToggle?: () => void now: number refreshing?: boolean @@ -423,7 +429,7 @@ function MetricLineRenderer({ const resetLabel = line.resetsAt ? resetTimerDisplayMode === "absolute" - ? formatResetAbsoluteLabel(now, line.resetsAt) + ? formatResetAbsoluteLabel(now, line.resetsAt, timeFormatMode) : formatResetRelativeLabel(now, line.resetsAt) : null const resetTooltipText = line.resetsAt @@ -431,6 +437,7 @@ function MetricLineRenderer({ nowMs: now, resetsAtIso: line.resetsAt, visibleMode: resetTimerDisplayMode, + timeFormat: timeFormatMode, }) : null diff --git a/src/hooks/app/use-settings-bootstrap.test.ts b/src/hooks/app/use-settings-bootstrap.test.ts index 9eae9816..7a5d0679 100644 --- a/src/hooks/app/use-settings-bootstrap.test.ts +++ b/src/hooks/app/use-settings-bootstrap.test.ts @@ -17,6 +17,7 @@ const { loadResetTimerDisplayModeMock, loadStartOnLoginMock, loadThemeModeMock, + loadTimeFormatModeMock, migrateLegacyTraySettingsMock, normalizePluginSettingsMock, savePluginSettingsMock, @@ -36,6 +37,7 @@ const { loadResetTimerDisplayModeMock: vi.fn(), loadStartOnLoginMock: vi.fn(), loadThemeModeMock: vi.fn(), + loadTimeFormatModeMock: vi.fn(), migrateLegacyTraySettingsMock: vi.fn(), normalizePluginSettingsMock: vi.fn(), savePluginSettingsMock: vi.fn(), @@ -61,6 +63,7 @@ vi.mock("@/lib/settings", () => ({ DEFAULT_RESET_TIMER_DISPLAY_MODE: "relative", DEFAULT_START_ON_LOGIN: false, DEFAULT_THEME_MODE: "system", + DEFAULT_TIME_FORMAT_MODE: "auto", getEnabledPluginIds: getEnabledPluginIdsMock, loadAutoUpdateInterval: loadAutoUpdateIntervalMock, loadDisplayMode: loadDisplayModeMock, @@ -70,6 +73,7 @@ vi.mock("@/lib/settings", () => ({ loadResetTimerDisplayMode: loadResetTimerDisplayModeMock, loadStartOnLogin: loadStartOnLoginMock, loadThemeMode: loadThemeModeMock, + loadTimeFormatMode: loadTimeFormatModeMock, migrateLegacyTraySettings: migrateLegacyTraySettingsMock, normalizePluginSettings: normalizePluginSettingsMock, savePluginSettings: savePluginSettingsMock, @@ -85,6 +89,7 @@ function createArgs() { setThemeMode: vi.fn(), setDisplayMode: vi.fn(), setResetTimerDisplayMode: vi.fn(), + setTimeFormatMode: vi.fn(), setGlobalShortcut: vi.fn(), setStartOnLogin: vi.fn(), setMenubarIconStyle: vi.fn(), @@ -111,6 +116,7 @@ describe("useSettingsBootstrap", () => { loadResetTimerDisplayModeMock.mockReset() loadStartOnLoginMock.mockReset() loadThemeModeMock.mockReset() + loadTimeFormatModeMock.mockReset() migrateLegacyTraySettingsMock.mockReset() normalizePluginSettingsMock.mockReset() savePluginSettingsMock.mockReset() @@ -134,6 +140,7 @@ describe("useSettingsBootstrap", () => { loadThemeModeMock.mockResolvedValue("dark") loadDisplayModeMock.mockResolvedValue("used") loadResetTimerDisplayModeMock.mockResolvedValue("relative") + loadTimeFormatModeMock.mockResolvedValue("auto") loadGlobalShortcutMock.mockResolvedValue("CommandOrControl+Shift+O") loadMenubarIconStyleMock.mockResolvedValue("provider") loadStartOnLoginMock.mockResolvedValue(true) diff --git a/src/hooks/app/use-settings-bootstrap.ts b/src/hooks/app/use-settings-bootstrap.ts index fcc7df09..6468fe20 100644 --- a/src/hooks/app/use-settings-bootstrap.ts +++ b/src/hooks/app/use-settings-bootstrap.ts @@ -15,6 +15,7 @@ import { DEFAULT_RESET_TIMER_DISPLAY_MODE, DEFAULT_START_ON_LOGIN, DEFAULT_THEME_MODE, + DEFAULT_TIME_FORMAT_MODE, getEnabledPluginIds, loadAutoUpdateInterval, loadDisplayMode, @@ -25,6 +26,7 @@ import { loadResetTimerDisplayMode, loadStartOnLogin, loadThemeMode, + loadTimeFormatMode, normalizePluginSettings, savePluginSettings, type AutoUpdateIntervalMinutes, @@ -34,6 +36,7 @@ import { type PluginSettings, type ResetTimerDisplayMode, type ThemeMode, + type TimeFormatMode, } from "@/lib/settings" type UseSettingsBootstrapArgs = { @@ -43,6 +46,7 @@ type UseSettingsBootstrapArgs = { setThemeMode: (value: ThemeMode) => void setDisplayMode: (value: DisplayMode) => void setResetTimerDisplayMode: (value: ResetTimerDisplayMode) => void + setTimeFormatMode: (value: TimeFormatMode) => void setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void @@ -58,6 +62,7 @@ export function useSettingsBootstrap({ setThemeMode, setDisplayMode, setResetTimerDisplayMode, + setTimeFormatMode, setGlobalShortcut, setStartOnLogin, setMenubarIconStyle, @@ -121,6 +126,13 @@ export function useSettingsBootstrap({ console.error("Failed to load reset timer display mode:", error) } + let storedTimeFormatMode = DEFAULT_TIME_FORMAT_MODE + try { + storedTimeFormatMode = await loadTimeFormatMode() + } catch (error) { + console.error("Failed to load time format mode:", error) + } + let storedGlobalShortcut = DEFAULT_GLOBAL_SHORTCUT try { storedGlobalShortcut = await loadGlobalShortcut() @@ -159,6 +171,7 @@ export function useSettingsBootstrap({ setThemeMode(storedThemeMode) setDisplayMode(storedDisplayMode) setResetTimerDisplayMode(storedResetTimerDisplayMode) + setTimeFormatMode(storedTimeFormatMode) setGlobalShortcut(storedGlobalShortcut) setStartOnLogin(storedStartOnLogin) setMenubarIconStyle(storedMenubarIconStyle) @@ -198,6 +211,7 @@ export function useSettingsBootstrap({ setResetTimerDisplayMode, setStartOnLogin, setThemeMode, + setTimeFormatMode, startBatch, ]) diff --git a/src/hooks/app/use-settings-display-actions.test.ts b/src/hooks/app/use-settings-display-actions.test.ts index 7d0db6a4..c939bb74 100644 --- a/src/hooks/app/use-settings-display-actions.test.ts +++ b/src/hooks/app/use-settings-display-actions.test.ts @@ -6,11 +6,13 @@ const { saveDisplayModeMock, saveResetTimerDisplayModeMock, saveThemeModeMock, + saveTimeFormatModeMock, } = vi.hoisted(() => ({ trackMock: vi.fn(), saveThemeModeMock: vi.fn(), saveDisplayModeMock: vi.fn(), saveResetTimerDisplayModeMock: vi.fn(), + saveTimeFormatModeMock: vi.fn(), })) vi.mock("@/lib/analytics", () => ({ @@ -21,6 +23,7 @@ vi.mock("@/lib/settings", () => ({ saveThemeMode: saveThemeModeMock, saveDisplayMode: saveDisplayModeMock, saveResetTimerDisplayMode: saveResetTimerDisplayModeMock, + saveTimeFormatMode: saveTimeFormatModeMock, })) import { useSettingsDisplayActions } from "@/hooks/app/use-settings-display-actions" @@ -31,15 +34,18 @@ describe("useSettingsDisplayActions", () => { saveThemeModeMock.mockReset() saveDisplayModeMock.mockReset() saveResetTimerDisplayModeMock.mockReset() + saveTimeFormatModeMock.mockReset() saveThemeModeMock.mockResolvedValue(undefined) saveDisplayModeMock.mockResolvedValue(undefined) saveResetTimerDisplayModeMock.mockResolvedValue(undefined) + saveTimeFormatModeMock.mockResolvedValue(undefined) }) it("tracks and applies display-related setting changes", () => { const setThemeMode = vi.fn() const setDisplayMode = vi.fn() const setResetTimerDisplayMode = vi.fn() + const setTimeFormatMode = vi.fn() const scheduleTrayIconUpdate = vi.fn() const { result } = renderHook(() => @@ -48,6 +54,7 @@ describe("useSettingsDisplayActions", () => { setDisplayMode, resetTimerDisplayMode: "relative", setResetTimerDisplayMode, + setTimeFormatMode, scheduleTrayIconUpdate, }) ) @@ -56,6 +63,7 @@ describe("useSettingsDisplayActions", () => { result.current.handleThemeModeChange("dark") result.current.handleDisplayModeChange("used") result.current.handleResetTimerDisplayModeChange("absolute") + result.current.handleTimeFormatModeChange("24h") }) expect(trackMock).toHaveBeenCalledWith("setting_changed", { setting: "theme", value: "dark" }) @@ -67,15 +75,21 @@ describe("useSettingsDisplayActions", () => { setting: "reset_timer_display_mode", value: "absolute", }) + expect(trackMock).toHaveBeenCalledWith("setting_changed", { + setting: "time_format_mode", + value: "24h", + }) expect(setThemeMode).toHaveBeenCalledWith("dark") expect(setDisplayMode).toHaveBeenCalledWith("used") expect(setResetTimerDisplayMode).toHaveBeenCalledWith("absolute") + expect(setTimeFormatMode).toHaveBeenCalledWith("24h") expect(scheduleTrayIconUpdate).toHaveBeenCalledWith("settings", 0) expect(saveThemeModeMock).toHaveBeenCalledWith("dark") expect(saveDisplayModeMock).toHaveBeenCalledWith("used") expect(saveResetTimerDisplayModeMock).toHaveBeenCalledWith("absolute") + expect(saveTimeFormatModeMock).toHaveBeenCalledWith("24h") }) it("toggles reset timer mode in both directions", () => { @@ -88,6 +102,7 @@ describe("useSettingsDisplayActions", () => { setDisplayMode: vi.fn(), resetTimerDisplayMode: mode, setResetTimerDisplayMode, + setTimeFormatMode: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }), { initialProps: { mode: "relative" as const } } @@ -114,12 +129,16 @@ describe("useSettingsDisplayActions", () => { saveDisplayModeMock.mockRejectedValueOnce(displayError) saveResetTimerDisplayModeMock.mockRejectedValueOnce(resetError) + const timeFormatError = new Error("time format failed") + saveTimeFormatModeMock.mockRejectedValueOnce(timeFormatError) + const { result } = renderHook(() => useSettingsDisplayActions({ setThemeMode: vi.fn(), setDisplayMode: vi.fn(), resetTimerDisplayMode: "relative", setResetTimerDisplayMode: vi.fn(), + setTimeFormatMode: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }) ) @@ -128,12 +147,14 @@ describe("useSettingsDisplayActions", () => { result.current.handleThemeModeChange("light") result.current.handleDisplayModeChange("left") result.current.handleResetTimerDisplayModeChange("relative") + result.current.handleTimeFormatModeChange("12h") }) await waitFor(() => { expect(errorSpy).toHaveBeenCalledWith("Failed to save theme mode:", themeError) expect(errorSpy).toHaveBeenCalledWith("Failed to save display mode:", displayError) expect(errorSpy).toHaveBeenCalledWith("Failed to save reset timer display mode:", resetError) + expect(errorSpy).toHaveBeenCalledWith("Failed to save time format mode:", timeFormatError) }) errorSpy.mockRestore() diff --git a/src/hooks/app/use-settings-display-actions.ts b/src/hooks/app/use-settings-display-actions.ts index 65dcc886..b75181ac 100644 --- a/src/hooks/app/use-settings-display-actions.ts +++ b/src/hooks/app/use-settings-display-actions.ts @@ -5,10 +5,12 @@ import { saveMenubarIconStyle, saveResetTimerDisplayMode, saveThemeMode, + saveTimeFormatMode, type DisplayMode, type MenubarIconStyle, type ResetTimerDisplayMode, type ThemeMode, + type TimeFormatMode, } from "@/lib/settings" type ScheduleTrayIconUpdate = (reason: "probe" | "settings" | "init", delayMs?: number) => void @@ -18,6 +20,7 @@ type UseSettingsDisplayActionsArgs = { setDisplayMode: (value: DisplayMode) => void resetTimerDisplayMode: ResetTimerDisplayMode setResetTimerDisplayMode: (value: ResetTimerDisplayMode) => void + setTimeFormatMode: (value: TimeFormatMode) => void setMenubarIconStyle: (value: MenubarIconStyle) => void scheduleTrayIconUpdate: ScheduleTrayIconUpdate } @@ -27,6 +30,7 @@ export function useSettingsDisplayActions({ setDisplayMode, resetTimerDisplayMode, setResetTimerDisplayMode, + setTimeFormatMode, setMenubarIconStyle, scheduleTrayIconUpdate, }: UseSettingsDisplayActionsArgs) { @@ -60,6 +64,14 @@ export function useSettingsDisplayActions({ handleResetTimerDisplayModeChange(next) }, [handleResetTimerDisplayModeChange, resetTimerDisplayMode]) + const handleTimeFormatModeChange = useCallback((mode: TimeFormatMode) => { + track("setting_changed", { setting: "time_format_mode", value: mode }) + setTimeFormatMode(mode) + void saveTimeFormatMode(mode).catch((error) => { + console.error("Failed to save time format mode:", error) + }) + }, [setTimeFormatMode]) + const handleMenubarIconStyleChange = useCallback((style: MenubarIconStyle) => { track("setting_changed", { setting: "menubar_icon_style", value: style }) setMenubarIconStyle(style) @@ -74,6 +86,7 @@ export function useSettingsDisplayActions({ handleDisplayModeChange, handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, + handleTimeFormatModeChange, handleMenubarIconStyleChange, } } diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx index 4fd401b5..d811d18a 100644 --- a/src/pages/overview.tsx +++ b/src/pages/overview.tsx @@ -1,12 +1,13 @@ import { ProviderCard } from "@/components/provider-card" import type { PluginDisplayState } from "@/lib/plugin-types" -import type { DisplayMode, ResetTimerDisplayMode } from "@/lib/settings" +import type { DisplayMode, ResetTimerDisplayMode, TimeFormatMode } from "@/lib/settings" interface OverviewPageProps { plugins: PluginDisplayState[] onRetryPlugin?: (pluginId: string) => void displayMode: DisplayMode resetTimerDisplayMode: ResetTimerDisplayMode + timeFormatMode?: TimeFormatMode onResetTimerDisplayModeToggle?: () => void } @@ -15,6 +16,7 @@ export function OverviewPage({ onRetryPlugin, displayMode, resetTimerDisplayMode, + timeFormatMode = "auto", onResetTimerDisplayModeToggle, }: OverviewPageProps) { if (plugins.length === 0) { @@ -43,6 +45,7 @@ export function OverviewPage({ scopeFilter="overview" displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} /> ))} diff --git a/src/pages/provider-detail.tsx b/src/pages/provider-detail.tsx index 670ee504..43a655d6 100644 --- a/src/pages/provider-detail.tsx +++ b/src/pages/provider-detail.tsx @@ -1,12 +1,13 @@ import { ProviderCard } from "@/components/provider-card" import type { PluginDisplayState } from "@/lib/plugin-types" -import type { DisplayMode, ResetTimerDisplayMode } from "@/lib/settings" +import type { DisplayMode, ResetTimerDisplayMode, TimeFormatMode } from "@/lib/settings" interface ProviderDetailPageProps { plugin: PluginDisplayState | null onRetry?: () => void displayMode: DisplayMode resetTimerDisplayMode: ResetTimerDisplayMode + timeFormatMode?: TimeFormatMode onResetTimerDisplayModeToggle?: () => void } @@ -15,6 +16,7 @@ export function ProviderDetailPage({ onRetry, displayMode, resetTimerDisplayMode, + timeFormatMode = "auto", onResetTimerDisplayModeToggle, }: ProviderDetailPageProps) { if (!plugin) { @@ -41,6 +43,7 @@ export function ProviderDetailPage({ scopeFilter="all" displayMode={displayMode} resetTimerDisplayMode={resetTimerDisplayMode} + timeFormatMode={timeFormatMode} onResetTimerDisplayModeToggle={onResetTimerDisplayModeToggle} /> ) diff --git a/src/pages/settings.test.tsx b/src/pages/settings.test.tsx index 9139f79e..4ac7452c 100644 --- a/src/pages/settings.test.tsx +++ b/src/pages/settings.test.tsx @@ -55,6 +55,8 @@ const defaultProps = { onDisplayModeChange: vi.fn(), resetTimerDisplayMode: "relative" as const, onResetTimerDisplayModeChange: vi.fn(), + timeFormatMode: "auto" as const, + onTimeFormatModeChange: vi.fn(), menubarIconStyle: "provider" as const, onMenubarIconStyleChange: vi.fn(), traySettingsPreview: { @@ -191,6 +193,36 @@ describe("SettingsPage", () => { expect(screen.getByText("Reset Timers")).toBeInTheDocument() }) + it("renders time format section heading", () => { + render() + expect(screen.getByText("Time Format")).toBeInTheDocument() + expect(screen.getByText("12-hour or 24-hour clock")).toBeInTheDocument() + }) + + it("updates time format mode to 12h", async () => { + const onTimeFormatModeChange = vi.fn() + render( + + ) + await userEvent.click(screen.getByRole("radio", { name: "12-hour" })) + expect(onTimeFormatModeChange).toHaveBeenCalledWith("12h") + }) + + it("updates time format mode to 24h", async () => { + const onTimeFormatModeChange = vi.fn() + render( + + ) + await userEvent.click(screen.getByRole("radio", { name: "24-hour" })) + expect(onTimeFormatModeChange).toHaveBeenCalledWith("24h") + }) + it("renders menubar icon section", () => { render() expect(screen.getByText("Menubar Icon")).toBeInTheDocument() diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index bc257202..a013fed4 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -26,13 +26,16 @@ import { MENUBAR_ICON_STYLE_OPTIONS, RESET_TIMER_DISPLAY_OPTIONS, THEME_OPTIONS, + TIME_FORMAT_OPTIONS, type AutoUpdateIntervalMinutes, type DisplayMode, type GlobalShortcut, type MenubarIconStyle, type ResetTimerDisplayMode, type ThemeMode, + type TimeFormatMode, } from "@/lib/settings"; +import { getTimeFormatter } from "@/lib/reset-tooltip"; import type { TraySettingsPreview } from "@/hooks/app/use-tray-icon"; import { cn } from "@/lib/utils"; @@ -268,6 +271,8 @@ interface SettingsPageProps { onDisplayModeChange: (value: DisplayMode) => void; resetTimerDisplayMode: ResetTimerDisplayMode; onResetTimerDisplayModeChange: (value: ResetTimerDisplayMode) => void; + timeFormatMode: TimeFormatMode; + onTimeFormatModeChange: (value: TimeFormatMode) => void; menubarIconStyle: MenubarIconStyle; onMenubarIconStyleChange: (value: MenubarIconStyle) => void; traySettingsPreview: TraySettingsPreview; @@ -289,6 +294,8 @@ export function SettingsPage({ onDisplayModeChange, resetTimerDisplayMode, onResetTimerDisplayModeChange, + timeFormatMode, + onTimeFormatModeChange, menubarIconStyle, onMenubarIconStyleChange, traySettingsPreview, @@ -381,10 +388,7 @@ export function SettingsPage({
{RESET_TIMER_DISPLAY_OPTIONS.map((option) => { const isActive = option.value === resetTimerDisplayMode; - const absoluteTimeExample = new Intl.DateTimeFormat(undefined, { - hour: "numeric", - minute: "2-digit", - }).format(new Date(2026, 1, 2, 11, 4)); + const absoluteTimeExample = getTimeFormatter(timeFormatMode).format(new Date(2026, 1, 2, 11, 4)); const example = option.value === "relative" ? "5h 12m" : `today at ${absoluteTimeExample}`; return (
+
+

Time Format

+

+ 12-hour or 24-hour clock +

+
+
+ {TIME_FORMAT_OPTIONS.map((option) => { + const isActive = option.value === timeFormatMode; + const example = getTimeFormatter(option.value).format(new Date(2026, 1, 2, 11, 4)); + return ( + + ); + })} +
+
+

Menubar Icon

diff --git a/src/stores/app-preferences-store.ts b/src/stores/app-preferences-store.ts index 98ced539..07a7f4be 100644 --- a/src/stores/app-preferences-store.ts +++ b/src/stores/app-preferences-store.ts @@ -7,12 +7,14 @@ import { DEFAULT_RESET_TIMER_DISPLAY_MODE, DEFAULT_START_ON_LOGIN, DEFAULT_THEME_MODE, + DEFAULT_TIME_FORMAT_MODE, type AutoUpdateIntervalMinutes, type DisplayMode, type GlobalShortcut, type MenubarIconStyle, type ResetTimerDisplayMode, type ThemeMode, + type TimeFormatMode, } from "@/lib/settings" type AppPreferencesStore = { @@ -20,6 +22,7 @@ type AppPreferencesStore = { themeMode: ThemeMode displayMode: DisplayMode resetTimerDisplayMode: ResetTimerDisplayMode + timeFormatMode: TimeFormatMode globalShortcut: GlobalShortcut startOnLogin: boolean menubarIconStyle: MenubarIconStyle @@ -27,6 +30,7 @@ type AppPreferencesStore = { setThemeMode: (value: ThemeMode) => void setDisplayMode: (value: DisplayMode) => void setResetTimerDisplayMode: (value: ResetTimerDisplayMode) => void + setTimeFormatMode: (value: TimeFormatMode) => void setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void @@ -38,6 +42,7 @@ const initialState = { themeMode: DEFAULT_THEME_MODE, displayMode: DEFAULT_DISPLAY_MODE, resetTimerDisplayMode: DEFAULT_RESET_TIMER_DISPLAY_MODE, + timeFormatMode: DEFAULT_TIME_FORMAT_MODE, globalShortcut: DEFAULT_GLOBAL_SHORTCUT, startOnLogin: DEFAULT_START_ON_LOGIN, menubarIconStyle: DEFAULT_MENUBAR_ICON_STYLE, @@ -49,6 +54,7 @@ export const useAppPreferencesStore = create((set) => ({ setThemeMode: (value) => set({ themeMode: value }), setDisplayMode: (value) => set({ displayMode: value }), setResetTimerDisplayMode: (value) => set({ resetTimerDisplayMode: value }), + setTimeFormatMode: (value) => set({ timeFormatMode: value }), setGlobalShortcut: (value) => set({ globalShortcut: value }), setStartOnLogin: (value) => set({ startOnLogin: value }), setMenubarIconStyle: (value) => set({ menubarIconStyle: value }), From 456d8d2263ab4c2c726c4ccd07659bed6d525f9c Mon Sep 17 00:00:00 2001 From: HDash <16350928+HDash@users.noreply.github.com> Date: Fri, 1 May 2026 13:51:53 +0100 Subject: [PATCH 4/4] test(time-format): address PR #427 review feedback - Rename TIME_FORMAT_KEY value "timeFormat" -> "timeFormatMode" to match the themeMode/displayMode/resetTimerDisplayMode pattern. - Make the new reset-tooltip tests locale-independent by deriving expected strings via getTimeFormatter(mode) instead of hardcoded "AM/PM" / "14:05" literals. - Add the missing setMenubarIconStyle stub to the three useSettingsDisplayActions test sites so test inputs match the hook signature. --- .../app/use-settings-display-actions.test.ts | 3 ++ src/lib/reset-tooltip.test.ts | 48 +++++++++---------- src/lib/settings.test.ts | 4 +- src/lib/settings.ts | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/hooks/app/use-settings-display-actions.test.ts b/src/hooks/app/use-settings-display-actions.test.ts index c939bb74..6f0c7832 100644 --- a/src/hooks/app/use-settings-display-actions.test.ts +++ b/src/hooks/app/use-settings-display-actions.test.ts @@ -55,6 +55,7 @@ describe("useSettingsDisplayActions", () => { resetTimerDisplayMode: "relative", setResetTimerDisplayMode, setTimeFormatMode, + setMenubarIconStyle: vi.fn(), scheduleTrayIconUpdate, }) ) @@ -103,6 +104,7 @@ describe("useSettingsDisplayActions", () => { resetTimerDisplayMode: mode, setResetTimerDisplayMode, setTimeFormatMode: vi.fn(), + setMenubarIconStyle: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }), { initialProps: { mode: "relative" as const } } @@ -139,6 +141,7 @@ describe("useSettingsDisplayActions", () => { resetTimerDisplayMode: "relative", setResetTimerDisplayMode: vi.fn(), setTimeFormatMode: vi.fn(), + setMenubarIconStyle: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }) ) diff --git a/src/lib/reset-tooltip.test.ts b/src/lib/reset-tooltip.test.ts index b236df3f..ab634bba 100644 --- a/src/lib/reset-tooltip.test.ts +++ b/src/lib/reset-tooltip.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest" -import { formatResetAbsoluteLabel, formatResetRelativeLabel, formatResetTooltipText } from "@/lib/reset-tooltip" +import { formatResetAbsoluteLabel, formatResetRelativeLabel, formatResetTooltipText, getTimeFormatter } from "@/lib/reset-tooltip" describe("reset-tooltip", () => { it("returns null for invalid reset timestamp", () => { @@ -93,34 +93,32 @@ describe("reset-tooltip", () => { ).toBe("Resets in 1h 5m") }) - it("formats absolute reset labels with 12-hour time format", () => { + it("formats absolute reset labels differently for 12h vs 24h", () => { const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() - const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() - const label = formatResetAbsoluteLabel(nowMs, resetsAtIso, "12h") - expect(label).toBeTruthy() - expect(label).toMatch(/AM|PM/i) - expect(label).toContain("2:05") - }) - - it("formats absolute reset labels with 24-hour time format", () => { - const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() - const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() - const label = formatResetAbsoluteLabel(nowMs, resetsAtIso, "24h") - expect(label).toBeTruthy() - expect(label).not.toMatch(/AM|PM/i) - expect(label).toContain("14:05") + const resetsAtMs = new Date(2026, 1, 3, 14, 5, 0).getTime() + const resetsAtIso = new Date(resetsAtMs).toISOString() + const expected12 = getTimeFormatter("12h").format(resetsAtMs) + const expected24 = getTimeFormatter("24h").format(resetsAtMs) + + expect(formatResetAbsoluteLabel(nowMs, resetsAtIso, "12h")).toBe(`Resets today at ${expected12}`) + expect(formatResetAbsoluteLabel(nowMs, resetsAtIso, "24h")).toBe(`Resets today at ${expected24}`) + // Sanity: the two formats differ at this hour. + expect(expected12).not.toBe(expected24) }) it("threads timeFormat through formatResetTooltipText", () => { const nowMs = new Date(2026, 1, 3, 0, 0, 0).getTime() - const resetsAtIso = new Date(2026, 1, 3, 14, 5, 0).toISOString() - const label = formatResetTooltipText({ - nowMs, - resetsAtIso, - visibleMode: "relative", - timeFormat: "24h", - }) - expect(label).toContain("14:05") - expect(label).not.toMatch(/AM|PM/i) + const resetsAtMs = new Date(2026, 1, 3, 14, 5, 0).getTime() + const resetsAtIso = new Date(resetsAtMs).toISOString() + const expected24 = getTimeFormatter("24h").format(resetsAtMs) + + expect( + formatResetTooltipText({ + nowMs, + resetsAtIso, + visibleMode: "relative", + timeFormat: "24h", + }) + ).toBe(`Resets today at ${expected24}`) }) }) diff --git a/src/lib/settings.test.ts b/src/lib/settings.test.ts index 4e496a03..27c5995b 100644 --- a/src/lib/settings.test.ts +++ b/src/lib/settings.test.ts @@ -193,7 +193,7 @@ describe("settings", () => { }) it("loads stored time format mode", async () => { - storeState.set("timeFormat", "24h") + storeState.set("timeFormatMode", "24h") await expect(loadTimeFormatMode()).resolves.toBe("24h") }) @@ -203,7 +203,7 @@ describe("settings", () => { }) it("falls back to default for invalid time format mode", async () => { - storeState.set("timeFormat", "invalid") + storeState.set("timeFormatMode", "invalid") await expect(loadTimeFormatMode()).resolves.toBe(DEFAULT_TIME_FORMAT_MODE) }) diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 253e52ae..e9dde8b4 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -30,7 +30,7 @@ const AUTO_UPDATE_SETTINGS_KEY = "autoUpdateInterval"; const THEME_MODE_KEY = "themeMode"; const DISPLAY_MODE_KEY = "displayMode"; const RESET_TIMER_DISPLAY_MODE_KEY = "resetTimerDisplayMode"; -const TIME_FORMAT_KEY = "timeFormat"; +const TIME_FORMAT_KEY = "timeFormatMode"; const MENUBAR_ICON_STYLE_KEY = "menubarIconStyle"; const LEGACY_TRAY_ICON_STYLE_KEY = "trayIconStyle"; const LEGACY_TRAY_SHOW_PERCENTAGE_KEY = "trayShowPercentage";