Skip to content

Commit aabe84b

Browse files
committed
refactor(theme): delegate to v0 createTheme as runtime
- createTheme() now creates a v0 theme instance for theme selection, cycling, and dark mode detection via usePrefersDark() - Bridge v0's selectedId to Vuetify's writable name computed - System theme detection delegated to v0's usePrefersDark() - Preserve full consumer API: name, current, themes, computedThemes, themeClasses, styles, change/cycle/toggle, install, global - Fix adapter.ts ThemeAdapter import path (@vuetify/v0/theme/adapters) - Fix adapter.ts ThemeAdapterSetupContext type (inline, not exported) - Keep original CSS generation and DOM injection in install() for backward compatibility with head/unhead integration
1 parent a4dbb15 commit aabe84b

2 files changed

Lines changed: 82 additions & 38 deletions

File tree

packages/vuetify/src/composables/theme/adapter.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
import { genCssVariables } from './colors'
33

44
// Utilities
5-
import { IN_BROWSER } from '@/util'
6-
import { ThemeAdapter } from '@vuetify/v0'
5+
import { ThemeAdapter } from '@vuetify/v0/theme/adapters'
76
import { watch } from 'vue'
7+
import { IN_BROWSER } from '@/util'
88

99
// Types
10-
import type { ThemeAdapterSetupContext } from '@vuetify/v0'
11-
import type { App } from 'vue'
10+
import type { App, ComputedRef, Ref } from 'vue'
1211
import type { InternalThemeDefinition } from './colors'
1312

