From 22361db2a9cda40d53b683672f649df0fc6563e3 Mon Sep 17 00:00:00 2001
From: Lightling <contact@lightling.xyz>
Date: Sun, 20 Apr 2025 15:19:17 -0400
Subject: [PATCH] system theme support + as default

---
 .../src/components/shared/theme-picker.vue    | 69 ++++++++++++++++---
 1 file changed, 61 insertions(+), 8 deletions(-)

diff --git a/projects/frontend/src/components/shared/theme-picker.vue b/projects/frontend/src/components/shared/theme-picker.vue
index d156b32..b4ae4c7 100644
--- a/projects/frontend/src/components/shared/theme-picker.vue
+++ b/projects/frontend/src/components/shared/theme-picker.vue
@@ -3,20 +3,39 @@ import { computed, onMounted, ref } from 'vue'
 
 import PrimeVueSelect from 'primevue/select'
 
-import type { SiteTheme } from '@goldenwere/mackenzii-types'
+import type { SiteTheme, SiteThemeType } from '@goldenwere/mackenzii-types'
 import { injectStylesheet } from 'src/utilities/dom'
 import { storage } from 'src/utilities/fetch'
 import { useRouteStore } from 'src/routes'
 
+type Theme = SiteTheme & { id: string }
+const systemTheme = {
+  id: 'SYSTEM',
+  displayName: 'System',
+  type: null as any,
+  url: null as any,
+}
+
 const routeStore = useRouteStore()
 const globalConfig = routeStore._globals
 const options = ref(globalConfig.themes)
-const optionsArray = computed(() => Object.keys(options.value).map(key => ({...options.value[key], id: key})))
+const optionsArray = computed<Theme[]>(() => {
+  const val = Object.keys(options.value).map(key => ({...options.value[key], id: key}))
+  if (val.find(other => other.type === 'dark') && val.find(other => other.type === 'light')) {
+    val.push(systemTheme)
+  }
+  return val
+})
 const currentTheme = ref<SiteTheme & { id: string }>(null as any)
+const currentSystem = ref<SiteThemeType>(null as any)
 let node: HTMLLinkElement
 
 const setModeClass = () => {
-  switch (currentTheme.value.type) {
+  const type = currentTheme.value.id === 'SYSTEM'
+    ? currentSystem.value
+    : currentTheme.value.type
+  
+  switch (type) {
     case 'dark':
       document.body.parentElement!.classList.add('app-dark')
       document.body.parentElement!.classList.remove('app-dark-hc')
@@ -50,29 +69,63 @@ const setModeClass = () => {
   }
 }
 
-const onThemeChosen = (event: Event) => {
+const getUrlOfTheme = (id: string) => {
+  if (id === 'SYSTEM') {
+    return optionsArray.value.find(other => other.type === currentSystem.value)!.url
+  } else {
+    return globalConfig.themes[id].url
+  }
+}
+
+const onThemeChosen = (event?: Event) => {
   const themeId = currentTheme.value.id
   storage.write(`${globalConfig.id}::currentTheme`, themeId)
-  node.setAttribute('href', globalConfig.themes[themeId].url)
+  node.setAttribute('href', getUrlOfTheme(themeId))
   setModeClass()
 }
 
+const onSystemThemeChanged = (query: MediaQueryListEvent | MediaQueryList) => {
+  if (query.matches) {
+    currentSystem.value = 'dark'
+  } else {
+    currentSystem.value = 'light'
+  }
+  onThemeChosen()
+}
+
+const setupSystemThemeListener = () => {
+  const themeListener = window.matchMedia('(prefers-color-scheme: dark)')
+  onSystemThemeChanged(themeListener)
+  themeListener.addEventListener('change', onSystemThemeChanged)
+}
+
+const loadThemeFromStorage = () => {
+  const themeId = storage.read<string>(`${globalConfig.id}::currentTheme`) || (
+    !!optionsArray.value.find(other => other.id === 'SYSTEM') ? 'SYSTEM' : Object.keys(globalConfig.themes)[0]
+  )
+  currentTheme.value = themeId === 'SYSTEM' ? systemTheme : { ...options.value[themeId], id: themeId }
+  node = injectStylesheet(getUrlOfTheme(themeId), 'theme-stylesheet')
+}
+
 onMounted(async () => {
-  const themeId = storage.read<string>(`${globalConfig.id}::currentTheme`) || Object.keys(globalConfig.themes)[0]
-  currentTheme.value = { ...options.value[themeId], id: themeId }
-  node = injectStylesheet(globalConfig.themes[themeId].url, 'theme-stylesheet')
+  loadThemeFromStorage()
+  setupSystemThemeListener()
   setModeClass()
 })
 </script>
 
 <template lang="pug">
 .theme-picker
+  label(
+    for='site theme'
+  ) Theme
   PrimeVueSelect(
     v-model='currentTheme'
     :options='optionsArray'
     @change='onThemeChosen($event)'
     optionLabel='displayName'
     placeholder='theme'
+    name='site theme'
   )
 </template>