From 32c5c2a00b83732d07b85c3fdb53395887f94562 Mon Sep 17 00:00:00 2001 From: Ariful Alam Date: Mon, 16 Mar 2026 16:45:19 +0600 Subject: [PATCH 1/4] feat: add compact mode setting Adds a Compact Mode toggle to the Settings panel that reduces font size and spacing across the entire UI for users who prefer a denser layout. - Persist compact mode preference via Tauri LazyStore (settings.json) - Apply/remove `compact` class on via useSettingsCompact hook - Scale all rem-based font sizes and spacing down with a single CSS rule - Wire setting through store, bootstrap, display actions, and settings page --- src/App.tsx | 10 ++++++++++ src/components/app/app-content.tsx | 6 ++++++ src/hooks/app/use-settings-bootstrap.ts | 13 +++++++++++++ src/hooks/app/use-settings-compact.ts | 7 +++++++ src/hooks/app/use-settings-display-actions.ts | 12 ++++++++++++ src/index.css | 5 +++++ src/lib/settings.ts | 13 +++++++++++++ src/pages/settings.tsx | 18 ++++++++++++++++++ src/stores/app-preferences-store.ts | 5 +++++ 9 files changed, 89 insertions(+) create mode 100644 src/hooks/app/use-settings-compact.ts diff --git a/src/App.tsx b/src/App.tsx index d5edc031..55196d9f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { useSettingsPluginActions } from "@/hooks/app/use-settings-plugin-action import { useSettingsPluginList } from "@/hooks/app/use-settings-plugin-list" import { useSettingsSystemActions } from "@/hooks/app/use-settings-system-actions" import { useSettingsTheme } from "@/hooks/app/use-settings-theme" +import { useSettingsCompact } from "@/hooks/app/use-settings-compact" import { useTrayIcon } from "@/hooks/app/use-tray-icon" import { track } from "@/lib/analytics" import { REFRESH_COOLDOWN_MS, savePluginSettings } from "@/lib/settings" @@ -58,6 +59,8 @@ function App() { setResetTimerDisplayMode, setGlobalShortcut, setStartOnLogin, + compactMode, + setCompactMode, } = useAppPreferencesStore( useShallow((state) => ({ autoUpdateInterval: state.autoUpdateInterval, @@ -72,6 +75,8 @@ function App() { setResetTimerDisplayMode: state.setResetTimerDisplayMode, setGlobalShortcut: state.setGlobalShortcut, setStartOnLogin: state.setStartOnLogin, + compactMode: state.compactMode, + setCompactMode: state.setCompactMode, })) ) @@ -120,12 +125,14 @@ function App() { setResetTimerDisplayMode, setGlobalShortcut, setStartOnLogin, + setCompactMode, setLoadingForPlugins, setErrorForPlugins, startBatch, }) useSettingsTheme(themeMode) + useSettingsCompact(compactMode) const { handleThemeModeChange, @@ -133,12 +140,14 @@ function App() { handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, handleMenubarIconStyleChange, + handleCompactModeChange, } = useSettingsDisplayActions({ setThemeMode, setDisplayMode, resetTimerDisplayMode, setResetTimerDisplayMode, setMenubarIconStyle, + setCompactMode, scheduleTrayIconUpdate, }) @@ -250,6 +259,7 @@ function App() { traySettingsPreview, onGlobalShortcutChange: handleGlobalShortcutChange, onStartOnLoginChange: handleStartOnLoginChange, + onCompactModeChange: handleCompactModeChange, }} /> ) diff --git a/src/components/app/app-content.tsx b/src/components/app/app-content.tsx index e362fa76..53e4e22e 100644 --- a/src/components/app/app-content.tsx +++ b/src/components/app/app-content.tsx @@ -35,6 +35,7 @@ export type AppContentActionProps = { traySettingsPreview: TraySettingsPreview onGlobalShortcutChange: (value: GlobalShortcut) => void onStartOnLoginChange: (value: boolean) => void + onCompactModeChange: (value: boolean) => void } export type AppContentProps = AppContentDerivedProps & AppContentActionProps @@ -55,6 +56,7 @@ export function AppContent({ traySettingsPreview, onGlobalShortcutChange, onStartOnLoginChange, + onCompactModeChange, }: AppContentProps) { const { activeView } = useAppUiStore( useShallow((state) => ({ @@ -70,6 +72,7 @@ export function AppContent({ globalShortcut, themeMode, startOnLogin, + compactMode, } = useAppPreferencesStore( useShallow((state) => ({ displayMode: state.displayMode, @@ -79,6 +82,7 @@ export function AppContent({ globalShortcut: state.globalShortcut, themeMode: state.themeMode, startOnLogin: state.startOnLogin, + compactMode: state.compactMode, })) ) @@ -115,6 +119,8 @@ export function AppContent({ onGlobalShortcutChange={onGlobalShortcutChange} startOnLogin={startOnLogin} onStartOnLoginChange={onStartOnLoginChange} + compactMode={compactMode} + onCompactModeChange={onCompactModeChange} /> ) } diff --git a/src/hooks/app/use-settings-bootstrap.ts b/src/hooks/app/use-settings-bootstrap.ts index fcc7df09..52deae09 100644 --- a/src/hooks/app/use-settings-bootstrap.ts +++ b/src/hooks/app/use-settings-bootstrap.ts @@ -9,6 +9,7 @@ import type { PluginMeta } from "@/lib/plugin-types" import { arePluginSettingsEqual, DEFAULT_AUTO_UPDATE_INTERVAL, + DEFAULT_COMPACT_MODE, DEFAULT_DISPLAY_MODE, DEFAULT_GLOBAL_SHORTCUT, DEFAULT_MENUBAR_ICON_STYLE, @@ -17,6 +18,7 @@ import { DEFAULT_THEME_MODE, getEnabledPluginIds, loadAutoUpdateInterval, + loadCompactMode, loadDisplayMode, loadGlobalShortcut, loadMenubarIconStyle, @@ -46,6 +48,7 @@ type UseSettingsBootstrapArgs = { setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void + setCompactMode: (value: boolean) => void setLoadingForPlugins: (ids: string[]) => void setErrorForPlugins: (ids: string[], error: string) => void startBatch: (pluginIds?: string[]) => Promise @@ -61,6 +64,7 @@ export function useSettingsBootstrap({ setGlobalShortcut, setStartOnLogin, setMenubarIconStyle, + setCompactMode, setLoadingForPlugins, setErrorForPlugins, startBatch, @@ -153,6 +157,13 @@ export function useSettingsBootstrap({ console.error("Failed to load menubar icon style:", error) } + let storedCompactMode = DEFAULT_COMPACT_MODE + try { + storedCompactMode = await loadCompactMode() + } catch (error) { + console.error("Failed to load compact mode:", error) + } + if (isMounted) { setPluginSettings(normalized) setAutoUpdateInterval(storedInterval) @@ -162,6 +173,7 @@ export function useSettingsBootstrap({ setGlobalShortcut(storedGlobalShortcut) setStartOnLogin(storedStartOnLogin) setMenubarIconStyle(storedMenubarIconStyle) + setCompactMode(storedCompactMode) const enabledIds = getEnabledPluginIds(normalized) setLoadingForPlugins(enabledIds) @@ -187,6 +199,7 @@ export function useSettingsBootstrap({ }, [ applyStartOnLogin, setAutoUpdateInterval, + setCompactMode, setDisplayMode, setErrorForPlugins, setGlobalShortcut, diff --git a/src/hooks/app/use-settings-compact.ts b/src/hooks/app/use-settings-compact.ts new file mode 100644 index 00000000..e9160a73 --- /dev/null +++ b/src/hooks/app/use-settings-compact.ts @@ -0,0 +1,7 @@ +import { useEffect } from "react" + +export function useSettingsCompact(compactMode: boolean) { + useEffect(() => { + document.documentElement.classList.toggle("compact", compactMode) + }, [compactMode]) +} diff --git a/src/hooks/app/use-settings-display-actions.ts b/src/hooks/app/use-settings-display-actions.ts index 65dcc886..f12cacc0 100644 --- a/src/hooks/app/use-settings-display-actions.ts +++ b/src/hooks/app/use-settings-display-actions.ts @@ -1,6 +1,7 @@ import { useCallback } from "react" import { track } from "@/lib/analytics" import { + saveCompactMode, saveDisplayMode, saveMenubarIconStyle, saveResetTimerDisplayMode, @@ -19,6 +20,7 @@ type UseSettingsDisplayActionsArgs = { resetTimerDisplayMode: ResetTimerDisplayMode setResetTimerDisplayMode: (value: ResetTimerDisplayMode) => void setMenubarIconStyle: (value: MenubarIconStyle) => void + setCompactMode: (value: boolean) => void scheduleTrayIconUpdate: ScheduleTrayIconUpdate } @@ -28,6 +30,7 @@ export function useSettingsDisplayActions({ resetTimerDisplayMode, setResetTimerDisplayMode, setMenubarIconStyle, + setCompactMode, scheduleTrayIconUpdate, }: UseSettingsDisplayActionsArgs) { const handleThemeModeChange = useCallback((mode: ThemeMode) => { @@ -69,11 +72,20 @@ export function useSettingsDisplayActions({ }) }, [scheduleTrayIconUpdate, setMenubarIconStyle]) + const handleCompactModeChange = useCallback((value: boolean) => { + track("setting_changed", { setting: "compact_mode", value: String(value) }) + setCompactMode(value) + void saveCompactMode(value).catch((error) => { + console.error("Failed to save compact mode:", error) + }) + }, [setCompactMode]) + return { handleThemeModeChange, handleDisplayModeChange, handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, handleMenubarIconStyleChange, + handleCompactModeChange, } } diff --git a/src/index.css b/src/index.css index ec8200cc..99ff687a 100644 --- a/src/index.css +++ b/src/index.css @@ -140,6 +140,11 @@ } } +/* Compact mode: reduce font size and spacing globally */ +html.compact { + font-size: 13px; +} + /* Panel-specific styles for transparent Tauri window */ html, body, diff --git a/src/lib/settings.ts b/src/lib/settings.ts index a94d0a7c..81274a7a 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -33,6 +33,7 @@ const LEGACY_TRAY_ICON_STYLE_KEY = "trayIconStyle"; const LEGACY_TRAY_SHOW_PERCENTAGE_KEY = "trayShowPercentage"; const GLOBAL_SHORTCUT_KEY = "globalShortcut"; const START_ON_LOGIN_KEY = "startOnLogin"; +const COMPACT_MODE_KEY = "compactMode"; export const DEFAULT_AUTO_UPDATE_INTERVAL: AutoUpdateIntervalMinutes = 15; export const DEFAULT_THEME_MODE: ThemeMode = "system"; @@ -41,6 +42,7 @@ export const DEFAULT_RESET_TIMER_DISPLAY_MODE: ResetTimerDisplayMode = "relative export const DEFAULT_MENUBAR_ICON_STYLE: MenubarIconStyle = "provider"; export const DEFAULT_GLOBAL_SHORTCUT: GlobalShortcut = null; export const DEFAULT_START_ON_LOGIN = false; +export const DEFAULT_COMPACT_MODE = false; const AUTO_UPDATE_INTERVALS: AutoUpdateIntervalMinutes[] = [5, 15, 30, 60]; const THEME_MODES: ThemeMode[] = ["system", "light", "dark"]; @@ -303,3 +305,14 @@ export async function saveStartOnLogin(value: boolean): Promise { await store.set(START_ON_LOGIN_KEY, value); await store.save(); } + +export async function loadCompactMode(): Promise { + const stored = await store.get(COMPACT_MODE_KEY); + if (typeof stored === "boolean") return stored; + return DEFAULT_COMPACT_MODE; +} + +export async function saveCompactMode(value: boolean): Promise { + await store.set(COMPACT_MODE_KEY, value); + await store.save(); +} diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 1f7abada..686f5b4b 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -270,6 +270,8 @@ interface SettingsPageProps { onGlobalShortcutChange: (value: GlobalShortcut) => void; startOnLogin: boolean; onStartOnLoginChange: (value: boolean) => void; + compactMode: boolean; + onCompactModeChange: (value: boolean) => void; } export function SettingsPage({ @@ -291,6 +293,8 @@ export function SettingsPage({ onGlobalShortcutChange, startOnLogin, onStartOnLoginChange, + compactMode, + onCompactModeChange, }: SettingsPageProps) { const sensors = useSensors( useSensor(PointerSensor), @@ -484,6 +488,20 @@ export function SettingsPage({ Start on login +
+

Compact Mode

+

+ Smaller text and tighter spacing +

+ +

Plugins

diff --git a/src/stores/app-preferences-store.ts b/src/stores/app-preferences-store.ts index 98ced539..29c2c5dc 100644 --- a/src/stores/app-preferences-store.ts +++ b/src/stores/app-preferences-store.ts @@ -1,6 +1,7 @@ import { create } from "zustand" import { DEFAULT_AUTO_UPDATE_INTERVAL, + DEFAULT_COMPACT_MODE, DEFAULT_DISPLAY_MODE, DEFAULT_GLOBAL_SHORTCUT, DEFAULT_MENUBAR_ICON_STYLE, @@ -23,6 +24,7 @@ type AppPreferencesStore = { globalShortcut: GlobalShortcut startOnLogin: boolean menubarIconStyle: MenubarIconStyle + compactMode: boolean setAutoUpdateInterval: (value: AutoUpdateIntervalMinutes) => void setThemeMode: (value: ThemeMode) => void setDisplayMode: (value: DisplayMode) => void @@ -30,6 +32,7 @@ type AppPreferencesStore = { setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void + setCompactMode: (value: boolean) => void resetState: () => void } @@ -41,6 +44,7 @@ const initialState = { globalShortcut: DEFAULT_GLOBAL_SHORTCUT, startOnLogin: DEFAULT_START_ON_LOGIN, menubarIconStyle: DEFAULT_MENUBAR_ICON_STYLE, + compactMode: DEFAULT_COMPACT_MODE, } export const useAppPreferencesStore = create((set) => ({ @@ -52,5 +56,6 @@ export const useAppPreferencesStore = create((set) => ({ setGlobalShortcut: (value) => set({ globalShortcut: value }), setStartOnLogin: (value) => set({ startOnLogin: value }), setMenubarIconStyle: (value) => set({ menubarIconStyle: value }), + setCompactMode: (value) => set({ compactMode: value }), resetState: () => set(initialState), })) From 8674bb465de427d63a143f596dfc2d15bf17e044 Mon Sep 17 00:00:00 2001 From: Ariful Alam Date: Mon, 16 Mar 2026 22:23:03 +0600 Subject: [PATCH 2/4] feat: replace compact mode toggle with Normal/Small/Compact options Replaces the single compact mode checkbox with three UI scale options (Normal, Small, Compact) using the same radio button pattern as other settings. Font sizes: Small = 14px, Compact = 11px. --- src/App.tsx | 18 ++++---- src/components/app/app-content.tsx | 13 +++--- src/hooks/app/use-settings-bootstrap.ts | 19 ++++---- src/hooks/app/use-settings-compact.ts | 9 ++-- src/hooks/app/use-settings-display-actions.ts | 21 ++++----- src/index.css | 7 ++- src/lib/settings.ts | 23 ++++++---- src/pages/settings.tsx | 43 +++++++++++++------ src/stores/app-preferences-store.ts | 11 ++--- 9 files changed, 98 insertions(+), 66 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 55196d9f..47889a13 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -59,8 +59,8 @@ function App() { setResetTimerDisplayMode, setGlobalShortcut, setStartOnLogin, - compactMode, - setCompactMode, + uiScale, + setUIScale, } = useAppPreferencesStore( useShallow((state) => ({ autoUpdateInterval: state.autoUpdateInterval, @@ -75,8 +75,8 @@ function App() { setResetTimerDisplayMode: state.setResetTimerDisplayMode, setGlobalShortcut: state.setGlobalShortcut, setStartOnLogin: state.setStartOnLogin, - compactMode: state.compactMode, - setCompactMode: state.setCompactMode, + uiScale: state.uiScale, + setUIScale: state.setUIScale, })) ) @@ -125,14 +125,14 @@ function App() { setResetTimerDisplayMode, setGlobalShortcut, setStartOnLogin, - setCompactMode, + setUIScale, setLoadingForPlugins, setErrorForPlugins, startBatch, }) useSettingsTheme(themeMode) - useSettingsCompact(compactMode) + useSettingsCompact(uiScale) const { handleThemeModeChange, @@ -140,14 +140,14 @@ function App() { handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, handleMenubarIconStyleChange, - handleCompactModeChange, + handleUIScaleChange, } = useSettingsDisplayActions({ setThemeMode, setDisplayMode, resetTimerDisplayMode, setResetTimerDisplayMode, setMenubarIconStyle, - setCompactMode, + setUIScale, scheduleTrayIconUpdate, }) @@ -259,7 +259,7 @@ function App() { traySettingsPreview, onGlobalShortcutChange: handleGlobalShortcutChange, onStartOnLoginChange: handleStartOnLoginChange, - onCompactModeChange: handleCompactModeChange, + onUIScaleChange: handleUIScaleChange, }} /> ) diff --git a/src/components/app/app-content.tsx b/src/components/app/app-content.tsx index 53e4e22e..cdd4bceb 100644 --- a/src/components/app/app-content.tsx +++ b/src/components/app/app-content.tsx @@ -14,6 +14,7 @@ import type { MenubarIconStyle, ResetTimerDisplayMode, ThemeMode, + UIScale, } from "@/lib/settings" type AppContentDerivedProps = { @@ -35,7 +36,7 @@ export type AppContentActionProps = { traySettingsPreview: TraySettingsPreview onGlobalShortcutChange: (value: GlobalShortcut) => void onStartOnLoginChange: (value: boolean) => void - onCompactModeChange: (value: boolean) => void + onUIScaleChange: (value: UIScale) => void } export type AppContentProps = AppContentDerivedProps & AppContentActionProps @@ -56,7 +57,7 @@ export function AppContent({ traySettingsPreview, onGlobalShortcutChange, onStartOnLoginChange, - onCompactModeChange, + onUIScaleChange, }: AppContentProps) { const { activeView } = useAppUiStore( useShallow((state) => ({ @@ -72,7 +73,7 @@ export function AppContent({ globalShortcut, themeMode, startOnLogin, - compactMode, + uiScale, } = useAppPreferencesStore( useShallow((state) => ({ displayMode: state.displayMode, @@ -82,7 +83,7 @@ export function AppContent({ globalShortcut: state.globalShortcut, themeMode: state.themeMode, startOnLogin: state.startOnLogin, - compactMode: state.compactMode, + uiScale: state.uiScale, })) ) @@ -119,8 +120,8 @@ export function AppContent({ onGlobalShortcutChange={onGlobalShortcutChange} startOnLogin={startOnLogin} onStartOnLoginChange={onStartOnLoginChange} - compactMode={compactMode} - onCompactModeChange={onCompactModeChange} + uiScale={uiScale} + onUIScaleChange={onUIScaleChange} /> ) } diff --git a/src/hooks/app/use-settings-bootstrap.ts b/src/hooks/app/use-settings-bootstrap.ts index 52deae09..0e7c9c6e 100644 --- a/src/hooks/app/use-settings-bootstrap.ts +++ b/src/hooks/app/use-settings-bootstrap.ts @@ -9,7 +9,7 @@ import type { PluginMeta } from "@/lib/plugin-types" import { arePluginSettingsEqual, DEFAULT_AUTO_UPDATE_INTERVAL, - DEFAULT_COMPACT_MODE, + DEFAULT_UI_SCALE, DEFAULT_DISPLAY_MODE, DEFAULT_GLOBAL_SHORTCUT, DEFAULT_MENUBAR_ICON_STYLE, @@ -18,7 +18,7 @@ import { DEFAULT_THEME_MODE, getEnabledPluginIds, loadAutoUpdateInterval, - loadCompactMode, + loadUIScale, loadDisplayMode, loadGlobalShortcut, loadMenubarIconStyle, @@ -36,6 +36,7 @@ import { type PluginSettings, type ResetTimerDisplayMode, type ThemeMode, + type UIScale, } from "@/lib/settings" type UseSettingsBootstrapArgs = { @@ -48,7 +49,7 @@ type UseSettingsBootstrapArgs = { setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void - setCompactMode: (value: boolean) => void + setUIScale: (value: UIScale) => void setLoadingForPlugins: (ids: string[]) => void setErrorForPlugins: (ids: string[], error: string) => void startBatch: (pluginIds?: string[]) => Promise @@ -64,7 +65,7 @@ export function useSettingsBootstrap({ setGlobalShortcut, setStartOnLogin, setMenubarIconStyle, - setCompactMode, + setUIScale, setLoadingForPlugins, setErrorForPlugins, startBatch, @@ -157,11 +158,11 @@ export function useSettingsBootstrap({ console.error("Failed to load menubar icon style:", error) } - let storedCompactMode = DEFAULT_COMPACT_MODE + let storedUIScale = DEFAULT_UI_SCALE try { - storedCompactMode = await loadCompactMode() + storedUIScale = await loadUIScale() } catch (error) { - console.error("Failed to load compact mode:", error) + console.error("Failed to load UI scale:", error) } if (isMounted) { @@ -173,7 +174,7 @@ export function useSettingsBootstrap({ setGlobalShortcut(storedGlobalShortcut) setStartOnLogin(storedStartOnLogin) setMenubarIconStyle(storedMenubarIconStyle) - setCompactMode(storedCompactMode) + setUIScale(storedUIScale) const enabledIds = getEnabledPluginIds(normalized) setLoadingForPlugins(enabledIds) @@ -199,7 +200,7 @@ export function useSettingsBootstrap({ }, [ applyStartOnLogin, setAutoUpdateInterval, - setCompactMode, + setUIScale, setDisplayMode, setErrorForPlugins, setGlobalShortcut, diff --git a/src/hooks/app/use-settings-compact.ts b/src/hooks/app/use-settings-compact.ts index e9160a73..144fc974 100644 --- a/src/hooks/app/use-settings-compact.ts +++ b/src/hooks/app/use-settings-compact.ts @@ -1,7 +1,10 @@ import { useEffect } from "react" +import type { UIScale } from "@/lib/settings" -export function useSettingsCompact(compactMode: boolean) { +export function useSettingsCompact(uiScale: UIScale) { useEffect(() => { - document.documentElement.classList.toggle("compact", compactMode) - }, [compactMode]) + const root = document.documentElement + root.classList.remove("small", "compact") + if (uiScale !== "normal") root.classList.add(uiScale) + }, [uiScale]) } diff --git a/src/hooks/app/use-settings-display-actions.ts b/src/hooks/app/use-settings-display-actions.ts index f12cacc0..05504681 100644 --- a/src/hooks/app/use-settings-display-actions.ts +++ b/src/hooks/app/use-settings-display-actions.ts @@ -1,7 +1,7 @@ import { useCallback } from "react" import { track } from "@/lib/analytics" import { - saveCompactMode, + saveUIScale, saveDisplayMode, saveMenubarIconStyle, saveResetTimerDisplayMode, @@ -10,6 +10,7 @@ import { type MenubarIconStyle, type ResetTimerDisplayMode, type ThemeMode, + type UIScale, } from "@/lib/settings" type ScheduleTrayIconUpdate = (reason: "probe" | "settings" | "init", delayMs?: number) => void @@ -20,7 +21,7 @@ type UseSettingsDisplayActionsArgs = { resetTimerDisplayMode: ResetTimerDisplayMode setResetTimerDisplayMode: (value: ResetTimerDisplayMode) => void setMenubarIconStyle: (value: MenubarIconStyle) => void - setCompactMode: (value: boolean) => void + setUIScale: (value: UIScale) => void scheduleTrayIconUpdate: ScheduleTrayIconUpdate } @@ -30,7 +31,7 @@ export function useSettingsDisplayActions({ resetTimerDisplayMode, setResetTimerDisplayMode, setMenubarIconStyle, - setCompactMode, + setUIScale, scheduleTrayIconUpdate, }: UseSettingsDisplayActionsArgs) { const handleThemeModeChange = useCallback((mode: ThemeMode) => { @@ -72,13 +73,13 @@ export function useSettingsDisplayActions({ }) }, [scheduleTrayIconUpdate, setMenubarIconStyle]) - const handleCompactModeChange = useCallback((value: boolean) => { - track("setting_changed", { setting: "compact_mode", value: String(value) }) - setCompactMode(value) - void saveCompactMode(value).catch((error) => { - console.error("Failed to save compact mode:", error) + const handleUIScaleChange = useCallback((value: UIScale) => { + track("setting_changed", { setting: "ui_scale", value }) + setUIScale(value) + void saveUIScale(value).catch((error) => { + console.error("Failed to save UI scale:", error) }) - }, [setCompactMode]) + }, [setUIScale]) return { handleThemeModeChange, @@ -86,6 +87,6 @@ export function useSettingsDisplayActions({ handleResetTimerDisplayModeChange, handleResetTimerDisplayModeToggle, handleMenubarIconStyleChange, - handleCompactModeChange, + handleUIScaleChange, } } diff --git a/src/index.css b/src/index.css index 99ff687a..ec6c06bb 100644 --- a/src/index.css +++ b/src/index.css @@ -140,9 +140,12 @@ } } -/* Compact mode: reduce font size and spacing globally */ +/* UI scale: reduce font size and spacing globally */ +html.small { + font-size: 14px; +} html.compact { - font-size: 13px; + font-size: 11px; } /* Panel-specific styles for transparent Tauri window */ diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 81274a7a..6d76170a 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -33,7 +33,7 @@ const LEGACY_TRAY_ICON_STYLE_KEY = "trayIconStyle"; const LEGACY_TRAY_SHOW_PERCENTAGE_KEY = "trayShowPercentage"; const GLOBAL_SHORTCUT_KEY = "globalShortcut"; const START_ON_LOGIN_KEY = "startOnLogin"; -const COMPACT_MODE_KEY = "compactMode"; +const UI_SCALE_KEY = "uiScale"; export const DEFAULT_AUTO_UPDATE_INTERVAL: AutoUpdateIntervalMinutes = 15; export const DEFAULT_THEME_MODE: ThemeMode = "system"; @@ -42,7 +42,14 @@ export const DEFAULT_RESET_TIMER_DISPLAY_MODE: ResetTimerDisplayMode = "relative export const DEFAULT_MENUBAR_ICON_STYLE: MenubarIconStyle = "provider"; export const DEFAULT_GLOBAL_SHORTCUT: GlobalShortcut = null; export const DEFAULT_START_ON_LOGIN = false; -export const DEFAULT_COMPACT_MODE = false; +export type UIScale = "normal" | "small" | "compact"; +export const DEFAULT_UI_SCALE: UIScale = "normal"; +const UI_SCALE_VALUES: UIScale[] = ["normal", "small", "compact"]; +export const UI_SCALE_OPTIONS: { value: UIScale; label: string }[] = [ + { value: "normal", label: "Normal" }, + { value: "small", label: "Small" }, + { value: "compact", label: "Compact" }, +]; const AUTO_UPDATE_INTERVALS: AutoUpdateIntervalMinutes[] = [5, 15, 30, 60]; const THEME_MODES: ThemeMode[] = ["system", "light", "dark"]; @@ -306,13 +313,13 @@ export async function saveStartOnLogin(value: boolean): Promise { await store.save(); } -export async function loadCompactMode(): Promise { - const stored = await store.get(COMPACT_MODE_KEY); - if (typeof stored === "boolean") return stored; - return DEFAULT_COMPACT_MODE; +export async function loadUIScale(): Promise { + const stored = await store.get(UI_SCALE_KEY); + if (typeof stored === "string" && UI_SCALE_VALUES.includes(stored as UIScale)) return stored as UIScale; + return DEFAULT_UI_SCALE; } -export async function saveCompactMode(value: boolean): Promise { - await store.set(COMPACT_MODE_KEY, value); +export async function saveUIScale(value: UIScale): Promise { + await store.set(UI_SCALE_KEY, value); await store.save(); } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 686f5b4b..5c684f30 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -26,12 +26,14 @@ import { MENUBAR_ICON_STYLE_OPTIONS, RESET_TIMER_DISPLAY_OPTIONS, THEME_OPTIONS, + UI_SCALE_OPTIONS, type AutoUpdateIntervalMinutes, type DisplayMode, type GlobalShortcut, type MenubarIconStyle, type ResetTimerDisplayMode, type ThemeMode, + type UIScale, } from "@/lib/settings"; import type { TraySettingsPreview } from "@/hooks/app/use-tray-icon"; import { cn } from "@/lib/utils"; @@ -270,8 +272,8 @@ interface SettingsPageProps { onGlobalShortcutChange: (value: GlobalShortcut) => void; startOnLogin: boolean; onStartOnLoginChange: (value: boolean) => void; - compactMode: boolean; - onCompactModeChange: (value: boolean) => void; + uiScale: UIScale; + onUIScaleChange: (value: UIScale) => void; } export function SettingsPage({ @@ -293,8 +295,8 @@ export function SettingsPage({ onGlobalShortcutChange, startOnLogin, onStartOnLoginChange, - compactMode, - onCompactModeChange, + uiScale, + onUIScaleChange, }: SettingsPageProps) { const sensors = useSensors( useSensor(PointerSensor), @@ -489,18 +491,31 @@ export function SettingsPage({

-

Compact Mode

+

UI Scale

- Smaller text and tighter spacing + Adjust text size and spacing

- +
+
+ {UI_SCALE_OPTIONS.map((option) => { + const isActive = option.value === uiScale; + return ( + + ); + })} +
+

Plugins

diff --git a/src/stores/app-preferences-store.ts b/src/stores/app-preferences-store.ts index 29c2c5dc..d6a3ed72 100644 --- a/src/stores/app-preferences-store.ts +++ b/src/stores/app-preferences-store.ts @@ -1,7 +1,7 @@ import { create } from "zustand" import { DEFAULT_AUTO_UPDATE_INTERVAL, - DEFAULT_COMPACT_MODE, + DEFAULT_UI_SCALE, DEFAULT_DISPLAY_MODE, DEFAULT_GLOBAL_SHORTCUT, DEFAULT_MENUBAR_ICON_STYLE, @@ -10,6 +10,7 @@ import { DEFAULT_THEME_MODE, type AutoUpdateIntervalMinutes, type DisplayMode, + type UIScale, type GlobalShortcut, type MenubarIconStyle, type ResetTimerDisplayMode, @@ -24,7 +25,7 @@ type AppPreferencesStore = { globalShortcut: GlobalShortcut startOnLogin: boolean menubarIconStyle: MenubarIconStyle - compactMode: boolean + uiScale: UIScale setAutoUpdateInterval: (value: AutoUpdateIntervalMinutes) => void setThemeMode: (value: ThemeMode) => void setDisplayMode: (value: DisplayMode) => void @@ -32,7 +33,7 @@ type AppPreferencesStore = { setGlobalShortcut: (value: GlobalShortcut) => void setStartOnLogin: (value: boolean) => void setMenubarIconStyle: (value: MenubarIconStyle) => void - setCompactMode: (value: boolean) => void + setUIScale: (value: UIScale) => void resetState: () => void } @@ -44,7 +45,7 @@ const initialState = { globalShortcut: DEFAULT_GLOBAL_SHORTCUT, startOnLogin: DEFAULT_START_ON_LOGIN, menubarIconStyle: DEFAULT_MENUBAR_ICON_STYLE, - compactMode: DEFAULT_COMPACT_MODE, + uiScale: DEFAULT_UI_SCALE, } export const useAppPreferencesStore = create((set) => ({ @@ -56,6 +57,6 @@ export const useAppPreferencesStore = create((set) => ({ setGlobalShortcut: (value) => set({ globalShortcut: value }), setStartOnLogin: (value) => set({ startOnLogin: value }), setMenubarIconStyle: (value) => set({ menubarIconStyle: value }), - setCompactMode: (value) => set({ compactMode: value }), + setUIScale: (value) => set({ uiScale: value }), resetState: () => set(initialState), })) From 6d576307fc089189d03481de39cd739079db6f41 Mon Sep 17 00:00:00 2001 From: Ariful Alam Date: Wed, 18 Mar 2026 15:06:29 +0600 Subject: [PATCH 3/4] test: update bootstrap test for uiScale setting --- src/hooks/app/use-settings-bootstrap.test.ts | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/hooks/app/use-settings-bootstrap.test.ts b/src/hooks/app/use-settings-bootstrap.test.ts index 9eae9816..5dfcd26f 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, + loadUIScaleMock, migrateLegacyTraySettingsMock, normalizePluginSettingsMock, savePluginSettingsMock, @@ -36,6 +37,7 @@ const { loadResetTimerDisplayModeMock: vi.fn(), loadStartOnLoginMock: vi.fn(), loadThemeModeMock: vi.fn(), + loadUIScaleMock: 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_UI_SCALE: "normal", getEnabledPluginIds: getEnabledPluginIdsMock, loadAutoUpdateInterval: loadAutoUpdateIntervalMock, loadDisplayMode: loadDisplayModeMock, @@ -70,6 +73,7 @@ vi.mock("@/lib/settings", () => ({ loadResetTimerDisplayMode: loadResetTimerDisplayModeMock, loadStartOnLogin: loadStartOnLoginMock, loadThemeMode: loadThemeModeMock, + loadUIScale: loadUIScaleMock, migrateLegacyTraySettings: migrateLegacyTraySettingsMock, normalizePluginSettings: normalizePluginSettingsMock, savePluginSettings: savePluginSettingsMock, @@ -88,6 +92,7 @@ function createArgs() { setGlobalShortcut: vi.fn(), setStartOnLogin: vi.fn(), setMenubarIconStyle: vi.fn(), + setUIScale: vi.fn(), setLoadingForPlugins: vi.fn(), setErrorForPlugins: vi.fn(), startBatch: vi.fn().mockResolvedValue(undefined), @@ -111,6 +116,7 @@ describe("useSettingsBootstrap", () => { loadResetTimerDisplayModeMock.mockReset() loadStartOnLoginMock.mockReset() loadThemeModeMock.mockReset() + loadUIScaleMock.mockReset() migrateLegacyTraySettingsMock.mockReset() normalizePluginSettingsMock.mockReset() savePluginSettingsMock.mockReset() @@ -137,6 +143,7 @@ describe("useSettingsBootstrap", () => { loadGlobalShortcutMock.mockResolvedValue("CommandOrControl+Shift+O") loadMenubarIconStyleMock.mockResolvedValue("provider") loadStartOnLoginMock.mockResolvedValue(true) + loadUIScaleMock.mockResolvedValue("normal") migrateLegacyTraySettingsMock.mockResolvedValue(undefined) savePluginSettingsMock.mockResolvedValue(undefined) getEnabledPluginIdsMock.mockReturnValue(["codex"]) @@ -152,6 +159,25 @@ describe("useSettingsBootstrap", () => { expect(enableAutostartMock).not.toHaveBeenCalled() }) + it("falls back to default UI scale when loading fails", async () => { + const uiScaleError = new Error("ui scale unavailable") + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + loadUIScaleMock.mockRejectedValueOnce(uiScaleError) + const args = createArgs() + + renderHook(() => useSettingsBootstrap(args)) + + await waitFor(() => { + expect(errorSpy).toHaveBeenCalledWith( + "Failed to load UI scale:", + uiScaleError + ) + expect(args.setUIScale).toHaveBeenCalledWith("normal") + }) + + errorSpy.mockRestore() + }) + it("falls back to default reset timer mode when loading fails", async () => { const resetModeError = new Error("reset timer mode unavailable") const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) From fab62490498ee5029142a37910c00029110efedf Mon Sep 17 00:00:00 2001 From: Ariful Alam Date: Wed, 18 Mar 2026 15:10:53 +0600 Subject: [PATCH 4/4] refactor: address review feedback on UI scale implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isUIScale() type-guard to settings.ts (consistent with other guards) - Rename useSettingsCompact → useSettingsUIScale for clarity - Add saveUIScale mock and handleUIScaleChange test to display-actions tests --- src/App.tsx | 4 +-- .../app/use-settings-display-actions.test.ts | 35 +++++++++++++++++++ ...gs-compact.ts => use-settings-ui-scale.ts} | 2 +- src/lib/settings.ts | 6 +++- 4 files changed, 43 insertions(+), 4 deletions(-) rename src/hooks/app/{use-settings-compact.ts => use-settings-ui-scale.ts} (82%) diff --git a/src/App.tsx b/src/App.tsx index 47889a13..5e50071a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import { useSettingsPluginActions } from "@/hooks/app/use-settings-plugin-action import { useSettingsPluginList } from "@/hooks/app/use-settings-plugin-list" import { useSettingsSystemActions } from "@/hooks/app/use-settings-system-actions" import { useSettingsTheme } from "@/hooks/app/use-settings-theme" -import { useSettingsCompact } from "@/hooks/app/use-settings-compact" +import { useSettingsUIScale } from "@/hooks/app/use-settings-ui-scale" import { useTrayIcon } from "@/hooks/app/use-tray-icon" import { track } from "@/lib/analytics" import { REFRESH_COOLDOWN_MS, savePluginSettings } from "@/lib/settings" @@ -132,7 +132,7 @@ function App() { }) useSettingsTheme(themeMode) - useSettingsCompact(uiScale) + useSettingsUIScale(uiScale) const { handleThemeModeChange, diff --git a/src/hooks/app/use-settings-display-actions.test.ts b/src/hooks/app/use-settings-display-actions.test.ts index 7d0db6a4..b0898dbc 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, + saveUIScaleMock, } = vi.hoisted(() => ({ trackMock: vi.fn(), saveThemeModeMock: vi.fn(), saveDisplayModeMock: vi.fn(), saveResetTimerDisplayModeMock: vi.fn(), + saveUIScaleMock: vi.fn(), })) vi.mock("@/lib/analytics", () => ({ @@ -21,6 +23,7 @@ vi.mock("@/lib/settings", () => ({ saveThemeMode: saveThemeModeMock, saveDisplayMode: saveDisplayModeMock, saveResetTimerDisplayMode: saveResetTimerDisplayModeMock, + saveUIScale: saveUIScaleMock, })) import { useSettingsDisplayActions } from "@/hooks/app/use-settings-display-actions" @@ -31,15 +34,18 @@ describe("useSettingsDisplayActions", () => { saveThemeModeMock.mockReset() saveDisplayModeMock.mockReset() saveResetTimerDisplayModeMock.mockReset() + saveUIScaleMock.mockReset() saveThemeModeMock.mockResolvedValue(undefined) saveDisplayModeMock.mockResolvedValue(undefined) saveResetTimerDisplayModeMock.mockResolvedValue(undefined) + saveUIScaleMock.mockResolvedValue(undefined) }) it("tracks and applies display-related setting changes", () => { const setThemeMode = vi.fn() const setDisplayMode = vi.fn() const setResetTimerDisplayMode = vi.fn() + const setUIScale = vi.fn() const scheduleTrayIconUpdate = vi.fn() const { result } = renderHook(() => @@ -48,6 +54,7 @@ describe("useSettingsDisplayActions", () => { setDisplayMode, resetTimerDisplayMode: "relative", setResetTimerDisplayMode, + setUIScale, scheduleTrayIconUpdate, }) ) @@ -88,6 +95,7 @@ describe("useSettingsDisplayActions", () => { setDisplayMode: vi.fn(), resetTimerDisplayMode: mode, setResetTimerDisplayMode, + setUIScale: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }), { initialProps: { mode: "relative" as const } } @@ -120,6 +128,7 @@ describe("useSettingsDisplayActions", () => { setDisplayMode: vi.fn(), resetTimerDisplayMode: "relative", setResetTimerDisplayMode: vi.fn(), + setUIScale: vi.fn(), scheduleTrayIconUpdate: vi.fn(), }) ) @@ -138,4 +147,30 @@ describe("useSettingsDisplayActions", () => { errorSpy.mockRestore() }) + + it("tracks and persists UI scale change", async () => { + const setUIScale = vi.fn() + + const { result } = renderHook(() => + useSettingsDisplayActions({ + setThemeMode: vi.fn(), + setDisplayMode: vi.fn(), + resetTimerDisplayMode: "relative", + setResetTimerDisplayMode: vi.fn(), + setUIScale, + scheduleTrayIconUpdate: vi.fn(), + }) + ) + + act(() => { + result.current.handleUIScaleChange("compact") + }) + + expect(trackMock).toHaveBeenCalledWith("setting_changed", { setting: "ui_scale", value: "compact" }) + expect(setUIScale).toHaveBeenCalledWith("compact") + + await waitFor(() => { + expect(saveUIScaleMock).toHaveBeenCalledWith("compact") + }) + }) }) diff --git a/src/hooks/app/use-settings-compact.ts b/src/hooks/app/use-settings-ui-scale.ts similarity index 82% rename from src/hooks/app/use-settings-compact.ts rename to src/hooks/app/use-settings-ui-scale.ts index 144fc974..4163e329 100644 --- a/src/hooks/app/use-settings-compact.ts +++ b/src/hooks/app/use-settings-ui-scale.ts @@ -1,7 +1,7 @@ import { useEffect } from "react" import type { UIScale } from "@/lib/settings" -export function useSettingsCompact(uiScale: UIScale) { +export function useSettingsUIScale(uiScale: UIScale) { useEffect(() => { const root = document.documentElement root.classList.remove("small", "compact") diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 6d76170a..c087d6d6 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -313,9 +313,13 @@ export async function saveStartOnLogin(value: boolean): Promise { await store.save(); } +export function isUIScale(value: unknown): value is UIScale { + return typeof value === "string" && UI_SCALE_VALUES.includes(value as UIScale); +} + export async function loadUIScale(): Promise { const stored = await store.get(UI_SCALE_KEY); - if (typeof stored === "string" && UI_SCALE_VALUES.includes(stored as UIScale)) return stored as UIScale; + if (isUIScale(stored)) return stored; return DEFAULT_UI_SCALE; }