Skip to content

Commit

Permalink
refactor: Avoid needing to directly inspect shiny's bootstrap_deps() (
Browse files Browse the repository at this point in the history
  • Loading branch information
gadenbuie committed Apr 10, 2024
1 parent 1f13e89 commit 52dfc4b
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 120 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to `shinyswatch` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED] - YYYY-MM-DD

### New features

* `shinyswatch.theme_picker_ui()` gains a `default` argument to set the initial theme. (#22)

### Internal changes

* We've restructured the dependencies used to provide a shinyswatch theme. This change should not affect users of shinyswatch, but it will prevent accidentally including more than one shinyswatch themes on the same page. (#32)

* The theme picker now transitions between themes more smoothly. That said, we do still recommend using the theme picker only while developing your app. (#32)

## [0.5.1] - 2024-03-07

* Add typed attributes in the theme's color class for stronger type checking.
Expand Down
2 changes: 1 addition & 1 deletion examples/page-sidebar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_slider("n", "N", min=0, max=100, value=20),
shinyswatch.theme_picker_ui(),
shinyswatch.theme_picker_ui("zephyr"),
),
ui.card(ui.output_plot("plot")),
title="Shiny Sidebar Page",
Expand Down
2 changes: 1 addition & 1 deletion shinyswatch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Bootswatch + Bootstrap 5 themes for Shiny"""

__version__ = "0.5.1"
__version__ = "0.5.1.9000"

from . import theme
from ._get_theme import get_theme
Expand Down
99 changes: 77 additions & 22 deletions shinyswatch/_get_theme_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,47 @@

from htmltools import HTMLDependency

# from shiny.ui._html_deps_external import bootstrap_deps_suppress
from shiny._versions import bootstrap as shiny_bootstrap_version

from ._assert import assert_theme
from ._bsw5 import BSW5_THEME_NAME, bsw5_version
from ._shiny import base_dep_version, bs5_path, bs_dep_no_files

bs5_path = os.path.join(os.path.dirname(__file__), "bs5")


def suppress_shiny_bootstrap() -> list[HTMLDependency]:
return [
# shiny > 0.8.1 will (likely) split bootstrap into separate js/css deps
HTMLDependency(
name="bootstrap-js",
version=shiny_bootstrap_version + ".9999",
),
HTMLDependency(
name="bootstrap-css",
version=shiny_bootstrap_version + ".9999",
),
# shiny <= 0.8.1 loads bootstrap as a single dep
HTMLDependency(
name="bootstrap",
version=shiny_bootstrap_version + ".9999",
),
# Disable ionRangeSlider
# TODO: Remove this when ionRangeSlider no longer requires Sass for BS5+
HTMLDependency(
name="preset-shiny-ionrangeslider",
version="9999",
),
]


def dep_shinyswatch_bootstrap_js() -> HTMLDependency:
return HTMLDependency(
name="shinyswatch-js",
version=bsw5_version,
source={"package": "shinyswatch", "subdir": bs5_path},
script={"src": "bootstrap.bundle.min.js"},
)


def get_theme_deps(name: BSW5_THEME_NAME) -> list[HTMLDependency]:
Expand All @@ -33,40 +71,57 @@ def get_theme_deps(name: BSW5_THEME_NAME) -> list[HTMLDependency]:
# This is to prevent the Shiny bootstrap stylesheet from being loaded and instead load the bootswatch + bootstrap stylesheet
# _Disable_ bootstrap html dep
# Prevents bootstrap from being loaded at a later time (Ex: shiny.ui.card() https://github.com/rstudio/py-shiny/blob/d08af1a8534677c7026b60559cd5eafc5f6608d7/shiny/ui/_navs.py#L983)
HTMLDependency(
name="bootstrap",
version=base_dep_version,
),
# Use a custom version of bootstrap with no stylesheets/JS
bs_dep_no_files,
# Add in the matching JS files
HTMLDependency(
name="bootstrap-js",
version=bsw5_version,
source={"package": "shinyswatch", "subdir": bs5_path},
script={"src": "bootstrap.bundle.min.js"},
),
*suppress_shiny_bootstrap(),
dep_shinyswatch_bootstrap_js(),
# Shinyswatch - bootstrap / bootswatch css
HTMLDependency(
name=f"bootswatch-{name}-and-bootstrap",
name="shinyswatch-css",
version=bsw5_version,
source={"package": "shinyswatch", "subdir": subdir},
stylesheet=[{"href": "bootswatch.min.css"}],
),
# ## End Bootswatch
#
# ## Start ionRangeSlider
# Disable ionRangeSlider
HTMLDependency(
name="preset-shiny-ionrangeslider",
version=base_dep_version,
),
# Shinyswatch - ionRangeSlider css
HTMLDependency(
name=f"bootswatch-{name}-ionrangeslider",
name="shinyswatch-ionrangeslider",
version=bsw5_version,
source={"package": "shinyswatch", "subdir": subdir},
stylesheet=[{"href": "shinyswatch-ionRangeSlider.css"}],
),
# ## End ionRangeSlider
]


# TODO: Update this list if the css files in the dependency above change
deps_shinyswatch_css_files = [
"bootswatch.min.css",
"shinyswatch-ionRangeSlider.css",
]


def deps_shinyswatch_all(initial: str = "superhero") -> list[HTMLDependency]:
assert_theme(name=initial)

return [
*suppress_shiny_bootstrap(),
dep_shinyswatch_bootstrap_js(),
HTMLDependency(
name="shinyswatch-all-css",
version=bsw5_version,
source={"package": "shinyswatch", "subdir": "bsw5"},
stylesheet=[
shinyswatch_all_initial_css(initial, "bootswatch.min.css"),
shinyswatch_all_initial_css(initial, "shinyswatch-ionRangeSlider.css"),
], # type: ignore
all_files=True,
),
]


def shinyswatch_all_initial_css(theme: str, css_file: str) -> dict[str, str]:
return {
"href": os.path.join(theme, css_file),
"data-shinyswatch-css": css_file,
"data-shinyswatch-theme": theme,
}
36 changes: 0 additions & 36 deletions shinyswatch/_shiny.py

This file was deleted.

99 changes: 39 additions & 60 deletions shinyswatch/_theme_picker.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from htmltools import HTMLDependency, TagList
from shiny import reactive, render, req, ui
from __future__ import annotations

from htmltools import HTMLDependency
from shiny import reactive, ui
from shiny.session import require_active_session

from . import __version__ as shinyswatch_version
from ._bsw5 import BSW5_THEME_NAME, bsw5_themes
from ._get_theme_deps import get_theme_deps
from ._shiny import base_dep_version

default_theme_name = "superhero"
from ._get_theme_deps import deps_shinyswatch_all, deps_shinyswatch_css_files

theme_name: reactive.Value[BSW5_THEME_NAME] = reactive.Value(default_theme_name)
# Use a counter to force the new theme to be registered as a dependency
counter: reactive.Value[int] = reactive.Value(0)


def theme_picker_ui() -> ui.TagChild:
def theme_picker_ui(default: BSW5_THEME_NAME = "superhero") -> ui.TagChild:
"""
Theme picker - UI
Expand All @@ -25,6 +21,11 @@ def theme_picker_ui() -> ui.TagChild:
* Do not include more than one theme picker in your app.
* Do not call the theme picker UI / server inside a module.
Parameters
----------
default
The default theme to be selected when the theme picker is first loaded.
Returns
-------
:
Expand All @@ -43,33 +44,30 @@ def theme_picker_ui() -> ui.TagChild:
style="color: var(--bs-danger); background-color: var(--bs-light); display: none;",
id="shinyswatch_picker_warning",
),
ui.tags.script(
"""
(function() {
const display_warning = setTimeout(function() {
window.document.querySelector("#shinyswatch_picker_warning").style.display = 'block';
}, 1000);
Shiny.addCustomMessageHandler('shinyswatch-hide-warning', function(message) {
window.clearTimeout(display_warning);
});
Shiny.addCustomMessageHandler('shinyswatch-refresh', function(message) {
window.location.reload();
});
})()
"""
),
ui.input_select(
id="shinyswatch_theme_picker",
label="Select a theme:",
# TODO-barret; selected
selected=None,
choices=[],
selected=default,
choices=bsw5_themes,
),
get_theme_deps(default_theme_name),
ui.output_ui("shinyswatch_theme_deps"),
theme_picker_deps(default),
)


