Skip to content

Commit f7b1719

Browse files
committed
fix(ui): color mode reactiveness
1 parent 4449f09 commit f7b1719

3 files changed

Lines changed: 68 additions & 13 deletions

File tree

‎packages/devtools/client/app.vue‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useEventListener, useEyeDropper } from'@vueuse/core'
33
import { computed, onMounted, ref, watch, watchEffect } from'vue'
44
import { useHead } from'#imports'
5-
import { getColorMode, showConnectionWarning, useClient, useInjectionClient } from'~/composables/client'
5+
import { getColorMode, setEmbedderColorMode, showConnectionWarning, useClient, useInjectionClient } from'~/composables/client'
66
import { useCopy } from'~/composables/editor'
77
import { setupFrameNav } from'~/composables/frame-nav'
88
import { WS_DEBOUNCE_TIME } from'~/composables/rpc'
@@ -63,15 +63,27 @@ function onConnected() {
6363
}, WS_DEBOUNCE_TIME)
6464
}
6565
66+
// Mirror the host app's scheme while we are embedded in one, so DevTools matches
67+
// the app it inspects. Passing `undefined` through (no host) hands control back
68+
// to the OS preference rather than pinning the last host value.
6669
watch(
6770
() =>client.value?.app.colorMode.value,
6871
(mode) => {
69-
if (mode)
70-
colorMode.value=mode
72+
setEmbedderColorMode(mode)
7173
},
7274
{ immediate: true },
7375
)
7476
77+
// The resolved scheme is derived, not stored, so applying it to the document is
78+
// ours to do.
79+
watchEffect(() => {
80+
if (!import.meta.client)
81+
return
82+
const isDark =colorMode.value==='dark'
83+
document.documentElement.classList.toggle('dark', isDark)
84+
document.documentElement.classList.toggle('light', !isDark)
85+
})
86+
7587
useEventListener('keydown', (e) => {
7688
if (e.code==='KeyD'&&e.altKey) {
7789
client.value?.devtools.close()

‎packages/devtools/client/components/IframeView.vue‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script lang="ts">
22
import { useElementBounding } from'@vueuse/core'
33
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watchEffect } from'vue'
4-
import { getColorMode, useInjectionClient } from'~/composables/client'
4+
import { getColorMode, setEmbedderColorMode, useInjectionClient } from'~/composables/client'
55
66
const iframeCacheMap =newMap<string, HTMLIFrameElement>()
7+
const observedDocuments =newWeakSet<HTMLElement>()
78
</script>
89

910
<script setup lang="ts">
@@ -69,13 +70,28 @@ function syncColorMode() {
6970
return
7071
try {
7172
const html =iframeEl.value.contentWindow.document.querySelector('html')
72-
html?.classList.toggle('dark', colorMode.value==='dark')
73-
html?.classList.toggle('light', colorMode.value==='dark')
73+
if (!html)
74+
return
75+
76+
const isDark =colorMode.value==='dark'
77+
html.classList.toggle('dark', isDark)
78+
html.classList.toggle('light', !isDark)
79+
80+
// Iframes are cached and this runs from a watcher, an `onload` and a timer —
81+
// observe each document once instead of stacking observers on it.
82+
if (observedDocuments.has(html))
83+
return
84+
observedDocuments.add(html)
7485
7586
const ob =newMutationObserver(() => {
76-
colorMode.value=iframeEl.value?.contentWindow?.document.querySelector('html')?.classList.contains('dark') ?'dark':'light'
87+
const tabScheme =html.classList.contains('dark') ?'dark':'light'
88+
// The push above trips this observer too. Only a *divergence* from what we
89+
// render means the tab picked a scheme of its own; treating our own write
90+
// as a choice would pin the UI to it and stop it following the OS.
91+
if (tabScheme!==colorMode.value)
92+
setEmbedderColorMode(tabScheme)
7793
})
78-
ob.observe(html!, { attributes: true, attributeFilter: ['class'] })
94+
ob.observe(html, { attributes: true, attributeFilter: ['class'] })
7995
}
8096
catch (e) {
8197
}

‎packages/devtools/client/composables/client.ts‎

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Unhead } from '@unhead/schema'
33
importtype{DevToolsRpcClient}from'@vitejs/devtools-kit/client'
44
importtype{ComputedRef}from'vue'
55
importtype{useRoute,useRouter}from'#imports'
6-
import{useColorMode}from'@vueuse/core'
6+
import{usePreferredDark}from'@vueuse/core'
77
import{computed,ref}from'vue'
88
import{useState}from'#imports'
99
import{renderMarkdown}from'./client-services/markdown'
@@ -54,10 +54,37 @@ export const showConnectionWarning = computed(() => {
5454
returnconnectionTimeout.value&&!useClient().value
5555
})
5656

57-
exportfunctiongetColorMode(){
58-
returnuseColorMode({
59-
storageKey: 'nuxt-devtools-color-mode',
60-
})
57+
exporttypeColorScheme='dark'|'light'
58+
59+
/**
60+
* Color scheme pushed down by whatever embeds this client — the host app's
61+
* resolved scheme (`useClientColorMode`), or a custom-tab iframe carrying its
62+
* own toggle.
63+
*
64+
* Deliberately *not* persisted. This mirrors someone else's state, so storing
65+
* it overwrote our own "follow the OS" default, permanently: nothing ever wrote
66+
* `auto` back. Every surface without an embedder (the standalone hub UI, the
67+
* client opened directly, `nuxi dev client`) was then frozen on whatever the OS
68+
* happened to be the last time an embedder was attached.
69+
*/
70+
constembedderColorMode=ref<ColorScheme>()
71+
72+
exportfunctionsetEmbedderColorMode(mode: ColorScheme|undefined){
73+
embedderColorMode.value=mode
74+
}
75+
76+
letcolorMode: ComputedRef<ColorScheme>|undefined
77+
78+
/**
79+
* The scheme the DevTools UI renders in: the embedder's when there is one,
80+
* otherwise the live OS `prefers-color-scheme`.
81+
*/
82+
exportfunctiongetColorMode(): ComputedRef<ColorScheme>{
83+
if(!colorMode){
84+
constpreferredDark=usePreferredDark()
85+
colorMode=computed(()=>embedderColorMode.value??(preferredDark.value ? 'dark' : 'light'))
86+
}
87+
returncolorMode
6188
}
6289

6390
exportfunctionuseInjectionClient(): ComputedRef<NuxtDevtoolsIframeClient>{

0 commit comments

Comments
 (0)