bslibreact 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bslibreact/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Server helpers for bslibreact: React components reproducing bslib and Shiny markup.
2
+
3
+ Provides page dependency discovery so a client bundle can request the same
4
+ CSS and JavaScript a bslib page would attach on its own, and a hosted-input
5
+ helper for widgets rendered server-side and displayed by the client. The
6
+ matching React components live in the ``bslibreact`` npm package.
7
+ """
8
+
9
+ from ._dep import bslibreact_dep
10
+ from ._deps import page_deps
11
+ from ._host import host_input
12
+ from ._version import __version__
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ "bslibreact_dep",
17
+ "host_input",
18
+ "page_deps",
19
+ ]
bslibreact/_catalog.py ADDED
@@ -0,0 +1,493 @@
1
+ """The markup catalog: sample UI calls and the live shiny markup they produce.
2
+
3
+ This is the Python half of the spec in ``docs/markup-catalog.md``. bslib
4
+ (and py-shiny) are the ground truth for what bslibreact's React components
5
+ should render: instead of committing HTML fixtures that go stale the moment
6
+ shiny's markup changes, this module holds a catalog of *calls* -- Python
7
+ source text -- that get evaluated against whatever version of ``shiny`` is
8
+ actually installed, by :func:`catalog_eval`. ``tools/markup/render_py.py``
9
+ turns the result into the JSON file the JS test suite reads.
10
+
11
+ Sample names are frozen: they must match the R catalog's names wherever
12
+ py-shiny has the component (see ``docs/markup-catalog.md``, "Sample names").
13
+ Renaming or removing one is a deliberate, visible edit -- ``test_catalog.py``
14
+ pins the whole name set as a literal.
15
+
16
+ Six of the R catalog's names have no Python entry, because ``shiny`` alone
17
+ cannot produce them (there is no "extra" third-party kind here the way R's
18
+ DT/reactable/htmlwidgets samples exist):
19
+
20
+ - ``plotOutput_with_spinner`` -- wraps a plot in ``shinycssloaders``.
21
+ - ``DTOutput``, ``DTOutput_no_fill`` -- the ``DT`` package.
22
+ - ``htmlwidget_output`` -- the generic ``htmlwidgets`` output shape.
23
+ Python's analogue lives in the separate ``shinywidgets`` package.
24
+ - ``reactableOutput`` -- the ``reactable`` package.
25
+ - ``icon`` -- R's ``shiny::icon()`` renders Font Awesome markup that ships
26
+ with base R shiny. py-shiny has no built-in icon helper at all (an icon is
27
+ just whatever ``TagChild`` you pass), so there is no ``shiny``-only call
28
+ that stands in for it.
29
+
30
+ ``call`` strings are Python source, evaluated by :func:`catalog_eval` in a
31
+ small fixed namespace exposing only ``ui`` (``shiny.ui``). They are fixed
32
+ strings written by this module, never user input, so a plain ``eval()`` is
33
+ fine here -- the empty ``__builtins__`` just keeps the namespace from growing
34
+ by accident as this file evolves.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import hashlib
40
+ from dataclasses import dataclass
41
+ from typing import TYPE_CHECKING, Literal, Sequence
42
+
43
+ from shiny import ui
44
+
45
+ if TYPE_CHECKING:
46
+ from htmltools import TagChild
47
+
48
+ SampleKind = Literal["tag", "page"]
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Sample:
53
+ """One catalog entry: a name, and the call that produces its markup."""
54
+
55
+ name: str
56
+ kind: SampleKind
57
+ group: str
58
+ call: str
59
+
60
+
61
+ def _s(name: str, kind: SampleKind, group: str, call: str) -> Sample:
62
+ return Sample(name=name, kind=kind, group=group, call=call)
63
+
64
+
65
+ # Kept from the original R kit (`tools/kit/dump-markup.R`), same sample
66
+ # names, translated to the equivalent py-shiny call.
67
+ SAMPLES: tuple[Sample, ...] = (
68
+ _s(
69
+ "selectInput",
70
+ "tag",
71
+ "input",
72
+ 'ui.input_select("x", "Quick pick", choices=["10", "20", "30"])',
73
+ ),
74
+ _s(
75
+ "selectizeInput",
76
+ "tag",
77
+ "input",
78
+ 'ui.input_selectize("x", "Genes", choices=["A", "B", "C"], selected="A", '
79
+ "multiple=True)",
80
+ ),
81
+ _s(
82
+ "radioButtons",
83
+ "tag",
84
+ "input",
85
+ 'ui.input_radio_buttons("x", "Values", choices={"row": "Row z-score", '
86
+ '"none": "Expression"})',
87
+ ),
88
+ _s(
89
+ "checkboxInput",
90
+ "tag",
91
+ "input",
92
+ 'ui.input_checkbox("x", "Cluster genes", value=True)',
93
+ ),
94
+ _s(
95
+ "sliderInput",
96
+ "tag",
97
+ "input",
98
+ 'ui.input_slider("x", "Bins", min=1, max=50, value=30)',
99
+ ),
100
+ _s("actionButton", "tag", "input", 'ui.input_action_button("x", "Go")'),
101
+ _s("plotOutput", "tag", "output", 'ui.output_plot("x")'),
102
+ _s(
103
+ "card_full_screen",
104
+ "tag",
105
+ "card",
106
+ 'ui.card(ui.card_header("Title"), ui.card_body("Body"), full_screen=True)',
107
+ ),
108
+ _s(
109
+ "sidebar_layout",
110
+ "page",
111
+ "page",
112
+ 'ui.page_sidebar(ui.sidebar(ui.input_select("x", "Pick", choices=["A"]), '
113
+ "width=290), "
114
+ 'ui.card(ui.card_header("Main"), ui.card_body("Content")), title="Demo")',
115
+ ),
116
+ _s(
117
+ "selectizeInput_server_placeholder",
118
+ "tag",
119
+ "input",
120
+ 'ui.input_selectize("x", "Pick genes", choices=[], multiple=True, '
121
+ 'options={"placeholder": "type to search..."})',
122
+ ),
123
+ _s(
124
+ "textAreaInput",
125
+ "tag",
126
+ "input",
127
+ 'ui.input_text_area("x", None, rows=3, placeholder="GENE1, GENE2, GENE3...")',
128
+ ),
129
+ _s("textOutput", "tag", "output", 'ui.output_text("x")'),
130
+ _s(
131
+ "verbatimTextOutput",
132
+ "tag",
133
+ "output",
134
+ 'ui.output_code("x", placeholder=False)',
135
+ ),
136
+ _s(
137
+ "details_summary",
138
+ "tag",
139
+ "misc",
140
+ 'ui.tags.details(ui.tags.summary("Paste a list"), '
141
+ 'ui.input_text_area("x", None, rows=3))',
142
+ ),
143
+ _s(
144
+ "card_bare_children",
145
+ "tag",
146
+ "card",
147
+ 'ui.card(ui.card_header("Current selection"), ui.output_text("count"), '
148
+ 'ui.output_code("selected", placeholder=False))',
149
+ ),
150
+ _s(
151
+ "actionButton_class_override",
152
+ "tag",
153
+ "input",
154
+ 'ui.input_action_button("x", "Apply", class_="btn-sm")',
155
+ ),
156
+ _s(
157
+ "fileInput",
158
+ "tag",
159
+ "input",
160
+ 'ui.input_file("x", "de_results.csv", accept=".csv")',
161
+ ),
162
+ _s(
163
+ "helpText_hr",
164
+ "tag",
165
+ "misc",
166
+ 'ui.TagList(ui.hr(), ui.help_text("Some help text here."))',
167
+ ),
168
+ _s(
169
+ "layout_columns_value_box",
170
+ "tag",
171
+ "layout",
172
+ 'ui.layout_columns(ui.value_box("PC1 variance", ui.output_text("pc1"), '
173
+ 'theme="primary"), '
174
+ 'ui.value_box("PC2 variance", ui.output_text("pc2"), theme="warning"), '
175
+ 'ui.value_box("Genes used", ui.output_text("ngenes"), theme="secondary"), '
176
+ "fill=False, col_widths=[4, 4, 4])",
177
+ ),
178
+ _s(
179
+ "plotOutput_click",
180
+ "tag",
181
+ "output",
182
+ 'ui.output_plot("x", height="560px", click=True)',
183
+ ),
184
+ _s(
185
+ "card_footer",
186
+ "tag",
187
+ "card",
188
+ 'ui.card(ui.card_header("Title"), ui.card_body("Body"), '
189
+ 'ui.card_footer("Footer text"))',
190
+ ),
191
+ _s(
192
+ "sidebar_layout_titled",
193
+ "tag",
194
+ "layout",
195
+ 'ui.layout_sidebar(ui.sidebar("filters", title="Filters", width=260), '
196
+ 'ui.div("main"))',
197
+ ),
198
+ _s(
199
+ "sidebar_layout_right_closed",
200
+ "tag",
201
+ "layout",
202
+ 'ui.layout_sidebar(ui.sidebar("controls", title="Selected study", '
203
+ 'position="right", '
204
+ 'width=360, open="closed", id="details_pane"), ui.div("main"))',
205
+ ),
206
+ _s(
207
+ "accordion_checkbox_groups",
208
+ "tag",
209
+ "layout",
210
+ 'ui.accordion(ui.accordion_panel("Organism", '
211
+ 'ui.input_checkbox_group("organisms", None, '
212
+ 'choices={}), value="organism"), ui.accordion_panel("Data source", '
213
+ 'ui.input_checkbox_group("sources", None, choices={}), value="source"), '
214
+ 'id="filters", multiple=True, open=["organism", "source"])',
215
+ ),
216
+ _s(
217
+ "textInput",
218
+ "tag",
219
+ "input",
220
+ 'ui.input_text("q", None, placeholder="Search accession, title or abstract", '
221
+ 'width="100%")',
222
+ ),
223
+ _s(
224
+ "actionLink",
225
+ "tag",
226
+ "input",
227
+ 'ui.input_action_link("clear", "Clear all filters", class_="small")',
228
+ ),
229
+ _s(
230
+ "downloadButton",
231
+ "tag",
232
+ "input",
233
+ 'ui.download_button("qc_pdf", "PDF", class_="btn-sm ms-auto")',
234
+ ),
235
+ _s(
236
+ "sliderInput_no_ticks",
237
+ "tag",
238
+ "input",
239
+ 'ui.input_slider("font_size", "Font size", min=10, max=26, value=21, step=1, '
240
+ "ticks=False)",
241
+ ),
242
+ _s(
243
+ "selectizeInput_remove_button",
244
+ "tag",
245
+ "input",
246
+ 'ui.input_selectize("columns", "Columns", choices=[], multiple=True, '
247
+ 'width="100%", '
248
+ 'options={"plugins": ["remove_button"]})',
249
+ ),
250
+ _s(
251
+ "selectizeInput_max_options",
252
+ "tag",
253
+ "input",
254
+ 'ui.input_selectize("gene", "Search for a gene", choices=[], '
255
+ 'options={"placeholder": "Symbol or Ensembl id", "maxOptions": 50})',
256
+ ),
257
+ _s(
258
+ "radioButtons_inline",
259
+ "tag",
260
+ "input",
261
+ 'ui.input_radio_buttons("geom", "Plot", choices={"box": "Boxplot", '
262
+ '"violin": "Violin"}, inline=True)',
263
+ ),
264
+ _s(
265
+ "numericInput",
266
+ "tag",
267
+ "input",
268
+ 'ui.input_numeric("loading_pc", "Loadings for PC", value=1, min=1, max=10, '
269
+ "step=1)",
270
+ ),
271
+ _s(
272
+ "passwordInput",
273
+ "tag",
274
+ "input",
275
+ 'ui.input_password("api_key", "API key", placeholder="Paste your key")',
276
+ ),
277
+ _s("uiOutput", "tag", "output", 'ui.output_ui("x")'),
278
+ _s("uiOutput_fill", "tag", "output", 'ui.output_ui("x", fill=True)'),
279
+ _s(
280
+ "page_fillable",
281
+ "page",
282
+ "page",
283
+ 'ui.page_fillable(ui.tags.h2("Title"), padding=8, gap=4)',
284
+ ),
285
+ _s(
286
+ "layout_columns_fill",
287
+ "tag",
288
+ "layout",
289
+ 'ui.layout_columns(ui.card(ui.card_header("Left"), ui.card_body("Left body")), '
290
+ 'ui.card(ui.card_header("Right"), ui.card_body("Right body")), col_widths=[7, '
291
+ "5])",
292
+ ),
293
+ _s(
294
+ "page_navbar",
295
+ "page",
296
+ "page",
297
+ 'ui.page_navbar(ui.nav_panel("Browse", "x"), ui.nav_panel("Overview", "y"), '
298
+ "ui.nav_spacer(), "
299
+ 'ui.nav_control(ui.output_ui("chip", inline=True)), '
300
+ 'ui.nav_control(ui.input_dark_mode(id="dark_mode")), title="Demo", id="nav", '
301
+ 'theme=ui.Theme("shiny"), '
302
+ 'navbar_options=ui.navbar_options(bg="var(--brand-roast)", theme="dark"), '
303
+ 'fillable=["Browse"], footer=ui.div("foot"))',
304
+ ),
305
+ _s(
306
+ "page_navbar_icon",
307
+ "page",
308
+ "page",
309
+ 'ui.page_navbar(ui.nav_panel("Explorer", "x", '
310
+ 'icon=ui.tags.i(class_="bi bi-bar-chart-line")), '
311
+ 'ui.nav_panel("About", "y", icon=ui.tags.i(class_="bi bi-info-circle")), '
312
+ 'title="Demo", id="nav", theme=ui.Theme("shiny"))',
313
+ ),
314
+ _s(
315
+ "page_navbar_sidebar",
316
+ "page",
317
+ "page",
318
+ 'ui.page_navbar(ui.nav_panel("Build", ui.div("build content")), '
319
+ 'ui.nav_panel("About", ui.div("about content")), title="Demo", id="nav", '
320
+ 'sidebar=ui.sidebar(ui.div("chat"), id="dock", position="left", open="closed", '
321
+ "width=600), "
322
+ 'footer=ui.tags.footer("foot"))',
323
+ ),
324
+ _s(
325
+ "sidebar_custom_class",
326
+ "tag",
327
+ "layout",
328
+ 'ui.layout_sidebar(ui.sidebar(ui.div("a"), width=420, '
329
+ 'class_="my-sidebar"), ui.div("main"))',
330
+ ),
331
+ # `page_navbar_dropdown` replaces the old hand-corrected fixture: a
332
+ # live render needs no correction for a shape `as.character()` could not
333
+ # represent faithfully.
334
+ _s(
335
+ "page_navbar_dropdown",
336
+ "page",
337
+ "page",
338
+ 'ui.page_navbar(ui.nav_panel("Review", "x"), ui.nav_menu("Docs", '
339
+ 'ui.nav_panel("Guide", "g"), '
340
+ 'ui.nav_panel("FAQ", "f")), id="nav", title="Demo")',
341
+ ),
342
+ # Added for coverage the original kit did not have (see docs/markup-catalog.md).
343
+ _s(
344
+ "page_navbar_not_fillable",
345
+ "page",
346
+ "page",
347
+ 'ui.page_navbar(ui.nav_panel("One", "a"), ui.nav_panel("Two", "b"), '
348
+ 'title="Demo", id="nav", '
349
+ "fillable=False)",
350
+ ),
351
+ _s(
352
+ "sidebar_layout_gap_padding",
353
+ "tag",
354
+ "layout",
355
+ 'ui.layout_sidebar(ui.sidebar("filters", width=260), ui.div("main"), '
356
+ 'gap="1rem", padding="1rem")',
357
+ ),
358
+ _s(
359
+ "layout_columns_single_child",
360
+ "tag",
361
+ "layout",
362
+ 'ui.layout_columns(ui.card(ui.card_header("Solo"), '
363
+ 'ui.card_body("Only one column")))',
364
+ ),
365
+ _s(
366
+ "card_id",
367
+ "tag",
368
+ "card",
369
+ 'ui.card(ui.card_header("Title"), ui.card_body("Body"), id="my_card")',
370
+ ),
371
+ _s(
372
+ "card_height",
373
+ "tag",
374
+ "card",
375
+ 'ui.card(ui.card_header("Title"), ui.card_body("Body"), height="300px")',
376
+ ),
377
+ _s(
378
+ "card_no_fill",
379
+ "tag",
380
+ "card",
381
+ 'ui.card(ui.card_header("Title"), ui.card_body("Body"), fill=False)',
382
+ ),
383
+ _s(
384
+ "tooltip",
385
+ "tag",
386
+ "tooltip",
387
+ 'ui.tooltip(ui.input_action_button("btn", "Hover me"), "Extra information", '
388
+ 'id="tt")',
389
+ ),
390
+ _s(
391
+ "popover",
392
+ "tag",
393
+ "tooltip",
394
+ 'ui.popover(ui.input_action_button("btn2", "Click me"), "Popover body", '
395
+ 'title="More info", id="pop")',
396
+ ),
397
+ _s(
398
+ "value_box",
399
+ "tag",
400
+ "card",
401
+ 'ui.value_box("KPI", "42", "up 3% this week", theme="primary")',
402
+ ),
403
+ _s("uiOutput_inline", "tag", "output", 'ui.output_ui("x", inline=True)'),
404
+ _s("input_dark_mode", "tag", "input", 'ui.input_dark_mode(id="dark_mode")'),
405
+ _s(
406
+ "input_switch",
407
+ "tag",
408
+ "input",
409
+ 'ui.input_switch("x", "Enable feature", value=True)',
410
+ ),
411
+ _s(
412
+ "input_task_button",
413
+ "tag",
414
+ "input",
415
+ 'ui.input_task_button("go", "Run", label_busy="Running...")',
416
+ ),
417
+ _s(
418
+ "accordion",
419
+ "tag",
420
+ "layout",
421
+ 'ui.accordion(ui.accordion_panel("Section A", "Body A", value="a"), '
422
+ 'ui.accordion_panel("Section B", "Body B", value="b"), id="acc")',
423
+ ),
424
+ _s(
425
+ "navset_card_tab",
426
+ "tag",
427
+ "nav",
428
+ 'ui.navset_card_tab(ui.nav_panel("Tab 1", "One"), '
429
+ 'ui.nav_panel("Tab 2", "Two"))',
430
+ ),
431
+ _s(
432
+ "navset_pill",
433
+ "tag",
434
+ "nav",
435
+ 'ui.navset_pill(ui.nav_panel("Tab 1", "One"), ui.nav_panel("Tab 2", "Two"))',
436
+ ),
437
+ _s(
438
+ "checkboxGroupInput",
439
+ "tag",
440
+ "input",
441
+ 'ui.input_checkbox_group("opts", "Options", choices={"a": "Alpha", '
442
+ '"b": "Beta"}, selected=["a"])',
443
+ ),
444
+ _s("dateInput", "tag", "input", 'ui.input_date("d", "Date")'),
445
+ _s("dateRangeInput", "tag", "input", 'ui.input_date_range("dr", "Date range")'),
446
+ )
447
+
448
+
449
+ def markup_catalog(kind: Sequence[str] | None = None) -> tuple[Sample, ...]:
450
+ """Return catalog samples, optionally filtered to the given ``kind`` values."""
451
+ if kind is None:
452
+ return SAMPLES
453
+ kinds = set(kind)
454
+ return tuple(sample for sample in SAMPLES if sample.kind in kinds)
455
+
456
+
457
+ def catalog_names(kind: Sequence[str] | None = None) -> list[str]:
458
+ """Names of the catalog samples matching ``kind`` (all, if omitted)."""
459
+ return [sample.name for sample in markup_catalog(kind)]
460
+
461
+
462
+ def catalog_eval(names: Sequence[str]) -> list["TagChild"]:
463
+ """Evaluate each named sample's ``call`` and return the resulting tags.
464
+
465
+ Uses a fixed namespace exposing only ``ui`` (``shiny.ui``) plus an empty
466
+ ``__builtins__``. Every ``call`` string is one of this module's own
467
+ literals (see :data:`SAMPLES`), not user input, so a plain ``eval()`` is
468
+ appropriate here.
469
+ """
470
+ by_name = {sample.name: sample for sample in SAMPLES}
471
+ namespace = {"ui": ui, "__builtins__": {}}
472
+ results: list["TagChild"] = []
473
+ for name in names:
474
+ sample = by_name.get(name)
475
+ if sample is None:
476
+ raise ValueError(f"unknown catalog sample: {name!r}")
477
+ results.append(eval(sample.call, namespace)) # noqa: S307
478
+ return results
479
+
480
+
481
+ def catalog_hash(samples: Sequence[Sample] | None = None) -> str:
482
+ """A stable hash of the catalog's names, kinds, groups and call text.
483
+
484
+ Part of the catalog render key (see ``tools/markup/render_py.py``): it changes
485
+ whenever a sample is added, removed, or its call text edited, so a stale
486
+ cache gets re-rendered.
487
+ """
488
+ selected = samples if samples is not None else SAMPLES
489
+ lines = "\n".join(
490
+ f"{sample.name}|{sample.kind}|{sample.group}|{sample.call}"
491
+ for sample in selected
492
+ )
493
+ return hashlib.sha256(lines.encode("utf-8")).hexdigest()
bslibreact/_dep.py ADDED
@@ -0,0 +1,73 @@
1
+ """``bslibreact_dep()``: the vendored JS/CSS bundle as an HTMLDependency.
2
+
3
+ Mirrors the same pattern ``posit-dev/shinyreact``'s own Python package uses
4
+ for its vendored bundle (``pkg-py/src/shinyreact/_dep.py`` in that repo):
5
+ the bundle in ``www/`` is vendored in by ``make update-dist`` from
6
+ ``pkg-js/dist/bslibreact.js``, and the version string gets an mtime suffix
7
+ in a dev checkout so a rebuild still busts the browser cache even though the
8
+ release version hasn't changed.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ from htmltools import HTMLDependency
16
+
17
+ from ._version import __version__
18
+
19
+ _WWW_DIR = Path(__file__).parent / "www"
20
+
21
+ # Only reachable from a repo checkout (this package installed editable from
22
+ # source), never from an installed wheel -- a wheel has no `pkg-js/` sibling
23
+ # at all. Its presence is the "dev checkout" signal `_bundle_version()` uses.
24
+ _JS_PACKAGE_JSON = Path(__file__).parents[3] / "pkg-js" / "package.json"
25
+
26
+
27
+ def _file_mtime_int(path: Path) -> int | None:
28
+ """Return ``path``'s mtime in whole seconds, or ``None`` if it doesn't exist."""
29
+ try:
30
+ return int(path.stat().st_mtime)
31
+ except FileNotFoundError:
32
+ return None
33
+
34
+
35
+ def _bundle_version() -> str:
36
+ """The package version, suffixed with the bundle's mtime in a dev checkout.
37
+
38
+ An installed package serves ``/lib/bslibreact-<version>/``. In a repo
39
+ checkout it is ``/lib/bslibreact-<version>+<mtime>/`` instead, so
40
+ ``make update-dist`` still cache-busts the browser. The mtime goes in a
41
+ PEP 440 local segment (``+``), which stays valid after any version,
42
+ pre-releases included.
43
+ """
44
+ if _JS_PACKAGE_JSON.is_file():
45
+ mtime = _file_mtime_int(_WWW_DIR / "bslibreact.js")
46
+ if mtime is not None:
47
+ return f"{__version__}+{mtime}"
48
+ return __version__
49
+
50
+
51
+ def bslibreact_dep() -> HTMLDependency:
52
+ """The bslibreact JS (and CSS, if built) bundle as an HTMLDependency.
53
+
54
+ The bundle sets ``window.bslibreact``. Attach it only for a client with no
55
+ build step that reads the kit from that global; a client built with Vite
56
+ bundles the kit itself.
57
+
58
+ The stylesheet is attached only when ``www/bslibreact.css`` actually
59
+ exists -- the bundle may ship JS only -- so this never points a
60
+ ``<link>`` at a file that isn't there. Likewise the JS file itself may be
61
+ absent before the first ``make update-dist``; :func:`_bundle_version`
62
+ handles that (falls back to the bare release string) instead of
63
+ erroring, since importing ``bslibreact`` must work before the bundle
64
+ has ever been built.
65
+ """
66
+ css = _WWW_DIR / "bslibreact.css"
67
+ return HTMLDependency(
68
+ name="bslibreact",
69
+ version=_bundle_version(),
70
+ source={"subdir": str(_WWW_DIR)},
71
+ script={"src": "bslibreact.js", "defer": ""},
72
+ stylesheet={"href": "bslibreact.css"} if css.exists() else None,
73
+ )
bslibreact/_deps.py ADDED
@@ -0,0 +1,103 @@
1
+ """``page_deps()``: the CSS/JS a bslibreact page needs, without a real page.
2
+
3
+ bslibreact's React components reproduce bslib markup, but some of that
4
+ markup depends on component CSS and JS that a normal bslib/shiny app only
5
+ picks up because the *components themselves* are somewhere on the page (the
6
+ ion.rangeSlider assets a slider needs, selectize's assets, bslib's own
7
+ component JS). A React page that never calls ``ui.input_slider()`` directly
8
+ still needs those files if it renders a `<SliderInput>`. ``page_deps()``
9
+ closes that gap: it renders a representative sample of the catalog (or a
10
+ caller-chosen subset) purely to ask htmltools what dependencies that markup
11
+ would pull in, then hands back just the dependency tags.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Sequence
18
+
19
+ from htmltools import TagList
20
+ from shiny.ui import Theme
21
+
22
+ from ._catalog import catalog_eval, catalog_names
23
+
24
+ if TYPE_CHECKING:
25
+ from htmltools import TagChild
26
+
27
+ # Every page function shiny ships (`page_bootstrap`, `page_navbar`, ...)
28
+ # attaches these itself. Including them here too would be harmless (htmltools
29
+ # de-duplicates dependencies by name when hoisting to <head>) but misleading:
30
+ # a caller reading page_deps()'s output should see only the extra things
31
+ # bslibreact needs, not the baseline every shiny page already has.
32
+ _ALWAYS_EXCLUDED = frozenset({"bootstrap", "jquery"})
33
+
34
+
35
+ def _validate_theme(theme: object) -> None:
36
+ """Raise ``TypeError`` unless ``theme`` is one of the accepted shapes, so a
37
+ wrong ``theme=`` fails loudly even though the value is not used yet."""
38
+ if theme is None:
39
+ return
40
+ if isinstance(theme, (str, Path)):
41
+ return
42
+ if isinstance(theme, Theme):
43
+ return
44
+ raise TypeError(
45
+ "page_deps(theme=...) must be None, a str or Path to a _brand.yml, "
46
+ f"or a shiny.ui.Theme instance; got {type(theme).__name__}. "
47
+ "py-shiny resolves component CSS against whatever theme the page "
48
+ "itself declares, so this value is validated but otherwise unused."
49
+ )
50
+
51
+
52
+ def page_deps(
53
+ theme: str | Path | Theme | None = None,
54
+ *extra: "TagChild",
55
+ components: Sequence[str] | None = None,
56
+ ) -> TagList:
57
+ """The HTML dependencies bslibreact's components need at runtime.
58
+
59
+ Attach the result to a shinyreact page, for example
60
+ ``page_react(page_deps(app_theme), theme=app_theme)``. A client with no
61
+ build step also attaches :func:`bslibreact_dep`.
62
+ It works by rendering a representative sample of the markup catalog --
63
+ the same sample calls the catalog renderer uses for ``bslibreact
64
+ markup`` -- and reading back whichever :class:`~htmltools.HTMLDependency`
65
+ objects that markup pulled in, minus the ones every shiny page already
66
+ attaches on its own (``bootstrap``, ``jquery``).
67
+
68
+ Args:
69
+ theme: The page's theme: ``None``, a ``str``/``Path`` to a
70
+ ``_brand.yml``, or a :class:`shiny.ui.Theme`. Validated but not
71
+ used yet (py-shiny themes component CSS from the page's own
72
+ theme); it keeps the call the same as R's ``page_deps()`` and
73
+ leaves room to use it later.
74
+ *extra: Additional tags to render alongside the catalog sample --
75
+ typically the actual inputs and outputs your page uses, so their
76
+ dependencies are included too even if the catalog doesn't happen
77
+ to cover them.
78
+ components: Catalog sample names to render, instead of every
79
+ ``"tag"``/``"page"`` sample. Pass ``[]`` to render none of the
80
+ catalog (only ``*extra``). Unknown names raise ``ValueError``.
81
+
82
+ Returns:
83
+ A :class:`~htmltools.TagList` of :class:`~htmltools.HTMLDependency`
84
+ objects, ready to splat into a page.
85
+ """
86
+ _validate_theme(theme)
87
+
88
+ if components is None:
89
+ names: Sequence[str] = catalog_names(("tag", "page"))
90
+ else:
91
+ names = list(components)
92
+ known = set(catalog_names())
93
+ for name in names:
94
+ if name not in known:
95
+ raise ValueError(
96
+ "page_deps(components=...) named an unknown catalog "
97
+ f"sample: {name!r}"
98
+ )
99
+
100
+ samples = catalog_eval(names)
101
+ rendered = TagList(*samples, *extra).tagify()
102
+ deps = rendered.get_dependencies()
103
+ return TagList(*(dep for dep in deps if dep.name not in _ALWAYS_EXCLUDED))
bslibreact/_host.py ADDED
@@ -0,0 +1,116 @@
1
+ """``host_input()``: mount one server-rendered Shiny widget inside a React page.
2
+
3
+ The ui.tsx pattern puts the client in charge of markup, but a handful of
4
+ widgets (a color picker, a fancy date range control, anything a team already
5
+ has a battle-tested ``renderUI`` for) do not need a React rebuild right
6
+ away. ``host_input()`` is the escape hatch: it registers an ordinary
7
+ ``@render.ui`` output on the server, and hands back the output id your React
8
+ component should mount at (conventionally via a `<SliderInput id="bins_ui"
9
+ />`-style hosted element).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING, Any, Callable, Protocol
15
+
16
+ from htmltools import Tag, TagList
17
+ from shiny import render
18
+
19
+ if TYPE_CHECKING:
20
+ from htmltools import TagChild
21
+
22
+
23
+ class _OutputsLike(Protocol):
24
+ """The one sliver of ``shiny.session.Outputs`` that ``host_input()`` needs.
25
+
26
+ A structural type, not an import of ``Outputs`` itself: the real
27
+ class and a test double both satisfy this without either one needing to
28
+ inherit from the other, and it documents exactly what ``host_input()``
29
+ depends on.
30
+
31
+ The real ``Outputs.__call__`` is overloaded: called with a bare renderer
32
+ it registers immediately, called with only keyword arguments (as
33
+ ``host_input()`` always does) it returns a decorator instead. Modelling
34
+ only that second shape here, instead of merging both into one signature
35
+ with an optional positional ``renderer``, is what lets a real
36
+ ``Outputs`` instance satisfy this Protocol structurally -- a flattened
37
+ signature does not match an overloaded one under strict type checking.
38
+ """
39
+
40
+ def __call__(
41
+ self,
42
+ *,
43
+ id: str | None = None,
44
+ suspend_when_hidden: bool = True,
45
+ priority: int = 0,
46
+ ) -> Callable[[Any], Any]: ...
47
+
48
+
49
+ def _check_no_id_collision(tag: "TagChild", name: str) -> None:
50
+ """Raise ``ValueError`` if some element inside ``tag`` already has id ``name``.
51
+
52
+ Walks ``Tag.children`` (a :class:`~htmltools.TagList`, which flattens
53
+ nested lists as it is iterated) looking for an ``id`` attribute equal to
54
+ ``name``. A collision means the widget's own markup would fight with the
55
+ output wrapper Shiny binds to for the same DOM id, so it is caught here
56
+ instead of being left to produce a confusing duplicate-id bug in the browser.
57
+ """
58
+ if isinstance(tag, Tag):
59
+ if tag.attrs.get("id") == name:
60
+ raise ValueError(
61
+ f"host_input(): the widget passed for id={name!r} contains its own "
62
+ f"element with that same id. Give the widget a different id, or pass "
63
+ "a different suffix= to host_input() so the output id it registers "
64
+ "doesn't collide with markup the widget itself produces."
65
+ )
66
+ for child in tag.children:
67
+ _check_no_id_collision(child, name)
68
+ elif isinstance(tag, TagList):
69
+ for child in tag:
70
+ _check_no_id_collision(child, name)
71
+
72
+
73
+ def host_input(
74
+ output: "_OutputsLike",
75
+ id: str,
76
+ widget: Callable[[], "TagChild"],
77
+ suffix: str = "_ui",
78
+ ) -> str:
79
+ """Register ``widget`` as a hosted output and return its output id.
80
+
81
+ Registers ``output[f"{id}{suffix}"]`` as an ``@render.ui`` output that
82
+ calls ``widget()`` lazily -- once per render, the same as any other
83
+ Shiny output -- and checks the result for an id collision (see
84
+ :func:`_check_no_id_collision`) before returning it. The registration
85
+ uses ``suspend_when_hidden=False``: Shiny normally skips rendering an
86
+ output it believes is hidden, based on the *server-rendered* DOM
87
+ visibility of its container, but a hosted output's container is a React
88
+ element the client mounts on its own terms, which Shiny's own visibility
89
+ tracking cannot see. Without this, a hosted widget can simply never
90
+ render on some page shapes.
91
+
92
+ Args:
93
+ output: The ``output`` object from your server function.
94
+ id: The base id. Your React element reads this output at
95
+ ``f"{id}{suffix}"`` (the default suffix makes that ``"bins_ui"``
96
+ for ``id="bins"``).
97
+ widget: A zero-argument callable returning the widget's markup, e.g.
98
+ ``lambda: ui.input_slider("bins", "Bins", 1, 50, 30)``. Called
99
+ fresh on every render, exactly like the body of any
100
+ ``@render.ui`` function.
101
+ suffix: Appended to ``id`` to form the registered output name.
102
+
103
+ Returns:
104
+ The output name (``f"{id}{suffix}"``) the widget was registered
105
+ under -- pass this straight to your React element's ``id`` prop.
106
+ """
107
+ name = f"{id}{suffix}"
108
+
109
+ @output(id=name, suspend_when_hidden=False)
110
+ @render.ui
111
+ def _() -> "TagChild":
112
+ tag = widget()
113
+ _check_no_id_collision(tag, name)
114
+ return tag
115
+
116
+ return name
bslibreact/_version.py ADDED
@@ -0,0 +1,16 @@
1
+ """The installed package version.
2
+
3
+ Read from installed package metadata, so it always matches what ``pip show``
4
+ reports. The fallback only applies to a source tree imported without being
5
+ installed at all; ``uv sync`` installs the project in editable mode, which
6
+ writes the metadata.
7
+ """
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ try:
12
+ __version__ = version("bslibreact")
13
+ except PackageNotFoundError:
14
+ __version__ = "0.0.0"
15
+
16
+ __all__ = ["__version__"]
@@ -0,0 +1,9 @@
1
+ (function(c){"use strict";let R;function U(e){R=e}function v(){const e=R??window.shinyreact;if(!e)throw new Error("window.shinyreact is not available, and no module was passed to provideShinyReact(). Is the shinyreact page dependency loaded, or did an npm-tier app forget to call provideShinyReact() at startup?");return e}function Y(e,t,n){return v().useShinyInput(e,t,n)}function G(e,t,n){return v().useSetShinyInput(e,t,n)}function J(e){return v().useShinyInputValue(e)}function Z(e,t){return v().useShinyOutputValue(e,t)}function X(e){return v().useShinyOutputStatus(e)}function Q(e){return v().useShinyOutputError(e)}function P(){return v().useShinyInitialized()}function K(){return v().useShinyBusy()}function ee(e,t){return v().useShinyMessageHandler(e,t)}const g=e=>v().ShinyOutput(e),te=e=>v().ImageOutput(e),$=e=>v().ShinyModuleProvider(e);function o(...e){return e.filter(Boolean).join(" ")}var _={exports:{}},w={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var ae=Symbol.for("react.transitional.element"),ne=Symbol.for("react.fragment");function A(e,t,n){var l=null;if(n!==void 0&&(l=""+n),t.key!==void 0&&(l=""+t.key),"key"in t){n={};for(var i in t)i!=="key"&&(n[i]=t[i])}else n=t;return t=n.ref,{$$typeof:ae,type:e,key:l,ref:t!==void 0?t:null,props:n}}w.Fragment=ne,w.jsx=A,w.jsxs=A,_.exports=w;var a=_.exports;const B=c.createContext(void 0);function le({namespace:e,children:t}){return a.jsx(B.Provider,{value:e,children:a.jsx($,{namespace:e,children:t})})}function f(e){const t=c.useContext(B);return t?`${t}-${e}`:e}function m({id:e}){return a.jsx(g,{id:f(e),namespace:null,className:"shiny-html-output"})}const ie=m,se=m,re=m,oe=m,ue=m,ce=m,de=m,be=m,pe=m,me=m,fe=m,he=m,ve=m,xe=m;function k(e){var l,i;if(typeof window>"u")return;const t=(i=(l=window.Shiny)==null?void 0:l.shinyapp)==null?void 0:i.$inputValues;if(!t)return;if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const n=Object.keys(t).find(s=>s.startsWith(`${e}:`));return n===void 0?void 0:t[n]}function ge(e){const t=f(e),[n,l]=c.useState(()=>({id:t,value:k(t)}));let i=n;return n.id!==t&&(i={id:t,value:k(t)},l(i)),c.useEffect(()=>{const s=window.$;if(!s)return;const u=r=>{const b=r;(b==null?void 0:b.name)===t&&l({id:t,value:b.value})};return s(document).on("shiny:inputchanged",u),()=>{s(document).off("shiny:inputchanged",u)}},[t]),i.value}function ye({children:e}){return a.jsx("span",{className:"help-block",children:e})}function je({id:e,width:t="100%",height:n="400px",className:l,click:i}){const s=f(i??"");return a.jsx(g,{id:f(e),namespace:null,className:o("shiny-plot-output html-fill-item",l),style:{width:t,height:n},"data-click-id":i?s:void 0,"data-click-clip":i?"TRUE":void 0})}function Ne({id:e,className:t}){return a.jsx(g,{id:f(e),namespace:null,className:o("shiny-text-output",t)})}function we({id:e,className:t}){return a.jsx(g,{id:f(e),namespace:null,tagName:"pre",className:o("shiny-text-output noplaceholder",t)})}function Se({id:e,className:t,fill:n=!0}){return a.jsx(g,{id:f(e),namespace:null,className:o("datatables html-widget html-widget-output shiny-report-size",n&&"html-fill-item",t),style:{width:"100%",height:"auto"}})}function Ie({id:e,className:t,height:n="auto",width:l="auto"}){const i=f(e);return a.jsx(g,{id:i,namespace:null,className:o("reactable html-widget html-widget-output shiny-report-size html-fill-item",t),style:{width:l,height:n},"data-reactable-output":i})}function Ce({id:e,widgetClass:t,height:n="400px",width:l="100%",className:i}){return a.jsx(g,{id:f(e),namespace:null,className:o(`${t} html-widget html-widget-output shiny-report-size html-fill-item`,i),style:{width:l,height:n}})}function Ee({id:e,className:t,fill:n=!1,inline:l=!1}){return a.jsx(g,{id:f(e),namespace:null,tagName:l?"span":"div",className:o("shiny-html-output",n&&"html-fill-item html-fill-container",t)})}function M({text:e,children:t}){return a.jsxs("bslib-tooltip",{placement:"auto",bsOptions:"[]","data-require-bs-version":"5","data-require-bs-caller":"tooltip()",children:[a.jsx("template",{dangerouslySetInnerHTML:{__html:e}}),t]})}const Te="M20 5C20 4.4 19.6 4 19 4H13C12.4 4 12 3.6 12 3C12 2.4 12.4 2 13 2H21C21.6 2 22 2.4 22 3V11C22 11.6 21.6 12 21 12C20.4 12 20 11.6 20 11V5ZM4 19C4 19.6 4.4 20 5 20H11C11.6 20 12 20.4 12 21C12 21.6 11.6 22 11 22H3C2.4 22 2 21.6 2 21V13C2 12.4 2.4 12 3 12C3.6 12 4 12.4 4 13V19Z";function Oe({fullScreen:e=!1,fill:t=!0,height:n,className:l,id:i,children:s}){const u=c.useId(),r=f(i??"");c.useLayoutEffect(()=>{var p,h;(h=(p=window.bslib)==null?void 0:p.Card)==null||h.initializeAllCards()},[]);const b=n?{height:n}:void 0;return a.jsxs("div",{id:i?r:e?`bslib-card-${u}`:void 0,className:o("card bslib-card bslib-mb-spacing html-fill-container",i&&"bslib-card-input",t&&"html-fill-item",l),"data-bslib-card-init":"","data-full-screen":e?"false":void 0,"data-require-bs-caller":"card()","data-require-bs-version":"5",style:b,children:[s,e&&a.jsx(M,{text:"Expand",children:a.jsx("button",{"aria-expanded":"false","aria-label":"Expand card",className:"bslib-full-screen-enter badge rounded-pill",children:a.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",style:{height:"1em",width:"1em",fill:"currentColor"},"aria-hidden":"true",role:"img",children:a.jsx("path",{d:Te})})})})]})}function Re({className:e,id:t,children:n}){return a.jsx("div",{className:o("card-header bslib-gap-spacing",e),id:t,children:n})}function Pe({fill:e=!0,className:t,style:n,html:l,children:i}){return a.jsx("div",{className:o("card-body bslib-gap-spacing html-fill-item",e&&"html-fill-container",t),style:{marginTop:"auto",marginBottom:"auto",flex:"1 1 auto",...n},...l!==void 0?{dangerouslySetInnerHTML:{__html:l}}:{},children:l===void 0?i:void 0})}function $e({className:e,children:t}){return a.jsx("div",{className:o("card-footer",e),children:t})}function _e({title:e,theme:t="primary",children:n}){return c.useLayoutEffect(()=>{var l,i;(i=(l=window.bslib)==null?void 0:l.Card)==null||i.initializeAllCards()},[]),a.jsx("div",{className:o("card bslib-card bslib-mb-spacing html-fill-item html-fill-container bslib-value-box",`bg-${t}`),"data-bslib-card-init":"","data-require-bs-caller":"card() value_box()","data-require-bs-version":"5 5",children:a.jsx("div",{className:"card-body bslib-gap-spacing html-fill-item html-fill-container",style:{marginTop:"auto",marginBottom:"auto",flex:"1 1 auto"},children:a.jsxs("div",{className:"value-box-area html-fill-item html-fill-container",children:[a.jsx("p",{className:"value-box-title",children:e}),n]})})})}function Ae({colWidths:e,fill:t=!0,className:n,id:l,children:i}){return a.jsx("bslib-layout-columns",{id:l,class:o("bslib-grid grid bslib-mb-spacing",t&&"html-fill-item",n),"col-widths-sm":e.join(","),"data-require-bs-version":"5","data-require-bs-caller":"layout_columns()",children:c.Children.toArray(i).map((s,u)=>a.jsx("div",{className:"bslib-grid-item bslib-gap-spacing html-fill-container",children:s},u))})}function L(e,t,n=!0){const l=P(),[i,s]=c.useState(!1),u=typeof window<"u"&&!!window.Shiny,r=n&&u,[b,p]=c.useState(r);return r!==b&&(p(r),r||s(!1)),c.useEffect(()=>{if(!r||!l)return;const h=e.current,d=window.Shiny;if(!h||!d)return;let x=!1;const j=t(h)??h,y=()=>{x||s(!0)},C=N=>{y(),console.error("bslibreact: binding to Shiny failed",N)};try{d.initializeInputs(j),Promise.resolve(d.bindAll(j)).then(y,C)}catch(N){C(N)}return()=>{x=!0,d.unbindAll(h,!0)}},[r,l]),r?i:!0}function E({sidebar:e,sidebarWidth:t=250,title:n,position:l="left",open:i="open",id:s,collapsibleMobile:u=!0,openMobile:r="closed",border:b,borderRadius:p,sidebarClassName:h,gap:d,padding:x,children:j}){const y=c.useId(),C=f(s??""),N=s?C:`bslib-sidebar-${y}`,V=c.useRef(null),q=c.useRef(null);c.useLayoutEffect(()=>{var D,W;(W=(D=window.bslib)==null?void 0:D.Sidebar)==null||W.initCollapsibleAll()},[]);const F=L(V,()=>q.current,!!s);return a.jsxs("div",{ref:q,className:o("bslib-sidebar-layout bslib-mb-spacing html-fill-item",l==="right"&&"sidebar-right",i==="closed"&&"sidebar-collapsed"),"data-bslib-sidebar-border":b===void 0?void 0:b?"true":"false","data-bslib-sidebar-border-radius":p===void 0?void 0:p?"true":"false","data-bslib-sidebar-init":"TRUE","data-collapsible-desktop":"true","data-collapsible-mobile":u?"true":"false","data-open-desktop":i,"data-open-mobile":r,"data-require-bs-caller":"layout_sidebar()","data-require-bs-version":"5",style:{"--_sidebar-width":`${t}px`},children:[a.jsx("div",{className:"main bslib-gap-spacing html-fill-container",children:a.jsx("main",{className:"bslib-page-main bslib-gap-spacing html-fill-item html-fill-container",children:F&&j})}),a.jsx("aside",{ref:V,id:N,className:o("sidebar",s&&"bslib-sidebar-input",h),hidden:!0,"data-resizable":"",children:a.jsxs("div",{className:"sidebar-content bslib-gap-spacing",style:d!==void 0||x!==void 0?{gap:d,padding:x}:void 0,children:[n!==void 0&&a.jsx("header",{className:"sidebar-title",children:n}),F&&e]})}),a.jsx("button",{className:"collapse-toggle",type:"button",title:"Toggle sidebar","aria-expanded":i==="open"?"true":"false","aria-controls":N,children:a.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 16 16",className:"bi bi-chevron-left collapse-icon",style:{fill:"currentColor",verticalAlign:"-0.125em"},"aria-hidden":"true",role:"img",children:a.jsx("path",{fillRule:"evenodd",d:"M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"})})})]})}const H="bslib-html-height";function S(e,t={},n=!0){c.useLayoutEffect(()=>{let l=document.getElementById(H);const i=n&&!l;i&&(l=document.createElement("style"),l.id=H,l.textContent="html { height: 100%; }",document.head.appendChild(l));const s=document.body,u=s.className,r=s.getAttribute("style");return s.className=e,Object.assign(s.style,t),()=>{s.className=u,r===null?s.removeAttribute("style"):s.setAttribute("style",r),i&&(l==null||l.remove())}},[e,JSON.stringify(t),n])}function Be({title:e,sidebar:t,sidebarWidth:n=250,children:l}){return S("bslib-page-fill bslib-gap-spacing bslib-flow-mobile html-fill-container bslib-page-sidebar",{padding:"0px",gap:"0px"}),a.jsxs(a.Fragment,{children:[a.jsx("div",{className:"navbar navbar-static-top",children:a.jsx("div",{className:"container-fluid",children:a.jsx("h1",{className:"bslib-page-title navbar-brand",children:e})})}),a.jsx(E,{sidebar:t,sidebarWidth:n,border:!1,borderRadius:!1,collapsibleMobile:!1,openMobile:"always",children:l})]})}function ke({padding:e=12,gap:t=12,children:n}){return S("bslib-page-fill bslib-gap-spacing bslib-flow-mobile html-fill-container",{padding:`${e}px`,gap:`${t}px`}),a.jsx(a.Fragment,{children:n})}const z=c.createContext(!1),T="bslib-navbar-collapse";function Me({title:e,bg:t,navbarTheme:n,tabStrip:l,footer:i,sidebar:s,sidebarId:u,sidebarWidth:r=250,sidebarOpen:b="open",fillable:p=!0,children:h}){const d=p||!!s;S(d?o("bslib-page-fill bslib-gap-spacing bslib-flow-mobile html-fill-container bslib-page-navbar",!!s&&"has-page-sidebar"):"bslib-page-navbar",d?{padding:"0px",gap:"0px"}:{},d);const x=c.useRef(null);c.useLayoutEffect(()=>{const y=x.current;if(!(!y||!t))return y.style.setProperty("background-color",t,"important"),()=>{y.style.removeProperty("background-color")}},[t]);const j=a.jsx("div",{className:o("tab-content",p&&"html-fill-item html-fill-container"),children:a.jsx(z.Provider,{value:p,children:h})});return a.jsxs(a.Fragment,{children:[a.jsx("nav",{ref:x,className:o("navbar navbar-default navbar-static-top",n&&"navbar-inverse"),role:"navigation","data-bs-theme":n??"auto",children:a.jsxs("div",{className:"container-fluid",children:[a.jsxs("div",{className:"navbar-header",children:[a.jsxs("button",{type:"button",className:"navbar-toggle collapsed","data-toggle":"collapse","data-target":`#${T}`,"data-bs-toggle":"collapse","data-bs-target":`#${T}`,children:[a.jsx("span",{className:"sr-only visually-hidden",children:"Toggle navigation"}),a.jsx("span",{className:"icon-bar"}),a.jsx("span",{className:"icon-bar"}),a.jsx("span",{className:"icon-bar"})]}),a.jsx("span",{className:"navbar-brand",children:e})]}),a.jsx("div",{className:"navbar-collapse collapse",id:T,children:l})]})}),s?a.jsx("div",{className:o(p&&"html-fill-item html-fill-container"),children:a.jsxs(E,{sidebar:s,sidebarWidth:r,id:u,open:b,openMobile:b,collapsibleMobile:!0,border:!1,borderRadius:!1,children:[j,i]})}):a.jsxs("div",{className:o("container-fluid",p&&"html-fill-item html-fill-container"),children:[j,i]})]})}function Le(e){return"dropdown"in e}function He(e){return e.replace(/\s+/g,"-")}function I(e,t){return`tab-${e}-${He(t)}`}function ze({id:e,panels:t,children:n}){const l=f(e),i=c.useRef(null),s=L(i,r=>r.parentElement);let u=0;return a.jsxs("ul",{ref:i,id:l,className:"nav navbar-nav nav-underline shiny-tab-input",children:[t.map(r=>{if(Le(r)){let p=!1;const h=r.dropdown.map(d=>{const x=u===0;return u++,x&&(p=!0),a.jsx("li",{children:a.jsxs("a",{href:`#${I(e,d.value)}`,"data-toggle":"tab","data-bs-toggle":"tab","data-value":d.value,className:o("dropdown-item",x&&"active"),children:[a.jsx(O,{icon:d.icon}),d.label]})},d.value)});return a.jsxs("li",{className:"dropdown nav-item",children:[a.jsxs("a",{href:"#",className:o("dropdown-toggle nav-link",p&&"active"),"data-toggle":"dropdown","data-bs-toggle":"dropdown","data-value":r.label,children:[a.jsx(O,{icon:r.icon}),r.label,a.jsx("b",{className:"caret"})]}),a.jsx("ul",{className:"dropdown-menu",children:h})]},r.label)}const b=u===0;return u++,a.jsx("li",{className:"nav-item",children:a.jsxs("a",{href:`#${I(e,r.value)}`,"data-toggle":"tab","data-bs-toggle":"tab","data-value":r.value,className:o("nav-link",b&&"active"),children:[a.jsx(O,{icon:r.icon}),r.label]})},r.value)}),s&&n]})}function O({icon:e}){if(!e)return null;const t=/^fa[a-z]? /.test(e)?e:`fas ${e}`,n=t.split(" ").find(l=>l.startsWith("fa-"))??e;return a.jsx("i",{className:`${t} fa-fw`,role:"presentation","aria-label":`${n.replace(/^fa-/,"")} icon`})}function Ve(){return a.jsx("div",{className:"bslib-nav-spacer"})}function qe({children:e}){return a.jsx("li",{className:"bslib-nav-item nav-item form-inline",children:e})}const Fe={"--bslib-navbar-margin":"0"};function De({navId:e,value:t,active:n=!1,fillable:l,children:i}){const s=c.useContext(z),u=l??s,r=o("tab-pane",u&&"html-fill-item html-fill-container bslib-gap-spacing",n&&"active");return a.jsx("div",{className:r,"data-value":t,id:I(e,t),style:u?Fe:void 0,children:i})}const We=Object.freeze(Object.defineProperty({__proto__:null,ActionButton:ie,ActionLink:se,Card:Oe,CardBody:Pe,CardFooter:$e,CardHeader:Re,CheckboxInput:re,DTOutput:Se,DarkModeInput:oe,DownloadButton:ue,FileInput:ce,HelpText:ye,HostedInput:m,ImageOutput:te,LayoutColumns:Ae,LayoutSidebar:E,Module:le,NavItem:qe,NavSpacer:Ve,NavbarTabStrip:ze,NumericInput:de,PageFillable:ke,PageNavbar:Me,PageSidebar:Be,PasswordInput:be,PlotOutput:je,RadioButtons:pe,ReactableOutput:Ie,SelectInput:me,SelectizeInput:fe,ShinyModuleProvider:$,ShinyOutput:g,SliderInput:he,TabPane:De,TextAreaInput:ve,TextInput:xe,TextOutput:Ne,Tooltip:M,UiOutput:Ee,ValueBox:_e,VerbatimTextOutput:we,WidgetOutput:Ce,cx:o,provideShinyReact:U,tabPaneId:I,useHostedInputValue:ge,useNs:f,usePageBody:S,useSetShinyInput:G,useShinyBusy:K,useShinyInitialized:P,useShinyInput:Y,useShinyInputValue:J,useShinyMessageHandler:ee,useShinyOutputError:Q,useShinyOutputStatus:X,useShinyOutputValue:Z},Symbol.toStringTag,{value:"Module"}));window.bslibreact=We})(window.shinyreact.React);
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.5
2
+ Name: bslibreact
3
+ Version: 0.1.0
4
+ Summary: Server helpers for bslibreact, React components that reproduce bslib and Shiny markup
5
+ Project-URL: Homepage, https://github.com/samuelbharti/bslibreact
6
+ Project-URL: Repository, https://github.com/samuelbharti/bslibreact
7
+ Project-URL: Changelog, https://github.com/samuelbharti/bslibreact/blob/main/pkg-py/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/samuelbharti/bslibreact/issues
9
+ Author-email: Samuel Bharti <samuelbharti.io@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bslib,react,shiny,shinyreact
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: htmltools>=0.7.0
24
+ Requires-Dist: shiny>=1.8.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # bslibreact (Python)
28
+
29
+ [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)
30
+
31
+ Server helpers for [bslibreact](https://github.com/samuelbharti/bslibreact):
32
+ a React component kit that reproduces the exact markup `bslib` and Shiny
33
+ render, for apps built with [shinyreact](https://github.com/posit-dev/shinyreact)'s
34
+ `ui.tsx` pattern. This package is the Python half. It ships no UI of its own;
35
+ the components live in the npm package `bslibreact`.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install bslibreact shinyreact
41
+ ```
42
+
43
+ ## Use
44
+
45
+ ```python
46
+ from pathlib import Path
47
+ from shiny import App, render, ui
48
+ from shinyreact import page_react
49
+ from bslibreact import page_deps, host_input
50
+
51
+ app_theme = (
52
+ ui.Theme.from_brand(Path(__file__).parent)
53
+ if (Path(__file__).parent / "_brand.yml").exists()
54
+ else ui.Theme()
55
+ )
56
+ app_ui = page_react(page_deps(app_theme), title="Demo", theme=app_theme)
57
+
58
+
59
+ def server(input, output, session):
60
+ host_input(output, "bins", lambda: ui.input_slider("bins", "Bins", 1, 50, 30))
61
+
62
+
63
+ app = App(app_ui, server)
64
+ ```
65
+
66
+ `page_deps()` attaches the CSS and JS that bslibreact's React components
67
+ need at runtime (ion.rangeSlider, selectize, and friends), the same way a
68
+ plain bslib page would pick them up automatically. A client with no build
69
+ step also attaches `bslibreact_dep()`, the prebuilt component bundle it
70
+ reads as `window.bslibreact`. `host_input()` mounts one server-rendered
71
+ Shiny widget (here, a slider) inside a React page and hands back the output
72
+ id your `<SliderInput id="bins_ui" />` element should read.
73
+
74
+ See the [root README](https://github.com/samuelbharti/bslibreact#readme) for
75
+ the full picture across R, Python and JS, and `docs/` in the repository for
76
+ the porting guide and the markup catalog spec this package's catalog
77
+ renderer implements.
@@ -0,0 +1,11 @@
1
+ bslibreact/__init__.py,sha256=JgmdxsGIrsm1JWq7VcRyyyKhKLgWM3bLDaAnaTb0GC0,600
2
+ bslibreact/_catalog.py,sha256=eJRcEMNr2LAbkXKYAA-LWIMyz6F0zCqWM1GwSJ8xV9U,15925
3
+ bslibreact/_dep.py,sha256=ZbI-9E84MnhsT-ut4KsSI2Bj0gE0ODcyJYW5AMCvruQ,2899
4
+ bslibreact/_deps.py,sha256=cwklhhPJqDVM72UuRVlPNfS62aviNSqiJmVmt0AVm7E,4423
5
+ bslibreact/_host.py,sha256=iSSuRaHuXPA9WoX27h1l-HzqiBhvQr9QqkwznQbK-4M,4851
6
+ bslibreact/_version.py,sha256=GiUc8okB8Dx7_XWfs1lw7ZuslpgQ5GMwxMlaaDmhI2M,477
7
+ bslibreact/www/bslibreact.js,sha256=ugB3coc6_N0Pl81-MI4ShbttWlMs3vLhi0wFkFbuJXw,15622
8
+ bslibreact-0.1.0.dist-info/METADATA,sha256=DDVszUjUhWsz93_kHnf6X9gCTGxAWxJlPdjCNMzgd_M,3041
9
+ bslibreact-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ bslibreact-0.1.0.dist-info/licenses/LICENSE,sha256=u-yPv1bpA5DwVMUswncGzTfXyQQv_RvF5fqRoaKL32w,1070
11
+ bslibreact-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samuel Bharti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.