From cab09b4e2f2625bd3aa369d0aee45ccc32482663 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 11 Jul 2024 09:54:13 +0000 Subject: [PATCH 1/7] Add `static` folder for static files, e.g. stylesheets --- aiidalab_widgets_base/static/__init__.py | 0 aiidalab_widgets_base/static/styles/README.md | 3 +++ aiidalab_widgets_base/static/styles/__init__.py | 0 aiidalab_widgets_base/static/styles/global.css | 0 4 files changed, 3 insertions(+) create mode 100644 aiidalab_widgets_base/static/__init__.py create mode 100644 aiidalab_widgets_base/static/styles/README.md create mode 100644 aiidalab_widgets_base/static/styles/__init__.py create mode 100644 aiidalab_widgets_base/static/styles/global.css diff --git a/aiidalab_widgets_base/static/__init__.py b/aiidalab_widgets_base/static/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aiidalab_widgets_base/static/styles/README.md b/aiidalab_widgets_base/static/styles/README.md new file mode 100644 index 000000000..9ec5be400 --- /dev/null +++ b/aiidalab_widgets_base/static/styles/README.md @@ -0,0 +1,3 @@ +# Stylesheets for AiiDAlab Widgets Base + +This folder contains `.css` stylesheets, which are loaded on any import from the AiiDAlab widgets base package. diff --git a/aiidalab_widgets_base/static/styles/__init__.py b/aiidalab_widgets_base/static/styles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aiidalab_widgets_base/static/styles/global.css b/aiidalab_widgets_base/static/styles/global.css new file mode 100644 index 000000000..e69de29bb From fd643e709dc02d93624c88251b0eb2a158b5cfb5 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 11 Jul 2024 09:54:13 +0000 Subject: [PATCH 2/7] Add css loader utility --- aiidalab_widgets_base/utils/loaders.py | 38 ++++++++++++++++++++++++++ tests/test_loaders.py | 8 ++++++ 2 files changed, 46 insertions(+) create mode 100644 aiidalab_widgets_base/utils/loaders.py create mode 100644 tests/test_loaders.py diff --git a/aiidalab_widgets_base/utils/loaders.py b/aiidalab_widgets_base/utils/loaders.py new file mode 100644 index 000000000..40e16bb60 --- /dev/null +++ b/aiidalab_widgets_base/utils/loaders.py @@ -0,0 +1,38 @@ +from importlib.resources import Package, files + +from IPython.display import Javascript, display + + +def load_css_stylesheet(package: Package, filename: str = ""): + """Load a CSS stylesheet from a package and inject it into the DOM. + + Parameters + ---------- + `package` : `Package` + The package where the CSS file is located. + `filename` : `str`, optional + The name of the CSS file to load. + If not provided, all CSS files in the package will be loaded. + """ + root = files(package) + + filenames = [] + if filename: + filenames.append(filename) + else: + filenames.extend( + path.name + for path in root.iterdir() + if path.is_file() and path.name.endswith(".css") + ) + + for filename in filenames: + stylesheet = (root / filename).read_text() + display( + Javascript(f""" + var style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = `{stylesheet}`; + document.head.appendChild(style); + """) + ) diff --git a/tests/test_loaders.py b/tests/test_loaders.py new file mode 100644 index 000000000..21caa9186 --- /dev/null +++ b/tests/test_loaders.py @@ -0,0 +1,8 @@ +from aiidalab_widgets_base.utils.loaders import load_css_stylesheet + + +def test_load_css_stylesheet(): + """Test `load_css_stylesheet` function.""" + package = "aiidalab_widgets_base.static.styles" + load_css_stylesheet(package=package, filename="global.css") + load_css_stylesheet(package=package) From 77b4cea82be649ab149b2067fafffbe7af9b8ece Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 11 Jul 2024 09:54:13 +0000 Subject: [PATCH 3/7] Load CSS stylesheets on `from aiidalab_widgets_base...` import --- aiidalab_widgets_base/__init__.py | 35 ++++++++++++++++++------------- setup.cfg | 4 ++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 63b2bf7c2..16ad05f38 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -1,7 +1,5 @@ """Reusable widgets for AiiDAlab applications.""" -from aiida.manage import get_profile - _WARNING_TEMPLATE = """

Warning:

