diff --git a/CHANGELOG.md b/CHANGELOG.md index 9169c6b2..0ebac236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [#503](https://github.com/equinor/webviz-config/pull/503) - Added `__main__.py`. This will allow users to do `python -m webviz_config [...]` in addition to `webviz [...]`. - [#510](https://github.com/equinor/webviz-config/pull/510) - Added command line option `--debug` which enables selected underlying Dash development features. +- [#496](https://github.com/equinor/webviz-config/pull/496) - Implemented automatic menu creation from YAML file with new `wcc` Menu component (see https://github.com/equinor/webviz-core-components/pull/154). ## [0.3.4] - 2021-08-30 diff --git a/examples/basic_example.yaml b/examples/basic_example.yaml index 58f99409..a77077a8 100644 --- a/examples/basic_example.yaml +++ b/examples/basic_example.yaml @@ -3,9 +3,13 @@ title: Reek Webviz Demonstration -pages: +options: + menu: + initially_pinned: True - - title: Front page +layout: + + - page: Front page content: - BannerImage: image: ./example_banner.png @@ -13,27 +17,27 @@ pages: - Webviz created from configuration file. - Some other text, potentially with strange letters like Åre, Smørbukk Sør. - - title: Markdown example + - page: Markdown example content: - Markdown: markdown_file: ./example-markdown.md - - title: Table example + - page: Table example content: - DataTable: csv_file: ./example_data.csv - - title: PDF example + - page: PDF example content: - EmbedPdf: pdf_file: ./example.pdf - - title: Syntax highlighting example + - page: Syntax highlighting example content: - SyntaxHighlighter: filename: ./basic_example.yaml - - title: Plot a table + - page: Plot a table content: - TablePlotter: csv_file: ./example_data.csv @@ -64,7 +68,7 @@ pages: phone: +47 12345678 email: some@email.com - - title: Plot a table (locked) + - page: Plot a table (locked) content: - TablePlotter: csv_file: ./example_data.csv @@ -79,7 +83,7 @@ pages: phone: 12345678 email: someother@email.com - - title: Pivot Table + - page: Pivot Table content: - PivotTable: csv_file: ./example_data.csv diff --git a/examples/basic_example_advanced_menu.yaml b/examples/basic_example_advanced_menu.yaml new file mode 100644 index 00000000..bc566ca9 --- /dev/null +++ b/examples/basic_example_advanced_menu.yaml @@ -0,0 +1,109 @@ +# This file demonstrates the most basic usage of webviz in a FMU setting +# The configuration files uses YAML (https://en.wikipedia.org/wiki/YAML). + +title: Reek Webviz Demonstration + +options: + menu: + show_logo: True + bar_position: left + drawer_position: left + initially_pinned: True + +layout: + - section: Section + content: + - page: Front page + icon: home + content: + - BannerImage: + image: ./example_banner.png + title: My banner image + - Webviz created from configuration file. + - Some other text, potentially with strange letters like Åre, Smørbukk Sør. + + - group: Other + icon: label + content: + - page: Markdown example + content: + - Markdown: + markdown_file: ./example-markdown.md + + - page: PDF example + content: + - EmbedPdf: + pdf_file: ./example.pdf + + - page: Syntax highlighting example + content: + - SyntaxHighlighter: + filename: ./basic_example.yaml + + - group: Tables + icon: table_chart + content: + - page: Table example + content: + - DataTable: + csv_file: ./example_data.csv + + - page: Plot a table + content: + - TablePlotter: + csv_file: ./example_data.csv + # Everything below are examples of optional settings + filter_cols: + - Well + - Segment + - Average permeability (D) + plot_options: + type: bar + facet_col: Well + color: Segment + barmode: group + filter_defaults: + Well: + - A-1H + - A-2H + - C-1H + column_color_discrete_maps: + # Supports css color codes, rgb and hex code. + # Note that hex code needs quotes '' to not be read as a comment + Segment: + A: '#ff0000' + B: rgb(0,255,0) + C: blue + contact_person: + name: Ola Nordmann + phone: +47 12345678 + email: some@email.com + + - page: Plot a table (locked) + content: + - TablePlotter: + csv_file: ./example_data.csv + lock: true + plot_options: + x: Well + y: Initial reservoir pressure (bar) + size: Average permeability (D) + facet_col: Segment + contact_person: + name: Kari Nordmann + phone: 12345678 + email: someother@email.com + + - page: Pivot Table + content: + - PivotTable: + csv_file: ./example_data.csv + options: + cols: + - Well + rows: + - Segment + vals: + - Average permeability (D) + aggregatorName: Average + rendererName: Table Heatmap diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..af74f849 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +# Sets the window size of the browser (crucial in --headless mode). +from selenium.webdriver.chrome.options import Options + + +def pytest_setup_options(): + options = Options() + options.add_argument("--window-size=1920,1080") + return options diff --git a/tests/test_portable.py b/tests/test_portable.py index 29245381..8255052b 100644 --- a/tests/test_portable.py +++ b/tests/test_portable.py @@ -23,5 +23,5 @@ def test_portable(dash_duo, tmp_path): "plot-a-table", "pivot-table", ]: - dash_duo.wait_for_element(f"#{page}").click() + dash_duo.wait_for_element(f".Menu__Page[href='/{page}']").click() assert dash_duo.get_logs() == [], "browser console should contain no error" diff --git a/tests/test_schema.py b/tests/test_schema.py index 5bf96fad..6d2bdbbe 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,17 +1,22 @@ import pathlib - +import pytest import yaml import jsonschema from webviz_config._docs._create_schema import create_schema -def test_schema(): +@pytest.mark.parametrize( + "config_file_path", + [ + (pathlib.Path("examples") / "basic_example.yaml"), + (pathlib.Path("examples") / "basic_example_advanced_menu.yaml"), + ], +) +def test_schema(config_file_path: pathlib.Path): """Tests both that the generated schema is valid, and that the input configuration is valid according to the schema. """ - config = yaml.safe_load( - (pathlib.Path("examples") / "basic_example.yaml").read_text() - ) + config = yaml.safe_load(config_file_path.read_text()) jsonschema.validate(instance=config, schema=create_schema()) diff --git a/webviz_config/_config_parser.py b/webviz_config/_config_parser.py index d26541b2..56f2eb90 100644 --- a/webviz_config/_config_parser.py +++ b/webviz_config/_config_parser.py @@ -284,7 +284,6 @@ def _generate_page_id(self, title: str) -> str: return page_id def clean_configuration(self) -> None: - # pylint: disable=too-many-branches """Various cleaning and checks of the raw configuration read from the user provided yaml configuration file. """ @@ -299,7 +298,7 @@ def clean_configuration(self) -> None: if "title" not in self.configuration: self.configuration["title"] = "Webviz - Powered by Dash" - if "pages" not in self.configuration: + if "pages" not in self.configuration and "layout" not in self.configuration: raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" "The configuration file does not have " @@ -307,7 +306,9 @@ def clean_configuration(self) -> None: f"{terminal_colors.END}" ) - if not isinstance(self.configuration["pages"], list): + if "pages" in self.configuration and not isinstance( + self.configuration["pages"], list + ): raise ParserError( f"{terminal_colors.RED}{terminal_colors.BOLD}" "The configuration input belonging to the " @@ -315,65 +316,27 @@ def clean_configuration(self) -> None: f"{terminal_colors.END}" ) - for page_number, page in enumerate(self.configuration["pages"]): - - if "title" not in page: - raise ParserError( - f"{terminal_colors.RED}{terminal_colors.BOLD}" - f"Page number {page_number + 1} does " - "not have the title specified." - f"{terminal_colors.END}" - ) - - if "id" not in page: - page["id"] = self._generate_page_id(page["title"]) - elif page["id"] in self._page_ids: - raise ParserError( - f"{terminal_colors.RED}{terminal_colors.BOLD}" - "You have more than one page " - "with the same `id`." - f"{terminal_colors.END}" - ) - - self._page_ids.append(page["id"]) - - if "content" not in page: - page["content"] = [] - elif not isinstance(page["content"], list): - raise ParserError( - f"{terminal_colors.RED}{terminal_colors.BOLD}" - "The content of page number " - f"{page_number + 1} should be a list." - f"{terminal_colors.END}" - ) - - plugins = [e for e in page["content"] if isinstance(e, dict)] - - for plugin in plugins: - plugin_name = next(iter(plugin)) - plugin_variables = next(iter(plugin.values())) - kwargs = {} if plugin_variables is None else {**plugin_variables} + if "pages" in self.configuration: + warnings.warn( + "The configuration keyword `pages` is going to be deprecated. " + "Consider using the new `layout` keyword instead.", + PendingDeprecationWarning, + ) - if plugin_name not in ConfigParser.STANDARD_PLUGINS: - raise ParserError( - f"{terminal_colors.RED}{terminal_colors.BOLD}" - "You have included a plugin with " - f"name `{plugin_name}` in your " - "configuration file. This is not a " - "standard plugin." - f"{terminal_colors.END}" - ) + if "layout" in self.configuration and not isinstance( + self.configuration["layout"], list + ): + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "The configuration input belonging to the " + "`layout` keyword should be a list." + f"{terminal_colors.END}" + ) - plugin["_call_signature"] = _call_signature( - plugin_name, - kwargs, - self._config_folder, - ) + if "pageContents" not in self.configuration: + self.configuration["pageContents"] = [] - self._assets.update(getattr(webviz_config.plugins, plugin_name).ASSETS) - self._plugin_metadata[ - plugin_name - ] = webviz_config.plugins.PLUGIN_METADATA[plugin_name] + self._parse_navigation() @property def configuration(self) -> dict: @@ -393,3 +356,189 @@ def plugin_metadata(self) -> Dict[str, dict]: plugins included in the configuration file. """ return self._plugin_metadata + + def _recursively_parse_navigation_item(self, items: dict, level: int) -> list: + navigation_items: list = [] + for item_number, item in enumerate(items): + if "section" in item: + if level > 0: + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "Sections can only be defined on the first level" + f"{terminal_colors.END}" + ) + navigation_items.append( + { + "type": "section", + "title": item["section"], + "icon": item["icon"] if "icon" in item.keys() else None, + "content": self._recursively_parse_navigation_item( + item["content"], level + 1 + ) + if "content" in item.keys() + else [], + } + ) + elif "group" in item: + navigation_items.append( + { + "type": "group", + "title": item["group"], + "icon": item["icon"] if "icon" in item.keys() else None, + "content": self._recursively_parse_navigation_item( + item["content"], level + 1 + ) + if "content" in item.keys() + else [], + } + ) + elif "page" in item or "title" in item: + + page_title = item["page"] if "page" in item else item["title"] + if "id" not in item: + item["id"] = self._generate_page_id(page_title) + elif item["id"] in self._page_ids: + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "You have more than one page " + "with the same `id`." + f"{terminal_colors.END}" + ) + + self._page_ids.append(item["id"]) + + if "content" not in item: + item["content"] = [] + elif not isinstance(item["content"], list): + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "The content of page number " + f"{item_number + 1} should be a list." + f"{terminal_colors.END}" + ) + + self.configuration["pageContents"].append(item) + + navigation_items.append( + { + "type": "page", + "title": page_title, + "href": "/" + item["id"], + "icon": item["icon"] if "icon" in item.keys() else None, + } + ) + + plugins = [e for e in item["content"] if isinstance(e, dict)] + for plugin in plugins: + plugin_name = next(iter(plugin)) + plugin_variables = next(iter(plugin.values())) + kwargs = {} if plugin_variables is None else {**plugin_variables} + + if plugin_name not in ConfigParser.STANDARD_PLUGINS: + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "You have included a plugin with " + f"name `{plugin_name}` in your " + "configuration file. This is not a " + "standard plugin." + f"{terminal_colors.END}" + ) + + plugin["_call_signature"] = _call_signature( + plugin_name, + kwargs, + self._config_folder, + ) + + self._assets.update( + getattr(webviz_config.plugins, plugin_name).ASSETS + ) + self._plugin_metadata[ + plugin_name + ] = webviz_config.plugins.PLUGIN_METADATA[plugin_name] + return navigation_items + + def _parse_navigation(self) -> None: + # pylint: disable=too-many-branches + """Returns a list of navigation items""" + + if "layout" in self.configuration: + self.configuration[ + "navigation_items" + ] = self._recursively_parse_navigation_item(self.configuration["layout"], 0) + elif "pages" in self.configuration: + self.configuration[ + "navigation_items" + ] = self._recursively_parse_navigation_item(self.configuration["pages"], 0) + + options_found = False + if "options" in self.configuration: + if "menu" in self.configuration["options"]: + options_found = True + + if "bar_position" not in self.configuration["options"]["menu"]: + self.configuration["options"]["menu"]["bar_position"] = "left" + elif self.configuration["options"]["menu"]["bar_position"] not in [ + "left", + "top", + "right", + "bottom", + ]: + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "Invalid option for options > menu > bar_position: " + f"{self.configuration['options']['menu']['bar_position']}. " + "Please select one of the following options: left, top, right, bottom." + f"{terminal_colors.END}" + ) + + if "drawer_position" not in self.configuration["options"]["menu"]: + self.configuration["options"]["menu"]["drawer_position"] = "left" + elif self.configuration["options"]["menu"]["drawer_position"] not in [ + "left", + "right", + ]: + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "Invalid option for options > menu > drawer_position: " + f"{self.configuration['options']['menu']['drawer_position']}. " + "Please select one of the following options: left, right." + f"{terminal_colors.END}" + ) + + if "initially_pinned" not in self.configuration["options"]["menu"]: + self.configuration["options"]["menu"]["initially_pinned"] = False + elif not isinstance( + self.configuration["options"]["menu"]["initially_pinned"], bool + ): + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "Invalid option for options > menu > initially_pinned: " + f"{self.configuration['options']['menu']['initially_pinned']}. " + "Please select a boolean value: True, False" + f"{terminal_colors.END}" + ) + + if "show_logo" not in self.configuration["options"]["menu"]: + self.configuration["options"]["menu"]["show_logo"] = True + elif not isinstance( + self.configuration["options"]["menu"]["show_logo"], bool + ): + raise ParserError( + f"{terminal_colors.RED}{terminal_colors.BOLD}" + "Invalid option for options > menu > initially_pinned: " + f"{self.configuration['options']['menu']['show_logo']}. " + "Please select a boolean value: True, False" + f"{terminal_colors.END}" + ) + + if not options_found: + if "options" not in self.configuration: + self.configuration["options"] = {} + + self.configuration["options"]["menu"] = { + "bar_position": "left", + "drawer_position": "left", + "initially_pinned": False, + "show_logo": True, + } diff --git a/webviz_config/_docs/_create_schema.py b/webviz_config/_docs/_create_schema.py index 395dbed8..46c92a12 100644 --- a/webviz_config/_docs/_create_schema.py +++ b/webviz_config/_docs/_create_schema.py @@ -8,35 +8,126 @@ JSON_SCHEMA = { "$id": "https://github.com/equinor/webviz-config", "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Person", + "title": "Webviz", "type": "object", "properties": { "title": {"description": "Title of your Webviz application.", "type": "string"}, "shared_settings": {"type": "object"}, - "pages": { - "description": "Define the pages in your Webviz application.", - "type": "array", - "minLength": 1, - "items": { + "options": { + "type": "object", + "menu": { + "description": "Define the menu options.", "type": "object", "properties": { - "title": {"description": "Title of the page", "type": "string"}, - "content": { - "description": "Content on the page", - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - ] - }, + "show_logo": { + "description": "State if a logo shall be shown in the menu.", + "type": "boolean", + }, + "bar_position": { + "description": "Define where the menu bar shall be positioned:" + " left, top, right, bottom.", + "type": "string", + }, + "drawer_position": { + "description": "Define where the menu drawer shall be positioned:" + " left or right.", + "type": "string", + }, + "initially_pinned": { + "description": "State if the menu shall be pinned when initially showing.", + "type": "boolean", }, }, - "required": ["title", "content"], "additionalProperties": False, }, }, + "layout": { + "description": "Define the pages (and potential sections and groups)" + " in your Webviz application.", + "type": "array", + "minLength": 1, + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "section": { + "description": "Title of the section.", + "type": "string", + }, + "icon": { + "description": "Optionally set an icon for the section.", + "type": "string", + }, + "content": { + "description": "Define the pages (and potential subgroups)" + " of this section.", + "type": "array", + "minLength": 1, + "items": { + "type": "object", + "oneOf": [ + {"$ref": "#/properties/layout/items/oneOf/1"}, + {"$ref": "#/properties/layout/items/oneOf/2"}, + ], + }, + }, + }, + "required": ["section", "content"], + "additionalProperties": False, + }, + { + "type": "object", + "properties": { + "group": { + "description": "Title of the group.", + "type": "string", + }, + "icon": { + "description": "Optionally define an icon for the group.", + "type": "string", + }, + "content": { + "description": "Content of the group.", + "type": "array", + "minLength": 1, + "items": { + "oneOf": [ + {"$ref": "#/properties/layout/items/oneOf/1"}, + {"$ref": "#/properties/layout/items/oneOf/2"}, + ] + }, + }, + }, + "required": ["group", "content"], + "additionalProperties": False, + }, + { + "type": "object", + "properties": { + "page": { + "description": "Title of the page.", + "type": "string", + }, + "icon": { + "description": "Optionally define an icon for the page.", + "type": "string", + }, + "content": { + "description": "Content of the page.", + "type": "array", + "minLength": 1, + "items": {"oneOf": [{"type": "string"}]}, + }, + }, + "required": ["page", "content"], + "additionalProperties": False, + }, + ], + }, + }, }, - "required": ["title", "pages"], + "required": ["title", "layout"], "additionalProperties": False, } @@ -73,8 +164,8 @@ def json_type(typehint: Any) -> dict: json_schema = copy.deepcopy(JSON_SCHEMA) # fmt: off - content_schemas = json_schema["properties"]["pages"][ # type: ignore - "items"]["properties"]["content"]["items"]["oneOf"] + content_schemas = json_schema["properties"]["layout"][ # type: ignore + "items"]["oneOf"][2]["properties"]["content"]["items"]["oneOf"] # fmt: on for package_doc in get_plugin_documentation().values(): diff --git a/webviz_config/templates/copy_data_template.py.jinja2 b/webviz_config/templates/copy_data_template.py.jinja2 index 3a9a0cea..ec16fa07 100644 --- a/webviz_config/templates/copy_data_template.py.jinja2 +++ b/webviz_config/templates/copy_data_template.py.jinja2 @@ -69,7 +69,7 @@ WEBVIZ_FACTORY_REGISTRY.initialize(app_instance_info, factory_settings_dict) plugins = [] -{% for page in pages %} +{% for page in pageContents %} {% for content in page.content %} {% if content is not string %} plugins.append(webviz_config.plugins.{{ content._call_signature[0] }}) diff --git a/webviz_config/templates/webviz_template.py.jinja2 b/webviz_config/templates/webviz_template.py.jinja2 index ad40084f..cf890991 100644 --- a/webviz_config/templates/webviz_template.py.jinja2 +++ b/webviz_config/templates/webviz_template.py.jinja2 @@ -11,7 +11,8 @@ import threading import datetime from pathlib import Path, PosixPath, WindowsPath -from dash import html, dcc, Dash, Input, Output +from dash import html, dcc, Dash, Input, Output, callback_context +import webviz_core_components as wcc from flask_talisman import Talisman import webviz_config import webviz_config.plugins @@ -111,7 +112,7 @@ if {{ not portable }} and not webviz_config.is_reload_process(): app.layout = html.Div() else: page_content = {} - {% for page in pages %} + {% for page in pageContents %} page_content["{{page.id}}"] = [] {% for content in page.content -%} {% if content is string %} @@ -126,45 +127,40 @@ else: {% endfor %} app.layout = html.Div( className="layoutWrapper", - children=[html.Div( - children=[dcc.Location( - id='url', refresh=True), - html.Div( - className="sideWrapper", - children=[ - {% for page in pages %} - dcc.Link( - {%- if loop.first -%} - "", - id="logo", - className="styledLogo", - href="/", - {%- else -%} - "{{page.title}}", - # We will create a webviz-core-components - # component instead of styling dcc.Link's, - # then we can more easily change className to - # selectedButton for current page. - className="styledButton", - id="{{page.id}}", - href="/{{page.id}}", - {%- endif -%} + children=[ + html.Div( + children=[ + dcc.Location( + id='location', refresh=True), + wcc.Menu( + id="main-menu", + showLogo={{options.menu.show_logo}}, + menuBarPosition="{{options.menu.bar_position}}", + menuDrawerPosition="{{options.menu.drawer_position}}", + initiallyPinned={{options.menu.initially_pinned}}, + navigationItems={{navigation_items}}, ) - {{- "" if loop.last else "," -}} - {% endfor %} - ])]), - html.Div(className="pageContent", id="page-content")]) + ] + ), + html.Div(className="pageContent", id="page-content") + ] + ) WEBVIZ_FACTORY_REGISTRY.cleanup_resources_after_plugin_init() oauth2 = webviz_config.Oauth2(app.server) if use_oauth2 else None @app.callback(Output("page-content", "children"), - Input("url", "pathname")) -def update_page(pathname): - pathname = pathname.replace("/", "") - if not pathname: - pathname = list(page_content.keys())[0] + [Input("main-menu", "url"), Input("location", "pathname")]) +def update_page(url, pathname): + ctx = callback_context + if ctx.triggered: + if ctx.triggered[0]["prop_id"].split('.')[0] == "main-menu": + pathname = url.replace("/", "") + else: + pathname = pathname.replace("/", "") + if not pathname: + pathname = next(iter(page_content)) return page_content.get(pathname, "Oooppss... Page not found.") {{ "WEBVIZ_ASSETS.directly_host_assets(app)" if not portable else ""}} diff --git a/webviz_config/themes/_default_theme.py b/webviz_config/themes/_default_theme.py index 404f6483..88b566e4 100644 --- a/webviz_config/themes/_default_theme.py +++ b/webviz_config/themes/_default_theme.py @@ -10,5 +10,13 @@ default_theme.assets = glob.glob( str(pathlib.Path(__file__).resolve().parent / "default_assets" / "*") ) +default_theme.assets.append( + str( + pathlib.Path(__file__).resolve().parent.parent + / "_docs" + / "static" + / "webviz-logo.svg" + ) +) default_theme.plotly_theme = templates["plotly"].to_plotly_json() diff --git a/webviz_config/themes/default_assets/default_logo.svg b/webviz_config/themes/default_assets/default_logo.svg deleted file mode 100644 index 29eea187..00000000 --- a/webviz_config/themes/default_assets/default_logo.svg +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Viz - - - Web - - diff --git a/webviz_config/themes/default_assets/default_theme.css b/webviz_config/themes/default_assets/default_theme.css index 2f6089fa..6b19291b 100644 --- a/webviz_config/themes/default_assets/default_theme.css +++ b/webviz_config/themes/default_assets/default_theme.css @@ -17,18 +17,6 @@ - Clearing - Media Queries */ -h1, h2, h3, h4, h5, h6, p, a, div, button { - font-family: helvetica; -} - -#logo { - height: 100px; - width: 200px; - margin: 20px 0px; - background-size: contain; - background-image: url("default_logo.svg"); - background-repeat: no-repeat; -} /* Style Variables @@ -50,6 +38,30 @@ h1, h2, h3, h4, h5, h6, p, a, div, button { } /* csslint ignore:end */ +body { + line-height: 1.6; + font-weight: 400; + font-size: 16px; + font-family: var(--menuLinkFont); + color: #323232; +} + +#LogoLarge { + height: 100px; + width: 200px; + background-size: contain; + background-image: url("webviz-logo.svg"); + background-repeat: no-repeat; +} + +#LogoSmall { + height: 64px; + width: 64px; + background-size: contain; + background-image: url("webviz-logo.svg"); + background-repeat: no-repeat; +} + /* Plotly.js –––––––––––––––––––––––––––––––––––––––––––––––––– */ /* plotly.js's modebar's z-index is 1001 by default @@ -467,4 +479,4 @@ there. /* Larger than phablet (also point when grid becomes active) */ /* Larger than tablet */ /* Larger than desktop */ -/* Larger than Desktop HD */ \ No newline at end of file +/* Larger than Desktop HD */