From fdb1371e12c5f872217a58592e3fdfd74a5a08c4 Mon Sep 17 00:00:00 2001 From: Mario Buikhuizen Date: Wed, 15 Nov 2023 17:28:29 +0100 Subject: [PATCH] feat: add theme support --- ipyvuetify/Themes.py | 47 +++++++++++++---- js/src/Themes.js | 83 +++-------------------------- js/src/VuetifyView.js | 118 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 156 insertions(+), 92 deletions(-) diff --git a/ipyvuetify/Themes.py b/ipyvuetify/Themes.py index e2b3e88a..44b32f00 100644 --- a/ipyvuetify/Themes.py +++ b/ipyvuetify/Themes.py @@ -8,10 +8,16 @@ class Themes: def __init__(self): self.light = ThemeColors( _theme_name="light", - primary="#1976D2", - secondary="#424242", - accent="#82B1FF", - error="#FF5252", + background="#FFFFFF", + surface="#FFFFFF", + surface_bright="#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", @@ -19,10 +25,16 @@ def __init__(self): self.dark = ThemeColors( _theme_name="dark", - primary="#2196F3", - secondary="#424242", - accent="#FF4081", - error="#FF5252", + background="#121212", + surface="#212121", + surface_bright="#ccbfd6", + surface_variant="#a3a3a3", + on_surface_variant="#424242", + primary="#BB86FC", + primary_darken_1="#3700B3", + secondary="#03DAC5", + secondary_darken_1="#03DAC5", + error="#CF6679", info="#2196F3", success="#4CAF50", warning="#FB8C00", @@ -48,6 +60,14 @@ def __init__(self): self.themes = Themes() +class ColorNotAvailable(Unicode): + def get(self, *ignored): + raise AttributeError(f"The theme color '{self.name}' is no longer available in Vuetify 3") + + def set(self, *ignored): + raise AttributeError(f"The theme color '{self.name}' is no longer available in Vuetify 3") + + class ThemeColors(Widget): _model_name = Unicode("ThemeColorsModel").tag(sync=True) @@ -60,14 +80,21 @@ class ThemeColors(Widget): _theme_name = Unicode().tag(sync=True) + accent = ColorNotAvailable() + anchor = ColorNotAvailable() + background = Unicode().tag(sync=True) + surface = Unicode().tag(sync=True) + surface_bright = Unicode().tag(sync=True) + surface_variant = Unicode().tag(sync=True) + on_surface_variant = Unicode().tag(sync=True) primary = Unicode().tag(sync=True) + primary_darken_1 = Unicode().tag(sync=True) secondary = Unicode().tag(sync=True) - accent = Unicode().tag(sync=True) + secondary_darken_1 = Unicode().tag(sync=True) error = Unicode().tag(sync=True) info = Unicode().tag(sync=True) success = Unicode().tag(sync=True) warning = Unicode().tag(sync=True) - anchor = Unicode(None, allow_none=True).tag(sync=True) theme = Theme() diff --git a/js/src/Themes.js b/js/src/Themes.js index 27883fed..dc345f5e 100644 --- a/js/src/Themes.js +++ b/js/src/Themes.js @@ -1,7 +1,5 @@ /* eslint camelcase: off */ import { WidgetModel } from "@jupyter-widgets/base"; -// import colors from "@mariobuikhuizen/vuetify/lib/util/colors"; -// import vuetify from "./plugins/vuetify"; export class ThemeModel extends WidgetModel { defaults() { @@ -20,40 +18,6 @@ export class ThemeModel extends WidgetModel { constructor(...args) { super(...args); - - // if (!vuetify) { - // return; - // } - - // if (ThemeModel.themeManager) { - // ThemeModel.themeManager.themeChanged.connect(() => { - // if (this.get("dark") === null) { - // vuetify.framework.theme.dark = - // document.body.dataset.jpThemeLight === "false"; - // this.set("dark_jlab", vuetify.framework.theme.dark); - // this.save_changes(); - // } - // }, this); - // } - // - // if (this.get("dark") !== null) { - // vuetify.framework.theme.dark = this.get("dark"); - // } else if (document.body.dataset.jpThemeLight) { - // vuetify.framework.theme.dark = - // document.body.dataset.jpThemeLight === "false"; - // this.set("dark_jlab", vuetify.framework.theme.dark); - // this.save_changes(); - // } else if (document.body.classList.contains("theme-dark")) { - // vuetify.framework.theme.dark = true; - // this.set("dark", true); - // this.save_changes(); - // } else if (document.body.classList.contains("theme-light")) { - // this.set("dark", false); - // this.save_changes(); - // } - // this.on("change:dark", () => { - // vuetify.framework.theme.dark = this.get("dark"); - // }); } } @@ -71,61 +35,28 @@ export class ThemeColorsModel extends WidgetModel { _view_module_version: "0.1.11", _model_module_version: "0.1.11", _theme_name: null, + background: null, + surface: null, + surface_bright: null, + surface_variant: null, + on_surface_variant: null, primary: null, + primary_darken_1: null, secondary: null, - accent: null, + secondary_darken_1: null, error: null, info: null, success: null, warning: null, - anchor: null, }, }; } constructor(...args) { super(...args); - - // if (!vuetify) { - // return; - // } - - const themeName = this.get("_theme_name"); - - // this.keys() - // .filter((prop) => !prop.startsWith("_")) - // .forEach((prop) => { - // vuetify.framework.theme.themes[themeName][prop] = convertColor( - // this.get(prop) - // ); - // this.on(`change:${prop}`, () => { - // vuetify.framework.theme.themes[themeName][prop] = convertColor( - // this.get(prop) - // ); - // }); - // }); } } ThemeColorsModel.serializers = { ...WidgetModel.serializers, }; - -function convertColor(colorStr) { - if (colorStr == null) { - return null; - } - - if (colorStr.startsWith("colors")) { - const parts = colorStr.split(".").slice(1); - let result = colors; - - parts.forEach((part) => { - result = result[part]; - }); - - return result; - } - - return colorStr; -} diff --git a/js/src/VuetifyView.js b/js/src/VuetifyView.js index daa19078..5ea3f9af 100644 --- a/js/src/VuetifyView.js +++ b/js/src/VuetifyView.js @@ -1,10 +1,12 @@ import * as Vue from "vue"; // eslint-disable-line import/no-extraneous-dependencies import { VueView, createViewContext, vueRender } from "jupyter-vue"; import "vuetify/styles"; -import { createVuetify } from "vuetify"; +import colors from "vuetify/lib/util/colors.mjs"; +import { createVuetify, useTheme } from "vuetify"; import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; import { VDataTable } from "vuetify/labs/VDataTable"; +import { ThemeColorsModel, ThemeModel } from "./Themes"; const observer = new MutationObserver((mutationList, observer) => { for (const mutation of mutationList) { @@ -24,11 +26,6 @@ observer.observe(document.body, { childList: true }); export class VuetifyView extends VueView { addPlugins(vueApp) { - console.log( - Vue.compile( - `test` - ) - ); super.addPlugins(vueApp); const vuetify = createVuetify({ components: { ...components, VDataTable }, @@ -39,6 +36,28 @@ export class VuetifyView extends VueView { vueApp.use(vuetify); } + async beforeViewRender() { + await super.beforeViewRender(); + const models = await Promise.all( + Object.values(this.model.widget_manager._models) + ); + this.themeModel = models.find((m) => m instanceof ThemeModel); + models + .filter((m) => m instanceof ThemeColorsModel) + .forEach((m) => { + if (m.get("_theme_name") === "light") { + this.themeLightModel = m; + } else if (m.get("_theme_name") === "dark") { + this.themeDarkModel = m; + } + }); + } + + onSetup() { + super.onSetup(); + this.setupTheme(); + } + /* used in pages using nodeps */ vueRender() { return Vue.h({ @@ -50,4 +69,91 @@ export class VuetifyView extends VueView { }, }); } + + setupTheme() { + this.theme = useTheme(); + + if (ThemeModel.themeManager) { + const setAutoTheme = () => { + if (this.themeModel.get("dark") === null) { + const isDark = document.body.dataset.jpThemeLight === "false"; + this.theme.global.name.value = isDark ? "dark" : "light"; + this.themeModel.set("dark_jlab", isDark); + this.themeModel.save_changes(); + } + }; + ThemeModel.themeManager.themeChanged.connect(() => { + setAutoTheme(); + }, this); + setAutoTheme(); + } + + const onDark = () => { + this.theme.global.name.value = this.themeModel.get("dark") + ? "dark" + : "light"; + }; + + const onColorsLight = () => { + this.theme.themes.value.light.colors = getColors(this.themeLightModel); + }; + onColorsLight(); + + const onColorsDark = () => { + this.theme.themes.value.dark.colors = getColors(this.themeDarkModel); + }; + onColorsDark(); + + if (this.themeModel.get("dark") !== null) { + onDark(); + } else if (document.body.dataset.jpThemeLight) { + const isDark = document.body.dataset.jpThemeLight === "false"; + this.theme.global.name.value = isDark ? "dark" : "light"; + this.themeModel.set("dark_jlab", isDark); + this.themeModel.save_changes(); + } else if (document.body.classList.contains("theme-dark")) { + this.theme.global.name.value = "dark"; + this.themeModel.set("dark", true); + this.themeModel.save_changes(); + } else if (document.body.classList.contains("theme-light")) { + this.themeModel.set("dark", false); + this.themeModel.save_changes(); + } + + Vue.onMounted(() => { + this.themeModel.on("change:dark", onDark); + this.themeLightModel.on("change", onColorsLight); + this.themeDarkModel.on("change", onColorsDark); + }); + + Vue.onUnmounted(() => { + this.themeModel.off("change:dark", onDark); + this.themeLightModel.off("change", onColorsLight); + this.themeDarkModel.off("change", onColorsDark); + }); + } +} + +function parseColor(colorStr) { + const parts = colorStr.split(".").slice(1); + let result = colors; + + parts.forEach((part) => { + result = result[part]; + }); + + return typeof result === "string" ? result : result.base; +} + +function getColors(colorModel) { + return _.mapKeys( + _.mapValues( + _.pickBy( + { ...colorModel.attributes }, + (v, k) => !k.startsWith("_") && k !== "accent" && k !== "anchor" + ), + (v) => (v.startsWith("colors.") ? parseColor(v) : v) + ), + (v, k) => k.replace(/_/g, "-") + ); }