theme.mjs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. // Utilities
  2. import { computed, inject, provide, ref, watch, watchEffect } from 'vue';
  3. import { createRange, darken, getCurrentInstance, getForeground, getLuma, IN_BROWSER, lighten, mergeDeep, parseColor, propsFactory, RGBtoHex } from "../util/index.mjs"; // Types
  4. export const ThemeSymbol = Symbol.for('vuetify:theme');
  5. export const makeThemeProps = propsFactory({
  6. theme: String
  7. }, 'theme');
  8. function genDefaults() {
  9. return {
  10. defaultTheme: 'light',
  11. variations: {
  12. colors: [],
  13. lighten: 0,
  14. darken: 0
  15. },
  16. themes: {
  17. light: {
  18. dark: false,
  19. colors: {
  20. background: '#FFFFFF',
  21. surface: '#FFFFFF',
  22. 'surface-bright': '#FFFFFF',
  23. 'surface-light': '#EEEEEE',
  24. 'surface-variant': '#424242',
  25. 'on-surface-variant': '#EEEEEE',
  26. primary: '#1867C0',
  27. 'primary-darken-1': '#1F5592',
  28. secondary: '#48A9A6',
  29. 'secondary-darken-1': '#018786',
  30. error: '#B00020',
  31. info: '#2196F3',
  32. success: '#4CAF50',
  33. warning: '#FB8C00'
  34. },
  35. variables: {
  36. 'border-color': '#000000',
  37. 'border-opacity': 0.12,
  38. 'high-emphasis-opacity': 0.87,
  39. 'medium-emphasis-opacity': 0.60,
  40. 'disabled-opacity': 0.38,
  41. 'idle-opacity': 0.04,
  42. 'hover-opacity': 0.04,
  43. 'focus-opacity': 0.12,
  44. 'selected-opacity': 0.08,
  45. 'activated-opacity': 0.12,
  46. 'pressed-opacity': 0.12,
  47. 'dragged-opacity': 0.08,
  48. 'theme-kbd': '#212529',
  49. 'theme-on-kbd': '#FFFFFF',
  50. 'theme-code': '#F5F5F5',
  51. 'theme-on-code': '#000000'
  52. }
  53. },
  54. dark: {
  55. dark: true,
  56. colors: {
  57. background: '#121212',
  58. surface: '#212121',
  59. 'surface-bright': '#ccbfd6',
  60. 'surface-light': '#424242',
  61. 'surface-variant': '#a3a3a3',
  62. 'on-surface-variant': '#424242',
  63. primary: '#2196F3',
  64. 'primary-darken-1': '#277CC1',
  65. secondary: '#54B6B2',
  66. 'secondary-darken-1': '#48A9A6',
  67. error: '#CF6679',
  68. info: '#2196F3',
  69. success: '#4CAF50',
  70. warning: '#FB8C00'
  71. },
  72. variables: {
  73. 'border-color': '#FFFFFF',
  74. 'border-opacity': 0.12,
  75. 'high-emphasis-opacity': 1,
  76. 'medium-emphasis-opacity': 0.70,
  77. 'disabled-opacity': 0.50,
  78. 'idle-opacity': 0.10,
  79. 'hover-opacity': 0.04,
  80. 'focus-opacity': 0.12,
  81. 'selected-opacity': 0.08,
  82. 'activated-opacity': 0.12,
  83. 'pressed-opacity': 0.16,
  84. 'dragged-opacity': 0.08,
  85. 'theme-kbd': '#212529',
  86. 'theme-on-kbd': '#FFFFFF',
  87. 'theme-code': '#343434',
  88. 'theme-on-code': '#CCCCCC'
  89. }
  90. }
  91. }
  92. };
  93. }
  94. function parseThemeOptions() {
  95. let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : genDefaults();
  96. const defaults = genDefaults();
  97. if (!options) return {
  98. ...defaults,
  99. isDisabled: true
  100. };
  101. const themes = {};
  102. for (const [key, theme] of Object.entries(options.themes ?? {})) {
  103. const defaultTheme = theme.dark || key === 'dark' ? defaults.themes?.dark : defaults.themes?.light;
  104. themes[key] = mergeDeep(defaultTheme, theme);
  105. }
  106. return mergeDeep(defaults, {
  107. ...options,
  108. themes
  109. });
  110. }
  111. // Composables
  112. export function createTheme(options) {
  113. const parsedOptions = parseThemeOptions(options);
  114. const name = ref(parsedOptions.defaultTheme);
  115. const themes = ref(parsedOptions.themes);
  116. const computedThemes = computed(() => {
  117. const acc = {};
  118. for (const [name, original] of Object.entries(themes.value)) {
  119. const theme = acc[name] = {
  120. ...original,
  121. colors: {
  122. ...original.colors
  123. }
  124. };
  125. if (parsedOptions.variations) {
  126. for (const name of parsedOptions.variations.colors) {
  127. const color = theme.colors[name];
  128. if (!color) continue;
  129. for (const variation of ['lighten', 'darken']) {
  130. const fn = variation === 'lighten' ? lighten : darken;
  131. for (const amount of createRange(parsedOptions.variations[variation], 1)) {
  132. theme.colors[`${name}-${variation}-${amount}`] = RGBtoHex(fn(parseColor(color), amount));
  133. }
  134. }
  135. }
  136. }
  137. for (const color of Object.keys(theme.colors)) {
  138. if (/^on-[a-z]/.test(color) || theme.colors[`on-${color}`]) continue;
  139. const onColor = `on-${color}`;
  140. const colorVal = parseColor(theme.colors[color]);
  141. theme.colors[onColor] = getForeground(colorVal);
  142. }
  143. }
  144. return acc;
  145. });
  146. const current = computed(() => computedThemes.value[name.value]);
  147. const styles = computed(() => {
  148. const lines = [];
  149. if (current.value?.dark) {
  150. createCssClass(lines, ':root', ['color-scheme: dark']);
  151. }
  152. createCssClass(lines, ':root', genCssVariables(current.value));
  153. for (const [themeName, theme] of Object.entries(computedThemes.value)) {
  154. createCssClass(lines, `.v-theme--${themeName}`, [`color-scheme: ${theme.dark ? 'dark' : 'normal'}`, ...genCssVariables(theme)]);
  155. }
  156. const bgLines = [];
  157. const fgLines = [];
  158. const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors)));
  159. for (const key of colors) {
  160. if (/^on-[a-z]/.test(key)) {
  161. createCssClass(fgLines, `.${key}`, [`color: rgb(var(--v-theme-${key})) !important`]);
  162. } else {
  163. 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`]);
  164. createCssClass(fgLines, `.text-${key}`, [`color: rgb(var(--v-theme-${key})) !important`]);
  165. createCssClass(fgLines, `.border-${key}`, [`--v-border-color: var(--v-theme-${key})`]);
  166. }
  167. }
  168. lines.push(...bgLines, ...fgLines);
  169. return lines.map((str, i) => i === 0 ? str : ` ${str}`).join('');
  170. });
  171. function getHead() {
  172. return {
  173. style: [{
  174. children: styles.value,
  175. id: 'vuetify-theme-stylesheet',
  176. nonce: parsedOptions.cspNonce || false
  177. }]
  178. };
  179. }
  180. function install(app) {
  181. if (parsedOptions.isDisabled) return;
  182. const head = app._context.provides.usehead;
  183. if (head) {
  184. if (head.push) {
  185. const entry = head.push(getHead);
  186. if (IN_BROWSER) {
  187. watch(styles, () => {
  188. entry.patch(getHead);
  189. });
  190. }
  191. } else {
  192. if (IN_BROWSER) {
  193. head.addHeadObjs(computed(getHead));
  194. watchEffect(() => head.updateDOM());
  195. } else {
  196. head.addHeadObjs(getHead());
  197. }
  198. }
  199. } else {
  200. let styleEl = IN_BROWSER ? document.getElementById('vuetify-theme-stylesheet') : null;
  201. if (IN_BROWSER) {
  202. watch(styles, updateStyles, {
  203. immediate: true
  204. });
  205. } else {
  206. updateStyles();
  207. }
  208. function updateStyles() {
  209. if (typeof document !== 'undefined' && !styleEl) {
  210. const el = document.createElement('style');
  211. el.type = 'text/css';
  212. el.id = 'vuetify-theme-stylesheet';
  213. if (parsedOptions.cspNonce) el.setAttribute('nonce', parsedOptions.cspNonce);
  214. styleEl = el;
  215. document.head.appendChild(styleEl);
  216. }
  217. if (styleEl) styleEl.innerHTML = styles.value;
  218. }
  219. }
  220. }
  221. const themeClasses = computed(() => parsedOptions.isDisabled ? undefined : `v-theme--${name.value}`);
  222. return {
  223. install,
  224. isDisabled: parsedOptions.isDisabled,
  225. name,
  226. themes,
  227. current,
  228. computedThemes,
  229. themeClasses,
  230. styles,
  231. global: {
  232. name,
  233. current
  234. }
  235. };
  236. }
  237. export function provideTheme(props) {
  238. getCurrentInstance('provideTheme');
  239. const theme = inject(ThemeSymbol, null);
  240. if (!theme) throw new Error('Could not find Vuetify theme injection');
  241. const name = computed(() => {
  242. return props.theme ?? theme.name.value;
  243. });
  244. const current = computed(() => theme.themes.value[name.value]);
  245. const themeClasses = computed(() => theme.isDisabled ? undefined : `v-theme--${name.value}`);
  246. const newTheme = {
  247. ...theme,
  248. name,
  249. current,
  250. themeClasses
  251. };
  252. provide(ThemeSymbol, newTheme);
  253. return newTheme;
  254. }
  255. export function useTheme() {
  256. getCurrentInstance('useTheme');
  257. const theme = inject(ThemeSymbol, null);
  258. if (!theme) throw new Error('Could not find Vuetify theme injection');
  259. return theme;
  260. }
  261. function createCssClass(lines, selector, content) {
  262. lines.push(`${selector} {\n`, ...content.map(line => ` ${line};\n`), '}\n');
  263. }
  264. function genCssVariables(theme) {
  265. const lightOverlay = theme.dark ? 2 : 1;
  266. const darkOverlay = theme.dark ? 1 : 2;
  267. const variables = [];
  268. for (const [key, value] of Object.entries(theme.colors)) {
  269. const rgb = parseColor(value);
  270. variables.push(`--v-theme-${key}: ${rgb.r},${rgb.g},${rgb.b}`);
  271. if (!key.startsWith('on-')) {
  272. variables.push(`--v-theme-${key}-overlay-multiplier: ${getLuma(value) > 0.18 ? lightOverlay : darkOverlay}`);
  273. }
  274. }
  275. for (const [key, value] of Object.entries(theme.variables)) {
  276. const color = typeof value === 'string' && value.startsWith('#') ? parseColor(value) : undefined;
  277. const rgb = color ? `${color.r}, ${color.g}, ${color.b}` : undefined;
  278. variables.push(`--v-${key}: ${rgb ?? value}`);
  279. }
  280. return variables;
  281. }
  282. //# sourceMappingURL=theme.mjs.map