UNPKG

vuetify

Version:

Vue Material Component Framework

382 lines (381 loc) 13 kB
// Utilities import { computed, getCurrentScope, inject, onScopeDispose, provide, ref, shallowRef, toRef, watch, watchEffect } from 'vue'; import { consoleWarn, createRange, darken, deprecate, getCurrentInstance, getForeground, getLuma, IN_BROWSER, lighten, mergeDeep, parseColor, propsFactory, RGBtoHex, SUPPORTS_MATCH_MEDIA } from "../util/index.js"; // Types export const ThemeSymbol = Symbol.for('vuetify:theme'); export const makeThemeProps = propsFactory({ theme: String }, 'theme'); function genDefaults() { return { defaultTheme: 'light', prefix: 'v-', variations: { colors: [], lighten: 0, darken: 0 }, themes: { light: { dark: false, colors: { background: '#FFFFFF', surface: '#FFFFFF', 'surface-bright': '#FFFFFF', 'surface-light': '#EEEEEE', 'surface-variant': '#424242', 'on-surface-variant': '#EEEEEE', primary: '#1867C0', 'primary-darken-1': '#1F5592', secondary: '#48A9A6', 'secondary-darken-1': '#018786', error: '#B00020', info: '#2196F3', success: '#4CAF50', warning: '#FB8C00' }, variables: { 'border-color': '#000000', 'border-opacity': 0.12, 'high-emphasis-opacity': 0.87, 'medium-emphasis-opacity': 0.60, 'disabled-opacity': 0.38, 'idle-opacity': 0.04, 'hover-opacity': 0.04, 'focus-opacity': 0.12, 'selected-opacity': 0.08, 'activated-opacity': 0.12, 'pressed-opacity': 0.12, 'dragged-opacity': 0.08, 'theme-kbd': '#EEEEEE', 'theme-on-kbd': '#000000', 'theme-code': '#F5F5F5', 'theme-on-code': '#000000' } }, dark: { dark: true, colors: { background: '#121212', surface: '#212121', 'surface-bright': '#ccbfd6', 'surface-light': '#424242', 'surface-variant': '#c8c8c8', 'on-surface-variant': '#000000', primary: '#2196F3', 'primary-darken-1': '#277CC1', secondary: '#54B6B2', 'secondary-darken-1': '#48A9A6', error: '#CF6679', info: '#2196F3', success: '#4CAF50', warning: '#FB8C00' }, variables: { 'border-color': '#FFFFFF', 'border-opacity': 0.12, 'high-emphasis-opacity': 1, 'medium-emphasis-opacity': 0.70, 'disabled-opacity': 0.50, 'idle-opacity': 0.10, 'hover-opacity': 0.04, 'focus-opacity': 0.12, 'selected-opacity': 0.08, 'activated-opacity': 0.12, 'pressed-opacity': 0.16, 'dragged-opacity': 0.08, 'theme-kbd': '#424242', 'theme-on-kbd': '#FFFFFF', 'theme-code': '#343434', 'theme-on-code': '#CCCCCC' } } }, stylesheetId: 'vuetify-theme-stylesheet', scoped: false, unimportant: false, utilities: true }; } function parseThemeOptions() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : genDefaults(); const defaults = genDefaults(); if (!options) return { ...defaults, isDisabled: true }; const themes = {}; for (const [key, theme] of Object.entries(options.themes ?? {})) { const defaultTheme = theme.dark || key === 'dark' ? defaults.themes?.dark : defaults.themes?.light; themes[key] = mergeDeep(defaultTheme, theme); } return mergeDeep(defaults, { ...options, themes }); } function createCssClass(lines, selector, content, scope) { lines.push(`${getScopedSelector(selector, scope)} {\n`, ...content.map(line => ` ${line};\n`), '}\n'); } function genCssVariables(theme, prefix) { const lightOverlay = theme.dark ? 2 : 1; const darkOverlay = theme.dark ? 1 : 2; const variables = []; for (const [key, value] of Object.entries(theme.colors)) { const rgb = parseColor(value); variables.push(`--${prefix}theme-${key}: ${rgb.r},${rgb.g},${rgb.b}`); if (!key.startsWith('on-')) { variables.push(`--${prefix}theme-${key}-overlay-multiplier: ${getLuma(value) > 0.18 ? lightOverlay : darkOverlay}`); } } for (const [key, value] of Object.entries(theme.variables)) { const color = typeof value === 'string' && value.startsWith('#') ? parseColor(value) : undefined; const rgb = color ? `${color.r}, ${color.g}, ${color.b}` : undefined; variables.push(`--${prefix}${key}: ${rgb ?? value}`); } return variables; } function genVariation(name, color, variations) { const object = {}; if (variations) { for (const variation of ['lighten', 'darken']) { const fn = variation === 'lighten' ? lighten : darken; for (const amount of createRange(variations[variation], 1)) { object[`${name}-${variation}-${amount}`] = RGBtoHex(fn(parseColor(color), amount)); } } } return object; } function genVariations(colors, variations) { if (!variations) return {}; let variationColors = {}; for (const name of variations.colors) { const color = colors[name]; if (!color) continue; variationColors = { ...variationColors, ...genVariation(name, color, variations) }; } return variationColors; } function genOnColors(colors) { const onColors = {}; for (const color of Object.keys(colors)) { if (color.startsWith('on-') || colors[`on-${color}`]) continue; const onColor = `on-${color}`; const colorVal = parseColor(colors[color]); onColors[onColor] = getForeground(colorVal); } return onColors; } function getScopedSelector(selector, scope) { if (!scope) return selector; const scopeSelector = `:where(${scope})`; return selector === ':root' ? scopeSelector : `${scopeSelector} ${selector}`; } function upsertStyles(id, cspNonce, styles) { const styleEl = getOrCreateStyleElement(id, cspNonce); if (!styleEl) return; styleEl.innerHTML = styles; } function getOrCreateStyleElement(id, cspNonce) { if (!IN_BROWSER) return null; let style = document.getElementById(id); if (!style) { style = document.createElement('style'); style.id = id; style.type = 'text/css'; if (cspNonce) style.setAttribute('nonce', cspNonce); document.head.appendChild(style); } return style; } // Composables export function createTheme(options) { const parsedOptions = parseThemeOptions(options); const _name = shallowRef(parsedOptions.defaultTheme); const themes = ref(parsedOptions.themes); const systemName = shallowRef('light'); const name = computed({ get() { return _name.value === 'system' ? systemName.value : _name.value; }, set(val) { _name.value = val; } }); const computedThemes = computed(() => { const acc = {}; for (const [name, original] of Object.entries(themes.value)) { const colors = { ...original.colors, ...genVariations(original.colors, parsedOptions.variations) }; acc[name] = { ...original, colors: { ...colors, ...genOnColors(colors) } }; } return acc; }); const current = toRef(() => computedThemes.value[name.value]); const styles = computed(() => { const lines = []; const important = parsedOptions.unimportant ? '' : ' !important'; const scoped = parsedOptions.scoped ? parsedOptions.prefix : ''; if (current.value?.dark) { createCssClass(lines, ':root', ['color-scheme: dark'], parsedOptions.scope); } createCssClass(lines, ':root', genCssVariables(current.value, parsedOptions.prefix), parsedOptions.scope); for (const [themeName, theme] of Object.entries(computedThemes.value)) { createCssClass(lines, `.${parsedOptions.prefix}theme--${themeName}`, [`color-scheme: ${theme.dark ? 'dark' : 'normal'}`, ...genCssVariables(theme, parsedOptions.prefix)], parsedOptions.scope); } if (parsedOptions.utilities) { const bgLines = []; const fgLines = []; const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors))); for (const key of colors) { if (key.startsWith('on-')) { createCssClass(fgLines, `.${key}`, [`color: rgb(var(--${parsedOptions.prefix}theme-${key}))${important}`], parsedOptions.scope); } else { createCssClass(bgLines, `.${scoped}bg-${key}`, [`--${parsedOptions.prefix}theme-overlay-multiplier: var(--${parsedOptions.prefix}theme-${key}-overlay-multiplier)`, `background-color: rgb(var(--${parsedOptions.prefix}theme-${key}))${important}`, `color: rgb(var(--${parsedOptions.prefix}theme-on-${key}))${important}`], parsedOptions.scope); createCssClass(fgLines, `.${scoped}text-${key}`, [`color: rgb(var(--${parsedOptions.prefix}theme-${key}))${important}`], parsedOptions.scope); createCssClass(fgLines, `.${scoped}border-${key}`, [`--${parsedOptions.prefix}border-color: var(--${parsedOptions.prefix}theme-${key})`], parsedOptions.scope); } } lines.push(...bgLines, ...fgLines); } return lines.map((str, i) => i === 0 ? str : ` ${str}`).join(''); }); const themeClasses = toRef(() => parsedOptions.isDisabled ? undefined : `${parsedOptions.prefix}theme--${name.value}`); const themeNames = toRef(() => Object.keys(computedThemes.value)); if (SUPPORTS_MATCH_MEDIA) { const media = window.matchMedia('(prefers-color-scheme: dark)'); function updateSystemName() { systemName.value = media.matches ? 'dark' : 'light'; } updateSystemName(); media.addEventListener('change', updateSystemName, { passive: true }); if (getCurrentScope()) { onScopeDispose(() => { media.removeEventListener('change', updateSystemName); }); } } function install(app) { if (parsedOptions.isDisabled) return; const head = app._context.provides.usehead; if (head) { function getHead() { return { style: [{ textContent: styles.value, id: parsedOptions.stylesheetId, nonce: parsedOptions.cspNonce || false }] }; } if (head.push) { const entry = head.push(getHead); if (IN_BROWSER) { watch(styles, () => { entry.patch(getHead); }); } } else { if (IN_BROWSER) { head.addHeadObjs(toRef(getHead)); watchEffect(() => head.updateDOM()); } else { head.addHeadObjs(getHead()); } } } else { if (IN_BROWSER) { watch(styles, updateStyles, { immediate: true }); } else { updateStyles(); } function updateStyles() { upsertStyles(parsedOptions.stylesheetId, parsedOptions.cspNonce, styles.value); } } } function change(themeName) { if (!themeNames.value.includes(themeName)) { consoleWarn(`Theme "${themeName}" not found on the Vuetify theme instance`); return; } name.value = themeName; } function cycle() { let themeArray = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : themeNames.value; const currentIndex = themeArray.indexOf(name.value); const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % themeArray.length; change(themeArray[nextIndex]); } function toggle() { let themeArray = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['light', 'dark']; cycle(themeArray); } const globalName = new Proxy(name, { get(target, prop) { return Reflect.get(target, prop); }, set(target, prop, val) { if (prop === 'value') { deprecate(`theme.global.name.value = ${val}`, `theme.change('${val}')`); } return Reflect.set(target, prop, val); } }); return { install, change, cycle, toggle, isDisabled: parsedOptions.isDisabled, name, themes, current, computedThemes, prefix: parsedOptions.prefix, themeClasses, styles, global: { name: globalName, current } }; } export function provideTheme(props) { getCurrentInstance('provideTheme'); const theme = inject(ThemeSymbol, null); if (!theme) throw new Error('Could not find Vuetify theme injection'); const name = toRef(() => props.theme ?? theme.name.value); const current = toRef(() => theme.themes.value[name.value]); const themeClasses = toRef(() => theme.isDisabled ? undefined : `${theme.prefix}theme--${name.value}`); const newTheme = { ...theme, name, current, themeClasses }; provide(ThemeSymbol, newTheme); return newTheme; } export function useTheme() { getCurrentInstance('useTheme'); const theme = inject(ThemeSymbol, null); if (!theme) throw new Error('Could not find Vuetify theme injection'); return theme; } //# sourceMappingURL=theme.js.map