diff --git a/examples/assets/dark_theme.png b/examples/assets/dark_theme.png
new file mode 100644
index 0000000000..a3c7ebae9b
Binary files /dev/null and b/examples/assets/dark_theme.png differ
diff --git a/examples/assets/template.png b/examples/assets/template.png
new file mode 100644
index 0000000000..f6b4545072
Binary files /dev/null and b/examples/assets/template.png differ
diff --git a/examples/user_guide/Templates.ipynb b/examples/user_guide/Templates.ipynb
index 26b1e1a57e..d86cd54486 100644
--- a/examples/user_guide/Templates.ipynb
+++ b/examples/user_guide/Templates.ipynb
@@ -64,11 +64,51 @@
"As we can see the template defines a number of custom blocks, which can be overridden by extending this default template."
]
},
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import panel as pn\n",
+ "import numpy as np\n",
+ "import holoviews as hv\n",
+ "\n",
+ "pn.extension()"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Using custom templates"
+ "## Using default templates\n",
+ "\n",
+ "For a large variety of use cases we do not need complete control over the exact layout of each individual component on the page we just want to achieve a more polished look and feel. For these cases Panel ships with a number of default templates, which are defined by declaring three main content areas on the page, which can be populated as desired:\n",
+ "\n",
+ "* **`header`**: The header area of the HTML page\n",
+ "* **`sidebar`**: A collapsible sidebar\n",
+ "* **`main`**: The main area of the application\n",
+ "\n",
+ "These three areas behave very similarly to other Panel layout components and have list-like semantics. This means we can easily append new components into these areas. Unlike other layout components however, the contents of the areas is fixed once rendered. If you need a dynamic layout you should therefore insert a regular Panel layout component (e.g. a `Column` or `Row`) and modify it in place once added to one of the content areas. \n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Supported themes\n",
+ "\n",
+ "Panel ships with a number of these default themes built on different CSS frameworks:\n",
+ " \n",
+ "* `MaterialTemplate`: Built on [Material Components for the web](https://material.io/develop/web/)\n",
+ "* `BootstrapTemplate`: Built on [Bootstrap v4](https://getbootstrap.com/docs/4.0/getting-started/introduction/)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us construct a very simple app containing two plots in the `main` area and two widgets in the sidebar based on the `BootstrapTemplate` class:"
]
},
{
@@ -77,10 +117,145 @@
"metadata": {},
"outputs": [],
"source": [
- "import panel as pn\n",
- "import holoviews as hv\n",
+ "bootstrap = pn.template.BootstrapTemplate(title='Bootstrap Template')\n",
"\n",
- "pn.extension()"
+ "pn.config.sizing_mode = 'stretch_width'\n",
+ "\n",
+ "xs = np.linspace(0, np.pi)\n",
+ "freq = pn.widgets.FloatSlider(name=\"Frequency\", start=0, end=10, value=2)\n",
+ "phase = pn.widgets.FloatSlider(name=\"Phase\", start=0, end=np.pi)\n",
+ "\n",
+ "@pn.depends(freq=freq, phase=phase)\n",
+ "def sine(freq, phase):\n",
+ " return hv.Curve((xs, np.sin(xs*freq+phase))).opts(\n",
+ " responsive=True, min_height=400)\n",
+ "\n",
+ "@pn.depends(freq=freq, phase=phase)\n",
+ "def cosine(freq, phase):\n",
+ " return hv.Curve((xs, np.cos(xs*freq+phase))).opts(\n",
+ " responsive=True, min_height=400)\n",
+ "\n",
+ "bootstrap.sidebar.append(freq)\n",
+ "bootstrap.sidebar.append(phase)\n",
+ "\n",
+ "bootstrap.main.append(\n",
+ " pn.Row(\n",
+ " pn.Card(hv.DynamicMap(sine), title='Sine'),\n",
+ " pn.Card(hv.DynamicMap(cosine), title='Cosine')\n",
+ " )\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "\n",
+ "\n",
+ "
Bootstrap Template: A simple example demonstrating the Bootstrap template
\n",
+ "\n",
+ "\n",
+ "A `Template` can be served or displayed just like any other Panel component, i.e. using `.servable()` or `.show()`."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Theming\n",
+ "\n",
+ "Default template classes provide a unified approach to theming, which currently allow specifying custom CSS and the Bokeh `Theme` to apply to the `Template`. The way it is implemented a user declares a generic `Theme` class and the `Template` loads the specific implementation for a particular `Template`. To make this more concrete, by default a Template uses the `DefaultTheme`, but then uses the `find_theme` method to look up the implementation of that theme for the Template being used:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from panel.template import DefaultTheme\n",
+ "\n",
+ "DefaultTheme.find_theme(pn.template.MaterialTemplate)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To implement your own theme you should therefore declare a generic class for use by the enduser and a specific implementation for all the themes that should be supported, e.g. here is an example of what the definition of a dark theme might look like:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import param\n",
+ "\n",
+ "from panel.template.theme import Theme\n",
+ "from bokeh.themes import DARK_MINIMAL\n",
+ "\n",
+ "class DarkTheme(Theme):\n",
+ " \"\"\"\n",
+ " The DarkTheme provides a dark color palette\n",
+ " \"\"\"\n",
+ "\n",
+ " bokeh_theme = param.ClassSelector(class_=(Theme, str), default=DARK_MINIMAL)\n",
+ "\n",
+ "class MaterialDarkTheme(DarkTheme):\n",
+ "\n",
+ " # css = param.Filename() Here we could declare some custom CSS to apply\n",
+ " \n",
+ " # This tells Panel to use this implementation\n",
+ " _template = pn.template.MaterialTemplate"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To apply the theme we now merely have to provide the generic `DarkTheme` class to the Template (we will import the `DarkTheme` that ships with panel here:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from panel.template import DarkTheme\n",
+ "\n",
+ "dark_material = pn.template.MaterialTemplate(title='Material Template', theme=DarkTheme)\n",
+ "\n",
+ "dark_material.sidebar.append(freq)\n",
+ "dark_material.sidebar.append(phase)\n",
+ "\n",
+ "dark_material.main.append(\n",
+ " pn.Row(\n",
+ " pn.Card(hv.DynamicMap(sine), title='Sine'),\n",
+ " pn.Card(hv.DynamicMap(cosine), title='Cosine')\n",
+ " )\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "\n",
+ "\n",
+ "
Dark Theme: The MaterialTemplate with a DarkTheme applied
\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Using custom templates"
]
},
{
@@ -195,7 +370,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Loading template from file\n",
+ "### Loading template from file\n",
"\n",
"If the template is larger it is often cleaner to define it in a separate file. You can either read it in as a string, or use the official loading mechanism available for Jinja2 templates by defining a `Environment` along with a `loader`."
]
@@ -219,9 +394,29 @@
}
],
"metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
"language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
"name": "python",
- "pygments_lexer": "ipython3"
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.5"
+ },
+ "widgets": {
+ "application/vnd.jupyter.widget-state+json": {
+ "state": {},
+ "version_major": 2,
+ "version_minor": 0
+ }
}
},
"nbformat": 4,
diff --git a/panel/io/server.py b/panel/io/server.py
index da579f8d82..3dd52bd6b6 100644
--- a/panel/io/server.py
+++ b/panel/io/server.py
@@ -41,12 +41,12 @@ def _server_url(url, port):
return 'http://%s:%d%s' % (url.split(':')[0], port, "/")
def _eval_panel(panel, server_id, title, location, doc):
- from ..template import Template
+ from ..template import BaseTemplate
from ..pane import panel as as_panel
if isinstance(panel, FunctionType):
panel = panel()
- if isinstance(panel, Template):
+ if isinstance(panel, BaseTemplate):
return panel._modify_doc(server_id, title, doc, location)
return as_panel(panel)._modify_doc(server_id, title, doc, location)
diff --git a/panel/layout/card.py b/panel/layout/card.py
index 927480160a..38e34c0916 100644
--- a/panel/layout/card.py
+++ b/panel/layout/card.py
@@ -36,7 +36,10 @@ class Card(Column):
A valid CSS color to apply to the header text.""")
header_css_classes = param.List(['card-header'], doc="""
- CSS claasses to apply to the heaader element.""")
+ CSS classes to apply to the header element.""")
+
+ title_css_classes = param.List(['card-title'], doc="""
+ CSS classes to apply to the header title.""")
margin = param.Parameter(default=5)
@@ -46,12 +49,12 @@ class Card(Column):
_bokeh_model = BkCard
- _rename = dict(Column._rename, title=None, header=None)
+ _rename = dict(Column._rename, title=None, header=None, title_css_classes=None)
def __init__(self, *objects, **params):
self._header_layout = Row(css_classes=['card-header-row'])
super(Card, self).__init__(*objects, **params)
- self.param.watch(self._update_header, ['title', 'header'])
+ self.param.watch(self._update_header, ['title', 'header', 'title_css_classes'])
self._update_header()
def _cleanup(self, root):
@@ -70,7 +73,7 @@ def _process_param_change(self, params):
def _update_header(self, *events):
from ..pane import HTML, panel
if self.header is None:
- item = HTML('%s' % (self.title or ""), css_classes=['card-title'])
+ item = HTML('%s' % (self.title or ""), css_classes=self.title_css_classes)
else:
item = panel(self.header)
self._header_layout[:] = [item]
diff --git a/panel/template/__init__.py b/panel/template/__init__.py
new file mode 100644
index 0000000000..65d1e73ce7
--- /dev/null
+++ b/panel/template/__init__.py
@@ -0,0 +1,4 @@
+from .base import Template, BaseTemplate # noqa
+from .bootstrap import BootstrapTemplate # noqa
+from .material import MaterialTemplate # noqa
+from .theme import DarkTheme, DefaultTheme # noqa
diff --git a/panel/template.py b/panel/template/base.py
similarity index 65%
rename from panel/template.py
rename to panel/template/base.py
index 008d8faf28..f95972ccff 100644
--- a/panel/template.py
+++ b/panel/template/base.py
@@ -17,56 +17,32 @@
from six import string_types
from pyviz_comms import JupyterCommManager as _JupyterCommManager
-from .config import config, panel_extension
-from .io.model import add_to_doc
-from .io.notebook import render_template
-from .io.save import save
-from .io.state import state
-from .layout import Column
-from .models.comm_manager import CommManager
-from .pane import panel as _panel, HTML, Str, HoloViews
-from .viewable import ServableMixin, Viewable
-from .widgets import Button
+from ..config import config, panel_extension
+from ..io.model import add_to_doc
+from ..io.notebook import render_template
+from ..io.save import save
+from ..io.state import state
+from ..layout import Column, ListLike
+from ..models.comm_manager import CommManager
+from ..pane import panel as _panel, HTML, Str, HoloViews
+from ..viewable import ServableMixin, Viewable
+from ..widgets import Button
+from .theme import DefaultTheme, Theme
_server_info = (
'Running server:'
'https://localhost:{port}')
-class Template(param.Parameterized, ServableMixin):
- """
- A Template is a high-level component to render multiple Panel
- objects into a single HTML document defined through a Jinja2
- template. The Template object is given a Jinja2 template and then
- allows populating this template by adding Panel objects, which are
- given unique names. These unique names may then be referenced in
- the template to insert the rendered Panel object at a specific
- location. For instance, given a Jinja2 template that defines roots
- A and B like this:
+class BaseTemplate(param.Parameterized, ServableMixin):
-
{{ embed(roots.A) }}
-
{{ embed(roots.B) }}
-
- We can then populate the template by adding panel 'A' and 'B' to
- the Template object:
-
- template.add_panel('A', pn.panel('A'))
- template.add_panel('B', pn.panel('B'))
-
- Once a template has been fully populated it can be rendered using
- the same API as other Panel objects. Note that all roots that have
- been declared using the {{ embed(roots.A) }} syntax in the Jinja2
- template must be defined when rendered.
-
- Since embedding complex CSS frameworks inside a notebook can have
- undesirable side-effects and a notebook does not afford the same
- amount of screen space a Template may given separate template
- and nb_template objects. This allows for different layouts when
- served as a standalone server and when used in the notebook.
- """
+ # Dictionary of property overrides by bokeh Model type
+ _modifiers = {}
+
+ __abstract = True
def __init__(self, template=None, items=None, nb_template=None, **params):
- super(Template, self).__init__(**params)
+ super(BaseTemplate, self).__init__(**params)
if isinstance(template, string_types):
self._code = template
template = _Template(template)
@@ -80,9 +56,6 @@ def __init__(self, template=None, items=None, nb_template=None, **params):
self._render_variables = {}
self._server = None
self._layout = self._build_layout()
- items = {} if items is None else items
- for name, item in items.items():
- self.add_panel(name, item)
def _build_layout(self):
str_repr = Str(repr(self))
@@ -111,11 +84,16 @@ def __repr__(self):
return template.format(
cls=cls, objs=('%s' % spacer).join(objs), spacer=spacer)
+ def _apply_modifiers(self, viewable, mref):
+ model, _ = viewable._models[mref]
+ modifiers = self._modifiers.get(type(viewable), {})
+ viewable.param.set_param(**{k: v for k, v in modifiers.items()
+ if getattr(viewable, k) == viewable.param[k].default})
+
def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=True):
doc = doc or _curdoc()
title = title or 'Panel Application'
doc.title = title
-
col = Column()
preprocess_root = col.get_root(doc, comm)
ref = preprocess_root.ref['id']
@@ -131,10 +109,26 @@ def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=Tr
obj._documents[doc] = model
model.name = name
model.tags = tags
+ for o in obj.select():
+ self._apply_modifiers(o, mref)
add_to_doc(model, doc, hold=bool(comm))
+
state._fake_roots.append(ref)
state._views[ref] = (col, preprocess_root, doc, comm)
+ if location:
+ from ..io.location import Location
+ if isinstance(location, Location):
+ loc = location
+ elif doc in state._locations:
+ loc = state.location
+ else:
+ loc = Location()
+ state._locations[doc] = loc
+ loc_model = loc._get_model(doc, preprocess_root)
+ loc_model.name = 'location'
+ #doc.add_root(loc_model)
+
col._preprocess(preprocess_root)
col._documents[doc] = preprocess_root
doc.on_session_destroyed(col._server_destroy)
@@ -191,64 +185,6 @@ def _repr_mimebundle_(self, include=None, exclude=None):
# Public API
#----------------------------------------------------------------
- def add_panel(self, name, panel, tags=[]):
- """
- Add panels to the Template, which may then be referenced by
- the given name using the jinja2 embed macro.
-
- Arguments
- ---------
- name : str
- The name to refer to the panel by in the template
- panel : panel.Viewable
- A Panel component to embed in the template.
- """
- if name in self._render_items:
- raise ValueError('The name %s has already been used for '
- 'another panel. Ensure each panel '
- 'has a unique name by which it can be '
- 'referenced in the template.' % name)
- self._render_items[name] = (_panel(panel), tags)
- self._layout[0].object = repr(self)
-
- def add_variable(self, name, value):
- """
- Add parameters to the template, which may then be referenced
- by the given name in the Jinja2 template.
-
- Arguments
- ---------
- name : str
- The name to refer to the panel by in the template
- value : object
- Any valid Jinja2 variable type.
- """
- if name in self._render_variables:
- raise ValueError('The name %s has already been used for '
- 'another variable. Ensure each variable '
- 'has a unique name by which it can be '
- 'referenced in the template.' % name)
- self._render_variables[name] = value
-
- def server_doc(self, doc=None, title=None, location=None):
- """
- Returns a servable bokeh Document with the panel attached
-
- Arguments
- ---------
- doc : bokeh.Document (optional)
- The Bokeh Document to attach the panel to as a root,
- defaults to bokeh.io.curdoc()
- title : str
- A string title to give the Document
-
- Returns
- -------
- doc : bokeh.Document
- The Bokeh document the panel was attached to
- """
- return self._init_doc(doc, title=title, location=location)
-
def save(self, filename, title=None, resources=None, embed=False,
max_states=1000, max_opts=3, embed_json=False,
json_prefix='', save_path='./', load_path=None):
@@ -284,6 +220,28 @@ def save(self, filename, title=None, resources=None, embed=False,
self._render_variables, embed, max_states, max_opts,
embed_json, json_prefix, save_path, load_path)
+ def server_doc(self, doc=None, title=None, location=True):
+ """
+ Returns a servable bokeh Document with the panel attached
+
+ Arguments
+ ---------
+ doc : bokeh.Document (optional)
+ The Bokeh Document to attach the panel to as a root,
+ defaults to bokeh.io.curdoc()
+ title : str
+ A string title to give the Document
+ location : boolean or panel.io.location.Location
+ Whether to create a Location component to observe and
+ set the URL location.
+
+ Returns
+ -------
+ doc : bokeh.Document
+ The Bokeh document the panel was attached to
+ """
+ return self._init_doc(doc, title=title, location=location)
+
def select(self, selector=None):
"""
Iterates over the Template and any potential children in the
@@ -303,3 +261,177 @@ def select(self, selector=None):
for obj, _ in self._render_items.values():
objects += obj.select(selector)
return objects
+
+
+
+class BasicTemplate(BaseTemplate):
+ """
+ BasicTemplate provides a baseclass for templates with a basic
+ organization including a header, sidebar and main area. Unlike the
+ more generic Template class these default templates make it easy
+ for a user to generate an application with a polished look and
+ feel without having to write any Jinja2 template themselves.
+ """
+
+ header = param.ClassSelector(class_=ListLike, constant=True, doc="""
+ A list-like container which populates the header bar.""")
+
+ main = param.ClassSelector(class_=ListLike, constant=True, doc="""
+ A list-like container which populates the main area.""")
+
+ sidebar = param.ClassSelector(class_=ListLike, constant=True, doc="""
+ A list-like container which populates the sidebar.""")
+
+ title = param.String(doc="A title to show in the header.")
+
+ header_background = param.String(doc="Optional header background color override")
+
+ header_color = param.String(doc="Optional header text color override")
+
+ theme = param.ClassSelector(class_=Theme, default=DefaultTheme,
+ constant=True, is_instance=False, instantiate=False)
+
+ _css = None
+
+ _template = None
+
+ _modifiers = {}
+
+ __abstract = True
+
+ def __init__(self, **params):
+ if self._css and self._css not in config.css_files:
+ config.css_files.append(self._css)
+ template = self._template.read_text()
+ if 'header' not in params:
+ params['header'] = ListLike()
+ if 'main' not in params:
+ params['main'] = ListLike()
+ if 'sidebar' not in params:
+ params['sidebar'] = ListLike()
+ super(BasicTemplate, self).__init__(template=template, **params)
+ if self.theme:
+ theme = self.theme.find_theme(type(self))
+ if theme and theme.css and theme.css not in config.css_files:
+ config.css_files.append(theme.css)
+ self._update_vars()
+ self.main.param.watch(self._update_render_items, ['objects'])
+ self.sidebar.param.watch(self._update_render_items, ['objects'])
+ self.header.param.watch(self._update_render_items, ['objects'])
+ self.param.watch(self._update_vars, ['title', 'header_background',
+ 'header_color'])
+
+ def _init_doc(self, doc=None, comm=None, title=None, notebook=False, location=True):
+ doc = super(BasicTemplate, self)._init_doc(doc, comm, title, notebook, location)
+ if self.theme:
+ theme = self.theme.find_theme(type(self))
+ if theme and theme.bokeh_theme:
+ doc.theme = theme.bokeh_theme
+ return doc
+
+ def _update_vars(self, *args):
+ self._render_variables['app_title'] = self.title
+ self._render_variables['header_background'] = self.header_background
+ self._render_variables['header_color'] = self.header_color
+
+ def _update_render_items(self, event):
+ if event.obj is self.main:
+ tag = 'main'
+ elif event.obj is self.sidebar:
+ tag = 'nav'
+ elif event.obj is self.header:
+ tag = 'header'
+ for obj in event.old:
+ ref = str(id(obj))
+ if obj not in event.new and ref in self._render_items:
+ del self._render_items[ref]
+ for obj in event.new:
+ ref = str(id(obj))
+ if ref not in self._render_items:
+ self._render_items[ref] = (obj, [tag])
+ tags = [tags for _, tags in self._render_items.values()]
+ self._render_variables['nav'] = any('nav' in ts for ts in tags)
+ self._render_variables['header'] = any('header' in ts for ts in tags)
+
+
+
+class Template(BaseTemplate):
+ """
+ A Template is a high-level component to render multiple Panel
+ objects into a single HTML document defined through a Jinja2
+ template. The Template object is given a Jinja2 template and then
+ allows populating this template by adding Panel objects, which are
+ given unique names. These unique names may then be referenced in
+ the template to insert the rendered Panel object at a specific
+ location. For instance, given a Jinja2 template that defines roots
+ A and B like this:
+
+
{{ embed(roots.A) }}
+
{{ embed(roots.B) }}
+
+ We can then populate the template by adding panel 'A' and 'B' to
+ the Template object:
+
+ template.add_panel('A', pn.panel('A'))
+ template.add_panel('B', pn.panel('B'))
+
+ Once a template has been fully populated it can be rendered using
+ the same API as other Panel objects. Note that all roots that have
+ been declared using the {{ embed(roots.A) }} syntax in the Jinja2
+ template must be defined when rendered.
+
+ Since embedding complex CSS frameworks inside a notebook can have
+ undesirable side-effects and a notebook does not afford the same
+ amount of screen space a Template may given separate template
+ and nb_template objects. This allows for different layouts when
+ served as a standalone server and when used in the notebook.
+ """
+
+ def __init__(self, template=None, nb_template=None, items=None, **params):
+ super(Template, self).__init__(template, nb_template, **params)
+ items = {} if items is None else items
+ for name, item in items.items():
+ self.add_panel(name, item)
+
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
+ def add_panel(self, name, panel, tags=[]):
+ """
+ Add panels to the Template, which may then be referenced by
+ the given name using the jinja2 embed macro.
+
+ Arguments
+ ---------
+ name : str
+ The name to refer to the panel by in the template
+ panel : panel.Viewable
+ A Panel component to embed in the template.
+ """
+ if name in self._render_items:
+ raise ValueError('The name %s has already been used for '
+ 'another panel. Ensure each panel '
+ 'has a unique name by which it can be '
+ 'referenced in the template.' % name)
+ self._render_items[name] = (_panel(panel), tags)
+ self._layout[0].object = repr(self)
+
+ def add_variable(self, name, value):
+ """
+ Add parameters to the template, which may then be referenced
+ by the given name in the Jinja2 template.
+
+ Arguments
+ ---------
+ name : str
+ The name to refer to the panel by in the template
+ value : object
+ Any valid Jinja2 variable type.
+ """
+ if name in self._render_variables:
+ raise ValueError('The name %s has already been used for '
+ 'another variable. Ensure each variable '
+ 'has a unique name by which it can be '
+ 'referenced in the template.' % name)
+ self._render_variables[name] = value
diff --git a/panel/template/bootstrap/__init__.py b/panel/template/bootstrap/__init__.py
new file mode 100644
index 0000000000..cfcb2ff67b
--- /dev/null
+++ b/panel/template/bootstrap/__init__.py
@@ -0,0 +1,40 @@
+"""
+Bootstrap template based on the bootstrap.css library.
+"""
+import pathlib
+
+import param
+
+from ...layout import Card
+from ..base import BasicTemplate
+from ..theme import DarkTheme, DefaultTheme
+
+
+class BootstrapTemplate(BasicTemplate):
+ """
+ BootstrapTemplate
+ """
+
+ _css = pathlib.Path(__file__).parent / 'bootstrap.css'
+
+ _template = pathlib.Path(__file__).parent / 'bootstrap.html'
+
+ _modifiers = {
+ Card: {
+ 'button_css_classes': ['card-button']
+ },
+ }
+
+
+class BootstrapDefaultTheme(DefaultTheme):
+
+ css = param.Filename(default=pathlib.Path(__file__).parent / 'default.css')
+
+ _template = BootstrapTemplate
+
+
+class BootstrapDarkTheme(DarkTheme):
+
+ css = param.Filename(default=pathlib.Path(__file__).parent / 'dark.css')
+
+ _template = BootstrapTemplate
diff --git a/panel/template/bootstrap/bootstrap.css b/panel/template/bootstrap/bootstrap.css
new file mode 100644
index 0000000000..f0437e8894
--- /dev/null
+++ b/panel/template/bootstrap/bootstrap.css
@@ -0,0 +1,38 @@
+body {
+ height: 100vh
+}
+
+#container {
+ padding:0px;
+}
+
+#content {
+ height: 100vh;
+ margin: 0px;
+}
+
+#sidebar {
+ transition: all 0.2s cubic-bezier(0.945, 0.020, 0.270, 0.665);
+ transform-origin: center left; /* Set the transformed position of sidebar to center left side. */
+}
+
+#sidebar.active {
+ margin-left: -16.7%;
+}
+
+#main {
+ overflow-y: scroll;
+}
+
+#sidebarCollapse {
+ background: none;
+ border: none;
+}
+
+a.navbar-brand {
+ padding-left: 10px;
+}
+
+p.bk.card-button {
+ display: none;
+}
diff --git a/panel/template/bootstrap/bootstrap.html b/panel/template/bootstrap/bootstrap.html
new file mode 100644
index 0000000000..de98509bff
--- /dev/null
+++ b/panel/template/bootstrap/bootstrap.html
@@ -0,0 +1,67 @@
+{% extends base %}
+
+
+{% block postamble %}
+
+
+
+
+{% endblock %}
+
+
+{% block contents %}
+
+
+
+
+ {% if nav %}
+
+
+ {% for doc in docs %}
+ {% for root in doc.roots %}
+ {% if "nav" in root.tags %}
+ {{ embed(root) | indent(8) }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+ {% for doc in docs %}
+ {% for root in doc.roots %}
+ {% if "main" in root.tags %}
+ {{ embed(root) | indent(4) }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+ {% if nav %}
+
+ {% endif %}
+ {{ app_title }}
+ {% for doc in docs %}
+ {% for root in doc.roots %}
+ {% if "header" in root.tags %}
+ {{ embed(root) | indent(8) }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+
+
+{% if nav %}
+
+{% endif %}
+
+
+
+ {% for doc in docs %}
+ {% for root in doc.roots %}
+ {% if "main" in root.tags %}
+ {{ embed(root) | indent(4) }}
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/panel/template/theme.py b/panel/template/theme.py
new file mode 100644
index 0000000000..5b7243194d
--- /dev/null
+++ b/panel/template/theme.py
@@ -0,0 +1,61 @@
+import param
+
+from bokeh.themes import Theme as _BkTheme, _dark_minimal
+
+
+class Theme(param.Parameterized):
+ """
+ A Theme customizes the look and feel of a Template by providing
+ custom CSS and bokeh Theme class.
+
+ When adding a new theme a generic Theme class should be created,
+ which is what users will important and set on the Template.theme
+ parameter. Additionally a concrete implementation of the Theme
+ should be created specific to the particular Template being used.
+
+ For example when adding a DarkTheme there should be a
+ corresponding MaterialDarkTheme which declares the Template class
+ on its _template class variable. In this way a user can always use
+ the generic Theme but the actual CSS and bokeh Theme will depend
+ on the exact Template being used.
+ """
+
+ css = param.Filename()
+
+ bokeh_theme = param.ClassSelector(class_=(_BkTheme, str))
+
+ _template = None
+
+ __abstract = True
+
+ @classmethod
+ def find_theme(cls, template_type):
+ if cls._template is template_type:
+ return cls
+ for theme in param.concrete_descendents(cls).values():
+ if theme._template is template_type:
+ return theme
+
+
+class DefaultTheme(Theme):
+ """
+ The DefaultTheme uses the standard Panel color palette.
+ """
+
+
+
+BOKEH_DARK = dict(_dark_minimal.json)
+
+BOKEH_DARK['attrs']['Figure'].update({
+ "background_fill_color": "#3f3f3f",
+ "border_fill_color": "#2f2f2f",
+
+})
+
+class DarkTheme(Theme):
+ """
+ The DefaultTheme uses the standard Panel color palette.
+ """
+
+ bokeh_theme = param.ClassSelector(class_=(_BkTheme, str),
+ default=_BkTheme(json=BOKEH_DARK))
diff --git a/panel/viewable.py b/panel/viewable.py
index e0462fe7be..39e7209614 100644
--- a/panel/viewable.py
+++ b/panel/viewable.py
@@ -688,11 +688,11 @@ def server_doc(self, doc=None, title=None, location=True):
doc : bokeh.Document (optional)
The bokeh Document to attach the panel to as a root,
defaults to bokeh.io.curdoc()
+ title : str
+ A string title to give the Document
location : boolean or panel.io.location.Location
Whether to create a Location component to observe and
set the URL location.
- title : str
- A string title to give the Document
Returns
-------