13+
interface AdapterSetupContext {
14+
colors: ComputedRef<Record<string, Record<string, string>>>
15+
selectedId: Ref<string | null | undefined>
16+
isDark: Readonly<Ref<boolean>>
17+
}
18+
1419
export interface VuetifyThemeAdapterOptions {
1520
cspNonce?: string
1621
scope?: string
@@ -132,7 +137,8 @@ export class VuetifyThemeAdapter extends ThemeAdapter {
132137
return '@layer vuetify-utilities {\n' + lines.map(v => ` ${v}`).join('') + '\n}'
133138
}
134139

135-
setup <T extends ThemeAdapterSetupContext>(
140+
// @ts-expect-error Vue types mismatch between v0 and vuetify packages
141+
setup <T extends AdapterSetupContext> (
136142
app: App,
137143
context: T,
138144
_target?: string | HTMLElement | null,

packages/vuetify/src/composables/theme/index.ts

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
// Utilities
2+
import {
3+
createTheme as createV0Theme,
4+
usePrefersDark,
5+
} from '@vuetify/v0'
26
import {
37
computed,
4-
getCurrentScope,
58
inject,
6-
onScopeDispose,
79
provide,
810
ref,
911
shallowRef,
1012
toRef,
1113
watch,
1214
watchEffect,
1315
} from 'vue'
16+
import {
17+
genCssVariables,
18+
genOnColors,
19+
genVariations,
20+
parseThemeOptions,
21+
} from './colors'
1422
import {
1523
consoleWarn,
1624
deprecate,
1725
getCurrentInstance,
1826
IN_BROWSER,
1927
mergeDeep,
2028
propsFactory,
21-
SUPPORTS_MATCH_MEDIA,
2229
} from '@/util'
23-
import {
24-
genCssVariables,
25-
genOnColors,
26-
genVariations,
27-
parseThemeOptions,
28-
} from './colors'
2930

3031
// Types
3132
import type { VueHeadClient } from '@unhead/vue/client'
@@ -109,23 +110,78 @@ function getOrCreateStyleElement (id: string, cspNonce?: string) {
109110
// Composables
110111
export function createTheme (options?: ThemeOptions): ThemeInstance & { install: (app: App) => void } {
111112
const parsedOptions = parseThemeOptions(options)
112-
const _name = shallowRef(parsedOptions.defaultTheme)
113113
const themes = ref(parsedOptions.themes)
114-
const systemName = shallowRef('light')
114+
115+
// Build processed themes with variations + on-colors for v0 registration
116+
function buildV0Themes () {
117+
const processed: Record<string, { dark: boolean, colors: Record<string, string> }> = {}
118+
for (const [themeName, original] of Object.entries(themes.value)) {
119+
const defaultTheme = original.dark || themeName === 'dark'
120+
? themes.value.dark
121+
: themes.value.light
122+
123+
const merged = mergeDeep(defaultTheme, original) as InternalThemeDefinition
124+
const colors = {
125+
...merged.colors,
126+
...genVariations(merged.colors, parsedOptions.variations),
127+
}
128+
const fullColors = {
129+
...colors,
130+
...genOnColors(colors, merged.variables),
131+
}
132+
133+
processed[themeName] = {
134+
dark: merged.dark,
135+
colors: fullColors as Record<string, string>,
136+
}
137+
}
138+
return processed
139+
}
140+
141+
// Resolve default theme — v0 doesn't understand 'system'
142+
const isSystemDefault = parsedOptions.defaultTheme === 'system'
143+
const { matches: prefersDark } = usePrefersDark()
144+
const resolvedDefault = isSystemDefault
145+
? (prefersDark.value ? 'dark' : 'light')
146+
: parsedOptions.defaultTheme
147+
148+
// Create v0 theme instance
149+
const v0Theme = createV0Theme({
150+
reactive: true,
151+
foreground: false, // We handle on-colors ourselves via genOnColors
152+
default: resolvedDefault,
153+
themes: buildV0Themes(),
154+
})
155+
156+
// Bridge v0's selectedId to Vuetify's name ref
157+
const _name = shallowRef(parsedOptions.defaultTheme)
115158

116159
const name = computed({
117160
get () {
118-
return _name.value === 'system' ? systemName.value : _name.value
161+
if (_name.value === 'system') {
162+
return prefersDark.value ? 'dark' : 'light'
163+
}
164+
return String(v0Theme.selectedId.value ?? resolvedDefault)
119165
},
120166
set (val: string) {
121167
_name.value = val
168+
if (val !== 'system') {
169+
v0Theme.select(val)
170+
}
122171
},
123172
})
124173

174+
// When system preference changes and we're in system mode, update v0 selection
175+
watch(prefersDark, dark => {
176+
if (_name.value === 'system') {
177+
v0Theme.select(dark ? 'dark' : 'light')
178+
}
179+
})
180+
125181
const computedThemes = computed(() => {
126182
const acc: Record<string, InternalThemeDefinition> = {}
127-
for (const [name, original] of Object.entries(themes.value)) {
128-
const defaultTheme = original.dark || name === 'dark'
183+
for (const [themeName, original] of Object.entries(themes.value)) {
184+
const defaultTheme = original.dark || themeName === 'dark'
129185
? themes.value.dark
130186
: themes.value.light
131187

@@ -136,7 +192,7 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
136192
...genVariations(merged.colors, parsedOptions.variations),
137193
}
138194

139-
acc[name] = {
195+
acc[themeName] = {
140196
...merged,
141197
colors: {
142198
...colors,
@@ -207,24 +263,6 @@ export function createTheme (options?: ThemeOptions): ThemeInstance & { install:
207263
const themeClasses = toRef(() => parsedOptions.isDisabled ? undefined : `${parsedOptions.prefix}theme--${name.value}`)
208264
const themeNames = toRef(() => Object.keys(computedThemes.value))
209265

210-
if (SUPPORTS_MATCH_MEDIA) {
211-
const media = window.matchMedia('(prefers-color-scheme: dark)')
212-
213-
function updateSystemName () {
214-
systemName.value = media.matches ? 'dark' : 'light'
215-
}
216-
217-
updateSystemName()
218-
219-
media.addEventListener('change', updateSystemName, { passive: true })
220-
221-
if (getCurrentScope()) {
222-
onScopeDispose(() => {
223-
media.removeEventListener('change', updateSystemName)
224-
})
225-
}
226-
}
227-
228266
function install (app: App) {
229267
if (parsedOptions.isDisabled) return
230268

0 commit comments

Comments
 (0)