From aec06e5416d82d16e5e30390238c6b5f77e59ff3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 8 Feb 2023 19:15:50 +0100 Subject: [PATCH 01/25] Factor out theming to be independent of Templates --- panel/compiler.py | 76 +++--- panel/config.py | 32 ++- panel/io/resources.py | 2 + panel/template/__init__.py | 2 +- panel/template/base.py | 227 ++++++------------ panel/template/bootstrap/__init__.py | 72 ++---- panel/template/bootstrap/dark.css | 0 panel/template/bootstrap/default.css | 0 panel/template/fast/base.py | 50 +--- panel/template/fast/grid/__init__.py | 23 +- panel/template/fast/grid/dark.css | 19 -- panel/template/fast/grid/default.css | 4 - panel/template/fast/list/__init__.py | 20 -- panel/template/fast/list/dark.css | 1 - panel/template/fast/list/default.css | 1 - panel/template/golden/__init__.py | 23 -- panel/template/golden/dark.css | 26 -- panel/template/golden/default.css | 23 -- panel/template/material/__init__.py | 172 +------------ panel/template/react/__init__.py | 30 +-- panel/template/react/components.css | 7 - panel/template/react/dark.css | 18 -- panel/template/react/default.css | 12 - panel/template/theme/__init__.py | 78 ------ panel/template/vanilla/__init__.py | 23 -- panel/template/vanilla/default.css | 8 - panel/theme/__init__.py | 3 + panel/theme/base.py | 184 ++++++++++++++ panel/theme/bootstrap.py | 60 +++++ .../css/bootstrap.css} | 0 panel/{template/theme => theme/css}/dark.css | 0 .../{template/theme => theme/css}/default.css | 0 .../components.css => theme/css/fast.css} | 0 .../components.css => theme/css/material.css} | 134 ++--------- .../{template/fast/theme.py => theme/fast.py} | 56 ++++- panel/theme/js/fast_design.js | 107 +++++++++ panel/theme/material.py | 162 +++++++++++++ panel/viewable.py | 2 + 38 files changed, 769 insertions(+), 888 deletions(-) delete mode 100644 panel/template/bootstrap/dark.css delete mode 100644 panel/template/bootstrap/default.css delete mode 100644 panel/template/fast/grid/dark.css delete mode 100644 panel/template/fast/grid/default.css delete mode 100644 panel/template/fast/list/dark.css delete mode 100644 panel/template/fast/list/default.css delete mode 100644 panel/template/golden/dark.css delete mode 100644 panel/template/golden/default.css delete mode 100644 panel/template/react/components.css delete mode 100644 panel/template/react/dark.css delete mode 100644 panel/template/react/default.css delete mode 100644 panel/template/theme/__init__.py delete mode 100644 panel/template/vanilla/default.css create mode 100644 panel/theme/__init__.py create mode 100644 panel/theme/base.py create mode 100644 panel/theme/bootstrap.py rename panel/{template/bootstrap/components.css => theme/css/bootstrap.css} (100%) rename panel/{template/theme => theme/css}/dark.css (100%) rename panel/{template/theme => theme/css}/default.css (100%) rename panel/{template/fast/components.css => theme/css/fast.css} (100%) rename panel/{template/material/components.css => theme/css/material.css} (72%) rename panel/{template/fast/theme.py => theme/fast.py} (82%) create mode 100644 panel/theme/js/fast_design.js create mode 100644 panel/theme/material.py diff --git a/panel/compiler.py b/panel/compiler.py index 830c3e3b38..9e00745b8d 100644 --- a/panel/compiler.py +++ b/panel/compiler.py @@ -20,7 +20,7 @@ from .io.resources import RESOURCE_URLS from .reactive import ReactiveHTML from .template.base import BasicTemplate -from .template.theme import Theme +from .theme import Theme, Themer BUNDLE_DIR = pathlib.Path(__file__).parent / 'dist' / 'bundled' @@ -137,6 +137,17 @@ def write_bundled_zip(name, resource): f.write(fdata.decode('utf-8')) zip_obj.close() +def write_component_resources(name, component): + write_bundled_files(name, list(component._resources.get('css', {}).values()), BUNDLE_DIR, 'css') + write_bundled_files(name, list(component._resources.get('js', {}).values()), BUNDLE_DIR, 'js') + js_modules = [] + for tar_name, js_module in component._resources.get('js_modules', {}).items(): + if tar_name not in component._resources.get('tarball', {}): + js_modules.append(js_module) + write_bundled_files(name, js_modules, 'js', ext='mjs') + for tarball in component._resources.get('tarball', {}).values(): + write_bundled_tarball(tarball) + def bundle_resource_urls(verbose=False, external=True): # Collect shared resources for name, resource in RESOURCE_URLS.items(): @@ -152,17 +163,10 @@ def bundle_templates(verbose=False, external=True): for name, template in param.concrete_descendents(BasicTemplate).items(): if verbose: print(f'Bundling {name} resources') + # Bundle Template._resources if template._resources.get('bundle', True) and external: - write_bundled_files(name, list(template._resources.get('css', {}).values()), BUNDLE_DIR, 'css') - write_bundled_files(name, list(template._resources.get('js', {}).values()), BUNDLE_DIR, 'js') - js_modules = [] - for tar_name, js_module in template._resources.get('js_modules', {}).items(): - if tar_name not in template._resources.get('tarball', {}): - js_modules.append(js_module) - write_bundled_files(name, js_modules, 'js', ext='mjs') - for tarball in template._resources.get('tarball', {}).values(): - write_bundled_tarball(tarball) + write_component_resources(name, template) # Bundle CSS files in template dir template_dir = pathlib.Path(inspect.getfile(template)).parent @@ -198,33 +202,8 @@ def bundle_templates(verbose=False, external=True): tmpl_dest_dir = BUNDLE_DIR / tmpl_name shutil.copyfile(js, tmpl_dest_dir / os.path.basename(js)) - # Bundle template stylesheets - for scls, modifiers in template._modifiers.items(): - cls_modifiers = template._modifiers.get(scls, {}) - if 'stylesheets' not in cls_modifiers: - continue - # Find the Template class the options were first defined on - def_cls = [ - super_cls for super_cls in template.__mro__[::-1] - if getattr(super_cls, '_modifiers', {}).get(scls) is cls_modifiers - ][0] - def_path = pathlib.Path(inspect.getmodule(def_cls).__file__).parent - for sts in cls_modifiers['stylesheets']: - - if not isinstance(sts, str) or not sts.endswith('.css') or sts.startswith('http') or sts.startswith('/'): - continue - bundled_path = BUNDLE_DIR / def_cls.__name__.lower() / sts - shutil.copyfile(def_path / sts, bundled_path) - def bundle_themes(verbose=False, external=True): - # Bundle base themes - dest_dir = BUNDLE_DIR / 'theme' - theme_dir = pathlib.Path(inspect.getfile(Theme)).parent - dest_dir.mkdir(parents=True, exist_ok=True) - for css in glob.glob(str(theme_dir / '*.css')): - shutil.copyfile(css, dest_dir / os.path.basename(css)) - # Bundle Theme classes for name, theme in param.concrete_descendents(Theme).items(): if verbose: @@ -238,6 +217,33 @@ def bundle_themes(verbose=False, external=True): tmplt_bundle_dir.mkdir(parents=True, exist_ok=True) shutil.copyfile(theme.css, tmplt_bundle_dir / os.path.basename(theme.css)) + # Bundle themer stylesheets + for name, themer in param.concrete_descendents(Themer).items(): + if verbose: + print(f'Bundling {name} themer resources') + + # Bundle Themer._resources + if themer._resources.get('bundle', True) and external: + write_component_resources(name, themer) + + for scls, modifiers in themer._modifiers.items(): + cls_modifiers = themer._modifiers.get(scls, {}) + if 'stylesheets' not in cls_modifiers: + continue + + # Find the Themer class the options were first defined on + def_cls = [ + super_cls for super_cls in themer.__mro__[::-1] + if getattr(super_cls, '_modifiers', {}).get(scls) is cls_modifiers + ][0] + def_path = pathlib.Path(inspect.getmodule(def_cls).__file__).parent + for sts in cls_modifiers['stylesheets']: + if not isinstance(sts, str) or not sts.endswith('.css') or sts.startswith('http') or sts.startswith('/'): + continue + bundled_path = BUNDLE_DIR / def_cls.__name__.lower() / sts + bundled_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(def_path / sts, bundled_path) + def bundle_models(verbose=False, external=True): for imp in panel_extension._imports.values(): if imp.startswith('panel.models'): diff --git a/panel/config.py b/panel/config.py index 5895efaced..c0ba9d5ef2 100644 --- a/panel/config.py +++ b/panel/config.py @@ -5,6 +5,7 @@ """ import ast import copy +import importlib import inspect import os import sys @@ -21,6 +22,7 @@ from .io.logging import panel_log_handler from .io.state import state +from .theme import Themer __version__ = str(param.version.Version( fpath=__file__, archive_commit="$Format:%h$", reponame="panel")) @@ -115,6 +117,9 @@ class _config(_base_config): defer_load = param.Boolean(default=False, doc=""" Whether to defer load of rendered functions.""") + design_system = param.ObjectSelector(default=None, objects=[], doc=""" + The design system to use to style components.""") + exception_handler = param.Callable(default=None, doc=""" General exception handler for events.""") @@ -160,9 +165,6 @@ class _config(_base_config): template = param.ObjectSelector(default=None, doc=""" The default template to render served applications into.""") - theme = param.ObjectSelector(default='default', objects=['default', 'dark'], doc=""" - The theme to apply to the selected global template.""") - throttled = param.Boolean(default=False, doc=""" If sliders and inputs should be throttled until release of mouse.""") @@ -245,6 +247,9 @@ class _config(_base_config): Whether to inline JS and CSS resources. If disabled, resources are loaded from CDN if one is available.""") + _theme = param.ObjectSelector(default=None, objects=['default', 'dark'], allow_None=True, doc=""" + The theme to apply to components.""") + # Global parameters that are shared across all sessions _globals = [ 'admin_plugins', 'autoreload', 'comms', 'cookie_secret', @@ -477,6 +482,27 @@ def oauth_extra_params(self): else: return self._oauth_extra_params + @property + def theme(self): + if self._theme: + return self._theme + from .io.state import state + theme = state.session_args.get('theme', [b'default'])[0].decode('utf-8') + if theme in self.param._theme.objects: + return theme + return 'default' + + @property + def themer(self): + try: + importlib.import_module(f'panel.theme.{self.design_system}') + except Exception: + pass + themers = { + p.lower(): t for p, t in param.concrete_descendents(Themer).items() + } + return themers.get(self.design_system, Themer)(theme=self.theme) + if hasattr(_config.param, 'objects'): _params = _config.param.objects() diff --git a/panel/io/resources.py b/panel/io/resources.py index b7d5a22249..3841c861c5 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -386,6 +386,8 @@ def js_files(self): js_files = self.adjust_paths(files) js_files += list(config.js_files.values()) + if config.themer: + js_files += list(config.themer._resources.get('js', {}).values()) # Load requirejs last to avoid interfering with other libraries dist_dir = self.dist_dir diff --git a/panel/template/__init__.py b/panel/template/__init__.py index 935e5a2c42..2c0d2f3645 100644 --- a/panel/template/__init__.py +++ b/panel/template/__init__.py @@ -1,11 +1,11 @@ from ..config import _config +from ..theme import DarkTheme, DefaultTheme # noqa from .base import BaseTemplate, Template # noqa from .bootstrap import BootstrapTemplate # noqa from .fast import FastGridTemplate, FastListTemplate # noqa from .golden import GoldenTemplate # noqa from .material import MaterialTemplate # noqa from .react import ReactTemplate # noqa -from .theme import DarkTheme, DefaultTheme # noqa from .vanilla import VanillaTemplate # noqa templates = { diff --git a/panel/template/base.py b/panel/template/base.py index b437cf2d3e..c7a5c4cbfe 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -4,7 +4,6 @@ """ from __future__ import annotations -import functools import os import sys import uuid @@ -18,7 +17,6 @@ import param from bokeh.document.document import Document -from bokeh.models import ImportedStyleSheet from bokeh.settings import settings as _settings from pyviz_comms import JupyterCommManager as _JupyterCommManager @@ -39,11 +37,13 @@ ) from ..pane.image import ImageBase from ..reactive import ReactiveHTML +from ..theme import ( + THEMES, DefaultTheme, Theme, Themer, +) from ..util import isurl, url_path from ..viewable import Renderable, ServableMixin, Viewable from ..widgets import Button from ..widgets.indicators import BooleanIndicator, LoadingSpinner -from .theme import THEMES, DefaultTheme, Theme if TYPE_CHECKING: from bokeh.model import Model @@ -70,12 +70,6 @@ class ResourcesType(TypedDict): FAVICON_URL: str = "/static/extensions/panel/images/favicon.ico" -class Inherit: - """ - Singleton object to declare stylesheet inheritance. - """ - - class BaseTemplate(param.Parameterized, ServableMixin): location = param.Boolean(default=False, doc=""" @@ -84,6 +78,13 @@ class BaseTemplate(param.Parameterized, ServableMixin): either insert all available roots or explicitly embed the location root with : {{ embed(roots.location) }}.""") + theme = param.ClassSelector(class_=Theme, default=DefaultTheme, + constant=True, is_instance=False, instantiate=False) + + themer = param.ClassSelector(class_=Themer, default=Themer, constant=True, + is_instance=False, instantiate=False, doc=""" + A Themer applies a specific design system to a template.""") + # Dictionary of property overrides by Viewable type _modifiers: ClassVar[Dict[Type[Viewable], Dict[str, Any]]] = {} @@ -109,6 +110,7 @@ def __init__( self._documents: List[Document] = [] self._server = None self._layout = self._build_layout() + self._themer = self.themer(theme=self.theme) def _build_layout(self) -> Column: str_repr = Str(repr(self)) @@ -136,88 +138,6 @@ def __repr__(self) -> str: ]) return f'{type(self).__name__}{spacer}{objs}' - def _apply_hooks(self, viewable: Viewable, root: Model) -> None: - ref = root.ref['id'] - for o in viewable.select(): - self._apply_modifiers(o, ref) - - @classmethod - def _resolve_stylesheets(cls, value, defining_cls, inherited): - new_value = [] - for v in value: - if v is Inherit: - new_value.extend(inherited) - elif isinstance(v, str) and v.endswith('.css'): - if v.startswith('http'): - url = v - elif v.startswith('/'): - url = v[1:] - else: - url = os.path.join('bundled', defining_cls.__name__.lower(), v) - new_value.append(url) - else: - new_value.append(v) - return new_value - - @classmethod - @functools.lru_cache(maxsize=None) - def _resolve_modifiers(cls, vtype, theme): - """ - Iterate over the class hierarchy in reverse order and accumulate - all modifiers that apply to the objects class and its super classes. - """ - modifiers, child_modifiers = {}, {} - for scls in vtype.__mro__[::-1]: - cls_modifiers = cls._modifiers.get(scls, {}) - if cls_modifiers: - # Find the Template class the options were first defined on - def_cls = [ - super_cls for super_cls in cls.__mro__[::-1] - if getattr(super_cls, '_modifiers', {}).get(scls) is cls_modifiers - ][0] - - for prop, value in cls_modifiers.items(): - if prop == 'children': - continue - elif prop == 'stylesheets': - modifiers[prop] = cls._resolve_stylesheets(value, def_cls, modifiers.get(prop, [])) - else: - modifiers[prop] = value - if theme: - modifiers.update(theme._modifiers.get(scls, {})) - child_modifiers.update(cls_modifiers.get('children', {})) - return modifiers, child_modifiers - - @classmethod - def _apply_params(cls, viewable, mref, modifiers): - model, _ = viewable._models[mref] - params = { - k: v for k, v in modifiers.items() if k != 'children' and - getattr(viewable, k) == viewable.param[k].default - } - if 'stylesheets' in modifiers: - params['stylesheets'] = modifiers['stylesheets'] + viewable.stylesheets - props = viewable._process_param_change(params) - model.update(**props) - - @classmethod - def _apply_modifiers(cls, viewable: Viewable, mref: str, theme: Theme | None = None) -> None: - if mref not in viewable._models: - return - model, _ = viewable._models[mref] - modifiers, child_modifiers = cls._resolve_modifiers(type(viewable), theme) - modifiers = dict(modifiers) - if 'stylesheets' in modifiers: - modifiers['stylesheets'] = [ - ImportedStyleSheet(url=sts) if sts.endswith('.css') else sts - for sts in modifiers['stylesheets'] - ] - if child_modifiers: - for child in viewable: - cls._apply_params(child, mref, child_modifiers) - if modifiers: - cls._apply_params(viewable, mref, modifiers) - def _apply_root(self, name: str, model: Model, tags: List[str]) -> None: pass @@ -232,6 +152,7 @@ def _init_doc( title: Optional[str] = None, notebook: bool = False, location: bool | Location=True ): + # Initialize document document: Document = doc or curdoc_locked() self._documents.append(document) if document not in state._templates: @@ -249,18 +170,26 @@ def _init_doc( # which assume that all models are owned by a single root can # link objects across multiple roots in a template. col = Column() - preprocess_root = col.get_root(document, comm) - col._hooks.append(self._apply_hooks) + preprocess_root = col.get_root(document, comm, preprocess=False) + col._hooks.append(self._themer._apply_hooks) ref = preprocess_root.ref['id'] - objs, models = [], [] + # Add all render items to the document + objs, models = [], [] for name, (obj, tags) in self._render_items.items(): - if self._apply_hooks not in obj._hooks: - obj._hooks.append(self._apply_hooks) - # We skip preprocessing on the individual roots + + # Render root without pre-processing model = obj.get_root(document, comm, preprocess=False) + model.name = name + model.tags = tags mref = model.ref['id'] - document.on_session_destroyed(obj._server_destroy) # type: ignore + + # Insert themer as pre-processor + if self._themer._apply_hooks not in obj._hooks: + obj._hooks.append(self._themer._apply_hooks) + + # Alias model ref with the fake root ref to ensure that + # pre-processor correctly operates on fake root for sub in obj.select(Viewable): submodel = sub._models.get(mref) if submodel is None: @@ -268,13 +197,16 @@ def _init_doc( sub._models[ref] = submodel if isinstance(sub, HoloViews) and mref in sub._plots: sub._plots[ref] = sub._plots.get(mref) - obj._documents[document] = model - model.name = name - model.tags = tags + + # Apply any template specific hooks to root self._apply_root(name, model, tags) - add_to_doc(model, document, hold=bool(comm)) + + # Add document + obj._documents[document] = model objs.append(obj) models.append(model) + add_to_doc(model, document, hold=bool(comm)) + document.on_session_destroyed(obj._server_destroy) # type: ignore # Here we ensure that the preprocessor is run across all roots # and set up session cleanup hooks for the fake root. @@ -282,15 +214,18 @@ def _init_doc( state._views[ref] = (col, preprocess_root, document, comm) col.objects = objs preprocess_root.children[:] = models + preprocess_root.document = document col._preprocess(preprocess_root) col._documents[document] = preprocess_root document.on_session_destroyed(col._server_destroy) # type: ignore + # Apply the jinja2 template and update template variables if notebook: document.template = self.nb_template else: document.template = self.template document._template_variables.update(self._render_variables) + return document def _repr_mimebundle_( @@ -549,9 +484,6 @@ class BasicTemplate(BaseTemplate): header_color = param.String(doc=""" Optional header text color override.""") - theme = param.ClassSelector(class_=Theme, default=DefaultTheme, - constant=True, is_instance=False, instantiate=False) - location = param.Boolean(default=True, readonly=True) _actions = param.ClassSelector(default=TemplateActions(), class_=TemplateActions) @@ -578,8 +510,6 @@ class BasicTemplate(BaseTemplate): 'css': {}, 'js': {}, 'js_modules': {}, 'tarball': {} } - _modifiers: ClassVar[Dict[Type[Viewable], Dict[str, Any]]] = {} - __abstract = True def __init__(self, **params): @@ -636,28 +566,10 @@ def _init_doc( document = super()._init_doc(doc, comm, title, notebook, location) if self.notifications: state._notifications[document] = self.notifications - if self.theme: - theme = self._get_theme() - if theme and theme.bokeh_theme: - document.theme = theme.bokeh_theme + if self._themer.theme.bokeh_theme: + document.theme = self._themer.theme.bokeh_theme return document - def _apply_hooks(self, viewable: Viewable, root: Model) -> None: - ref = root.ref['id'] - theme = self._get_theme() - for o in viewable.select(): - self._apply_modifiers(o, ref, theme) - if theme and theme.bokeh_theme and root.document: - root.document.theme = theme.bokeh_theme - - def _get_theme(self) -> Theme | None: - for cls in type(self).__mro__: - try: - return self.theme.find_theme(cls)() - except Exception: - pass - return None - def _template_resources(self) -> ResourcesType: clsname = type(self).__name__ name = clsname.lower() @@ -681,12 +593,39 @@ def _template_resources(self) -> ResourcesType: 'raw_css': list(self.config.raw_css) } + theme = self._themer.theme + if theme and theme.base_css: + basename = os.path.basename(theme.base_css) + owner = type(theme).param.base_css.owner + owner_name = owner.__name__.lower() + if (BUNDLE_DIR / owner_name / basename).is_file(): + css_files['theme_base'] = dist_path + f'bundled/{owner_name}/{basename}' + elif isurl(theme.base_css): + css_files['theme_base'] = theme.base_css + elif resolve_custom_path(theme, theme.base_css): + css_files['theme_base'] = component_resource_path(owner, 'base_css', theme.base_css) + if theme and theme.css: + basename = os.path.basename(theme.css) + if (BUNDLE_DIR / name / basename).is_file(): + css_files['theme'] = dist_path + f'bundled/{name}/{basename}' + elif isurl(theme.css): + css_files['theme'] = theme.css + elif resolve_custom_path(theme, theme.css): + css_files['theme'] = component_resource_path(theme, 'css', theme.css) + resolved_resources: List[Literal['css', 'js', 'js_modules']] = ['css', 'js', 'js_modules'] + resources = dict(self._resources) + for rt, res in self._themer._resources.items(): + if rt in resources: + resources[rt] = dict(resources[rt], **res) + else: + resources[rt] = res + for resource_type in resolved_resources: - if resource_type not in self._resources: + if resource_type not in resources: continue resource_files = resource_types[resource_type] - for rname, resource in self._resources[resource_type].items(): + for rname, resource in resources[resource_type].items(): if resource.startswith(CDN_DIST): resource_path = resource.replace(f'{CDN_DIST}bundled/', '') elif resource.startswith(config.npm_cdn): @@ -768,27 +707,6 @@ def _template_resources(self) -> ResourcesType: elif resolve_custom_path(self, js): js_files[f'base_{js_name}'] = component_resource_path(self, '_js', js) - theme = self._get_theme() - if not theme: - return resource_types - if theme.base_css: - basename = os.path.basename(theme.base_css) - owner = type(theme).param.base_css.owner - owner_name = owner.__name__.lower() - if (BUNDLE_DIR / owner_name / basename).is_file(): - css_files['theme_base'] = dist_path + f'bundled/{owner_name}/{basename}' - elif isurl(theme.base_css): - css_files['theme_base'] = theme.base_css - elif resolve_custom_path(theme, theme.base_css): - css_files['theme_base'] = component_resource_path(owner, 'base_css', theme.base_css) - if theme.css: - basename = os.path.basename(theme.css) - if (BUNDLE_DIR / name / basename).is_file(): - css_files['theme'] = dist_path + f'bundled/{name}/{basename}' - elif isurl(theme.css): - css_files['theme'] = theme.css - elif resolve_custom_path(theme, theme.css): - css_files['theme'] = component_resource_path(theme, 'css', theme.css) return resource_types def _update_vars(self, *args) -> None: @@ -829,7 +747,7 @@ def _update_vars(self, *args) -> None: self._render_variables['header_color'] = self.header_color self._render_variables['main_max_width'] = self.main_max_width self._render_variables['sidebar_width'] = self.sidebar_width - self._render_variables['theme'] = self._get_theme() + self._render_variables['theme'] = self._themer.theme def _update_busy(self) -> None: if self.busy_indicator: @@ -857,15 +775,12 @@ def _update_render_items(self, event: param.parameterized.Event) -> None: del self._render_items[ref] new = event.new if isinstance(event.new, list) else event.new.values() - theme = self._get_theme() - if theme: - bk_theme = theme.bokeh_theme + if self._themer.theme.bokeh_theme: for o in new: if o in old: continue for hvpane in o.select(HoloViews): - if bk_theme: - hvpane.theme = bk_theme + hvpane.theme = self._themer.theme.bokeh_theme labels = {} for obj in new: diff --git a/panel/template/bootstrap/__init__.py b/panel/template/bootstrap/__init__.py index c34a13979b..28900dab13 100644 --- a/panel/template/bootstrap/__init__.py +++ b/panel/template/bootstrap/__init__.py @@ -9,12 +9,11 @@ import param -from ...io.resources import CSS_URLS, JS_URLS -from ...layout import Card -from ...viewable import Viewable -from ...widgets import Number, Tabulator -from ..base import BasicTemplate, Inherit, TemplateActions -from ..theme import DarkTheme, DefaultTheme +from ...theme import Themer +from ...theme.bootstrap import Bootstrap +from ..base import BasicTemplate, TemplateActions + +_ROOT = pathlib.Path(__file__).parent class BootstrapTemplateActions(TemplateActions): @@ -34,60 +33,17 @@ class BootstrapTemplate(BasicTemplate): sidebar_width = param.Integer(350, doc=""" The width of the sidebar in pixels. Default is 350.""") + themer = param.ClassSelector(class_=Themer, default=Bootstrap, constant=True, + is_instance=False, instantiate=False, doc=""" + A Themer applies a specific design system to a template.""") + _actions = param.ClassSelector(default=BootstrapTemplateActions(), class_=TemplateActions) - _css = pathlib.Path(__file__).parent / 'bootstrap.css' - - _template = pathlib.Path(__file__).parent / 'bootstrap.html' - - _modifiers = { - Card: { - 'children': {'margin': (10, 10)}, - 'button_css_classes': ['card-button'], - 'margin': (10, 5) - }, - Tabulator: { - 'theme': 'bootstrap4' - }, - Viewable: { - 'stylesheets': [Inherit, 'components.css'] - } - } + _css = [_ROOT / "bootstrap.css"] + + _template = _ROOT / 'bootstrap.html' - _resources = { - 'css': { - 'bootstrap': CSS_URLS['bootstrap5'] - }, - 'js': { - 'jquery': JS_URLS['jQuery'], - 'bootstrap': JS_URLS['bootstrap5'] - } - } def _update_vars(self, *args) -> None: super()._update_vars(*args) - theme = self._render_variables['theme'] - self._render_variables['html_attrs'] = f'data-bs-theme="{theme._bs_theme}"' - - -class BootstrapDefaultTheme(DefaultTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - _bs_theme = 'light' - - _template = BootstrapTemplate - - -class BootstrapDarkTheme(DarkTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css') - - _bs_theme = 'dark' - - _modifiers = { - Number: { - 'default_color': 'var(--bs-body-color)' - } - } - - _template = BootstrapTemplate + themer = self.themer(theme=self.theme) + self._render_variables['html_attrs'] = f'data-bs-theme="{themer.theme._bs_theme}"' diff --git a/panel/template/bootstrap/dark.css b/panel/template/bootstrap/dark.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/panel/template/bootstrap/default.css b/panel/template/bootstrap/default.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/panel/template/fast/base.py b/panel/template/fast/base.py index 4b87664705..8b8d009de7 100644 --- a/panel/template/fast/base.py +++ b/panel/template/fast/base.py @@ -2,13 +2,11 @@ import param -from ...config import config from ...io.state import state -from ...viewable import Viewable -from ...widgets import Tabulator -from ..base import BasicTemplate, Inherit +from ...theme import THEMES, DefaultTheme +from ...theme.fast import Fast, Themer +from ..base import BasicTemplate from ..react import ReactTemplate -from ..theme import THEMES, DefaultTheme _ROOT = pathlib.Path(__file__).parent @@ -53,41 +51,14 @@ class FastBaseTemplate(BasicTemplate): What to wrap the main components into. Options are '' (i.e. none) and 'card' (Default). Could be extended to Accordion, Tab etc. in the future.""") - _css = [_ROOT / "fast.css"] + themer = param.ClassSelector(class_=Themer, default=Fast, constant=True, + is_instance=False, instantiate=False, doc=""" + A Themer applies a specific design system to a template.""") - _modifiers = { - Tabulator: { - 'theme': 'fast' - }, - Viewable: { - 'stylesheets': [Inherit, 'components.css'] - } - } + _css = [_ROOT / "fast.css"] _js = _ROOT / "js/fast_template.js" - _resources = { - 'js_modules': { - 'fast-colors': f'{config.npm_cdn}/@microsoft/fast-colors@5.3.1/dist/index.js', - 'fast': f'{config.npm_cdn}/@microsoft/fast-components@1.21.8/dist/fast-components.js' - }, - 'bundle': True, - 'tarball': { - 'fast-colors': { - 'tar': 'https://registry.npmjs.org/@microsoft/fast-colors/-/fast-colors-5.3.1.tgz', - 'src': 'package/', - 'dest': '@microsoft/fast-colors@5.3.1', - 'exclude': ['*.d.ts', '*.json', '*.md', '*/esm/*'] - }, - 'fast': { - 'tar': 'https://registry.npmjs.org/@microsoft/fast-components/-/fast-components-1.21.8.tgz', - 'src': 'package/', - 'dest': '@microsoft/fast-components@1.21.8', - 'exclude': ['*.d.ts', '*.json', '*.md', '*/esm/*'] - } - } - } - __abstract = True def __init__(self, **params): @@ -106,9 +77,8 @@ def __init__(self, **params): params["header_background"] = accent super().__init__(**params) - theme = self._get_theme() self.param.update({ - p: v for p, v in theme.style.param.values().items() + p: v for p, v in self._themer.theme.style.param.values().items() if p != 'name' and p in self.param and p not in params }) @@ -122,7 +92,7 @@ def _get_theme_from_query_args(): def _update_vars(self): super()._update_vars() - style = self._get_theme().style + style = self._themer.theme.style style.param.update({ p: getattr(self, p) for p in style.param if p != 'name' and p in self.param @@ -139,6 +109,6 @@ class FastGridBaseTemplate(FastBaseTemplate, ReactTemplate): Combines the FastTemplate and the React template. """ - _resources = dict(FastBaseTemplate._resources, js=ReactTemplate._resources['js']) + _resources = dict(js=ReactTemplate._resources['js']) __abstract = True diff --git a/panel/template/fast/grid/__init__.py b/panel/template/fast/grid/__init__.py index b809f0854c..358ef28905 100644 --- a/panel/template/fast/grid/__init__.py +++ b/panel/template/fast/grid/__init__.py @@ -1,14 +1,11 @@ """ -The Fast GridTemplate provides a grid layout based on React Grid +The FastGridTemplate provides a grid layout based on React Grid Layout similar to the Panel ReactTemplate but in the Fast.design style and enabling the use of Fast components. """ import pathlib -import param - from ..base import FastGridBaseTemplate -from ..theme import FastDarkTheme, FastDefaultTheme class FastGridTemplate(FastGridBaseTemplate): @@ -24,7 +21,7 @@ class FastGridTemplate(FastGridBaseTemplate): ... site="Panel", title="FastGridTemplate", accent="#A01346", ... sidebar=[pn.pane.Markdown("## Settings"), some_slider], ... ).servable() - >>> template.main[0:6,:]=some_python_object + >>> template.main[0:6,:] = some_python_object Some *accent* colors that work well are #A01346 (Fast), #00A170 (Mint), #DAA520 (Golden Rod), #2F4F4F (Dark Slate Grey), #F08080 (Light Coral) and #4099da (Summer Sky). @@ -37,19 +34,3 @@ class FastGridTemplate(FastGridBaseTemplate): ] _template = pathlib.Path(__file__).parent / "fast_grid_template.html" - - -class FastGridDefaultTheme(FastDefaultTheme): - """The Default Theme of the FastGridTemplate""" - - css = param.Filename(default=pathlib.Path(__file__).parent / "default.css") - - _template = FastGridTemplate - - -class FastGridDarkTheme(FastDarkTheme): - """The Dark Theme of the FastGridTemplate""" - - css = param.Filename(default=pathlib.Path(__file__).parent / "dark.css") - - _template = FastGridTemplate diff --git a/panel/template/fast/grid/dark.css b/panel/template/fast/grid/dark.css deleted file mode 100644 index df90918d16..0000000000 --- a/panel/template/fast/grid/dark.css +++ /dev/null @@ -1,19 +0,0 @@ -/* fast_grid_template/dark.css */ -#sidebar { - border-right: 2px solid var(--neutral-fill-rest); -} - -.react-grid-item > .react-resizable-handle::after { - content: ""; - position: absolute; - right: 3px; - bottom: 3px; - width: 5px; - height: 5px; - border-right: 2px solid rgb(255, 255, 255,0.95); - border-bottom: 2px solid rgb(252, 252, 252,0.95); // Update rgba values for color and transparency. -} - -.drag-handle { - background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBmaWxsPSJub25lIiBkPSJNMCAwaDI0djI0SDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xNiAxM2w2Ljk2NCA0LjA2Mi0yLjk3My44NSAyLjEyNSAzLjY4MS0xLjczMiAxLTIuMTI1LTMuNjgtMi4yMjMgMi4xNUwxNiAxM3ptLTItN2gydjJoNWExIDEgMCAwIDEgMSAxdjRoLTJ2LTNIMTB2MTBoNHYySDlhMSAxIDAgMCAxLTEtMXYtNUg2di0yaDJWOWExIDEgMCAwIDEgMS0xaDVWNnpNNCAxNHYySDJ2LTJoMnptMC00djJIMnYtMmgyem0wLTR2MkgyVjZoMnptMC00djJIMlYyaDJ6bTQgMHYySDZWMmgyem00IDB2MmgtMlYyaDJ6bTQgMHYyaC0yVjJoMnoiLz48L3N2Zz4='); -} diff --git a/panel/template/fast/grid/default.css b/panel/template/fast/grid/default.css deleted file mode 100644 index 10d976382b..0000000000 --- a/panel/template/fast/grid/default.css +++ /dev/null @@ -1,4 +0,0 @@ -/* fast default.css */ -.react-grid-item{ - background-color: var(--background-color) !important; -} diff --git a/panel/template/fast/list/__init__.py b/panel/template/fast/list/__init__.py index 0407055ced..f65867afe1 100644 --- a/panel/template/fast/list/__init__.py +++ b/panel/template/fast/list/__init__.py @@ -5,10 +5,7 @@ """ import pathlib -import param - from ..base import FastBaseTemplate -from ..theme import FastDarkTheme, FastDefaultTheme class FastListTemplate(FastBaseTemplate): @@ -48,20 +45,3 @@ class FastListTemplate(FastBaseTemplate): ] _template = pathlib.Path(__file__).parent / "fast_list_template.html" - - - -class FastListDefaultTheme(FastDefaultTheme): - """The Default Theme of the FastListTemplate""" - - css = param.Filename(default=pathlib.Path(__file__).parent / "default.css") - - _template = FastListTemplate - - -class FastListDarkTheme(FastDarkTheme): - """The Dark Theme of the FastListTemplate""" - - css = param.Filename(default=pathlib.Path(__file__).parent / "dark.css") - - _template = FastListTemplate diff --git a/panel/template/fast/list/dark.css b/panel/template/fast/list/dark.css deleted file mode 100644 index 6991ba0ce7..0000000000 --- a/panel/template/fast/list/dark.css +++ /dev/null @@ -1 +0,0 @@ -/* fast_template/dark.css */ diff --git a/panel/template/fast/list/default.css b/panel/template/fast/list/default.css deleted file mode 100644 index 80dd9052fc..0000000000 --- a/panel/template/fast/list/default.css +++ /dev/null @@ -1 +0,0 @@ -/* fast default.css */ diff --git a/panel/template/golden/__init__.py b/panel/template/golden/__init__.py index e9f8f0092d..4ae7f030f1 100644 --- a/panel/template/golden/__init__.py +++ b/panel/template/golden/__init__.py @@ -7,9 +7,7 @@ from ...config import config from ...io.resources import JS_URLS -from ...layout import Card from ..base import BasicTemplate -from ..theme import DarkTheme, DefaultTheme class GoldenTemplate(BasicTemplate): @@ -23,13 +21,6 @@ class GoldenTemplate(BasicTemplate): _template = pathlib.Path(__file__).parent / 'golden.html' - _modifiers = { - Card: { - 'children': {'margin': (10, 10)}, - 'button_css_classes': ['golden-card-button'] - }, - } - _resources = { 'css': { 'goldenlayout': f"{config.npm_cdn}/golden-layout@1.5.9/src/css/goldenlayout-base.css", @@ -43,17 +34,3 @@ class GoldenTemplate(BasicTemplate): def _apply_root(self, name, model, tags): if 'main' in tags: model.margin = (10, 15, 10, 10) - - -class GoldenDefaultTheme(DefaultTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - _template = GoldenTemplate - - -class GoldenDarkTheme(DarkTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css') - - _template = GoldenTemplate diff --git a/panel/template/golden/dark.css b/panel/template/golden/dark.css deleted file mode 100644 index 9dbfe987a6..0000000000 --- a/panel/template/golden/dark.css +++ /dev/null @@ -1,26 +0,0 @@ -@import url("https://golden-layout.com/assets/css/goldenlayout-dark-theme.css"); - -.lm_content { - border: 1px solid black; - border-top-color: black; - border-top-style: solid; - border-top-width: 1px; - border-right-color: black; - border-right-style: solid; - border-right-width: 1px; - border-bottom-color: black; - border-bottom-style: solid; - border-bottom-width: 1px; - border-left-color: black; - border-left-style: solid; - border-left-width: 1px; - border-image-source: initial; - border-image-slice: initial; - border-image-width: initial; - border-image-outset: initial; - border-image-repeat: initial; -} - -.lm_header .lm_tab.lm_active { - padding-bottom: 6px; -} diff --git a/panel/template/golden/default.css b/panel/template/golden/default.css deleted file mode 100644 index 2bbe223d8d..0000000000 --- a/panel/template/golden/default.css +++ /dev/null @@ -1,23 +0,0 @@ -@import url("https://golden-layout.com/assets/css/goldenlayout-light-theme.css"); - -.bk.bk-input-group { - color: black; -} - -.lm_content { - background-color: #ffffff; -} - -.lm_header .lm_tab { - background-color: #efefef; - padding-bottom: 4px !important; -} - -.lm_header .lm_tab.lm_active { - background-color: #ffffff; - padding-bottom: 5px !important; -} - -.lm_header { - background-color: white; -} diff --git a/panel/template/material/__init__.py b/panel/template/material/__init__.py index 983d050094..a6d6fd60bf 100644 --- a/panel/template/material/__init__.py +++ b/panel/template/material/__init__.py @@ -5,14 +5,11 @@ import param -from bokeh.themes import Theme as _BkTheme +from ...theme import Themer +from ...theme.material import Material +from ..base import BasicTemplate, TemplateActions -from ...config import config -from ...layout import Card -from ...viewable import Viewable -from ...widgets import Number, Tabulator -from ..base import BasicTemplate, Inherit, TemplateActions -from ..theme import DarkTheme, DefaultTheme +_ROOT = pathlib.Path(__file__).parent class MaterialTemplateActions(TemplateActions): @@ -36,162 +33,13 @@ class MaterialTemplate(BasicTemplate): sidebar_width = param.Integer(370, doc=""" The width of the sidebar in pixels. Default is 370.""") + themer = param.ClassSelector(class_=Themer, default=Material, constant=True, + is_instance=False, instantiate=False, doc=""" + A Themer applies a specific design system to a template.""") + _actions = param.ClassSelector( default=MaterialTemplateActions(), class_=TemplateActions) - _css = pathlib.Path(__file__).parent / 'material.css' - - _modifiers = { - Card: { - 'children': {'margin': (5, 10)}, - 'title_css_classes': ['mdc-card-title'], - 'css_classes': ['mdc-card'], - 'button_css_classes': ['mdc-button', 'mdc-card-button'], - 'margin': (10, 5) - }, - Tabulator: { - 'theme': 'materialize' - }, - Viewable: { - 'stylesheets': [Inherit, 'components.css'] - } - } - - _resources = { - 'css': { - 'material': f"{config.npm_cdn}/material-components-web@7.0.0/dist/material-components-web.min.css", - }, - 'js': { - 'material': f"{config.npm_cdn}/material-components-web@7.0.0/dist/material-components-web.min.js" - } - } - - _template = pathlib.Path(__file__).parent / 'material.html' - - -MATERIAL_FONT = "Roboto, sans-serif, Verdana" -MATERIAL_THEME = { - "attrs": { - "Axis": { - "major_label_text_font": MATERIAL_FONT, - "major_label_text_font_size": "1.025em", - "axis_label_standoff": 10, - "axis_label_text_font": MATERIAL_FONT, - "axis_label_text_font_size": "1.25em", - "axis_label_text_font_style": "normal", - }, - "Legend": { - "spacing": 8, - "glyph_width": 15, - "label_standoff": 8, - "label_text_font": MATERIAL_FONT, - "label_text_font_size": "1.025em", - }, - "ColorBar": { - "title_text_font": MATERIAL_FONT, - "title_text_font_size": "1.025em", - "title_text_font_style": "normal", - "major_label_text_font": MATERIAL_FONT, - "major_label_text_font_size": "1.025em", - }, - "Title": { - "text_font": MATERIAL_FONT, - "text_font_size": "1.15em", - }, - } -} - - -MATERIAL_DARK_100 = "rgb(48,48,48)" -MATERIAL_DARK_75 = "rgb(57,57,57)" -MATERIAL_DARK_50 = "rgb(66,66,66)" -MATERIAL_DARK_25 = "rgb(77,77,77)" -MATERIAL_TEXT_DIGITAL_DARK = "rgb(236,236,236)" - -MATERIAL_DARK_THEME = { - "attrs": { - "figure": { - "background_fill_color": MATERIAL_DARK_50, - "border_fill_color": MATERIAL_DARK_100, - "outline_line_color": MATERIAL_DARK_75, - "outline_line_alpha": 0.25, - }, - "Grid": {"grid_line_color": MATERIAL_TEXT_DIGITAL_DARK, "grid_line_alpha": 0.25}, - "Axis": { - "major_tick_line_alpha": 0, - "major_tick_line_color": MATERIAL_TEXT_DIGITAL_DARK, - "minor_tick_line_alpha": 0, - "minor_tick_line_color": MATERIAL_TEXT_DIGITAL_DARK, - "axis_line_alpha": 0, - "axis_line_color": MATERIAL_TEXT_DIGITAL_DARK, - "major_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, - "major_label_text_font": MATERIAL_FONT, - "major_label_text_font_size": "1.025em", - "axis_label_standoff": 10, - "axis_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, - "axis_label_text_font": MATERIAL_FONT, - "axis_label_text_font_size": "1.25em", - "axis_label_text_font_style": "normal", - }, - "Legend": { - "spacing": 8, - "glyph_width": 15, - "label_standoff": 8, - "label_text_color": MATERIAL_TEXT_DIGITAL_DARK, - "label_text_font": MATERIAL_FONT, - "label_text_font_size": "1.025em", - "border_line_alpha": 0, - "background_fill_alpha": 0.25, - "background_fill_color": MATERIAL_DARK_75, - }, - "ColorBar": { - "title_text_color": MATERIAL_TEXT_DIGITAL_DARK, - "title_text_font": MATERIAL_FONT, - "title_text_font_size": "1.025em", - "title_text_font_style": "normal", - "major_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, - "major_label_text_font": MATERIAL_FONT, - "major_label_text_font_size": "1.025em", - "background_fill_color": MATERIAL_DARK_75, - "major_tick_line_alpha": 0, - "bar_line_alpha": 0, - }, - "Title": { - "text_color": MATERIAL_TEXT_DIGITAL_DARK, - "text_font": MATERIAL_FONT, - "text_font_size": "1.15em", - }, - } -} - - -class MaterialDefaultTheme(DefaultTheme): - """ - The MaterialDefaultTheme is a light theme. - """ - - bokeh_theme = param.ClassSelector( - class_=(_BkTheme, str), default=_BkTheme(json=MATERIAL_THEME)) - - css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - _template = MaterialTemplate - - -class MaterialDarkTheme(DarkTheme): - """ - The MaterialDarkTheme is a Dark Theme in the style of Material Design - """ - - bokeh_theme = param.ClassSelector( - class_=(_BkTheme, str), default=_BkTheme(json=MATERIAL_DARK_THEME)) - - css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css') - - _modifiers = { - Number: { - 'default_color': 'var(--mdc-theme-on-background)' - } - } + _css = [_ROOT / "material.css"] - _template = MaterialTemplate + _template = _ROOT / 'material.html' diff --git a/panel/template/react/__init__.py b/panel/template/react/__init__.py index 5df3467f05..c0a685e07a 100644 --- a/panel/template/react/__init__.py +++ b/panel/template/react/__init__.py @@ -10,10 +10,8 @@ from ...config import config from ...depends import depends from ...io.resources import CSS_URLS -from ...layout import Card, GridSpec -from ...viewable import Viewable -from ..base import BasicTemplate, Inherit -from ..theme import DarkTheme, DefaultTheme +from ...layout import GridSpec +from ..base import BasicTemplate class ReactTemplate(BasicTemplate): @@ -46,16 +44,6 @@ class ReactTemplate(BasicTemplate): _template = pathlib.Path(__file__).parent / 'react.html' - _modifiers = { - Card: { - 'children': {'margin': (20, 20)}, - 'margin': (10, 5) - }, - Viewable: { - 'stylesheets': [Inherit, './components.css'] - } - } - _resources = { 'js': { 'react': f"{config.npm_cdn}/react@17/umd/react.production.min.js", @@ -64,7 +52,6 @@ class ReactTemplate(BasicTemplate): 'react-grid': "https://cdnjs.cloudflare.com/ajax/libs/react-grid-layout/1.1.1/react-grid-layout.min.js" }, 'css': { - 'bootstrap': CSS_URLS['bootstrap4'], 'font-awesome': CSS_URLS['font-awesome'] } } @@ -100,16 +87,3 @@ def _update_render_vars(self): self._render_variables['dimensions'] = self.dimensions self._render_variables['preventCollision'] = self.prevent_collision self._render_variables['saveLayout'] = self.save_layout - -class ReactDefaultTheme(DefaultTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - _template = ReactTemplate - - -class ReactDarkTheme(DarkTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css') - - _template = ReactTemplate diff --git a/panel/template/react/components.css b/panel/template/react/components.css deleted file mode 100644 index 79452498e0..0000000000 --- a/panel/template/react/components.css +++ /dev/null @@ -1,7 +0,0 @@ -.card-title { - position: absolute !important; -} - -.card-button { - display: none; -} diff --git a/panel/template/react/dark.css b/panel/template/react/dark.css deleted file mode 100644 index 56cd09d404..0000000000 --- a/panel/template/react/dark.css +++ /dev/null @@ -1,18 +0,0 @@ -#header-items { - color: white; -} - -.react-grid-item > .react-resizable-handle::after { - content: ""; - position: absolute; - right: 3px; - bottom: 3px; - width: 5px; - height: 5px; - border-right: 2px solid rgb(255, 255, 255,0.95); - border-bottom: 2px solid rgb(252, 252, 252,0.95); // Update rgba values for color and transparency. -} - -.drag-handle { - background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij48cGF0aCBmaWxsPSJub25lIiBkPSJNMCAwaDI0djI0SDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xNiAxM2w2Ljk2NCA0LjA2Mi0yLjk3My44NSAyLjEyNSAzLjY4MS0xLjczMiAxLTIuMTI1LTMuNjgtMi4yMjMgMi4xNUwxNiAxM3ptLTItN2gydjJoNWExIDEgMCAwIDEgMSAxdjRoLTJ2LTNIMTB2MTBoNHYySDlhMSAxIDAgMCAxLTEtMXYtNUg2di0yaDJWOWExIDEgMCAwIDEgMS0xaDVWNnpNNCAxNHYySDJ2LTJoMnptMC00djJIMnYtMmgyem0wLTR2MkgyVjZoMnptMC00djJIMlYyaDJ6bTQgMHYySDZWMmgyem00IDB2MmgtMlYyaDJ6bTQgMHYyaC0yVjJoMnoiLz48L3N2Zz4='); -} diff --git a/panel/template/react/default.css b/panel/template/react/default.css deleted file mode 100644 index dc4683eb5a..0000000000 --- a/panel/template/react/default.css +++ /dev/null @@ -1,12 +0,0 @@ -#sidebar { - background-color: white; - box-shadow: 0px 0px 1px; -} - -#sidebar-button { - color: white; -} - -.react-grid-item{ - background-color: white !important; -} diff --git a/panel/template/theme/__init__.py b/panel/template/theme/__init__.py deleted file mode 100644 index c72875a167..0000000000 --- a/panel/template/theme/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -import pathlib - -import param - -from bokeh.themes import Theme as _BkTheme, _dark_minimal - - -class Theme(param.Parameterized): - """ - A Theme customizes the look and feel of a Template by providing - custom CSS and bokeh Theme class. - - When adding a new theme a generic Theme class should be created, - which is what users will important and set on the Template.theme - parameter. Additionally a concrete implementation of the Theme - should be created specific to the particular Template being used. - - For example when adding a DarkTheme there should be a - corresponding MaterialDarkTheme which declares the Template class - on its _template class variable. In this way a user can always use - the generic Theme but the actual CSS and bokeh Theme will depend - on the exact Template being used. - """ - - base_css = param.Filename() - - css = param.Filename() - - bokeh_theme = param.ClassSelector(class_=(_BkTheme, str)) - - _modifiers = {} - - _template = None - - __abstract = True - - @classmethod - def find_theme(cls, template_type): - if cls._template is template_type: - return cls - for theme in param.concrete_descendents(cls).values(): - if theme._template is template_type: - return theme - - -class DefaultTheme(Theme): - """ - The DefaultTheme uses the standard Panel color palette. - """ - - base_css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - __abstract = True - - -BOKEH_DARK = dict(_dark_minimal.json) - -BOKEH_DARK['attrs']['Plot'].update({ - "background_fill_color": "#3f3f3f", - "border_fill_color": "#2f2f2f", -}) - -class DarkTheme(Theme): - """ - The DefaultTheme uses the standard Panel color palette. - """ - - base_css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css') - - bokeh_theme = param.ClassSelector(class_=(_BkTheme, str), - default=_BkTheme(json=BOKEH_DARK)) - - __abstract = True - -THEMES = { - 'default': DefaultTheme, - 'dark': DarkTheme -} diff --git a/panel/template/vanilla/__init__.py b/panel/template/vanilla/__init__.py index c0b8a2baa8..5ec3a05530 100644 --- a/panel/template/vanilla/__init__.py +++ b/panel/template/vanilla/__init__.py @@ -3,11 +3,7 @@ """ import pathlib -import param - -from ...layout import Card from ..base import BasicTemplate -from ..theme import DarkTheme, DefaultTheme class VanillaTemplate(BasicTemplate): @@ -19,25 +15,6 @@ class VanillaTemplate(BasicTemplate): _template = pathlib.Path(__file__).parent / 'vanilla.html' - _modifiers = { - Card: { - 'children': {'margin': (10, 10)}, - 'margin': (5, 5) - } - } - def _apply_root(self, name, model, tags): if 'main' in tags: model.margin = (10, 15, 10, 10) - - -class VanillaDefaultTheme(DefaultTheme): - - css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css') - - _template = VanillaTemplate - - -class VanillaDarkTheme(DarkTheme): - - _template = VanillaTemplate diff --git a/panel/template/vanilla/default.css b/panel/template/vanilla/default.css deleted file mode 100644 index 3340c89031..0000000000 --- a/panel/template/vanilla/default.css +++ /dev/null @@ -1,8 +0,0 @@ -#sidebar { - background-color: #F8F8F8; - box-shadow: 0px 0px 1px; -} - -#sidebar-button { - color: white; -} diff --git a/panel/theme/__init__.py b/panel/theme/__init__.py new file mode 100644 index 0000000000..a412373c30 --- /dev/null +++ b/panel/theme/__init__.py @@ -0,0 +1,3 @@ +from .base import ( # noqa + THEMES, DarkTheme, DefaultTheme, Theme, Themer, +) diff --git a/panel/theme/base.py b/panel/theme/base.py new file mode 100644 index 0000000000..01626331de --- /dev/null +++ b/panel/theme/base.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import functools +import os +import pathlib + +from typing import TYPE_CHECKING + +import param + +from bokeh.models import ImportedStyleSheet +from bokeh.themes import Theme as _BkTheme, _dark_minimal + +if TYPE_CHECKING: + from bokeh.model import Model + + from ..viewable import Viewable + + +class Inherit: + """ + Singleton object to declare stylesheet inheritance. + """ + + +class Theme(param.Parameterized): + + base_css = param.Filename() + + bokeh_theme = param.ClassSelector(class_=(_BkTheme, str), default=None) + + css = param.Filename() + + _modifiers = {} + + +BOKEH_DARK = dict(_dark_minimal.json) +BOKEH_DARK['attrs']['Plot'].update({ + "background_fill_color": "#2b3035", + "border_fill_color": "#212529", +}) + + +class DefaultTheme(Theme): + + base_css = param.Filename(default=pathlib.Path(__file__).parent / 'css' / 'default.css') + + _name = 'default' + + +class DarkTheme(Theme): + + base_css = param.Filename(default=pathlib.Path(__file__).parent / 'css' / 'dark.css') + + bokeh_theme = param.ClassSelector(class_=(_BkTheme, str), + default=_BkTheme(json=BOKEH_DARK)) + + _name = 'dark' + + +class Themer(param.Parameterized): + + theme = param.ClassSelector(class_=Theme) + + _modifiers = {} + + _resources = {} + + _themes = { + 'default': DefaultTheme, + 'dark': DarkTheme + } + + def __init__(self, theme=None, **params): + if isinstance(theme, type) and issubclass(theme, Theme): + theme = theme._name + theme = self._themes[theme]() + super().__init__(theme=theme, **params) + + def apply(self, viewable, root: Model, isolated: bool=True): + with root.document.models.freeze(): + self._reapply(viewable, root) + if self.theme and self.theme.bokeh_theme and root.document: + root.document.theme = self.theme.bokeh_theme + + def _reapply(self, viewable: Viewable, root: Model, isolated: bool=True) -> None: + ref = root.ref['id'] + for o in viewable.select(): + self._apply_modifiers(o, ref, self.theme, isolated) + + def _apply_hooks(self, viewable: Viewable, root: Model) -> None: + with root.document.models.freeze(): + self._reapply(viewable, root, isolated=False) + + @classmethod + def _apply_modifiers(cls, viewable: Viewable, mref: str, theme: Theme, isolated: bool) -> None: + if mref not in viewable._models: + return + model, _ = viewable._models[mref] + modifiers, child_modifiers = cls._resolve_modifiers(type(viewable), theme) + modifiers = dict(modifiers) + if 'stylesheets' in modifiers: + if isolated: + pre = list(cls._resources.get('css', [])) + if theme.base_css: + base_css = theme.base_css + if os.path.isfile(base_css): + base_css = os.path.join('bundled', 'theme', os.path.basename(base_css)) + pre.append(base_css) + else: + pre = [] + modifiers['stylesheets'] = [ + ImportedStyleSheet(url=sts) if sts.endswith('.css') else sts + for sts in pre+modifiers['stylesheets'] + ] + if child_modifiers: + for child in viewable: + cls._apply_params(child, mref, child_modifiers) + if modifiers: + cls._apply_params(viewable, mref, modifiers) + + @classmethod + def _resolve_stylesheets(cls, value, defining_cls, inherited): + new_value = [] + for v in value: + if v is Inherit: + new_value.extend(inherited) + elif isinstance(v, str) and v.endswith('.css'): + if v.startswith('http'): + url = v + elif v.startswith('/'): + url = v[1:] + else: + url = os.path.join('bundled', cls.__name__.lower(), v) + new_value.append(url) + else: + new_value.append(v) + return new_value + + @classmethod + @functools.cache + def _resolve_modifiers(cls, vtype, theme): + """ + Iterate over the class hierarchy in reverse order and accumulate + all modifiers that apply to the objects class and its super classes. + """ + modifiers, child_modifiers = {}, {} + for scls in vtype.__mro__[::-1]: + cls_modifiers = cls._modifiers.get(scls, {}) + if cls_modifiers: + # Find the Template class the options were first defined on + def_cls = [ + super_cls for super_cls in cls.__mro__[::-1] + if getattr(super_cls, '_modifiers', {}).get(scls) is cls_modifiers + ][0] + + for prop, value in cls_modifiers.items(): + if prop == 'children': + continue + elif prop == 'stylesheets': + modifiers[prop] = cls._resolve_stylesheets(value, def_cls, modifiers.get(prop, [])) + else: + modifiers[prop] = value + modifiers.update(theme._modifiers.get(scls, {})) + child_modifiers.update(cls_modifiers.get('children', {})) + return modifiers, child_modifiers + + @classmethod + def _apply_params(cls, viewable, mref, modifiers): + model, _ = viewable._models[mref] + params = { + k: v for k, v in modifiers.items() if k != 'children' and + getattr(viewable, k) == viewable.param[k].default + } + if 'stylesheets' in modifiers: + params['stylesheets'] = modifiers['stylesheets'] + viewable.stylesheets + props = viewable._process_param_change(params) + model.update(**props) + + +THEMES = { + 'default': DefaultTheme, + 'dark': DarkTheme +} diff --git a/panel/theme/bootstrap.py b/panel/theme/bootstrap.py new file mode 100644 index 0000000000..dece08e444 --- /dev/null +++ b/panel/theme/bootstrap.py @@ -0,0 +1,60 @@ +from ..io.resources import CSS_URLS, JS_URLS +from ..layout import Card +from ..viewable import Viewable +from ..widgets import Number, Tabulator +from .base import ( + DarkTheme, DefaultTheme, Inherit, Themer, +) + + +class BootstrapDefaultTheme(DefaultTheme): + """ + The BootstrapDefaultTheme is a light theme. + """ + + _bs_theme = 'light' + + +class BootstrapDarkTheme(DarkTheme): + """ + The BootstrapDarkTheme is a Dark Theme in the style of Bootstrap + """ + + _bs_theme = 'dark' + + _modifiers = { + Number: { + 'default_color': 'var(--mdc-theme-on-background)' + } + } + + +class Bootstrap(Themer): + + _modifiers = { + Card: { + 'children': {'margin': (10, 10)}, + 'button_css_classes': ['card-button'], + 'margin': (10, 5) + }, + Tabulator: { + 'theme': 'bootstrap4' + }, + Viewable: { + 'stylesheets': [Inherit, 'css/bootstrap.css'] + } + } + + _themes = { + 'default': BootstrapDefaultTheme, + 'dark': BootstrapDarkTheme + } + + _resources = { + 'css': { + 'bootstrap': CSS_URLS['bootstrap5'] + }, + 'js': { + 'bootstrap': JS_URLS['bootstrap5'] + } + } diff --git a/panel/template/bootstrap/components.css b/panel/theme/css/bootstrap.css similarity index 100% rename from panel/template/bootstrap/components.css rename to panel/theme/css/bootstrap.css diff --git a/panel/template/theme/dark.css b/panel/theme/css/dark.css similarity index 100% rename from panel/template/theme/dark.css rename to panel/theme/css/dark.css diff --git a/panel/template/theme/default.css b/panel/theme/css/default.css similarity index 100% rename from panel/template/theme/default.css rename to panel/theme/css/default.css diff --git a/panel/template/fast/components.css b/panel/theme/css/fast.css similarity index 100% rename from panel/template/fast/components.css rename to panel/theme/css/fast.css diff --git a/panel/template/material/components.css b/panel/theme/css/material.css similarity index 72% rename from panel/template/material/components.css rename to panel/theme/css/material.css index 965263b9b4..41371b85c2 100644 --- a/panel/template/material/components.css +++ b/panel/theme/css/material.css @@ -129,13 +129,13 @@ button.mdc-button.mdc-card-button { /* Slider styling */ .noUi-target { - background-color: var(--mdc-theme-secondary-lightened); + background-color: var(--mdc-theme-secondary); border: unset; box-shadow: unset; } .noUi-connects { - background-color: var(--mdc-theme-secondary-lightened); + background-color: var(--mdc-theme-secondary); border: unset; border-radius: 10px; box-shadow: unset; @@ -179,7 +179,7 @@ button.mdc-button.mdc-card-button { .bk-input { background-color: unset; border-radius: 4px; - border: 1px solid var(--mdc-theme-secondary); + border: 1px solid rgba(255, 255, 255, 0.23); color: var(--mdc-theme-on-surface); height: 1.4375em; line-height: 1.4375em; @@ -190,8 +190,8 @@ button.mdc-button.mdc-card-button { .bk-input[disabled] { background-color: unset; border: unset; - border: 1px dotted var(--mdc-theme-secondary-lightened); - color: var(--mdc-theme-secondary-lightened); + border: 1px dotted rgba(255, 255, 255, 0.23); + color: rgba(255, 255, 255, 0.23); } .bk-input:not([disabled]):hover { @@ -207,7 +207,6 @@ button.mdc-button.mdc-card-button { .bk-input-group > label:has(+ .bk-input-container), .bk-input-group > label:has(+ .bk-spin-wrapper), .bk-input-group > label:has(+ textarea) { - color: var(--mdc-theme-on-background); background-color: var(--mdc-theme-background); position: absolute; left: 0; @@ -221,9 +220,17 @@ button.mdc-button.mdc-card-button { margin: 5px 0; } -.bk-input-group > label:has(+ .bk-input-container .bk-input[disabled]), -.bk-input-group > label:has(+ select[disabled]) { - color: var(--mdc-theme-secondary-lightened); +/* Choices */ + +.choices.is-disabled .choices__inner { + background-color: unset; + border: unset; + border: 1px dotted rgba(255, 255, 255, 0.23); + color: rgba(255, 255, 255, 0.23); +} + +.choices.is-disabled .choices__input { + background-color: unset; } /* Number input */ @@ -251,33 +258,11 @@ select.bk-input:focus { /* MultiChoice */ .choices__inner { - border: 1px solid var(--mdc-theme-secondary); + border: 1px solid var(--mdc-theme-secondary-lightened); padding-top: 20px; } -.choices.is-disabled .choices__inner { - background-color: unset; - border: unset; - border: 1px dotted var(--mdc-theme-secondary-lightened); - color: var(--mdc-theme-secondary-lightened); -} - -.choices.is-disabled .choices__input { - background-color: unset; -} - -.choices__list--dropdown, .choices__list[aria-expanded] { - background-color: var(--mdc-theme-surface); - color: var(--mdc-theme-on-surface); -} - -.choices__list--dropdown .choices__item--selectable.is-highlighted, .choices__list[aria-expanded] .choices__item--selectable.is-highlighted { - background-color: var(--mdc-theme-primary); - color: var(--mdc-theme-on-primary); -} - .bk-input-group > label:has(+ div.choices) { - color: var(--mdc-theme-on-background); background-color: var(--mdc-theme-background); position: absolute; left: 0; @@ -291,10 +276,6 @@ select.bk-input:focus { margin: 5px 0; } -.bk-input-group > label:has(+ div.choices.is-disabled) { - color: var(--mdc-theme-secondary-lightened); -} - .choices__inner:hover { border: 1px solid var(--mdc-theme-secondary); transform: 200ms cubic-bezier(0, 0, 0.2, 1) 0ms; @@ -305,10 +286,6 @@ select.bk-input:focus { box-shadow: unset; } -.choices.is-disabled .choices__item { - opacity: 0.38; -} - /* Button widgets */ .bk-btn-group > button { @@ -328,55 +305,22 @@ select.bk-input:focus { .bk-btn-group > button.bk-btn-default:hover { background: var(--mdc-theme-primary) radial-gradient(circle, transparent 1%, var(--mdc-theme-primary) 1%) center/15000%; - color: var(--mdc-theme-on-primary); -} - -.bk-btn-group > button.bk-btn-primary:hover { - background: #428bca radial-gradient(circle, transparent 1%, #428bca 1%) center/15000%; } -.bk-btn-group > button.bk-btn-warning:hover { - background: #f0ad4e radial-gradient(circle, transparent 1%, #f0ad4e 1%) center/15000%; -} - -.bk-btn-group > button.bk-btn-danger:hover { - background: #d9534f radial-gradient(circle, transparent 1%, #d9534f 1%) center/15000%; -} - -.bk-btn-group > button.bk-btn-success:hover { - background: #5cb85c radial-gradient(circle, transparent 1%, #5cb85c 1%) center/15000%; -} - - -.bk-btn-group > button.bk-btn-light:hover { - background: white radial-gradient(circle, transparent 1%, white 1%) center/15000%; -} - -.bk-btn-group > button.bk-btn:active { +.bk-btn-group > button.bk-btn-default:active { background-color: white; background-size: 100%; transition: background 0s; } -.bk-btn-group > button.bk-btn.bk-btn-light:active { - background-color: black; - background-size: 100%; - transition: background 0s; -} - - .bk-menu { - background-color: var(--mdc-theme-surface); - color: var(--mdc-theme-on-surface); -} - -.bk-menu > :not(.bk-divider):hover, .bk-menu > :not(.bk-divider).bk-active { - background-color: var(--mdc-theme-primary); - color: var(--mdc-theme-on-primary); + color: var(--mdc-theme-primary); + border: 1px solid var(--mdc-theme-primary); } .bk-active.bk-btn-default { background-color: var(--mdc-theme-primary); + border: 1px solid var(--mdc-theme-primary); color: var(--mdc-theme-on-primary); } @@ -477,43 +421,9 @@ progress:not([value])::before { /* Tabulator */ .tabulator { - background-color: var(--mdc-theme-background); - color: var(--mdc-theme-on-background); -} - -.tabulator-row .tabulator-cell, .tabulator .tabulator-header .tabulator-col .tabulator-col-content { - padding: 10px; -} - -.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter { - right: -8px; -} - -.tabulator .tabulator-headers, .tabulator .tabulator-header .tabulator-col { - background-color: var(--mdc-theme-background); - color: var(--mdc-theme-on-background); -} - -.tabulator-row.tabulator-row-even { background-color: var(--mdc-theme-surface); - color: var(--mdc-theme-on-surface); } -.tabulator-row.tabulator-selectable:hover { - background-color: var(--mdc-theme-primary-lightened); - color: var(--mdc-theme-on-primary); -} - -.tabulator .tabulator-header .tabulator-col.tabulator-sortable:hover { +.tabulator .tabulator-headers, .tabulator .tabulator-header .tabulator-col { background-color: var(--mdc-theme-surface); - color: var(--mdc-theme-on-surface); -} - -.tabulator-row.tabulator-selected, .tabulator-row.tabulator-selected:hover { - background-color: var(--mdc-theme-primary); - color: var(--mdc-theme-on-primary); -} - -.tabulator-row .tabulator-cell.tabulator-editing { - border: 1px solid var(--mdc-theme-on-background); } diff --git a/panel/template/fast/theme.py b/panel/theme/fast.py similarity index 82% rename from panel/template/fast/theme.py rename to panel/theme/fast.py index 15ef1ceb7d..a53772aa41 100644 --- a/panel/template/fast/theme.py +++ b/panel/theme/fast.py @@ -1,16 +1,13 @@ -""" -Functionality for styling according to Fast.design -""" -import pathlib - import param from bokeh.themes import Theme as _BkTheme -from ...widgets import Number -from ..theme import DarkTheme, DefaultTheme - -_ROOT = pathlib.Path(__file__).parent / "css" +from ..config import config +from ..viewable import Viewable +from ..widgets import Number, Tabulator +from .base import ( + DarkTheme, DefaultTheme, Inherit, Themer, +) COLLAPSED_SVG_ICON = """ @@ -29,6 +26,7 @@ FONT_URL = "//fonts.googleapis.com/css?family=Open+Sans" + class FastStyle(param.Parameterized): """ The FastStyle class provides the different colors and icons used @@ -133,6 +131,7 @@ def create_bokeh_theme(self): shadow = False, ) + class FastDefaultTheme(DefaultTheme): style = param.ClassSelector(default=DEFAULT_STYLE, class_=FastStyle) @@ -159,3 +158,42 @@ class FastDarkTheme(DarkTheme): @property def bokeh_theme(self): return _BkTheme(json=self.style.create_bokeh_theme()) + + +class Fast(Themer): + + _modifiers = { + Tabulator: { + 'theme': 'fast' + }, + Viewable: { + 'stylesheets': [Inherit, 'css/fast.css'] + } + } + + _resources = { + 'js_modules': { + 'fast-colors': f'{config.npm_cdn}/@microsoft/fast-colors@5.3.1/dist/index.js', + 'fast': f'{config.npm_cdn}/@microsoft/fast-components@1.21.8/dist/fast-components.js' + }, + 'bundle': True, + 'tarball': { + 'fast-colors': { + 'tar': 'https://registry.npmjs.org/@microsoft/fast-colors/-/fast-colors-5.3.1.tgz', + 'src': 'package/', + 'dest': '@microsoft/fast-colors@5.3.1', + 'exclude': ['*.d.ts', '*.json', '*.md', '*/esm/*'] + }, + 'fast': { + 'tar': 'https://registry.npmjs.org/@microsoft/fast-components/-/fast-components-1.21.8.tgz', + 'src': 'package/', + 'dest': '@microsoft/fast-components@1.21.8', + 'exclude': ['*.d.ts', '*.json', '*.md', '*/esm/*'] + } + } + } + + _themes = { + 'default': FastDefaultTheme, + 'dark': FastDarkTheme + } diff --git a/panel/theme/js/fast_design.js b/panel/theme/js/fast_design.js new file mode 100644 index 0000000000..efa4dcb343 --- /dev/null +++ b/panel/theme/js/fast_design.js @@ -0,0 +1,107 @@ +import { + parseColorHexRGB +} from "../../js/@microsoft/fast-colors@5.3.1/dist/index.js"; + +import { + createColorPalette, + accentFillActiveBehavior, + accentFillHoverBehavior, + accentFillRestBehavior, + accentForegroundActiveBehavior, + accentForegroundCutRestBehavior, + accentForegroundFocusBehavior, + accentForegroundHoverBehavior, + accentForegroundRestBehavior, + neutralDividerRestBehavior, + neutralFillHoverBehavior, + neutralFillInputActiveBehavior, + neutralFillInputHoverBehavior, + neutralFillInputRestBehavior, + neutralFillRestBehavior, + neutralFillStealthActiveBehavior, + neutralFillStealthHoverBehavior, + neutralFillStealthRestBehavior, + neutralFocusBehavior, + neutralFocusInnerAccentBehavior, + neutralForegroundActiveBehavior, + neutralForegroundHoverBehavior, + neutralLayerFloatingBehavior, + neutralOutlineActiveBehavior, + neutralOutlineHoverBehavior, + neutralOutlineRestBehavior +} from "../../js/@microsoft/fast-components@1.21.8/dist/fast-components.js"; + +function standardize_color(str) { + var ctx = document.createElement('canvas').getContext('2d'); + ctx.fillStyle = str; + return ctx.fillStyle; +} + +function setAccentColor(color, selector) { + color = standardize_color(color); + const accent = color; + const palette = createColorPalette(parseColorHexRGB(accent)); + const provider = document.querySelector(selector); + provider.accentBaseColor = accent; + provider.accentPalette = palette; + } + +function setNeutralColor(color, selector) { + color = standardize_color(color); + const palette = createColorPalette(parseColorHexRGB(color)); + const provider = document.querySelector(selector); + provider.neutralPalette = palette; +} + +function setBackgroundColor(color, selector) { + color = standardize_color(color); + const provider = document.querySelector(selector); + provider.backgroundColor = color; +} + +function registerCSSCustomProperties(selector) { + const provider = document.querySelector(selector); + provider.registerCSSCustomProperty(accentFillActiveBehavior) + provider.registerCSSCustomProperty(neutralFillRestBehavior) + provider.registerCSSCustomProperty(accentFillHoverBehavior) + provider.registerCSSCustomProperty(accentFillRestBehavior) + provider.registerCSSCustomProperty(accentForegroundActiveBehavior) + provider.registerCSSCustomProperty(accentForegroundCutRestBehavior) + provider.registerCSSCustomProperty(accentForegroundFocusBehavior) + provider.registerCSSCustomProperty(accentForegroundHoverBehavior) + provider.registerCSSCustomProperty(accentForegroundRestBehavior) + provider.registerCSSCustomProperty(neutralDividerRestBehavior) + provider.registerCSSCustomProperty(neutralFillHoverBehavior) + provider.registerCSSCustomProperty(neutralFillInputActiveBehavior) + provider.registerCSSCustomProperty(neutralFillInputHoverBehavior) + provider.registerCSSCustomProperty(neutralFillInputRestBehavior) + provider.registerCSSCustomProperty(neutralFillRestBehavior) + provider.registerCSSCustomProperty(neutralFillStealthActiveBehavior) + provider.registerCSSCustomProperty(neutralFillStealthHoverBehavior) + provider.registerCSSCustomProperty(neutralFillStealthRestBehavior) + provider.registerCSSCustomProperty(neutralFocusBehavior) + provider.registerCSSCustomProperty(neutralFocusInnerAccentBehavior) + provider.registerCSSCustomProperty(neutralForegroundActiveBehavior) + provider.registerCSSCustomProperty(neutralForegroundHoverBehavior) + provider.registerCSSCustomProperty(neutralLayerFloatingBehavior) + provider.registerCSSCustomProperty(neutralOutlineActiveBehavior) + provider.registerCSSCustomProperty(neutralOutlineHoverBehavior) + provider.registerCSSCustomProperty(neutralOutlineRestBehavior) +} + +class FastDesignProvider { + register(element) { + registerCSSCustomProperties(element) + } + setAccentColor(value, element){ + setAccentColor(value, element); + } + setNeutralColor(value, element){ + setNeutralColor(value, element); + } + setBackgroundColor(value, element){ + setBackgroundColor(value, element) + } +} + +window.fastDesignProvider = new FastDesignProvider() diff --git a/panel/theme/material.py b/panel/theme/material.py new file mode 100644 index 0000000000..33078797ef --- /dev/null +++ b/panel/theme/material.py @@ -0,0 +1,162 @@ +import param + +from bokeh.themes import Theme as _BkTheme + +from ..config import config +from ..layout import Card +from ..viewable import Viewable +from ..widgets import Number, Tabulator +from .base import ( + DarkTheme, DefaultTheme, Inherit, Themer, +) + +MATERIAL_FONT = "Roboto, sans-serif, Verdana" +MATERIAL_THEME = { + "attrs": { + "Axis": { + "major_label_text_font": MATERIAL_FONT, + "major_label_text_font_size": "1.025em", + "axis_label_standoff": 10, + "axis_label_text_font": MATERIAL_FONT, + "axis_label_text_font_size": "1.25em", + "axis_label_text_font_style": "normal", + }, + "Legend": { + "spacing": 8, + "glyph_width": 15, + "label_standoff": 8, + "label_text_font": MATERIAL_FONT, + "label_text_font_size": "1.025em", + }, + "ColorBar": { + "title_text_font": MATERIAL_FONT, + "title_text_font_size": "1.025em", + "title_text_font_style": "normal", + "major_label_text_font": MATERIAL_FONT, + "major_label_text_font_size": "1.025em", + }, + "Title": { + "text_font": MATERIAL_FONT, + "text_font_size": "1.15em", + }, + } +} + +MATERIAL_DARK_100 = "rgb(48,48,48)" +MATERIAL_DARK_75 = "rgb(57,57,57)" +MATERIAL_DARK_50 = "rgb(66,66,66)" +MATERIAL_DARK_25 = "rgb(77,77,77)" +MATERIAL_TEXT_DIGITAL_DARK = "rgb(236,236,236)" + +MATERIAL_DARK_THEME = { + "attrs": { + "figure": { + "background_fill_color": MATERIAL_DARK_50, + "border_fill_color": MATERIAL_DARK_100, + "outline_line_color": MATERIAL_DARK_75, + "outline_line_alpha": 0.25, + }, + "Grid": {"grid_line_color": MATERIAL_TEXT_DIGITAL_DARK, "grid_line_alpha": 0.25}, + "Axis": { + "major_tick_line_alpha": 0, + "major_tick_line_color": MATERIAL_TEXT_DIGITAL_DARK, + "minor_tick_line_alpha": 0, + "minor_tick_line_color": MATERIAL_TEXT_DIGITAL_DARK, + "axis_line_alpha": 0, + "axis_line_color": MATERIAL_TEXT_DIGITAL_DARK, + "major_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, + "major_label_text_font": MATERIAL_FONT, + "major_label_text_font_size": "1.025em", + "axis_label_standoff": 10, + "axis_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, + "axis_label_text_font": MATERIAL_FONT, + "axis_label_text_font_size": "1.25em", + "axis_label_text_font_style": "normal", + }, + "Legend": { + "spacing": 8, + "glyph_width": 15, + "label_standoff": 8, + "label_text_color": MATERIAL_TEXT_DIGITAL_DARK, + "label_text_font": MATERIAL_FONT, + "label_text_font_size": "1.025em", + "border_line_alpha": 0, + "background_fill_alpha": 0.25, + "background_fill_color": MATERIAL_DARK_75, + }, + "ColorBar": { + "title_text_color": MATERIAL_TEXT_DIGITAL_DARK, + "title_text_font": MATERIAL_FONT, + "title_text_font_size": "1.025em", + "title_text_font_style": "normal", + "major_label_text_color": MATERIAL_TEXT_DIGITAL_DARK, + "major_label_text_font": MATERIAL_FONT, + "major_label_text_font_size": "1.025em", + "background_fill_color": MATERIAL_DARK_75, + "major_tick_line_alpha": 0, + "bar_line_alpha": 0, + }, + "Title": { + "text_color": MATERIAL_TEXT_DIGITAL_DARK, + "text_font": MATERIAL_FONT, + "text_font_size": "1.15em", + }, + } +} + + +class MaterialDefaultTheme(DefaultTheme): + """ + The MaterialDefaultTheme is a light theme. + """ + + bokeh_theme = param.ClassSelector( + class_=(_BkTheme, str), default=_BkTheme(json=MATERIAL_THEME)) + + +class MaterialDarkTheme(DarkTheme): + """ + The MaterialDarkTheme is a Dark Theme in the style of Material Design + """ + + bokeh_theme = param.ClassSelector( + class_=(_BkTheme, str), default=_BkTheme(json=MATERIAL_DARK_THEME)) + + _modifiers = { + Number: { + 'default_color': 'var(--mdc-theme-on-background)' + } + } + + +class Material(Themer): + + _modifiers = { + Card: { + 'children': {'margin': (5, 10)}, + 'title_css_classes': ['mdc-card-title'], + 'css_classes': ['mdc-card'], + 'button_css_classes': ['mdc-button', 'mdc-card-button'], + 'margin': (10, 5) + }, + Tabulator: { + 'theme': 'materialize' + }, + Viewable: { + 'stylesheets': [Inherit, 'css/material.css'] + } + } + + _themes = { + 'default': MaterialDefaultTheme, + 'dark': MaterialDarkTheme + } + + _resources = { + 'css': { + 'material': f"{config.npm_cdn}/material-components-web@7.0.0/dist/material-components-web.min.css", + }, + 'js': { + 'material': f"{config.npm_cdn}/material-components-web@7.0.0/dist/material-components-web.min.js" + } + } diff --git a/panel/viewable.py b/panel/viewable.py index 54ec88546c..96c4833673 100644 --- a/panel/viewable.py +++ b/panel/viewable.py @@ -565,6 +565,8 @@ def get_root( doc = init_doc(doc) root = self._get_model(doc, comm=comm) if preprocess: + if config.themer: + config.themer.apply(self, root, comm) self._preprocess(root) ref = root.ref['id'] state._views[ref] = (self, root, doc, comm) From a293ca9b41e6d51fdf105436896e0a3d315a6f31 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 18:30:51 +0000 Subject: [PATCH 02/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- panel/theme/js/fast_design.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/theme/js/fast_design.js b/panel/theme/js/fast_design.js index efa4dcb343..41fa2595f7 100644 --- a/panel/theme/js/fast_design.js +++ b/panel/theme/js/fast_design.js @@ -52,7 +52,7 @@ function setNeutralColor(color, selector) { const provider = document.querySelector(selector); provider.neutralPalette = palette; } - + function setBackgroundColor(color, selector) { color = standardize_color(color); const provider = document.querySelector(selector); From 8a7b475aa74e02efe8fba3e85fccb57b36addabc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 8 Feb 2023 19:56:06 +0100 Subject: [PATCH 03/25] Fix tests --- panel/config.py | 7 ++++--- panel/io/document.py | 4 +++- panel/tests/template/fast/test_fast_grid_template.py | 5 +++-- panel/tests/template/fast/test_fast_list_template.py | 5 +++-- panel/theme/base.py | 4 ++++ 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/panel/config.py b/panel/config.py index c0ba9d5ef2..261238aaed 100644 --- a/panel/config.py +++ b/panel/config.py @@ -487,9 +487,10 @@ def theme(self): if self._theme: return self._theme from .io.state import state - theme = state.session_args.get('theme', [b'default'])[0].decode('utf-8') - if theme in self.param._theme.objects: - return theme + if isinstance(state.session_args, dict) and state.session_args: + theme = state.session_args.get('theme', [b'default'])[0].decode('utf-8') + if theme in self.param._theme.objects: + return theme return 'default' @property diff --git a/panel/io/document.py b/panel/io/document.py index 19c0a6791c..315805c472 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -104,7 +104,9 @@ def _cleanup_doc(doc): def init_doc(doc: Optional[Document]) -> Document: curdoc = doc or curdoc_locked() - if not isinstance(curdoc, Document): + if curdoc is None: + curdoc = Document() + elif not isinstance(curdoc, Document): curdoc = curdoc._doc if not curdoc.session_context: return curdoc diff --git a/panel/tests/template/fast/test_fast_grid_template.py b/panel/tests/template/fast/test_fast_grid_template.py index d7014c291c..f14c71be38 100644 --- a/panel/tests/template/fast/test_fast_grid_template.py +++ b/panel/tests/template/fast/test_fast_grid_template.py @@ -11,7 +11,8 @@ from panel.layout import Column from panel.pane import HTML, HoloViews, Markdown from panel.param import Param -from panel.template.fast.grid import FastGridDarkTheme, FastGridTemplate +from panel.template.fast.grid import FastGridTemplate +from panel.theme.fast import FastDarkTheme from panel.widgets import Button hv.extension("bokeh") @@ -27,7 +28,7 @@ def test_template_theme_parameter(): doc = template.server_doc(Document()) assert doc.theme._json['attrs']['figure']['background_fill_color']=="#181818" - assert isinstance(template._get_theme(), FastGridDarkTheme) + assert isinstance(template._themer.theme, FastDarkTheme) COLLAPSED_ICON = """ diff --git a/panel/tests/template/fast/test_fast_list_template.py b/panel/tests/template/fast/test_fast_list_template.py index 17127c4254..2f119b2ded 100644 --- a/panel/tests/template/fast/test_fast_list_template.py +++ b/panel/tests/template/fast/test_fast_list_template.py @@ -4,10 +4,11 @@ import panel as pn from panel.pane import HoloViews, Markdown -from panel.template.fast.list import FastListDarkTheme, FastListTemplate +from panel.template.fast.list import FastListTemplate from panel.tests.template.fast.test_fast_grid_template import ( INFO, _create_hvplot, _fast_button_card, _sidebar_items, ) +from panel.theme.fast import FastDarkTheme ACCENT_COLOR = "#D2386C" @@ -20,7 +21,7 @@ def test_template_theme_parameter(): doc = template.server_doc(Document()) assert doc.theme._json['attrs']['figure']['background_fill_color']=="#181818" - assert isinstance(template._get_theme(), FastListDarkTheme) + assert isinstance(template._themer.theme, FastDarkTheme) def test_accepts_colors_by_name(): diff --git a/panel/theme/base.py b/panel/theme/base.py index 01626331de..a31d6ddc2e 100644 --- a/panel/theme/base.py +++ b/panel/theme/base.py @@ -78,6 +78,10 @@ def __init__(self, theme=None, **params): super().__init__(theme=theme, **params) def apply(self, viewable, root: Model, isolated: bool=True): + if not root.document: + self._reapply(viewable, root) + return + with root.document.models.freeze(): self._reapply(viewable, root) if self.theme and self.theme.bokeh_theme and root.document: From 58cc5e6652ef120e700ad17679f202e06060b3bb Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 12 Feb 2023 12:26:03 +0100 Subject: [PATCH 04/25] Implement isolated designs --- panel/compiler.py | 31 ++-- panel/config.py | 42 +++-- panel/io/resources.py | 4 +- panel/reactive.py | 61 +++++-- panel/template/base.py | 56 +++--- panel/template/bootstrap/__init__.py | 10 +- panel/template/bootstrap/bootstrap.css | 1 + panel/template/fast/base.py | 10 +- panel/template/fast/fast.css | 6 + .../fast/grid/fast_grid_template.html | 140 +++------------ .../fast/list/fast_list_template.html | 134 ++------------ panel/template/material/__init__.py | 6 +- panel/theme/__init__.py | 2 +- panel/theme/base.py | 67 +++++-- panel/theme/bootstrap.py | 14 +- panel/theme/css/bootstrap.css | 1 - panel/theme/css/bootstrap_dark.css | 170 ++++++++++++++++++ panel/theme/css/bootstrap_default.css | 133 ++++++++++++++ panel/theme/css/dark.css | 2 +- panel/theme/css/default.css | 2 +- panel/theme/css/material.css | 7 +- panel/theme/css/material_dark.css | 18 ++ panel/theme/css/material_default.css | 18 ++ panel/theme/fast.py | 26 ++- panel/theme/js/fast_design.js | 159 +++++++--------- panel/theme/material.py | 12 +- panel/viewable.py | 26 ++- 27 files changed, 698 insertions(+), 460 deletions(-) create mode 100644 panel/theme/css/bootstrap_dark.css create mode 100644 panel/theme/css/bootstrap_default.css create mode 100644 panel/theme/css/material_dark.css create mode 100644 panel/theme/css/material_default.css diff --git a/panel/compiler.py b/panel/compiler.py index 9e00745b8d..fdda1a99b6 100644 --- a/panel/compiler.py +++ b/panel/compiler.py @@ -20,8 +20,9 @@ from .io.resources import RESOURCE_URLS from .reactive import ReactiveHTML from .template.base import BasicTemplate -from .theme import Theme, Themer +from .theme import Design, Theme +BASE_DIR = pathlib.Path(__file__).parent BUNDLE_DIR = pathlib.Path(__file__).parent / 'dist' / 'bundled' #--------------------------------------------------------------------- @@ -31,6 +32,12 @@ def write_bundled_files(name, files, explicit_dir=None, ext=None): model_name = name.split('.')[-1].lower() for bundle_file in files: + if not bundle_file.startswith('http'): + dest_path = BUNDLE_DIR / name.lower() / bundle_file + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(BASE_DIR / 'theme' / bundle_file, dest_path) + continue + bundle_file = bundle_file.split('?')[0] try: response = requests.get(bundle_file) @@ -213,27 +220,27 @@ def bundle_themes(verbose=False, external=True): theme_bundle_dir.mkdir(parents=True, exist_ok=True) shutil.copyfile(theme.base_css, theme_bundle_dir / os.path.basename(theme.base_css)) if theme.css: - tmplt_bundle_dir = BUNDLE_DIR / theme._template.__name__.lower() + tmplt_bundle_dir = BUNDLE_DIR / theme.param.css.owner.__name__.lower() tmplt_bundle_dir.mkdir(parents=True, exist_ok=True) shutil.copyfile(theme.css, tmplt_bundle_dir / os.path.basename(theme.css)) - # Bundle themer stylesheets - for name, themer in param.concrete_descendents(Themer).items(): + # Bundle design stylesheets + for name, design in param.concrete_descendents(Design).items(): if verbose: - print(f'Bundling {name} themer resources') + print(f'Bundling {name} design resources') - # Bundle Themer._resources - if themer._resources.get('bundle', True) and external: - write_component_resources(name, themer) + # Bundle Design._resources + if design._resources.get('bundle', True) and external: + write_component_resources(name, design) - for scls, modifiers in themer._modifiers.items(): - cls_modifiers = themer._modifiers.get(scls, {}) + for scls, modifiers in design._modifiers.items(): + cls_modifiers = design._modifiers.get(scls, {}) if 'stylesheets' not in cls_modifiers: continue - # Find the Themer class the options were first defined on + # Find the Design class the options were first defined on def_cls = [ - super_cls for super_cls in themer.__mro__[::-1] + super_cls for super_cls in design.__mro__[::-1] if getattr(super_cls, '_modifiers', {}).get(scls) is cls_modifiers ][0] def_path = pathlib.Path(inspect.getmodule(def_cls).__file__).parent diff --git a/panel/config.py b/panel/config.py index 261238aaed..6bbfa2ff3d 100644 --- a/panel/config.py +++ b/panel/config.py @@ -22,7 +22,7 @@ from .io.logging import panel_log_handler from .io.state import state -from .theme import Themer +from .theme import Design __version__ = str(param.version.Version( fpath=__file__, archive_commit="$Format:%h$", reponame="panel")) @@ -117,7 +117,7 @@ class _config(_base_config): defer_load = param.Boolean(default=False, doc=""" Whether to defer load of rendered functions.""") - design_system = param.ObjectSelector(default=None, objects=[], doc=""" + design = param.ClassSelector(class_=Design, is_instance=False, default=Design, doc=""" The design system to use to style components.""") exception_handler = param.Callable(default=None, doc=""" @@ -364,7 +364,7 @@ def __getattribute__(self, attr): curdoc and attr not in session_config[curdoc]): new_obj = copy.copy(super().__getattribute__(attr)) setattr(self, attr, new_obj) - if attr in global_params: + if attr in global_params or attr == 'theme': return super().__getattribute__(attr) elif curdoc and curdoc in session_config and attr in session_config[curdoc]: return session_config[curdoc][attr] @@ -484,26 +484,18 @@ def oauth_extra_params(self): @property def theme(self): - if self._theme: - return self._theme from .io.state import state - if isinstance(state.session_args, dict) and state.session_args: + curdoc = state.curdoc + if curdoc and 'theme' in self._session_config.get(curdoc, {}): + return self._session_config[curdoc]['theme'] + elif self._theme_: + return self._theme_ + elif isinstance(state.session_args, dict) and state.session_args: theme = state.session_args.get('theme', [b'default'])[0].decode('utf-8') if theme in self.param._theme.objects: return theme return 'default' - @property - def themer(self): - try: - importlib.import_module(f'panel.theme.{self.design_system}') - except Exception: - pass - themers = { - p.lower(): t for p, t in param.concrete_descendents(Themer).items() - } - return themers.get(self.design_system, Themer)(theme=self.theme) - if hasattr(_config.param, 'objects'): _params = _config.param.objects() @@ -602,7 +594,21 @@ def __call__(self, *args, **params): 'will be skipped.' % arg) for k, v in params.items(): - if k in ['raw_css', 'css_files']: + if k == 'design' and isinstance(v, str): + try: + importlib.import_module(f'panel.theme.{self._design}') + except Exception: + pass + designs = { + p.lower(): t for p, t in param.concrete_descendents(Design).items() + } + if v not in designs: + raise ValueError( + f'Design {v!r} was not recognized, available design ' + f'systems include: {list(designs)}.' + ) + setattr(config, k, designs[v]) + elif k in ('css_files', 'raw_css'): if not isinstance(v, list): raise ValueError('%s should be supplied as a list, ' 'not as a %s type.' % diff --git a/panel/io/resources.py b/panel/io/resources.py index 3841c861c5..718cc42919 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -386,8 +386,8 @@ def js_files(self): js_files = self.adjust_paths(files) js_files += list(config.js_files.values()) - if config.themer: - js_files += list(config.themer._resources.get('js', {}).values()) + if config.design: + js_files += list(config.design._resources.get('js', {}).values()) # Load requirejs last to avoid interfering with other libraries dist_dir = self.dist_dir diff --git a/panel/reactive.py b/panel/reactive.py index 650cdb84b3..6444efc0cb 100644 --- a/panel/reactive.py +++ b/panel/reactive.py @@ -90,7 +90,7 @@ class Syncable(Renderable): _manual_params: ClassVar[List[str]] = [] # Mapping from parameter name to bokeh model property name - _rename: ClassVar[Mapping[str, str | None]] = {'loading': None} + _rename: ClassVar[Mapping[str, str | None]] = {} # Allows defining a mapping from model property name to a JS code # snippet that transforms the object before serialization @@ -107,6 +107,7 @@ class Syncable(Renderable): __abstract = True def __init__(self, **params): + self._themer = None super().__init__(**params) # Useful when updating model properties which trigger potentially @@ -148,16 +149,7 @@ def _linked_properties(self) -> Tuple[str]: ) def _get_properties(self, doc: Document) -> Dict[str, Any]: - properties = self._process_param_change(self._init_params()) - if 'stylesheets' in properties: - if doc and 'dist_url' in doc._template_variables: - dist_url = doc._template_variables['dist_url'] - else: - dist_url = CDN_DIST - for stylesheet in properties['stylesheets']: - if isinstance(stylesheet, ImportedStyleSheet): - patch_stylesheet(stylesheet, dist_url) - return properties + return self._process_param_change(self._init_params()) def _process_property_change(self, msg: Dict[str, Any]) -> Dict[str, Any]: """ @@ -316,12 +308,6 @@ def _update_model( if attr in self._events: del self._events[attr] - if 'stylesheets' in msg: - dist_url = doc._template_variables.get('dist_dir') - for stylesheet in msg['stylesheets']: - if isinstance(stylesheet, ImportedStyleSheet): - patch_stylesheet(stylesheet, dist_url) - try: model.update(**msg) finally: @@ -526,8 +512,49 @@ class Reactive(Syncable, Viewable): the parameters to other objects. """ + _rename: ClassVar[Mapping[str, str | None]] = { + 'design': None, 'loading': None + } + __abstract = True + #---------------------------------------------------------------- + # Private API + #---------------------------------------------------------------- + + def _init_params(self) -> Dict[str, Any]: + params, _ = self._design.params(self) if self._design else ({}, None) + for k, v in self.param.values().items(): + if k in self._synced_params and v is not None: + if k == 'stylesheets' and 'stylesheets' in params: + params['stylesheets'] = params['stylesheets'] + v + else: + params[k] = v + return params + + def _get_properties(self, doc: Document) -> Dict[str, Any]: + properties = super()._get_properties(doc) + if 'stylesheets' in properties: + if doc and 'dist_url' in doc._template_variables: + dist_url = doc._template_variables['dist_url'] + else: + dist_url = CDN_DIST + for stylesheet in properties['stylesheets']: + if isinstance(stylesheet, ImportedStyleSheet): + patch_stylesheet(stylesheet, dist_url) + return properties + + def _update_model( + self, events: Dict[str, param.parameterized.Event], msg: Dict[str, Any], + root: Model, model: Model, doc: Document, comm: Optional[Comm] + ) -> None: + if 'stylesheets' in msg: + dist_url = doc._template_variables.get('dist_dir') + for stylesheet in msg['stylesheets']: + if isinstance(stylesheet, ImportedStyleSheet): + patch_stylesheet(stylesheet, dist_url) + super()._update_model(events, msg, root, model, doc, comm) + #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- diff --git a/panel/template/base.py b/panel/template/base.py index c7a5c4cbfe..13838a1e32 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -38,7 +38,7 @@ from ..pane.image import ImageBase from ..reactive import ReactiveHTML from ..theme import ( - THEMES, DefaultTheme, Theme, Themer, + THEMES, DefaultTheme, Design, Theme, ) from ..util import isurl, url_path from ..viewable import Renderable, ServableMixin, Viewable @@ -81,9 +81,9 @@ class BaseTemplate(param.Parameterized, ServableMixin): theme = param.ClassSelector(class_=Theme, default=DefaultTheme, constant=True, is_instance=False, instantiate=False) - themer = param.ClassSelector(class_=Themer, default=Themer, constant=True, + design = param.ClassSelector(class_=Design, default=Design, constant=True, is_instance=False, instantiate=False, doc=""" - A Themer applies a specific design system to a template.""") + A Design applies a specific design system to a template.""") # Dictionary of property overrides by Viewable type _modifiers: ClassVar[Dict[Type[Viewable], Dict[str, Any]]] = {} @@ -110,7 +110,7 @@ def __init__( self._documents: List[Document] = [] self._server = None self._layout = self._build_layout() - self._themer = self.themer(theme=self.theme) + self._design = self.design(theme=self.theme) def _build_layout(self) -> Column: str_repr = Str(repr(self)) @@ -171,7 +171,7 @@ def _init_doc( # link objects across multiple roots in a template. col = Column() preprocess_root = col.get_root(document, comm, preprocess=False) - col._hooks.append(self._themer._apply_hooks) + col._hooks.append(self._design._apply_hooks) ref = preprocess_root.ref['id'] # Add all render items to the document @@ -184,9 +184,10 @@ def _init_doc( model.tags = tags mref = model.ref['id'] - # Insert themer as pre-processor - if self._themer._apply_hooks not in obj._hooks: - obj._hooks.append(self._themer._apply_hooks) + # Insert design as pre-processor and run it + self._design.apply(obj, model, isolated=False) + if self._design._apply_hooks not in obj._hooks: + obj._hooks.append(self._design._apply_hooks) # Alias model ref with the fake root ref to ensure that # pre-processor correctly operates on fake root @@ -224,8 +225,8 @@ def _init_doc( document.template = self.nb_template else: document.template = self.template - document._template_variables.update(self._render_variables) + document._template_variables.update(self._render_variables) return document def _repr_mimebundle_( @@ -566,8 +567,8 @@ def _init_doc( document = super()._init_doc(doc, comm, title, notebook, location) if self.notifications: state._notifications[document] = self.notifications - if self._themer.theme.bokeh_theme: - document.theme = self._themer.theme.bokeh_theme + if self._design.theme.bokeh_theme: + document.theme = self._design.theme.bokeh_theme return document def _template_resources(self) -> ResourcesType: @@ -593,7 +594,7 @@ def _template_resources(self) -> ResourcesType: 'raw_css': list(self.config.raw_css) } - theme = self._themer.theme + theme = self._design.theme if theme and theme.base_css: basename = os.path.basename(theme.base_css) owner = type(theme).param.base_css.owner @@ -604,6 +605,7 @@ def _template_resources(self) -> ResourcesType: css_files['theme_base'] = theme.base_css elif resolve_custom_path(theme, theme.base_css): css_files['theme_base'] = component_resource_path(owner, 'base_css', theme.base_css) + if theme and theme.css: basename = os.path.basename(theme.css) if (BUNDLE_DIR / name / basename).is_file(): @@ -614,8 +616,16 @@ def _template_resources(self) -> ResourcesType: css_files['theme'] = component_resource_path(theme, 'css', theme.css) resolved_resources: List[Literal['css', 'js', 'js_modules']] = ['css', 'js', 'js_modules'] + + # Resolve Design resources resources = dict(self._resources) - for rt, res in self._themer._resources.items(): + for rt, res in self._design._resources.items(): + if not isinstance(res, dict): + continue + res = { + name: url if isurl(url) else f'{type(self._design).__name__.lower()}/{url}' + for name, url in res.items() + } if rt in resources: resources[rt] = dict(resources[rt], **res) else: @@ -630,22 +640,26 @@ def _template_resources(self) -> ResourcesType: resource_path = resource.replace(f'{CDN_DIST}bundled/', '') elif resource.startswith(config.npm_cdn): resource_path = resource.replace(config.npm_cdn, '')[1:] - else: + elif resource.startswith('http:'): resource_path = url_path(resource) - prefixed = resource_path + else: + resource_path = resource + if resource_type == 'js_modules' and not (state.rel_path or use_cdn): prefixed_dist = f'./{dist_path}' else: prefixed_dist = dist_path - bundlepath = BUNDLE_DIR / prefixed.replace('/', os.path.sep) - if bundlepath: - resource_files[rname] = f'{prefixed_dist}bundled/{prefixed}' + + bundlepath = BUNDLE_DIR / resource_path.replace('/', os.path.sep) + if bundlepath.is_file(): + resource_files[rname] = f'{prefixed_dist}bundled/{resource_path}' elif isurl(resource): resource_files[rname] = resource elif resolve_custom_path(self, resource): resource_files[rname] = component_resource_path( self, f'_resources/{resource_type}', resource ) + print(resources, resource_files) for name, js in self.config.js_files.items(): if '//' not in js and state.rel_path: @@ -747,7 +761,7 @@ def _update_vars(self, *args) -> None: self._render_variables['header_color'] = self.header_color self._render_variables['main_max_width'] = self.main_max_width self._render_variables['sidebar_width'] = self.sidebar_width - self._render_variables['theme'] = self._themer.theme + self._render_variables['theme'] = self._design.theme def _update_busy(self) -> None: if self.busy_indicator: @@ -775,12 +789,12 @@ def _update_render_items(self, event: param.parameterized.Event) -> None: del self._render_items[ref] new = event.new if isinstance(event.new, list) else event.new.values() - if self._themer.theme.bokeh_theme: + if self._design.theme.bokeh_theme: for o in new: if o in old: continue for hvpane in o.select(HoloViews): - hvpane.theme = self._themer.theme.bokeh_theme + hvpane.theme = self._design.theme.bokeh_theme labels = {} for obj in new: diff --git a/panel/template/bootstrap/__init__.py b/panel/template/bootstrap/__init__.py index 28900dab13..785b6997e6 100644 --- a/panel/template/bootstrap/__init__.py +++ b/panel/template/bootstrap/__init__.py @@ -9,7 +9,7 @@ import param -from ...theme import Themer +from ...theme import Design from ...theme.bootstrap import Bootstrap from ..base import BasicTemplate, TemplateActions @@ -33,9 +33,9 @@ class BootstrapTemplate(BasicTemplate): sidebar_width = param.Integer(350, doc=""" The width of the sidebar in pixels. Default is 350.""") - themer = param.ClassSelector(class_=Themer, default=Bootstrap, constant=True, + design = param.ClassSelector(class_=Design, default=Bootstrap, constant=True, is_instance=False, instantiate=False, doc=""" - A Themer applies a specific design system to a template.""") + A Design applies a specific design system to a template.""") _actions = param.ClassSelector(default=BootstrapTemplateActions(), class_=TemplateActions) @@ -45,5 +45,5 @@ class BootstrapTemplate(BasicTemplate): def _update_vars(self, *args) -> None: super()._update_vars(*args) - themer = self.themer(theme=self.theme) - self._render_variables['html_attrs'] = f'data-bs-theme="{themer.theme._bs_theme}"' + design = self.design(theme=self.theme) + self._render_variables['html_attrs'] = f'data-bs-theme="{design.theme._bs_theme}"' diff --git a/panel/template/bootstrap/bootstrap.css b/panel/template/bootstrap/bootstrap.css index 11aa07521f..1b79256701 100644 --- a/panel/template/bootstrap/bootstrap.css +++ b/panel/template/bootstrap/bootstrap.css @@ -10,6 +10,7 @@ --bs-surface-color: var(--panel-on-surface-color); --bokeh-base-font: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; } + ::-webkit-scrollbar { width: 12px; } diff --git a/panel/template/fast/base.py b/panel/template/fast/base.py index 8b8d009de7..f2714b0336 100644 --- a/panel/template/fast/base.py +++ b/panel/template/fast/base.py @@ -4,7 +4,7 @@ from ...io.state import state from ...theme import THEMES, DefaultTheme -from ...theme.fast import Fast, Themer +from ...theme.fast import Design, Fast from ..base import BasicTemplate from ..react import ReactTemplate @@ -51,9 +51,9 @@ class FastBaseTemplate(BasicTemplate): What to wrap the main components into. Options are '' (i.e. none) and 'card' (Default). Could be extended to Accordion, Tab etc. in the future.""") - themer = param.ClassSelector(class_=Themer, default=Fast, constant=True, + design = param.ClassSelector(class_=Design, default=Fast, constant=True, is_instance=False, instantiate=False, doc=""" - A Themer applies a specific design system to a template.""") + A Design applies a specific design system to a template.""") _css = [_ROOT / "fast.css"] @@ -78,7 +78,7 @@ def __init__(self, **params): super().__init__(**params) self.param.update({ - p: v for p, v in self._themer.theme.style.param.values().items() + p: v for p, v in self._design.theme.style.param.values().items() if p != 'name' and p in self.param and p not in params }) @@ -92,7 +92,7 @@ def _get_theme_from_query_args(): def _update_vars(self): super()._update_vars() - style = self._themer.theme.style + style = self._design.theme.style style.param.update({ p: getattr(self, p) for p in style.param if p != 'name' and p in self.param diff --git a/panel/template/fast/fast.css b/panel/template/fast/fast.css index 6a3a570da2..c09e9a93bf 100644 --- a/panel/template/fast/fast.css +++ b/panel/template/fast/fast.css @@ -63,3 +63,9 @@ fast-card { padding: 1em; overflow: auto; } + +#pn-Modal { + --dialog-width: 80%; + --dialog-height: auto; + --background-color: var(--neutral-layer-floating); +} diff --git a/panel/template/fast/grid/fast_grid_template.html b/panel/template/fast/grid/fast_grid_template.html index 5eb10a1e44..2e3d67aea1 100644 --- a/panel/template/fast/grid/fast_grid_template.html +++ b/panel/template/fast/grid/fast_grid_template.html @@ -41,22 +41,24 @@ -
- +