Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
899325a
refactor: Avoid needing to directly inspect shiny's bootstrap_deps
gadenbuie Apr 3, 2024
5e166c8
fix: remove unused import
gadenbuie Apr 3, 2024
35139e0
fix: version for bootstrap suppression
gadenbuie Apr 4, 2024
face07d
chore: Use `shinyswatch-{js,css,ionrangeslider`
gadenbuie Apr 4, 2024
4a0768e
feat: Add dep for all shinyswatch, refactor out reused components
gadenbuie Apr 4, 2024
7e02cb4
refactor: Refactor shinyswatch theme picker for faster smoother trans…
gadenbuie Apr 4, 2024
344ead5
chore: remove unused import
gadenbuie Apr 4, 2024
85220e9
feat: smooth theme transitions and initial theme load
gadenbuie Apr 5, 2024
e4447e7
chore: import from the future
gadenbuie Apr 5, 2024
13a665d
feat: safer old stylesheet removal to avoid FOUC
gadenbuie Apr 5, 2024
928fb7c
fix: clearTimeout not cance
gadenbuie Apr 5, 2024
bd1295c
refactor: finish refactoring shinyswatch-js
gadenbuie Apr 5, 2024
a681942
chore: Account for changing very quickly between themes
gadenbuie Apr 5, 2024
6758bd3
feat: Limit transition rule to transitioning only
gadenbuie Apr 5, 2024
31775dc
chore: format
gadenbuie Apr 5, 2024
aa7f348
fix(typo)
gadenbuie Apr 5, 2024
0a69de2
fix(typo): dataset.shinyswatchCss
gadenbuie Apr 5, 2024
e5c734e
factor out replaceShinyswatchCSS
gadenbuie Apr 5, 2024
df95807
fix copilot typo
gadenbuie Apr 5, 2024
1c192b1
move basedir into refactored function
gadenbuie Apr 5, 2024
349d7a9
refactor: getShinySwatchLibPath()
gadenbuie Apr 5, 2024
44029bf
send css file names from the python side
gadenbuie Apr 5, 2024
f7203d7
only transition when transitioning
gadenbuie Apr 5, 2024
6e6e9ee
not a function, just an array
gadenbuie Apr 5, 2024
298cdef
a final bit of refactoring
gadenbuie Apr 5, 2024
a1ca10d
chore: bump version, add changelog note
gadenbuie Apr 5, 2024
1cf43b3
changelog edits
gadenbuie Apr 5, 2024
28f5819
remove debug code
gadenbuie Apr 5, 2024
436ec1e
feat: User can provide default theme
gadenbuie Apr 5, 2024
b60a38b
chore: add a couple more comments
gadenbuie Apr 5, 2024
7b473f3
refactor(theme_picker.js): Use IIFE to avoid polluting global scope
gadenbuie Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"},
)
Comment on lines +41 to +47

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this only be included for shiny <= 0.8.1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this PR still removes the Bootstrap JS dependency. We'll take that out in a follow up PR once shiny has some run-time Bootstrap compatibility checks. Then we'll require latest shiny in shinswatch.



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;
}
Loading