def theme_picker_deps(initial: str = "superhero") -> list[HTMLDependency]:
return [
*deps_shinyswatch_all(initial),
HTMLDependency(
name="shinyswatch-theme-picker",
version=shinyswatch_version,
source={"package": "shinyswatch", "subdir": "picker"},
stylesheet={"href": "theme_picker.css"},
script={"src": "theme_picker.js"},
),
]


def theme_picker_server() -> None:
"""
Theme picker - Server
Expand All @@ -85,38 +83,19 @@ def theme_picker_server() -> None:

session = require_active_session(None)
input = session.input
output = session.output

@reactive.Effect
@reactive.effect
@reactive.event(input.shinyswatch_theme_picker)
async def _():
counter.set(counter() + 1)
if theme_name() != input.shinyswatch_theme_picker():
theme_name.set(input.shinyswatch_theme_picker())
await session.send_custom_message("shinyswatch-refresh", {})

@output
@render.ui
def shinyswatch_theme_deps(): # pyright: ignore[reportUnusedFunction]
req(theme_name())

# Get the theme dependencies and set them to a version that will always be registered
theme_deps = get_theme_deps(theme_name())
incremented_version = HTMLDependency(
name="VersionOnly",
version=f"{base_dep_version}.{counter()}",
).version
for theme_dep in theme_deps:
theme_dep.version = incremented_version
# Return dependencies in a TagList so they can all be utilized
return TagList(theme_deps)

@reactive.Effect
async def _():
ui.update_selectize(
"shinyswatch_theme_picker",
selected=theme_name(),
choices=bsw5_themes,

await session.send_custom_message(
"shinyswatch-pick-theme",
{
"theme": input.shinyswatch_theme_picker(),
"sheets": deps_shinyswatch_css_files,
},
)
# Disable the warning message

@reactive.effect
async def _():
await session.send_custom_message("shinyswatch-hide-warning", {})
3 changes: 3 additions & 0 deletions shinyswatch/picker/theme_picker.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[data-shinyswatch-transitioning] * {
transition: all var(--shinyswatch-transition-duration, 100ms) ease-in-out;
}

0 comments on commit 52dfc4b

Please sign in to comment.