Page MenuHomeDevCentral

D3989.id10346.diff
No OneTemporary

D3989.id10346.diff

diff --git a/frontend/src/components/AppNavbar.vue b/frontend/src/components/AppNavbar.vue
--- a/frontend/src/components/AppNavbar.vue
+++ b/frontend/src/components/AppNavbar.vue
@@ -3,9 +3,11 @@
import { RouterLink } from 'vue-router'
import { configApi } from '@/plugins/api'
import { useAuth } from '@/composables/useAuth'
+import { useDarkMode } from '@/composables/useDarkMode'
const config = ref(null)
const { isAuthenticated, logout } = useAuth()
+const { isDark, toggleDarkMode } = useDarkMode()
onMounted(async () => {
try {
@@ -41,6 +43,18 @@
</RouterLink>
<div class="flex items-center gap-4">
+ <button
+ @click="toggleDarkMode"
+ class="text-gray-400 hover:text-white transition-colors"
+ :aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
+ >
+ <svg v-if="isDark" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+ <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" />
+ </svg>
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
+ </svg>
+ </button>
<RouterLink
v-if="isAuthenticated"
to="/admin"
@@ -62,7 +76,6 @@
>
Admin
</RouterLink>
-
</div>
</div>
</div>
diff --git a/frontend/src/components/__tests__/useDarkMode.test.js b/frontend/src/components/__tests__/useDarkMode.test.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/__tests__/useDarkMode.test.js
@@ -0,0 +1,91 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+const localStorageMock = (() => {
+ let store = {}
+ return {
+ getItem: vi.fn((key) => store[key] || null),
+ setItem: vi.fn((key, value) => { store[key] = String(value) }),
+ removeItem: vi.fn((key) => { delete store[key] }),
+ clear: () => { store = {} },
+ }
+})()
+
+Object.defineProperty(window, 'localStorage', { value: localStorageMock })
+
+Object.defineProperty(window, 'matchMedia', {
+ value: vi.fn((query) => ({
+ matches: false,
+ media: query,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ })),
+})
+
+describe('useDarkMode', () => {
+ beforeEach(() => {
+ localStorageMock.clear()
+ vi.clearAllMocks()
+ document.documentElement.classList.remove('dark')
+ vi.resetModules()
+ })
+
+ it('defaults to system preference when no stored value', async () => {
+ window.matchMedia.mockReturnValue({ matches: true })
+ const { useDarkMode } = await import('@/composables/useDarkMode')
+ const { isDark, toggleDarkMode } = useDarkMode()
+
+ expect(isDark).toBeDefined()
+ expect(typeof toggleDarkMode).toBe('function')
+ })
+
+ it('toggleDarkMode switches the value', async () => {
+ const { useDarkMode } = await import('@/composables/useDarkMode')
+ const { isDark, toggleDarkMode } = useDarkMode()
+
+ const initial = isDark.value
+ toggleDarkMode()
+
+ expect(isDark.value).toBe(!initial)
+ })
+
+ it('toggleDarkMode persists to localStorage', async () => {
+ const { useDarkMode } = await import('@/composables/useDarkMode')
+ const { toggleDarkMode } = useDarkMode()
+
+ toggleDarkMode()
+
+ expect(localStorageMock.setItem).toHaveBeenCalledWith(
+ 'servpulse_dark_mode',
+ expect.any(String)
+ )
+ })
+
+ it('toggleDarkMode adds dark class to html', async () => {
+ document.documentElement.classList.remove('dark')
+ const { useDarkMode } = await import('@/composables/useDarkMode')
+ const { isDark, toggleDarkMode } = useDarkMode()
+
+ if (isDark.value) {
+ toggleDarkMode()
+ }
+
+ toggleDarkMode()
+
+ expect(document.documentElement.classList.contains('dark')).toBe(true)
+ })
+
+ it('toggleDarkMode removes dark class from html', async () => {
+ document.documentElement.classList.add('dark')
+ const { useDarkMode } = await import('@/composables/useDarkMode')
+ const { isDark, toggleDarkMode } = useDarkMode()
+
+ if (!isDark.value) {
+ toggleDarkMode()
+ }
+
+ toggleDarkMode()
+
+ expect(document.documentElement.classList.contains('dark')).toBe(false)
+ })
+})
+Z
\ No newline at end of file
diff --git a/frontend/src/composables/useDarkMode.js b/frontend/src/composables/useDarkMode.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/composables/useDarkMode.js
@@ -0,0 +1,39 @@
+import { ref, watch, onMounted } from 'vue'
+
+const STORAGE_KEY = 'servpulse_dark_mode'
+
+const isDark = ref(false)
+
+function applyTheme(dark) {
+ if (dark) {
+ document.documentElement.classList.add('dark')
+ } else {
+ document.documentElement.classList.remove('dark')
+ }
+}
+
+function initTheme() {
+ const stored = localStorage.getItem(STORAGE_KEY)
+
+ if (stored !== null) {
+ isDark.value = stored === 'true'
+ } else {
+ isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
+ }
+
+ applyTheme(isDark.value)
+}
+
+function toggleDarkMode() {
+ isDark.value = !isDark.value
+ localStorage.setItem(STORAGE_KEY, String(isDark.value))
+ applyTheme(isDark.value)
+}
+
+export function useDarkMode() {
+ onMounted(() => {
+ initTheme()
+ })
+
+ return { isDark, toggleDarkMode }
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Mar 6, 01:25 (13 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3500247
Default Alt Text
D3989.id10346.diff (5 KB)

Event Timeline