@@ -27,22 +25,31 @@ def is_running_in_jupyter(): return False -# load the default profile if no profile is loaded, and raise a deprecation warning -# this is a temporary solution to avoid breaking existing notebooks -# this will be removed in the next major release -if is_running_in_jupyter() and get_profile() is None: - # if no profile is loaded, load the default profile and raise a deprecation warning - from aiida import load_profile +if is_running_in_jupyter(): + from aiida.manage import get_profile from IPython.display import HTML, display - load_profile() + # load the default profile if no profile is loaded, and raise a deprecation warning + # this is a temporary solution to avoid breaking existing notebooks + # this will be removed in the next major release + if get_profile() is None: + # if no profile is loaded, load the default profile and raise a deprecation warning + from aiida import load_profile + + load_profile() + + profile = get_profile() + assert profile is not None, "Failed to load the default profile" + + # raise a deprecation warning + warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) + display(warning) + + from .static import styles + from .utils.loaders import load_css_stylesheet - profile = get_profile() - assert profile is not None, "Failed to load the default profile" + load_css_stylesheet(package=styles) - # raise a deprecation warning - warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) - display(warning) from .computational_resources import ( ComputationalResourcesWidget, diff --git a/setup.cfg b/setup.cfg index 53706087f..b5b886124 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,10 @@ docs = pydata-sphinx-theme~=0.15 myst-nb~=1.1 + +[options.package_data] +aiidalab_widgets_base.static.styles = *.css + [bumpver] current_version = "v2.3.0a1" version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]" From 50a43d834928c12ad4eed9b2b71662f058bce6d6 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 21 Aug 2024 13:18:32 +0000 Subject: [PATCH 4/7] Refactor default profile loading --- aiidalab_widgets_base/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index 16ad05f38..a9f4dec55 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -11,6 +11,20 @@ """ +def load_default_profile(): + """Loads the default profile if none loaded and warn of deprecation.""" + from aiida import load_profile + + load_profile() + + profile = get_profile() + assert profile is not None, "Failed to load the default profile" + + # raise a deprecation warning + warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) + display(warning) + + # We only detect profile and throw a warning if it is on the notebook # It is not necessary to do this in the unit tests def is_running_in_jupyter(): @@ -33,17 +47,7 @@ def is_running_in_jupyter(): # this is a temporary solution to avoid breaking existing notebooks # this will be removed in the next major release if get_profile() is None: - # if no profile is loaded, load the default profile and raise a deprecation warning - from aiida import load_profile - - load_profile() - - profile = get_profile() - assert profile is not None, "Failed to load the default profile" - - # raise a deprecation warning - warning = HTML(_WARNING_TEMPLATE.format(profile=profile.name, version="v3.0.0")) - display(warning) + load_default_profile() from .static import styles from .utils.loaders import load_css_stylesheet From a88a81d5f094972af7f4166cd20c646461a6660b Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 21 Aug 2024 13:22:34 +0000 Subject: [PATCH 5/7] Simplify CSS loading utility --- aiidalab_widgets_base/__init__.py | 7 ++--- aiidalab_widgets_base/utils/loaders.py | 37 +++++++++++--------------- tests/test_loaders.py | 14 +++++----- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/aiidalab_widgets_base/__init__.py b/aiidalab_widgets_base/__init__.py index a9f4dec55..af7470cf8 100644 --- a/aiidalab_widgets_base/__init__.py +++ b/aiidalab_widgets_base/__init__.py @@ -40,6 +40,8 @@ def is_running_in_jupyter(): if is_running_in_jupyter(): + from pathlib import Path + from aiida.manage import get_profile from IPython.display import HTML, display @@ -49,10 +51,9 @@ def is_running_in_jupyter(): if get_profile() is None: load_default_profile() - from .static import styles - from .utils.loaders import load_css_stylesheet + from .utils.loaders import load_css - load_css_stylesheet(package=styles) + load_css(css_path=Path(__file__).parent / "static/styles") from .computational_resources import ( diff --git a/aiidalab_widgets_base/utils/loaders.py b/aiidalab_widgets_base/utils/loaders.py index 40e16bb60..0fdade316 100644 --- a/aiidalab_widgets_base/utils/loaders.py +++ b/aiidalab_widgets_base/utils/loaders.py @@ -1,33 +1,28 @@ -from importlib.resources import Package, files +from __future__ import annotations + +from pathlib import Path from IPython.display import Javascript, display -def load_css_stylesheet(package: Package, filename: str = ""): - """Load a CSS stylesheet from a package and inject it into the DOM. +def load_css(css_path: Path | str) -> None: + """Load and inject CSS stylesheets into the DOM. Parameters ---------- - `package` : `Package` - The package where the CSS file is located. - `filename` : `str`, optional - The name of the CSS file to load. - If not provided, all CSS files in the package will be loaded. + `css_path` : `Path` | `str` + The path to the CSS stylesheet. If the path is a directory, + all CSS files in the directory will be loaded. """ - root = files(package) - - filenames = [] - if filename: - filenames.append(filename) - else: - filenames.extend( - path.name - for path in root.iterdir() - if path.is_file() and path.name.endswith(".css") - ) + path = Path(css_path) + + if not path.exists(): + raise FileNotFoundError(f"CSS file or directory not found: {path}") + + filenames = [*path.glob("*.css")] if path.is_dir() else [path] - for filename in filenames: - stylesheet = (root / filename).read_text() + for fn in filenames: + stylesheet = fn.read_text() display( Javascript(f""" var style = document.createElement('style'); diff --git a/tests/test_loaders.py b/tests/test_loaders.py index 21caa9186..23cee1b18 100644 --- a/tests/test_loaders.py +++ b/tests/test_loaders.py @@ -1,8 +1,10 @@ -from aiidalab_widgets_base.utils.loaders import load_css_stylesheet +from pathlib import Path +from aiidalab_widgets_base.utils.loaders import load_css -def test_load_css_stylesheet(): - """Test `load_css_stylesheet` function.""" - package = "aiidalab_widgets_base.static.styles" - load_css_stylesheet(package=package, filename="global.css") - load_css_stylesheet(package=package) + +def test_load_css(): + """Test `load_css` utility.""" + css_dir = Path("aiidalab_widgets_base/static/styles") + load_css(css_path=css_dir) + load_css(css_path=css_dir / "global.css") From f27a8265d84379fcdee809e3c7d4e840a961838f Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 30 Aug 2024 13:49:38 +0000 Subject: [PATCH 6/7] Add notebook test for CSS loader --- tests_notebooks/static/styles/test.css | 3 ++ tests_notebooks/test_notebook.ipynb | 67 ++++++++++++++++++++++++++ tests_notebooks/test_notebooks.py | 9 ++++ 3 files changed, 79 insertions(+) create mode 100644 tests_notebooks/static/styles/test.css create mode 100644 tests_notebooks/test_notebook.ipynb diff --git a/tests_notebooks/static/styles/test.css b/tests_notebooks/static/styles/test.css new file mode 100644 index 000000000..9e805f497 --- /dev/null +++ b/tests_notebooks/static/styles/test.css @@ -0,0 +1,3 @@ +.red-text { + color: rgb(255, 0, 0); +} diff --git a/tests_notebooks/test_notebook.ipynb b/tests_notebooks/test_notebook.ipynb new file mode 100644 index 000000000..edffd72d5 --- /dev/null +++ b/tests_notebooks/test_notebook.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as ipw" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiida import load_profile\n", + "\n", + "load_profile();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from aiidalab_widgets_base.utils.loaders import load_css\n", + "\n", + "load_css(css_path=\"static/styles/test.css\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "label = ipw.Label(\"Testing\")\n", + "label.add_class(\"red-text\")\n", + "display(label)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests_notebooks/test_notebooks.py b/tests_notebooks/test_notebooks.py index a1d6c3977..9fdf08f23 100644 --- a/tests_notebooks/test_notebooks.py +++ b/tests_notebooks/test_notebooks.py @@ -5,6 +5,15 @@ from selenium.webdriver.common.keys import Keys +def test_loaded_css(selenium_driver): + driver = selenium_driver("tests_notebooks/test_notebook.ipynb") + element = driver.find_element(By.CLASS_NAME, "red-text") + assert element.value_of_css_property("color") in ( + "rgba(255, 0, 0, 1)", # Chrome + "rgb(255, 0, 0)", # Firefox + ) + + def test_notebook_service_available(notebook_service): url, token = notebook_service response = requests.get(f"{url}/?token={token}") From 76690800842e90d26b6790f091b28802deb3e36f Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Sat, 24 Aug 2024 05:52:32 +0000 Subject: [PATCH 7/7] Document use of CSS --- docs/source/contribute/index.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/source/contribute/index.rst b/docs/source/contribute/index.rst index 47af92fc9..7a2a9675b 100644 --- a/docs/source/contribute/index.rst +++ b/docs/source/contribute/index.rst @@ -8,3 +8,20 @@ Contributions to the AiiDAlab widgets are highly welcome and can take different * `Report bugs `_. * `Feature requests `_. * Help us improve the documentation of widgets. + +************** +Widget styling +************** + +Though ``ipywidgets`` does provide some basic styling options via the ``layout`` and ``style`` attributes, it is often not enough to create a visually appealing widget. +As such, we recommend the use of `CSS `_ stylesheets to style your widgets. +These may be packaged under ``aiidalab_widgets_base/static/styles``, which are automatically loaded on import via the ``load_css`` utility. + +A ``global.css`` stylesheet is made available for global html-tag styling and ``ipywidgets`` or ``Jupyter`` style overrides. +For more specific widgets and components, please add a dedicated stylesheet. +Note that all stylesheets in the ``styles`` directory will be loaded on import. + +We recommend using classes to avoid style leaking outside of the target widget. +We also advise causion when using the `!important `_ flag on CSS properties, as it may interfere with other stylesheets. + +If you are unsure about the styling of your widget, feel free to ask for help on the `AiiDAlab Discourse channel `_.