diff --git a/README.md b/README.md index fa65882..e60aacd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Extension to create a theme +This extension is composed of a Python package named `jupyter_theme_editor` +for the server extension and a NPM package named `jupyter-theme-editor` +for the frontend extension. + ## Requirements - JupyterLab >= 3.0 @@ -24,6 +28,22 @@ To remove the extension, execute: pip uninstall jupyter_theme_editor ``` +## Troubleshoot + +If you are seeing the frontend extension, but it is not working, check +that the server extension is enabled: + +```bash +jupyter server extension list +``` + +If the server extension is installed and enabled, but you are not seeing +the frontend extension, check the frontend extension is installed: + +```bash +jupyter labextension list +``` + ## Contributing ### Development install @@ -41,6 +61,8 @@ The `jlpm` command is JupyterLab's pinned version of pip install -e . # Link your development version of the extension with JupyterLab jupyter labextension develop . --overwrite +# Server extension must be manually installed in develop mode +jupyter server extension enable jupyter_theme_editor # Rebuild extension Typescript source after making changes jlpm build ``` @@ -65,6 +87,8 @@ jupyter lab build --minimize=False ### Development uninstall ```bash +# Server extension must be manually disabled in develop mode +jupyter server extension disable jupyter_theme_editor pip uninstall jupyter_theme_editor ``` @@ -74,6 +98,22 @@ folder is located. Then you can remove the symlink named `jupyter-theme-editor` ### Testing the extension +#### Server tests + +This extension is using [Pytest](https://docs.pytest.org/) for Python code testing. + +Install test dependencies (needed only once): + +```sh +pip install -e ".[test]" +``` + +To execute them, run: + +```sh +pytest -vv -r ap --cov jupyter_theme_editor +``` + #### Frontend tests This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. diff --git a/binder/postBuild b/binder/postBuild index 37c8ae0..d8b2c36 100755 --- a/binder/postBuild +++ b/binder/postBuild @@ -32,6 +32,23 @@ _(sys.executable, "-m", "pip", "check") # install the labextension _(sys.executable, "-m", "pip", "install", "-e", ".") _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") +_( + sys.executable, + "-m", + "jupyter", + "serverextension", + "enable", + "jupyter_theme_editor", +) +_( + sys.executable, + "-m", + "jupyter", + "server", + "extension", + "enable", + "jupyter_theme_editor", +) # verify the environment the extension didn't break anything _(sys.executable, "-m", "pip", "check") diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..235278b --- /dev/null +++ b/conftest.py @@ -0,0 +1,8 @@ +import pytest + +pytest_plugins = ("pytest_jupyter.jupyter_server", ) + + +@pytest.fixture +def jp_server_config(jp_server_config): + return {"ServerApp": {"jpserver_extensions": {"jupyter_theme_editor": True}}} diff --git a/jest.config.js b/jest.config.js index 514c380..4f1bf3d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,7 +29,11 @@ module.exports = { testPathIgnorePatterns, transform, automock: false, - collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!src/**/*.d.ts', + '!src/**/.ipynb_checkpoints/*' + ], coverageDirectory: 'coverage', coverageReporters: ['lcov', 'text'], globals: { diff --git a/jupyter-config/nb-config/jupyter_theme_editor.json b/jupyter-config/nb-config/jupyter_theme_editor.json new file mode 100644 index 0000000..0c43d9c --- /dev/null +++ b/jupyter-config/nb-config/jupyter_theme_editor.json @@ -0,0 +1,7 @@ +{ + "NotebookApp": { + "nbserver_extensions": { + "jupyter_theme_editor": true + } + } +} diff --git a/jupyter-config/server-config/jupyter_theme_editor.json b/jupyter-config/server-config/jupyter_theme_editor.json new file mode 100644 index 0000000..1b5dd58 --- /dev/null +++ b/jupyter-config/server-config/jupyter_theme_editor.json @@ -0,0 +1,7 @@ +{ + "ServerApp": { + "jpserver_extensions": { + "jupyter_theme_editor": true + } + } +} diff --git a/jupyter_theme_editor/__init__.py b/jupyter_theme_editor/__init__.py index 555c6d3..65f03f0 100644 --- a/jupyter_theme_editor/__init__.py +++ b/jupyter_theme_editor/__init__.py @@ -1,4 +1,6 @@ from ._version import __version__ +from .handlers import setup_handlers + def _jupyter_labextension_paths(): @@ -6,3 +8,28 @@ def _jupyter_labextension_paths(): "src": "labextension", "dest": "jupyter-theme-editor" }] + + + +def _jupyter_server_extension_points(): + return [{ + "module": "jupyter_theme_editor" + }] + + +def _load_jupyter_server_extension(server_app): + """Registers the API handler to receive HTTP requests from the frontend extension. + + Parameters + ---------- + server_app: jupyterlab.labapp.LabApp + JupyterLab application instance + """ + setup_handlers(server_app.web_app) + name = "jupyter_theme_editor" + server_app.log.info(f"Registered {name} server extension") + + +# For backward compatibility with notebook server - useful for Binder/JupyterHub +load_jupyter_server_extension = _load_jupyter_server_extension + diff --git a/jupyter_theme_editor/handlers.py b/jupyter_theme_editor/handlers.py new file mode 100644 index 0000000..4cbdff1 --- /dev/null +++ b/jupyter_theme_editor/handlers.py @@ -0,0 +1,47 @@ +import json +from jupyter_server.base.handlers import APIHandler +from jupyter_server.utils import url_path_join +import tornado +from jinja2 import Environment, PackageLoader +from pathlib import Path + +class RouteHandler(APIHandler): + # The following decorator should be present on all verb methods (head, get, post, + # patch, put, delete, options) to ensure only authorized user can request the + # Jupyter server + + def initialize(self, env: Environment, path: str): + self._env = env + self._path = path + + @tornado.web.authenticated + def post(self): + # input_data is a dictionary with a key "name" + input_data = self.get_json_body() + new_input_data = {} + for key, value in input_data.items(): + value = str(input_data[key]) + new_value = value.strip() + new_key = key.replace('--jp-', '').replace('-', '_') + new_input_data[new_key] = new_value + + + j2_template = self._env.get_template(self._path) + output_data = j2_template.render(new_input_data) + self.set_header("content-type", "text/css") + self.set_header("cache-control", "no-cache") + self.set_header("content-disposition", + "attachment; filename=variables.css") + self.set_header("content-length", len(output_data.encode())) + self.finish(output_data) + + +def setup_handlers(web_app): + host_pattern = ".*$" + + base_url = web_app.settings["base_url"] + route_pattern = url_path_join( + base_url, 'jupyter-theme-editor', "send_cssProperties") + handlers = [(route_pattern, RouteHandler, {"env":Environment(loader=PackageLoader( + "jupyter_theme_editor", "templates")), "path": "template.css" })] + web_app.add_handlers(host_pattern, handlers) diff --git a/jupyter_theme_editor/templates/template.css b/jupyter_theme_editor/templates/template.css new file mode 100644 index 0000000..dd3d245 --- /dev/null +++ b/jupyter_theme_editor/templates/template.css @@ -0,0 +1,419 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +/* +The following CSS variables define the main, public API for styling JupyterLab. +These variables should be used by all plugins wherever possible. In other +words, plugins should not define custom colors, sizes, etc unless absolutely +necessary. This enables users to change the visual theme of JupyterLab +by changing these variables. + +Many variables appear in an ordered sequence (0,1,2,3). These sequences +are designed to work well together, so for example, `--jp-border-color1` should +be used with `--jp-layout-color1`. The numbers have the following meanings: + +* 0: super-primary, reserved for special emphasis +* 1: primary, most important under normal situations +* 2: secondary, next most important under normal situations +* 3: tertiary, next most important under normal situations + +Throughout JupyterLab, we are mostly following principles from Google's +Material Design when selecting colors. We are not, however, following +all of MD as it is not optimized for dense, information rich UIs. +*/ + +:root { + /* Elevation + * + * We style box-shadows using Material Design's idea of elevation. These particular numbers are taken from here: + * + * https://github.com/material-components/material-components-web + * https://material-components-web.appspot.com/elevation.html + */ + + --jp-shadow-base-lightness: 0; + --jp-shadow-umbra-color: rgba( + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + 0.2 + ); + --jp-shadow-penumbra-color: rgba( + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + 0.14 + ); + --jp-shadow-ambient-color: rgba( + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + var(--jp-shadow-base-lightness), + 0.12 + ); + --jp-elevation-z0: none; + --jp-elevation-z1: 0 2px 1px -1px var(--jp-shadow-umbra-color), + 0 1px 1px 0 var(--jp-shadow-penumbra-color), + 0 1px 3px 0 var(--jp-shadow-ambient-color); + --jp-elevation-z2: 0 3px 1px -2px var(--jp-shadow-umbra-color), + 0 2px 2px 0 var(--jp-shadow-penumbra-color), + 0 1px 5px 0 var(--jp-shadow-ambient-color); + --jp-elevation-z4: 0 2px 4px -1px var(--jp-shadow-umbra-color), + 0 4px 5px 0 var(--jp-shadow-penumbra-color), + 0 1px 10px 0 var(--jp-shadow-ambient-color); + --jp-elevation-z6: 0 3px 5px -1px var(--jp-shadow-umbra-color), + 0 6px 10px 0 var(--jp-shadow-penumbra-color), + 0 1px 18px 0 var(--jp-shadow-ambient-color); + --jp-elevation-z8: 0 5px 5px -3px var(--jp-shadow-umbra-color), + 0 8px 10px 1px var(--jp-shadow-penumbra-color), + 0 3px 14px 2px var(--jp-shadow-ambient-color); + --jp-elevation-z12: 0 7px 8px -4px var(--jp-shadow-umbra-color), + 0 12px 17px 2px var(--jp-shadow-penumbra-color), + 0 5px 22px 4px var(--jp-shadow-ambient-color); + --jp-elevation-z16: 0 8px 10px -5px var(--jp-shadow-umbra-color), + 0 16px 24px 2px var(--jp-shadow-penumbra-color), + 0 6px 30px 5px var(--jp-shadow-ambient-color); + --jp-elevation-z20: 0 10px 13px -6px var(--jp-shadow-umbra-color), + 0 20px 31px 3px var(--jp-shadow-penumbra-color), + 0 8px 38px 7px var(--jp-shadow-ambient-color); + --jp-elevation-z24: 0 11px 15px -7px var(--jp-shadow-umbra-color), + 0 24px 38px 3px var(--jp-shadow-penumbra-color), + 0 9px 46px 8px var(--jp-shadow-ambient-color); + + /* Borders + * + * The following variables, specify the visual styling of borders in JupyterLab. + */ + + --jp-border-width: {{border_width}}; + --jp-border-color0: {{border_color0}}; + --jp-border-color1: {{border_color1}}; + --jp-border-color2: {{border_color2}}; + --jp-border-color3: {{border_color3}}; + --jp-inverse-border-color: var(--md-grey-600); + --jp-border-radius: {{border_radius}}; + + /* UI Fonts + * + * The UI font CSS variables are used for the typography all of the JupyterLab + * user interface elements that are not directly user generated content. + * + * The font sizing here is done assuming that the body font size of --jp-ui-font-size1 + * is applied to a parent element. When children elements, such as headings, are sized + * in em all things will be computed relative to that body size. + */ + + --jp-ui-font-scale-factor: 1.2; + --jp-ui-font-size0: {{ui_font_size0}}; + --jp-ui-font-size1: {{ui_font_size1}}; /* Base font size */ + --jp-ui-font-size2: {{ui_font_size2}}; + --jp-ui-font-size3: {{ui_font_size3}}; + --jp-ui-font-family: {{ui_font_family}}; + + /* + * Use these font colors against the corresponding main layout colors. + * In a light theme, these go from dark to light. + */ + + /* Defaults use Material Design specification */ + --jp-ui-font-color0: {{ui_font_color0}}; + --jp-ui-font-color1: {{ui_font_color1}}; + --jp-ui-font-color2: {{ui_font_color2}}; + --jp-ui-font-color3: {{ui_font_color3}}; + + /* + * Use these against the brand/accent/warn/error colors. + * These will typically go from light to darker, in both a dark and light theme. + */ + + --jp-ui-inverse-font-color0: {{ui_inverse_font_color0}}; + --jp-ui-inverse-font-color1: {{ui_inverse_font_color1}}; + --jp-ui-inverse-font-color2: {{ui_inverse_font_color2}}; + --jp-ui-inverse-font-color3: {{ui_inverse_font_color3}}; + + /* Content Fonts + * + * Content font variables are used for typography of user generated content. + * + * The font sizing here is done assuming that the body font size of --jp-content-font-size1 + * is applied to a parent element. When children elements, such as headings, are sized + * in em all things will be computed relative to that body size. + */ + + --jp-content-line-height: 1.6; + --jp-content-font-scale-factor: 1.2; + --jp-content-font-size0: {{content_font_size0}}; + --jp-content-font-size1: {{content_font_size1}}; /* Base font size */ + --jp-content-font-size2: {{content_font_size2}}; + --jp-content-font-size3: {{content_font_size3}}; + --jp-content-font-size4: {{content_font_size4}}; + --jp-content-font-size5: {{content_font_size5}}; + + /* This gives a magnification of about 125% in presentation mode over normal. */ + --jp-content-presentation-font-size1: 17px; + --jp-content-heading-line-height: 1; + --jp-content-heading-margin-top: 1.2em; + --jp-content-heading-margin-bottom: 0.8em; + --jp-content-heading-font-weight: 500; + + /* Defaults use Material Design specification */ + --jp-content-font-color0: rgba(0, 0, 0, 1); + --jp-content-font-color1: rgba(0, 0, 0, 0.87); + --jp-content-font-color2: rgba(0, 0, 0, 0.54); + --jp-content-font-color3: rgba(0, 0, 0, 0.38); + --jp-content-link-color: var(--md-blue-700); + --jp-content-font-family: {{content_font_family}}; + + /* + * Code Fonts + * + * Code font variables are used for typography of code and other monospaces content. + */ + + --jp-code-font-size: {{code_font_size}}; + --jp-code-line-height: 1.3077; /* 17px for 13px base */ + --jp-code-padding: 5px; /* 5px for 13px base, codemirror highlighting needs integer px value */ + --jp-code-font-family-default: menlo, consolas, 'DejaVu Sans Mono', monospace; + --jp-code-font-family: {{code_font_family}}; + + /* This gives a magnification of about 125% in presentation mode over normal. */ + --jp-code-presentation-font-size: 16px; + + /* may need to tweak cursor width if you change font size */ + --jp-code-cursor-width0: 1.4px; + --jp-code-cursor-width1: 2px; + --jp-code-cursor-width2: 4px; + + /* Layout + * + * The following are the main layout colors use in JupyterLab. In a light + * theme these would go from light to dark. + */ + + --jp-layout-color0: {{layout_color0}}; + --jp-layout-color1: {{layout_color1}}; + --jp-layout-color2: {{layout_color2}}; + --jp-layout-color3: {{layout_color3}}; + --jp-layout-color4: {{layout_color4}}; + + /* Inverse Layout + * + * The following are the inverse layout colors use in JupyterLab. In a light + * theme these would go from dark to light. + */ + + --jp-inverse-layout-color0: {{inverse_layout_color0}}; + --jp-inverse-layout-color1: {{inverse_layout_color1}}; + --jp-inverse-layout-color2: {{inverse_layout_color2}}; + --jp-inverse-layout-color3: {{inverse_layout_color3}}; + --jp-inverse-layout-color4: {{inverse_layout_color4}}; + + /* Brand/accent */ + + --jp-brand-color0: {{brand_color0}}; + --jp-brand-color1: {{brand_color1}}; + --jp-brand-color2: {{brand_color2}}; + --jp-brand-color3: {{brand_color3}}; + --jp-brand-color4: {{brand_color4}}; + --jp-accent-color0: {{accent_color0}}; + --jp-accent-color1: {{accent_color1}}; + --jp-accent-color2: {{accent_color2}}; + --jp-accent-color3: {{accent_color3}}; + + /* State colors (warn, error, success, info) */ + + --jp-warn-color0: {{warn_color0}}; + --jp-warn-color1: {{warn_color1}}; + --jp-warn-color2: {{warn_color2}}; + --jp-warn-color3: {{warn_color3}}; + --jp-error-color0: {{error_color0}}; + --jp-error-color1: {{error_color1}}; + --jp-error-color2: {{error_color2}}; + --jp-error-color3: {{error_color0}}; + --jp-success-color0: {{success_color0}}; + --jp-success-color1: {{success_color1}}; + --jp-success-color2: {{success_color2}}; + --jp-success-color3: {{success_color3}}; + --jp-info-color0: {{info_color0}}; + --jp-info-color1: {{info_color1}}; + --jp-info-color2: {{info_color2}}; + --jp-info-color3: {{info_color3}};; + + /* Cell specific styles */ + + --jp-cell-padding: 5px; + --jp-cell-collapser-width: 8px; + --jp-cell-collapser-min-height: 20px; + --jp-cell-collapser-not-active-hover-opacity: 0.6; + --jp-cell-editor-background: var(--md-grey-100); + --jp-cell-editor-border-color: var(--md-grey-300); + --jp-cell-editor-box-shadow: inset 0 0 2px var(--md-blue-300); + --jp-cell-editor-active-background: var(--jp-layout-color0); + --jp-cell-editor-active-border-color: var(--jp-brand-color1); + --jp-cell-prompt-width: 64px; + --jp-cell-prompt-font-family: var(--jp-code-font-family-default); + --jp-cell-prompt-letter-spacing: 0; + --jp-cell-prompt-opacity: 1; + --jp-cell-prompt-not-active-opacity: 0.5; + --jp-cell-prompt-not-active-font-color: var(--md-grey-700); + + /* A custom blend of MD grey and blue 600 + * See https://meyerweb.com/eric/tools/color-blend/#546E7A:1E88E5:5:hex */ + --jp-cell-inprompt-font-color: #307fc1; + + /* A custom blend of MD grey and orange 600 + * https://meyerweb.com/eric/tools/color-blend/#546E7A:F4511E:5:hex */ + --jp-cell-outprompt-font-color: #bf5b3d; + + /* Notebook specific styles */ + + --jp-notebook-padding: 10px; + --jp-notebook-select-background: var(--jp-layout-color1); + --jp-notebook-multiselected-color: var(--md-blue-50); + + /* The scroll padding is calculated to fill enough space at the bottom of the + notebook to show one single-line cell (with appropriate padding) at the top + when the notebook is scrolled all the way to the bottom. We also subtract one + pixel so that no scrollbar appears if we have just one single-line cell in the + notebook. This padding is to enable a 'scroll past end' feature in a notebook. + */ + --jp-notebook-scroll-padding: calc( + 100% - var(--jp-code-font-size) * var(--jp-code-line-height) - + var(--jp-code-padding) - var(--jp-cell-padding) - 1px + ); + + /* Rendermime styles */ + + --jp-rendermime-error-background: #fdd; + --jp-rendermime-table-row-background: var(--md-grey-100); + --jp-rendermime-table-row-hover-background: var(--md-light-blue-50); + + /* Dialog specific styles */ + + --jp-dialog-background: rgba(0, 0, 0, 0.25); + + /* Console specific styles */ + + --jp-console-padding: 10px; + + /* Toolbar specific styles */ + + --jp-toolbar-border-color: var(--jp-border-color1); + --jp-toolbar-micro-height: 8px; + --jp-toolbar-background: var(--jp-layout-color1); + --jp-toolbar-box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.24); + --jp-toolbar-header-margin: 4px 4px 0 4px; + --jp-toolbar-active-background: var(--md-grey-300); + + /* Statusbar specific styles */ + + --jp-statusbar-height: 24px; + + /* Input field styles */ + + --jp-input-box-shadow: inset 0 0 2px var(--md-blue-300); + --jp-input-active-background: var(--jp-layout-color1); + --jp-input-hover-background: var(--jp-layout-color1); + --jp-input-background: var(--md-grey-100); + --jp-input-border-color: var(--jp-inverse-border-color); + --jp-input-active-border-color: var(--jp-brand-color1); + --jp-input-active-box-shadow-color: rgba(19, 124, 189, 0.3); + + /* General editor styles */ + + --jp-editor-selected-background: #d9d9d9; + --jp-editor-selected-focused-background: #d7d4f0; + --jp-editor-cursor-color: var(--jp-ui-font-color0); + + /* Code mirror specific styles */ + + --jp-mirror-editor-keyword-color: #008000; + --jp-mirror-editor-atom-color: #88f; + --jp-mirror-editor-number-color: #080; + --jp-mirror-editor-def-color: #00f; + --jp-mirror-editor-variable-color: var(--md-grey-900); + --jp-mirror-editor-variable-2-color: #05a; + --jp-mirror-editor-variable-3-color: #085; + --jp-mirror-editor-punctuation-color: #05a; + --jp-mirror-editor-property-color: #05a; + --jp-mirror-editor-operator-color: #a2f; + --jp-mirror-editor-comment-color: #408080; + --jp-mirror-editor-string-color: #ba2121; + --jp-mirror-editor-string-2-color: #708; + --jp-mirror-editor-meta-color: #a2f; + --jp-mirror-editor-qualifier-color: #555; + --jp-mirror-editor-builtin-color: #008000; + --jp-mirror-editor-bracket-color: #997; + --jp-mirror-editor-tag-color: #170; + --jp-mirror-editor-attribute-color: #00c; + --jp-mirror-editor-header-color: blue; + --jp-mirror-editor-quote-color: #090; + --jp-mirror-editor-link-color: #00c; + --jp-mirror-editor-error-color: #f00; + --jp-mirror-editor-hr-color: #999; + + /* + RTC user specific colors. + These colors are used for the cursor, username in the editor, + and the icon of the user. + */ + + --jp-collaborator-color1: #ffad8e; + --jp-collaborator-color2: #dac83d; + --jp-collaborator-color3: #72dd76; + --jp-collaborator-color4: #00e4d0; + --jp-collaborator-color5: #45d4ff; + --jp-collaborator-color6: #e2b1ff; + --jp-collaborator-color7: #ff9de6; + + /* Vega extension styles */ + + --jp-vega-background: white; + + /* Sidebar-related styles */ + + --jp-sidebar-min-width: 250px; + + /* Search-related styles */ + + --jp-search-toggle-off-opacity: 0.5; + --jp-search-toggle-hover-opacity: 0.8; + --jp-search-toggle-on-opacity: 1; + --jp-search-selected-match-background-color: rgb(245, 200, 0); + --jp-search-selected-match-color: black; + --jp-search-unselected-match-background-color: var( + --jp-inverse-layout-color0 + ); + --jp-search-unselected-match-color: var(--jp-ui-inverse-font-color0); + + /* Icon colors that work well with light or dark backgrounds */ + --jp-icon-contrast-color0: var(--md-purple-600); + --jp-icon-contrast-color1: var(--md-green-600); + --jp-icon-contrast-color2: var(--md-pink-600); + --jp-icon-contrast-color3: var(--md-blue-600); + + /* Button colors */ + --jp-accept-color-normal: var(--md-blue-700); + --jp-accept-color-hover: var(--md-blue-800); + --jp-accept-color-active: var(--md-blue-900); + --jp-warn-color-normal: var(--md-red-700); + --jp-warn-color-hover: var(--md-red-800); + --jp-warn-color-active: var(--md-red-900); + --jp-reject-color-normal: var(--md-grey-600); + --jp-reject-color-hover: var(--md-grey-700); + --jp-reject-color-active: var(--md-grey-800); + + /* File or activity icons and switch semantic variables */ + --jp-jupyter-icon-color: #f37626; + --jp-notebook-icon-color: #f37626; + --jp-json-icon-color: var(--md-orange-700); + --jp-console-icon-background-color: var(--md-blue-700); + --jp-console-icon-color: white; + --jp-terminal-icon-background-color: var(--md-grey-800); + --jp-terminal-icon-color: var(--md-grey-200); + --jp-text-editor-icon-color: var(--md-grey-700); + --jp-inspector-icon-color: var(--md-grey-700); + --jp-switch-color: var(--md-grey-400); + --jp-switch-true-position-color: var(--md-orange-900); + } \ No newline at end of file diff --git a/jupyter_theme_editor/tests/__init__.py b/jupyter_theme_editor/tests/__init__.py new file mode 100644 index 0000000..c2008d4 --- /dev/null +++ b/jupyter_theme_editor/tests/__init__.py @@ -0,0 +1 @@ +"""Python unit tests for jupyter_theme_editor.""" diff --git a/jupyter_theme_editor/tests/test_handlers.py b/jupyter_theme_editor/tests/test_handlers.py new file mode 100644 index 0000000..d5a839b --- /dev/null +++ b/jupyter_theme_editor/tests/test_handlers.py @@ -0,0 +1,13 @@ +import json + + +async def test_get_example(jp_fetch): + # When + response = await jp_fetch("jupyter-theme-editor", "get_example") + + # Then + assert response.code == 200 + payload = json.loads(response.body) + assert payload == { + "data": "This is /jupyter-theme-editor/get_example endpoint!" + } \ No newline at end of file diff --git a/package.json b/package.json index 42ea586..60c9ec0 100644 --- a/package.json +++ b/package.json @@ -58,9 +58,10 @@ "@jupyterlab/application": "^3.1.0 || ^4.0.0-alpha.16", "@jupyterlab/apputils": "^3.1.0 || ^4.0.0-alpha.16", "@jupyterlab/coreutils": "^5.1.0 || ^6.0.0-alpha.16", + "@jupyterlab/settingregistry": "^3.1.0 || ^4.0.0-alpha.16", "@jupyterlab/ui-components": "^3.1.0 || ^4.0.0-alpha.16", "@microsoft/fast-colors": "^5.3.1", - "@rjsf/core":"^3.1.0 || ^4.2.0", + "@rjsf/core": "^3.1.0 || ^4.2.0", "react": "^17.0.2" }, "devDependencies": { @@ -75,18 +76,19 @@ "eslint-config-prettier": "^6.15.0", "eslint-plugin-prettier": "^3.1.4", "jest": "^26.0.0", + "mkdirp": "^1.0.3", "npm-run-all": "^4.1.5", "prettier": "^2.1.1", "rimraf": "^3.0.2", "stylelint": "^14.3.0", - "stylelint-config-prettier": "^9.0.3", + "stylelint-config-prettier": "^9.0.4", "stylelint-config-recommended": "^6.0.0", "stylelint-config-standard": "~24.0.0", "stylelint-prettier": "^2.0.0", "ts-jest": "^26.0.0", "typescript": "~4.1.3" }, - "resolutions" : { + "resolutions": { "@rjsf/core": "^4.2.0" }, "sideEffects": [ @@ -98,6 +100,16 @@ "access": "public" }, "jupyterlab": { + "discovery": { + "server": { + "managers": [ + "pip" + ], + "base": { + "name": "jupyter_theme_editor" + } + } + }, "extension": true, "outputDir": "jupyter_theme_editor/labextension" } diff --git a/pyproject.toml b/pyproject.toml index cd3e9f1..0f4888d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ + "jupyter_server>=1.21,<3" ] dynamic = ["version", "description", "authors", "urls", "keywords"] @@ -32,7 +33,7 @@ test = [ "pytest", "pytest-asyncio", "pytest-cov", - "pytest-tornasync" + "pytest-jupyter[server]>=0.6.0" ] [tool.hatch.version] @@ -48,6 +49,8 @@ exclude = [".github", "binder"] [tool.hatch.build.targets.wheel.shared-data] "jupyter_theme_editor/labextension" = "share/jupyter/labextensions/jupyter-theme-editor" "install.json" = "share/jupyter/labextensions/jupyter-theme-editor/install.json" +"jupyter-config/server-config" = "etc/jupyter/jupyter_server_config.d" +"jupyter-config/nb-config" = "etc/jupyter/jupyter_notebook_config.d" [tool.hatch.build.hooks.version] path = "jupyter_theme_editor/_version.py" @@ -57,7 +60,7 @@ dependencies = ["hatch-jupyter-builder>=0.5"] build-function = "hatch_jupyter_builder.npm_builder" ensured-targets = [ "jupyter_theme_editor/labextension/static/style.js", - "jupyter_theme_editor//labextension/package.json", + "jupyter_theme_editor/labextension/package.json", ] skip-if-exists = ["jupyter_theme_editor/labextension/static/style.js"] diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 0000000..d212df2 --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,47 @@ +import { URLExt } from '@jupyterlab/coreutils'; + +import { ServerConnection } from '@jupyterlab/services'; + +/** + * Call the API extension + * + * @param endPoint API REST end point for the extension + * @param init Initial values for the request + * @returns The response body interpreted as JSON + */ +export async function requestAPI( + endPoint = '', + init: RequestInit = {} +): Promise { + // Make request to Jupyter API + const settings = ServerConnection.makeSettings(); + const requestUrl = URLExt.join( + settings.baseUrl, + 'jupyter-theme-editor', // API Namespace + endPoint + ); + + let response: Response; + try { + response = await ServerConnection.makeRequest(requestUrl, init, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error); + } + + let data: any = await response.text(); + /*console.log('Data send from the server is:', data);*/ + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.log('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} diff --git a/src/index.ts b/src/index.ts index 0700d73..f7914d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,21 +15,18 @@ const plugin: JupyterFrontEndPlugin = { id: 'jupyter-theme-editor:plugin', autoStart: true, requires: [IThemeManager], - activate: (app: JupyterFrontEnd, themeManager: IThemeManager) => { + activate: async (app: JupyterFrontEnd, themeManager: IThemeManager) => { const onThemeChanged = ( themeManager: IThemeManager, changes: IChangedArgs ) => { themeManager.themeChanged.disconnect(onThemeChanged); - const model = new ThemeEditorModel(); const view = new ThemeEditorView(model); view.addClass('jp-theme-editor-view-panel'); view.id = 'theme-editor'; view.title.icon = themeEditorIcon; app.shell.add(view, 'left'); - - console.log('JupyterLab extension jupyter-theme-editor is activated!'); }; themeManager.themeChanged.connect(onThemeChanged); } diff --git a/src/model.ts b/src/model.ts index 515dcd0..7bdc9ff 100644 --- a/src/model.ts +++ b/src/model.ts @@ -5,21 +5,25 @@ import { ColorRGBA64, hslToRGB, rgbToHSL, - ColorHSL + ColorHSL, + parseColorHexRGB } from '@microsoft/fast-colors'; +import Schema from './schema.json'; function stringtoHex(s: string) { - if (s === 'white') { - return '#000000'; - } - if (s === 'black') { + /* Convert other color formats present in variable.css to hexadecimal colors */ + const s1 = s.replace(/\s/g, ''); + if (s1 === 'white') { return '#ffffff'; } + if (s1 === 'black') { + return '#000000'; + } if (s.includes('rgba')) { - const s1 = s.split('rgba(')[1]; - const r = Number(s1.split(',')[0]); - const g = Number(s1.split(',')[1]); - const b = Number(s1.split(',')[2]); + const s2 = s1.split('rgba(')[1]; + const r = Number(s2.split(',')[0]); + const g = Number(s2.split(',')[1]); + const b = Number(s2.split(',')[2]); const hexstring = '#' + [r, g, b] @@ -34,51 +38,53 @@ function stringtoHex(s: string) { return s; } } -function hexToRGBA(h: string) { - let r = '0', - g = '0', - b = '0'; - - // 4 digits - if (h.length === 4) { - r = '0x' + h[1] + h[1]; - g = '0x' + h[2] + h[2]; - b = '0x' + h[3] + h[3]; - // 6 digits - } else if (h.length === 7) { - r = '0x' + h[1] + h[2]; - g = '0x' + h[3] + h[4]; - b = '0x' + h[5] + h[6]; +function defineColorProperties( + rootName: string, + palette: ColorPalette, + cssProperties: any, + steps: number +) { + const list = []; + for (let i = 0; i < steps; i++) { + const value = palette.palette[i].toStringHexRGB(); + const key = rootName + String(i); + list.push((cssProperties[key] = value)); } - const a = '1'; - const rgba = [Number(r) / 256, Number(g) / 256, Number(b) / 256, Number(a)]; - return rgba; + return list; } -function rgbaStringToColorRGBA64(colorValueRGBAstr: number[]) { - const colorRGBA64 = new ColorRGBA64( - colorValueRGBAstr[0], - colorValueRGBAstr[1], - colorValueRGBAstr[2], - colorValueRGBAstr[3] - ); - return colorRGBA64; +function defineFontProperties( + rootName: string, + fontList: string[], + cssProperties: any, + steps: number +) { + const list = []; + for (let i = 0; i < steps; i++) { + const value = fontList[i]; + const key = rootName + String(i); + list.push((cssProperties[key] = value)); + } + return list; } function getNumberFromCSSProperties(CSSInfos: any, name: string) { + /* Get the numerical value of a given ccs variable*/ const propertyValueStr = CSSInfos.getPropertyValue(name); const propertyValue = Number(propertyValueStr.split('px')[0]); return propertyValue; } function getFontSizeFromCSSProperties( + /* Get the font size value of a given indexed ccs variable*/ + /* If it is not in px but derived from the baseFontSize with a scaling, conversion is made */ CSSInfos: any, nameRoot: string, - step: number + index: number ) { const baseFontSize = getNumberFromCSSProperties(CSSInfos, nameRoot + '1'); - const s = CSSInfos.getPropertyValue(nameRoot + String(step)); + const s = CSSInfos.getPropertyValue(nameRoot + String(index)); if (s.includes('em')) { const s1 = Number(s.split('em')[0]); const font = s1 * baseFontSize; @@ -88,29 +94,28 @@ function getFontSizeFromCSSProperties( } } -function getHexColorFromCSSProperties(CSSInfos: any, name: string) { +function defineHexColorFromCSSProperties(CSSInfos: any, name: string) { + /* Define the hex color string from a css properties that is not defined this way in the css files */ + /* For instance colors are defined with string 'white' or 'rgba(0, 0, 0, 0.25)'*/ const propertyValueStr = CSSInfos.getPropertyValue(name); const hexcolor = stringtoHex(propertyValueStr); return hexcolor; } -function setCssColorProperties(name: string, values: ColorRGBA64[], start = 0) { - setCssProperties( - name, - values.map(v => v.toStringHexRGB()), - start +function definePaletteFromHexColor( + hexcolor: string, + steps: number, + cssVariable: string +) { + const colorRGBA64 = parseColorHexRGB( + hexcolor.replace(/\s/g, '').toUpperCase() ); + const palette = definePaletteFromColorRGBA64(colorRGBA64!, steps); + return palette; } -function setCssProperties(name: string, values: string[], start = 0) { - let counter = start; - for (const v of values) { - document.body.style.setProperty(name + String(counter), v); - counter++; - } -} - -function definePalette(color: ColorRGBA64, steps: number) { +function definePaletteFromColorRGBA64(color: ColorRGBA64, steps: number) { + /* define a palette from a single ColorRGBA64 color*/ const palette: ColorPalette = new ColorPalette({ baseColor: color, steps: steps, @@ -118,22 +123,9 @@ function definePalette(color: ColorRGBA64, steps: number) { }); return palette; } -function applyPalette(palette: ColorPalette, cssVariable: string) { - setCssColorProperties(cssVariable, palette.palette); -} - -function defineAndApplyPaletteFrom1hexColor( - hexcolor: string, - steps: number, - cssVariable: string -) { - const colorValueRGBAstr = hexToRGBA(hexcolor); - const colorRGBA64 = rgbaStringToColorRGBA64(colorValueRGBAstr); - const palette = definePalette(colorRGBA64, steps); - applyPalette(palette, cssVariable); -} function shiftLuminanceRGBAColor(colorRGBA: ColorRGBA64) { + /* Apply a shift defined by interval, to the luminance of a colorRGBA64 color*/ let l = getLuminanceRGBAColor(colorRGBA); const h = getHueRGBAColor(colorRGBA); const s = getSaturationRGBAColor(colorRGBA); @@ -169,31 +161,68 @@ function getHueRGBAColor(colorRGBA: ColorRGBA64) { const colorHSL = rgbToHSL(colorRGBA); return colorHSL.h; } + +function initializeFormData() { + const CSSInfos = window.getComputedStyle(document.body); + const formData = { + 'ui-font-family': 'system-ui', + 'content-font-family': 'system-ui', + 'code-font-family': 'monospace', + 'ui-font-size': getFontSizeFromCSSProperties( + CSSInfos, + '--jp-ui-font-size', + 1 + ), + 'content-font-size': getFontSizeFromCSSProperties( + CSSInfos, + '--jp-content-font-size', + 1 + ), + 'code-font-size': getNumberFromCSSProperties( + CSSInfos, + '--jp-code-font-size' + ), + 'border-width': getNumberFromCSSProperties(CSSInfos, '--jp-border-width'), + 'border-radius': getNumberFromCSSProperties(CSSInfos, '--jp-border-radius'), + 'layout-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-layout-color1' + ), + 'accent-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-accent-color1' + ), + 'border-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-border-color1' + ), + 'brand-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-brand-color1' + ), + 'error-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-error-color1' + ), + 'info-color': defineHexColorFromCSSProperties(CSSInfos, '--jp-info-color1'), + 'success-color': defineHexColorFromCSSProperties( + CSSInfos, + '--jp-success-color1' + ), + 'warn-color': defineHexColorFromCSSProperties(CSSInfos, '--jp-warn-color1') + }; + + return formData; +} + export class ThemeEditorModel extends VDomModel { private _uiFontScale: number; private _contentFontScale: number; private _schema: any; private _formData: any; private _formDataSetter: any; - private _fontList = [ - 'system-ui', - 'helvetica', - 'arial', - 'sans-serif', - 'JetBrains Mono', - 'Great Vibes', - 'Little Days', - 'Little Daisy', - 'Advertising Bold', - 'Aurella' - ]; - private _codeFontList = [ - 'menlo', - 'consolas', - 'DejaVu Sans Mono', - 'monospace', - 'Space Mono' - ]; + private _cssProperties: { [key: string]: string | null }; + constructor() { super(); const CSSInfos = window.getComputedStyle(document.body); @@ -206,230 +235,193 @@ export class ThemeEditorModel extends VDomModel { '--jp-content-font-scale-factor' ); - this._schema = { - type: 'object', - properties: { - 'layout-color': { - title: 'Layout color', - type: 'string' - }, - 'accent-color': { - title: 'Accent color', - type: 'string' - }, - 'border-color': { - title: 'Border color', - type: 'string' - }, - 'brand-color': { - title: 'Brand color', - type: 'string', - default: '' - }, - 'error-color': { - title: 'Error color', - type: 'string' - }, - 'info-color': { - title: 'Info color', - type: 'string' - }, - 'success-color': { - title: 'Success color', - type: 'string' - }, - 'warn-color': { - title: 'Warn color', - type: 'string' - }, - 'ui-font-size': { - title: 'UI Font size', - type: 'integer', - minimum: 6, - maximum: 30 - }, - 'content-font-size': { - title: 'Content font size', - type: 'integer', - minimum: 6, - maximum: 30 - }, - 'code-font-size': { - title: 'Code font size', - type: 'integer', - minimum: 6, - maximum: 30 - }, - 'border-width': { - title: 'Border width', - type: 'integer', - minimum: 1, - maximum: 10 - }, - 'border-radius': { - title: 'Border radius', - type: 'integer', - minimum: 1, - maximum: 10 - }, - 'ui-font-family': { - title: 'User Interface Font family', - type: 'string', - enum: this._fontList - }, - 'content-font-family': { - title: 'Content Font family', - type: 'string', - enum: this._fontList - }, - 'code-font-family': { - title: 'Code Font family', - type: 'string', - enum: this._codeFontList - } - } - }; - - this._formData = { - 'ui-font-family': 'helvetica', - 'content-font-family': 'system-ui', - 'code-font-family': 'consolas', - 'ui-font-size': getFontSizeFromCSSProperties( - CSSInfos, - '--jp-ui-font-size', - 1 - ), - 'content-font-size': getFontSizeFromCSSProperties( - CSSInfos, - '--jp-content-font-size', - 1 - ), - 'code-font-size': getNumberFromCSSProperties( - CSSInfos, - '--jp-code-font-size' - ), - 'border-width': getNumberFromCSSProperties(CSSInfos, '--jp-border-width'), - 'border-radius': getNumberFromCSSProperties( - CSSInfos, - '--jp-border-radius' - ), - 'layout-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-layout-color1' - ), - 'accent-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-accent-color1' - ), - 'border-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-border-color1' - ), - 'brand-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-brand-color1' - ), - 'error-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-error-color1' - ), - 'info-color': getHexColorFromCSSProperties(CSSInfos, '--jp-info-color1'), - 'success-color': getHexColorFromCSSProperties( - CSSInfos, - '--jp-success-color1' - ), - 'warn-color': getHexColorFromCSSProperties(CSSInfos, '--jp-warn-color1') - }; - + this._schema = Schema; + this._formData = initializeFormData(); + this._cssProperties = {}; this._formDataSetter = { 'ui-font-family': (value: string) => { - document.body.style.setProperty('--jp-ui-font-family', value); + return (this._cssProperties['--jp-ui-font-family'] = value); }, 'content-font-family': (value: string) => { - document.body.style.setProperty('--jp-content-font-family', value); + return (this._cssProperties['--jp-content-font-family'] = value); }, 'code-font-family': (value: string) => { - document.body.style.setProperty('--jp-code-font-family', value); + return (this._cssProperties['--jp-code-font-family'] = value); }, 'ui-font-size': (value: number) => { - const fontsize_list = []; - for (let i = 0; i < 4; i++) { - fontsize_list[i] = - String(Math.pow(this._uiFontScale, i - 1) * value) + 'px'; + const fontsizeList = []; + const steps = 4; + const rootName = '--jp-ui-font-size'; + for (let i = 0; i < steps; i++) { + const rounded_value = ( + Math.pow(this._uiFontScale, i - 1) * value + ).toFixed(3); + fontsizeList[i] = String(rounded_value) + 'px'; } - setCssProperties('--jp-ui-font-size', fontsize_list); + return defineFontProperties( + rootName, + fontsizeList, + this._cssProperties, + steps + ); }, 'content-font-size': (value: number) => { - const fontsize_list = []; - for (let i = 0; i < 6; i++) { - fontsize_list[i] = - String(Math.pow(this._contentFontScale, i - 1) * value) + 'px'; + const fontsizeList = []; + const steps = 6; + const rootName = '--jp-content-font-size'; + for (let i = 0; i < steps; i++) { + const rounded_value = ( + Math.pow(this._contentFontScale, i - 1) * value + ).toFixed(3); + fontsizeList[i] = String(rounded_value) + 'px'; } - setCssProperties('--jp-content-font-size', fontsize_list); + return defineFontProperties( + rootName, + fontsizeList, + this._cssProperties, + steps + ); }, 'code-font-size': (value: number) => { - document.body.style.setProperty( - '--jp-code-font-size', - String(value) + 'px' - ); + return (this._cssProperties['--jp-code-font-size'] = + String(value) + 'px'); }, 'border-width': (value: number) => { - document.body.style.setProperty( - '--jp-border-width', - String(value) + 'px' - ); + return (this._cssProperties['--jp-border-width'] = + String(value) + 'px'); }, 'border-radius': (value: number) => { - document.body.style.setProperty( - '--jp-border-radius', - String(value) + 'px' - ); + return (this._cssProperties['--jp-border-radius'] = + String(value) + 'px'); }, 'accent-color': (value: string) => { - defineAndApplyPaletteFrom1hexColor(value, 4, '--jp-accent-color'); + const rootName = '--jp-accent-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'border-color': (value: string) => { - defineAndApplyPaletteFrom1hexColor(value, 4, '--jp-border-color'); + const rootName = '--jp-border-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'brand-color': (value: string) => { - defineAndApplyPaletteFrom1hexColor(value, 4, '--jp-brand-color'); + const rootName = '--jp-brand-color'; + const steps = 5; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'error-color': (value: string) => { - defineAndApplyPaletteFrom1hexColor(value, 4, '--jp-error-color'); + const rootName = '--jp-error-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'info-color': (value: string) => { - defineAndApplyPaletteFrom1hexColor(value, 4, '--jp-info-color'); + const rootName = '--jp-info-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'layout-color': (value: string) => { - const colorValueRGBAstr = hexToRGBA(value); - const colorRGBA64 = rgbaStringToColorRGBA64(colorValueRGBAstr); - const colorRGBA64shifted = shiftLuminanceRGBAColor(colorRGBA64); - const palette = definePalette(colorRGBA64shifted, 4); - applyPalette(palette, '--jp-layout-color'); - + const hexValue = stringtoHex(value); + const colorRGBA64 = parseColorHexRGB( + hexValue.replace(/\s/g, '').toUpperCase() + ); + const colorRGBA64shifted = shiftLuminanceRGBAColor(colorRGBA64!); + const palette = definePaletteFromColorRGBA64(colorRGBA64shifted, 5); const shiftedLuminance = getLuminanceRGBAColor(colorRGBA64shifted); const complementaryL = 1 - shiftedLuminance; const inverseColorHSL = new ColorHSL( - getHueRGBAColor(colorRGBA64), - getSaturationRGBAColor(colorRGBA64), + getHueRGBAColor(colorRGBA64!), + getSaturationRGBAColor(colorRGBA64!), complementaryL ); const inverseColorRGBA64 = hslToRGB(inverseColorHSL, 1); - const inversePalette = definePalette(inverseColorRGBA64, 5); - applyPalette(inversePalette, '--jp-inverse-layout-color'); - applyPalette(inversePalette, '--jp-ui-font-color'); + const inversePalette = definePaletteFromColorRGBA64( + inverseColorRGBA64, + 5 + ); + const layoutColorList = defineColorProperties( + '--jp-layout-color', + palette, + this.cssProperties, + 5 + ); + const inverseLayoutColorList = defineColorProperties( + '--jp-inverse-layout-color', + inversePalette, + this.cssProperties, + 4 + ); + + const uiFontColorList = defineColorProperties( + '--jp-ui-font-color', + inversePalette, + this.cssProperties, + 4 + ); + const uiInverseFontColorList = defineColorProperties( + '--jp-ui-inverse-font-color', + palette, + this.cssProperties, + 4 + ); + + return [ + layoutColorList, + inverseLayoutColorList, + uiFontColorList, + uiInverseFontColorList + ]; }, 'success-color': (value: string) => { - const colorValueRGBAstr = hexToRGBA(value); - const colorRGBA64 = rgbaStringToColorRGBA64(colorValueRGBAstr); - const palette = definePalette(colorRGBA64, 4); - applyPalette(palette, '--jp-success-color'); + const rootName = '--jp-success-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); }, 'warn-color': (value: string) => { - const colorValueRGBAstr = hexToRGBA(value); - const colorRGBA64 = rgbaStringToColorRGBA64(colorValueRGBAstr); - const palette = definePalette(colorRGBA64, 4); - applyPalette(palette, '--jp-warn-color'); + const rootName = '--jp-warn-color'; + const steps = 4; + const palette = definePaletteFromHexColor(value, steps, rootName); + return defineColorProperties( + rootName, + palette, + this._cssProperties, + steps + ); } }; } @@ -437,6 +429,10 @@ export class ThemeEditorModel extends VDomModel { return this._schema; } + public get cssProperties(): any { + return this._cssProperties; + } + public get formData(): any { return this._formData; } @@ -445,9 +441,12 @@ export class ThemeEditorModel extends VDomModel { for (const key in this._formData) { const newValue = data[key]; this._formDataSetter[key](newValue); - /*this._formData[key] = newValue;*/ } + this._formData = data; + for (const key in this._cssProperties) { + document.body.style.setProperty(key, this._cssProperties[key]); + } this.stateChanged.emit(); } } diff --git a/src/schema.json b/src/schema.json new file mode 100644 index 0000000..642f6db --- /dev/null +++ b/src/schema.json @@ -0,0 +1,117 @@ +{ + "type": "object", + "properties": { + "layout-color": { + "title": "Layout color", + "type": "string" + }, + "accent-color": { + "title": "Accent color", + "type": "string" + }, + "border-color": { + "title": "Border color", + "type": "string" + }, + "brand-color": { + "title": "Brand color", + "type": "string", + "default": "" + }, + "error-color": { + "title": "Error color", + "type": "string" + }, + "info-color": { + "title": "Info color", + "type": "string" + }, + "success-color": { + "title": "Success color", + "type": "string" + }, + "warn-color": { + "title": "Warn color", + "type": "string" + }, + "ui-font-size": { + "title": "UI font size", + "type": "integer", + "minimum": 6, + "maximum": 30 + }, + "content-font-size": { + "title": "Content font size", + "type": "integer", + "minimum": 6, + "maximum": 30 + }, + "code-font-size": { + "title": "Code font size", + "type": "integer", + "minimum": 6, + "maximum": 30 + }, + "border-width": { + "title": "Border width", + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "border-radius": { + "title": "Border radius", + "type": "integer", + "minimum": 1, + "maximum": 10 + }, + "ui-font-family": { + "title": "User Interface Font family", + "type": "string", + "enum": [ + "Arial", + "Dancing Script", + "Dosis", + "Helvetica", + "JetBrains Mono", + "Lobster", + "Oxygen", + "Pacifico", + "Prompt", + "Righteous", + "Single Day", + "system-ui", + "Satisfy", + "Times New Roman", + "Ultra", + "Urbanist" + ] + }, + "content-font-family": { + "title": "Content Font family", + "type": "string", + "enum": [ + "Arial", + "Dancing Script", + "Dosis", + "Helvetica", + "JetBrains Mono", + "Lobster", + "Oxygen", + "Pacifico", + "Prompt", + "Righteous", + "Single Day", + "system-ui", + "Satisfy", + "Times New Roman", + "Ultra", + "Urbanist" + ] + }, + "code-font-family": { + "title": "Code Font family", + "type": "string", + "enum": ["monospace", "Space Mono", "DejaVuSans Mono"] + } + } +} diff --git a/src/view.tsx b/src/view.tsx index b8adfa3..aa70f89 100644 --- a/src/view.tsx +++ b/src/view.tsx @@ -3,11 +3,35 @@ import { ThemeEditorModel } from './model'; import React, { useState } from 'react'; import Form from '@rjsf/core'; import { ChromePicker } from '@hello-pangea/color-picker'; +import { requestAPI } from './handler'; + +function sendPostRequest(cssProperties: any) { + requestAPI('send_cssProperties', { + body: JSON.stringify(cssProperties), + method: 'POST' + }) + .then(data => { + const blob = new Blob([data as string], { type: 'text/css' }); + const url = URL.createObjectURL(blob); + const element = document.createElement('a'); + element.href = url; + element.download = 'variables.css'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }) + .catch(reason => { + console.error( + `The jupyter_theme_editor server extension appears to be missing.\n${reason}` + ); + }); +} interface IProps { formData: any; schema: any; uiSchema: any; + onSubmit: () => void; setformData: (value: any) => void; } @@ -17,6 +41,7 @@ function FormComponent(props: IProps) { schema={props.schema} formData={props.formData} uiSchema={props.uiSchema} + onSubmit={props.onSubmit} onChange={event => { props.setformData(event.formData); }} @@ -30,6 +55,7 @@ function ColorPicker(props: any) { <>