Source code for crystal_toolkit.helpers.layouts

from __future__ import annotations

import warnings
from typing import Any
from uuid import uuid4

import dash_mp_components as mpc
from dash import dcc, html
from monty.serialization import loadfn

from crystal_toolkit import MODULE_PATH
from crystal_toolkit.settings import SETTINGS

BULMA_CSS = {
    "external_url": "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css"
}

FONT_AWESOME_CSS = {
    "external_url": "https://use.fontawesome.com/releases/v5.6.3/css/all.css"
}

PRIMARY_COLOR = "hsl(171, 100%, 41%)"

# TODO: change "kind" kwarg to list / group is- modifiers together?

"""
Helper methods to make working with Bulma classes easier.
"""


def _update_css_class(kwargs, class_name):
    """
    Convenience function to update className while respecting
    any additional classNames already set.
    """
    if "className" in kwargs:
        kwargs["className"] += f" {class_name}"
    else:
        kwargs["className"] = class_name


[docs]class Section(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "section") super().__init__(*args, **kwargs)
[docs]class Container(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "container") super().__init__(*args, **kwargs)
[docs]class Block(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "block") super().__init__(*args, **kwargs)
[docs]class Columns(html.Div): def __init__( self, *args, desktop_only=False, centered=False, gapless=False, multiline=False, **kwargs, ): _update_css_class(kwargs, "columns") if desktop_only: kwargs["className"] += " is-desktop" if centered: kwargs["className"] += " is-centered" if gapless: kwargs["className"] += " is-gapless" if multiline: kwargs["className"] += " is-multiline" super().__init__(*args, **kwargs)
[docs]class Column(html.Div): def __init__(self, *args, size=None, offset=None, narrow=False, **kwargs): _update_css_class(kwargs, "column") if size: kwargs["className"] += f" is-{size}" if offset: kwargs["className"] += f" -is-offset-{size}" if narrow: kwargs["className"] += " is-narrow" super().__init__(*args, **kwargs)
[docs]class Button(html.Button): def __init__(self, *args, kind=None, size="normal", **kwargs): _update_css_class(kwargs, f"button is-{size}") if kind: kwargs["className"] += f" is-{kind}" super().__init__(*args, **kwargs)
[docs]class Error(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "notification is-danger") super().__init__(*args, **kwargs)
[docs]class MessageContainer(html.Article): def __init__(self, *args, kind="warning", size="normal", **kwargs): if kind: _update_css_class(kwargs, f"message is-{kind} is-{size}") else: _update_css_class(kwargs, f"message is-{size}") super().__init__(*args, **kwargs)
[docs]class MessageHeader(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "message-header") super().__init__(*args, **kwargs)
[docs]class MessageBody(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "message-body") super().__init__(*args, **kwargs)
[docs]class Icon(html.Span): def __init__(self, kind="download", fill="s", *args, **kwargs): """ Font-awesome icon. Good options for kind are "info-circle", "question-circle", "book", "code". """ _update_css_class(kwargs, "icon") if "fontastic" not in kind: # fontawesome styles (pre-distributed icons, e.g. download) super().__init__(html.I(className=f"fa{fill} fa-{kind}"), *args, **kwargs) else: # fontastic styles (custom icons, e.g. the MP app icons) super().__init__(html.I(className=kind), *args, **kwargs)
[docs]class Spinner(html.Button): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "button is-primary is-loading") kwargs["style"] = {"width": "35px", "height": "35px", "borderRadius": "35px"} kwargs["aria-label"] = "Loading" super().__init__(*args, **kwargs)
[docs]class Box(html.Div): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "box") super().__init__(*args, **kwargs)
[docs]class H1(html.H1): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-1") else: _update_css_class(kwargs, "title is-1") super().__init__(*args, **kwargs)
[docs]class H2(html.H2): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-2") else: _update_css_class(kwargs, "title is-2") super().__init__(*args, **kwargs)
[docs]class H3(html.H3): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-3") else: _update_css_class(kwargs, "title is-3") super().__init__(*args, **kwargs)
[docs]class H4(html.H4): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-4") else: _update_css_class(kwargs, "title is-4") super().__init__(*args, **kwargs)
[docs]class H5(html.H5): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-5") else: _update_css_class(kwargs, "title is-5") super().__init__(*args, **kwargs)
[docs]class H6(html.H6): def __init__(self, *args, subtitle=False, **kwargs): if subtitle: _update_css_class(kwargs, "subtitle is-6") else: _update_css_class(kwargs, "title is-6") super().__init__(*args, **kwargs)
[docs]class Tag(html.Div): def __init__( self, tag, tag_type="primary", tag_addon=None, tag_addon_type="primary", size="normal", *args, **kwargs, ): _update_css_class(kwargs, "tags") tags = [html.Span(tag, className=f"tag is-{tag_type} is-{size}")] if tag_addon: tags.append( html.Span(tag_addon, className=f"tag is-{tag_addon_type} is-{size}") ) kwargs["className"] += " has-addons" super().__init__(tags, *args, **kwargs)
[docs]class TagContainer(html.Div): def __init__(self, tags: list[Tag], *args, **kwargs): _update_css_class(kwargs, "field is-grouped is-grouped-multiline") tags = [html.Div(tag, className="control") for tag in tags] super().__init__(tags, *args, **kwargs)
[docs]class Textarea(html.Textarea): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "textarea") super().__init__(*args, **kwargs)
[docs]class Reveal(html.Details): def __init__(self, children=None, id=None, summary_id=None, title=None, **kwargs): if children is None: children = ["Loading..."] if id is None and isinstance(title, str): id = title if isinstance(title, str): title = H4( title, style={"display": "inline-block", "verticalAlign": "middle"} ) contents_id = f"{id}_contents" if id else None summary_id = summary_id or f"{id}_summary" kwargs["style"] = {"marginBottom": "1rem"} super().__init__( [ html.Summary(title, id=summary_id), html.Div( children, id=contents_id, style={"marginTop": "0.5rem", "marginLeft": "1.1rem"}, ), ], id=id, **kwargs, )
[docs]class Label(html.Label): def __init__(self, *args, **kwargs): _update_css_class(kwargs, "label") super().__init__(*args, **kwargs)
[docs]class Field(html.Div): def __init__( self, *args, addons=False, grouped=False, grouped_multiline=False, **kwargs ): _update_css_class(kwargs, "field") if addons: kwargs["className"] += " has-addons" if grouped: kwargs["className"] += " is-grouped" if grouped_multiline: kwargs["className"] += " is-grouped-multiline" super().__init__(*args, **kwargs)
[docs]class Control(html.Div): """ Control tag to wrap form elements, see https://bulma.io/documentation/form/general/ """ def __init__(self, *args, **kwargs): _update_css_class(kwargs, "control") super().__init__(*args, **kwargs)
[docs]def get_tooltip( tooltip_label: Any, tooltip_text: str, underline: bool = True, tooltip_id: str = "", wrapper_class: str = None, **kwargs, ): """ Uses the tooltip component from dash-mp-components to add a tooltip, typically for help text. This component uses react-tooltip under the hood. :param tooltip_label: text or component to display and apply hover behavior to :param tooltip_text: text to show on hover :param tooltip_id: unique id of the tooltip (will generate one if not supplied) :param wrapper_class: class to add to the span that wraps all the returned tooltip components (label + content) :param kwargs: additional props added to Tooltip component. See the components js file in dash-mp-components for a full list of props. :return: html.Span """ if not tooltip_id: tooltip_id = uuid4().hex tooltip_class = "tooltip-label" if underline else None return html.Span( [ html.Span( tooltip_label, className=tooltip_class, **{"data-tip": True, "data-for": tooltip_id}, ), mpc.Tooltip(tooltip_text, id=tooltip_id, **kwargs), ], className=wrapper_class, )
[docs]def get_data_list(data: dict[str, str]): """ Show a formatted table of data items. :param data: dictionary of label, value pairs :return: html.Div """ contents = [] for title, value in data.items(): if isinstance(title, str): title = Label(title) contents.append( html.Tr( [ html.Td(title, style={"vertical-align": "middle"}), html.Td(value, style={"vertical-align": "middle"}), ] ) ) return html.Table([html.Tbody(contents)], className="table")
[docs]def get_table(rows: list[list[Any]], header: list[str] | None = None) -> html.Table: """ Create a HTML table from a list of elements. :param rows: list of list of cell contents :return: html.Table """ contents = [] for row in rows: contents.append(html.Tr([html.Td(item) for item in row])) if not header: return html.Table([html.Tbody(contents)], className="table") else: header = html.Thead([html.Tr([html.Th(item) for item in header])]) return html.Table([header, html.Tbody(contents)], className="table")
DOI_CACHE = loadfn(MODULE_PATH / "apps/assets/doi_cache.json")
[docs]def cite_me( doi: str = None, manual_ref: str = None, cite_text: str = "Cite me" ) -> html.Div: """ Create a button to show users how to cite a particular resource. :param doi: DOI :param manual_ref: If DOI not available :param cite_text: Text to show as button label :return: A button """ if doi: component = mpc.PublicationButton(id=doi, doi=doi, showTooltip=True) elif manual_ref: warnings.warn("Please use the DOI if available.") component = mpc.PublicationButton( children=cite_text, id=manual_ref, url=manual_ref ) return component
[docs]def add_label_help(input, label, help): """ Combine an input, label, and tooltip text into a single consistent component. """ return mpc.FilterField(input, id=uuid4().hex, label=label, tooltip=help)
[docs]class Loading(dcc.Loading): def __init__(self, *args, **kwargs): super().__init__( *args, color=PRIMARY_COLOR, type="dot", debug=SETTINGS.DEBUG_MODE, **kwargs )
[docs]def get_breadcrumb(parts): if not parts: return html.Div() breadcrumbs = html.Nav( html.Ul( [ html.Li( dcc.Link(name, href=link), className=(None if idx != len(parts) - 1 else "is-active"), ) for idx, (name, link) in enumerate(parts.items()) ] ), className="breadcrumb", ) return breadcrumbs