123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- // Utilities
- import { computed, inject, provide, ref, watch, watchEffect } from 'vue';
- import { createRange, darken, getCurrentInstance, getLuma, IN_BROWSER, lighten, mergeDeep, parseColor, propsFactory, RGBtoHex } from "../util/index.mjs";
- import { APCAcontrast } from "../util/color/APCA.mjs"; // Types
- export const ThemeSymbol = Symbol.for('vuetify:theme');
- export const makeThemeProps = propsFactory({
- theme: String
- }, 'theme');
- const defaultThemeOptions = {
- defaultTheme: 'light',
- variations: {
- colors: [],
- lighten: 0,
- darken: 0
- },
- themes: {
- light: {
- dark: false,
- colors: {
- background: '#FFFFFF',
- surface: '#FFFFFF',
- 'surface-variant': '#424242',
- 'on-surface-variant': '#EEEEEE',
- primary: '#6200EE',
- 'primary-darken-1': '#3700B3',
- secondary: '#03DAC6',
- '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': '#212529',
- 'theme-on-kbd': '#FFFFFF',
- 'theme-code': '#F5F5F5',
- 'theme-on-code': '#000000'
- }
- },
- dark: {
- dark: true,
- colors: {
- background: '#121212',
- surface: '#212121',
- 'surface-variant': '#BDBDBD',
- 'on-surface-variant': '#424242',
- primary: '#BB86FC',
- 'primary-darken-1': '#3700B3',
- secondary: '#03DAC5',
- 'secondary-darken-1': '#03DAC5',
- 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': '#212529',
- 'theme-on-kbd': '#FFFFFF',
- 'theme-code': '#343434',
- 'theme-on-code': '#CCCCCC'
- }
- }
- }
- };
- function parseThemeOptions() {
- let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultThemeOptions;
- if (!options) return {
- ...defaultThemeOptions,
- isDisabled: true
- };
- const themes = {};
- for (const [key, theme] of Object.entries(options.themes ?? {})) {
- const defaultTheme = theme.dark || key === 'dark' ? defaultThemeOptions.themes?.dark : defaultThemeOptions.themes?.light;
- themes[key] = mergeDeep(defaultTheme, theme);
- }
- return mergeDeep(defaultThemeOptions, {
- ...options,
- themes
- });
- }
- // Composables
- export function createTheme(options) {
- const parsedOptions = parseThemeOptions(options);
- const name = ref(parsedOptions.defaultTheme);
- const themes = ref(parsedOptions.themes);
- const computedThemes = computed(() => {
- const acc = {};
- for (const [name, original] of Object.entries(themes.value)) {
- const theme = acc[name] = {
- ...original,
- colors: {
- ...original.colors
- }
- };
- if (parsedOptions.variations) {
- for (const name of parsedOptions.variations.colors) {
- const color = theme.colors[name];
- if (!color) continue;
- for (const variation of ['lighten', 'darken']) {
- const fn = variation === 'lighten' ? lighten : darken;
- for (const amount of createRange(parsedOptions.variations[variation], 1)) {
- theme.colors[`${name}-${variation}-${amount}`] = RGBtoHex(fn(parseColor(color), amount));
- }
- }
- }
- }
- for (const color of Object.keys(theme.colors)) {
- if (/^on-[a-z]/.test(color) || theme.colors[`on-${color}`]) continue;
- const onColor = `on-${color}`;
- const colorVal = parseColor(theme.colors[color]);
- const blackContrast = Math.abs(APCAcontrast(parseColor(0), colorVal));
- const whiteContrast = Math.abs(APCAcontrast(parseColor(0xffffff), colorVal));
- // TODO: warn about poor color selections
- // const contrastAsText = Math.abs(APCAcontrast(colorVal, colorToInt(theme.colors.background)))
- // const minContrast = Math.max(blackContrast, whiteContrast)
- // if (minContrast < 60) {
- // consoleInfo(`${key} theme color ${color} has poor contrast (${minContrast.toFixed()}%)`)
- // } else if (contrastAsText < 60 && !['background', 'surface'].includes(color)) {
- // consoleInfo(`${key} theme color ${color} has poor contrast as text (${contrastAsText.toFixed()}%)`)
- // }
- // Prefer white text if both have an acceptable contrast ratio
- theme.colors[onColor] = whiteContrast > Math.min(blackContrast, 50) ? '#fff' : '#000';
- }
- }
- return acc;
- });
- const current = computed(() => computedThemes.value[name.value]);
- const styles = computed(() => {
- const lines = [];
- if (current.value.dark) {
- createCssClass(lines, ':root', ['color-scheme: dark']);
- }
- createCssClass(lines, ':root', genCssVariables(current.value));
- for (const [themeName, theme] of Object.entries(computedThemes.value)) {
- createCssClass(lines, `.v-theme--${themeName}`, [`color-scheme: ${theme.dark ? 'dark' : 'normal'}`, ...genCssVariables(theme)]);
- }
- const bgLines = [];
- const fgLines = [];
- const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors)));
- for (const key of colors) {
- if (/^on-[a-z]/.test(key)) {
- createCssClass(fgLines, `.${key}`, [`color: rgb(var(--v-theme-${key})) !important`]);
- } else {
- createCssClass(bgLines, `.bg-${key}`, [`--v-theme-overlay-multiplier: var(--v-theme-${key}-overlay-multiplier)`, `background-color: rgb(var(--v-theme-${key})) !important`, `color: rgb(var(--v-theme-on-${key})) !important`]);
- createCssClass(fgLines, `.text-${key}`, [`color: rgb(var(--v-theme-${key})) !important`]);
- createCssClass(fgLines, `.border-${key}`, [`--v-border-color: var(--v-theme-${key})`]);
- }
- }
- lines.push(...bgLines, ...fgLines);
- return lines.map((str, i) => i === 0 ? str : ` ${str}`).join('');
- });
- function getHead() {
- return {
- style: [{
- children: styles.value,
- id: 'vuetify-theme-stylesheet',
- nonce: parsedOptions.cspNonce || false
- }]
- };
- }
- function install(app) {
- if (parsedOptions.isDisabled) return;
- const head = app._context.provides.usehead;
- if (head) {
- if (head.push) {
- const entry = head.push(getHead);
- if (IN_BROWSER) {
- watch(styles, () => {
- entry.patch(getHead);
- });
- }
- } else {
- if (IN_BROWSER) {
- head.addHeadObjs(computed(getHead));
- watchEffect(() => head.updateDOM());
- } else {
- head.addHeadObjs(getHead());
- }
- }
- } else {
- let styleEl = IN_BROWSER ? document.getElementById('vuetify-theme-stylesheet') : null;
- if (IN_BROWSER) {
- watch(styles, updateStyles, {
- immediate: true
- });
- } else {
- updateStyles();
- }
- function updateStyles() {
- if (typeof document !== 'undefined' && !styleEl) {
- const el = document.createElement('style');
- el.type = 'text/css';
- el.id = 'vuetify-theme-stylesheet';
- if (parsedOptions.cspNonce) el.setAttribute('nonce', parsedOptions.cspNonce);
- styleEl = el;
- document.head.appendChild(styleEl);
- }
- if (styleEl) styleEl.innerHTML = styles.value;
- }
- }
- }
- const themeClasses = computed(() => parsedOptions.isDisabled ? undefined : `v-theme--${name.value}`);
- return {
- install,
- isDisabled: parsedOptions.isDisabled,
- name,
- themes,
- current,
- computedThemes,
- themeClasses,
- styles,
- global: {
- name,
- 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 = computed(() => {
- return props.theme ?? theme?.name.value;
- });
- const themeClasses = computed(() => theme.isDisabled ? undefined : `v-theme--${name.value}`);
- const newTheme = {
- ...theme,
- name,
- 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;
- }
- function createCssClass(lines, selector, content) {
- lines.push(`${selector} {\n`, ...content.map(line => ` ${line};\n`), '}\n');
- }
- function genCssVariables(theme) {
- 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(`--v-theme-${key}: ${rgb.r},${rgb.g},${rgb.b}`);
- if (!key.startsWith('on-')) {
- variables.push(`--v-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(`--v-${key}: ${rgb ?? value}`);
- }
- return variables;
- }
- //# sourceMappingURL=theme.mjs.map
|