diff --git a/Makefile b/Makefile index e4460aff..1b6d8461 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,11 @@ FAICONS_WHEEL = faicons-$(FAICONS_VERSION)-py3-none-any.whl PLOTNINE_VERSION=0.0.0 PLOTNINE_WHEEL=plotnine-$(PLOTNINE_VERSION)-py3-none-any.whl +# libsass is built in gadenbuie/libsass-python +# NOTE: Update https://github.com/gadenbuie/libsass-python/blob/dev/.github/workflows/pyodide.yml +# Pyodide, Emscripten, or Python versions change here. +LIBSASS_WHEEL=libsass-0.23.0-cp312-abi3-pyodide_2024_0_wasm32.whl + VENV = venv PYBIN = $(VENV)/bin @@ -169,7 +174,8 @@ pyodide_packages_local: $(BUILD_DIR)/shinylive/pyodide/$(HTMLTOOLS_WHEEL) \ $(BUILD_DIR)/shinylive/pyodide/$(SHINY_WHEEL) \ $(BUILD_DIR)/shinylive/pyodide/$(SHINYWIDGETS_WHEEL) \ $(BUILD_DIR)/shinylive/pyodide/$(FAICONS_WHEEL) \ - $(BUILD_DIR)/shinylive/pyodide/$(PLOTNINE_WHEEL) + $(BUILD_DIR)/shinylive/pyodide/$(PLOTNINE_WHEEL) \ + $(BUILD_DIR)/shinylive/pyodide/$(LIBSASS_WHEEL) $(BUILD_DIR)/shinylive/pyodide/$(HTMLTOOLS_WHEEL): $(PACKAGE_DIR)/$(HTMLTOOLS_WHEEL) mkdir -p $(BUILD_DIR)/shinylive/pyodide @@ -200,6 +206,11 @@ $(BUILD_DIR)/shinylive/pyodide/$(PLOTNINE_WHEEL): $(PACKAGE_DIR)/$(PLOTNINE_WHEE rm -f $(BUILD_DIR)/shinylive/pyodide/plotnine*.whl cp $(PACKAGE_DIR)/$(PLOTNINE_WHEEL) $(BUILD_DIR)/shinylive/pyodide/$(PLOTNINE_WHEEL) +$(BUILD_DIR)/shinylive/pyodide/$(LIBSASS_WHEEL): $(PACKAGE_DIR)/$(LIBSASS_WHEEL) + mkdir -p $(BUILD_DIR)/shinylive/pyodide + rm -f $(BUILD_DIR)/shinylive/pyodide/libsass*.whl + cp $(PACKAGE_DIR)/$(LIBSASS_WHEEL) $(BUILD_DIR)/shinylive/pyodide/$(LIBSASS_WHEEL) + $(BUILD_DIR)/export_template/index.html: export_template/index.html mkdir -p $(BUILD_DIR)/export_template cp export_template/index.html $(BUILD_DIR)/export_template/index.html @@ -301,6 +312,10 @@ $(PACKAGE_DIR)/$(PLOTNINE_WHEEL): $(PYBIN) $(PACKAGE_DIR)/plotnine $(PYBIN)/pip install -e $(PACKAGE_DIR)/plotnine[build] . $(PYBIN)/activate && cd $(PACKAGE_DIR)/plotnine && make dist && mv dist/*.whl ../$(PLOTNINE_WHEEL) +$(PACKAGE_DIR)/$(LIBSASS_WHEEL): $(PYBIN) $(PACKAGE_DIR)/$(LIBSASS_WHEEL) + rm -f $(PACKAGE_DIR)/libsass*.whl + curl --fail -L https://pkg.garrickadenbuie.com/libsass-python/$(LIBSASS_WHEEL) -o $(PACKAGE_DIR)/$(LIBSASS_WHEEL) + ## Update the shinylive_lock.json file, based on shinylive_requirements.json update_packages_lock: $(PYBIN) $(BUILD_DIR)/shinylive/pyodide $(PYBIN)/pip install -r requirements-dev.txt diff --git a/examples/index.json b/examples/index.json index 26a81105..90cdd137 100644 --- a/examples/index.json +++ b/examples/index.json @@ -19,7 +19,8 @@ "insert_ui", "input_update", "extra_packages", - "fetch" + "fetch", + "brand" ] }, { diff --git a/examples/python/brand/Monda-OFL.txt b/examples/python/brand/Monda-OFL.txt new file mode 100644 index 00000000..4a58f717 --- /dev/null +++ b/examples/python/brand/Monda-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Monda Project Authors (https://github.com/googlefonts/mondaFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/python/brand/Monda.ttf b/examples/python/brand/Monda.ttf new file mode 100644 index 00000000..458b865a Binary files /dev/null and b/examples/python/brand/Monda.ttf differ diff --git a/examples/python/brand/_brand.yml b/examples/python/brand/_brand.yml new file mode 100644 index 00000000..2dd320bf --- /dev/null +++ b/examples/python/brand/_brand.yml @@ -0,0 +1,100 @@ +# Learn more about brand.yml at https://posit-dev.github.io/brand-yml +meta: + name: + full: "Retro Arcade Brand" + short: "RetroArc" + link: + home: https://retroarc.example.com + mastodon: https://mastodon.social/@retroarc + github: https://github.com/retroarc + linkedin: https://linkedin.com/company/retroarc + twitter: https://twitter.com/retroarc + facebook: https://facebook.com/retroarc + +# This example doesn't include a brand logo +# See: https://posit-dev.github.io/brand-yml/brand/logo.html +# logo: brand-yml.png + +color: + palette: + pink: "#E83E8C" + blue: "#007BFF" + cyan: "#17A2B8" + teal: "#20C997" + green: "#28A745" + yellow: "#FFD700" + orange: "#FF7F50" + red: "#FF3333" + purple: "#6F42C1" + indigo: "#6610F2" + black: "#1A1A1A" + white: "#F8F8F8" + foreground: black + background: white + primary: purple + success: green + info: cyan + warning: yellow + danger: orange + light: white + dark: black + +typography: + fonts: + - family: Quantico + source: google + weight: [700] + style: [normal, italic] + display: swap + - family: Monda + source: file + files: + - path: Monda.ttf + weight: 400..700 + - family: Share Tech Mono + source: bunny + weight: 400 + style: normal + display: swap + base: + family: Monda + size: 17px + weight: 400 + line-height: 1.5 + headings: + family: Quantico + weight: 400 + line-height: 1.2 + style: normal + monospace: + family: Share Tech Mono + size: 0.9em + weight: 400 + monospace-inline: + family: Share Tech Mono + # size: 0.9em + weight: 400 + color: yellow + background-color: "#1a1a1add" + monospace-block: + family: Share Tech Mono + size: 1.1em + weight: 400 + color: green + background-color: black + line-height: 1.4 + link: + weight: 400 + background-color: purple + color: white + decoration: "underline" + +defaults: + bootstrap: + defaults: + my-pink: "$brand-pink" + shiny: + theme: + preset: shiny + rules: | + .navbar-brand { color: $my-pink } diff --git a/examples/python/brand/_colors.scss b/examples/python/brand/_colors.scss new file mode 100644 index 00000000..618ba867 --- /dev/null +++ b/examples/python/brand/_colors.scss @@ -0,0 +1,116 @@ +// https://github.com/twbs/bootstrap/blob/v5.3.3/site/assets/scss/_colors.scss + +.bd-blue-100 { color: color-contrast($blue-100); background-color: $blue-100; } +.bd-blue-200 { color: color-contrast($blue-200); background-color: $blue-200; } +.bd-blue-300 { color: color-contrast($blue-300); background-color: $blue-300; } +.bd-blue-400 { color: color-contrast($blue-400); background-color: $blue-400; } +.bd-blue-500 { color: color-contrast($blue-500); background-color: $blue-500; } +.bd-blue-600 { color: color-contrast($blue-600); background-color: $blue-600; } +.bd-blue-700 { color: color-contrast($blue-700); background-color: $blue-700; } +.bd-blue-800 { color: color-contrast($blue-800); background-color: $blue-800; } +.bd-blue-900 { color: color-contrast($blue-900); background-color: $blue-900; } + +.bd-indigo-100 { color: color-contrast($indigo-100); background-color: $indigo-100; } +.bd-indigo-200 { color: color-contrast($indigo-200); background-color: $indigo-200; } +.bd-indigo-300 { color: color-contrast($indigo-300); background-color: $indigo-300; } +.bd-indigo-400 { color: color-contrast($indigo-400); background-color: $indigo-400; } +.bd-indigo-500 { color: color-contrast($indigo-500); background-color: $indigo-500; } +.bd-indigo-600 { color: color-contrast($indigo-600); background-color: $indigo-600; } +.bd-indigo-700 { color: color-contrast($indigo-700); background-color: $indigo-700; } +.bd-indigo-800 { color: color-contrast($indigo-800); background-color: $indigo-800; } +.bd-indigo-900 { color: color-contrast($indigo-900); background-color: $indigo-900; } + +.bd-purple-100 { color: color-contrast($purple-100); background-color: $purple-100; } +.bd-purple-200 { color: color-contrast($purple-200); background-color: $purple-200; } +.bd-purple-300 { color: color-contrast($purple-300); background-color: $purple-300; } +.bd-purple-400 { color: color-contrast($purple-400); background-color: $purple-400; } +.bd-purple-500 { color: color-contrast($purple-500); background-color: $purple-500; } +.bd-purple-600 { color: color-contrast($purple-600); background-color: $purple-600; } +.bd-purple-700 { color: color-contrast($purple-700); background-color: $purple-700; } +.bd-purple-800 { color: color-contrast($purple-800); background-color: $purple-800; } +.bd-purple-900 { color: color-contrast($purple-900); background-color: $purple-900; } + +.bd-pink-100 { color: color-contrast($pink-100); background-color: $pink-100; } +.bd-pink-200 { color: color-contrast($pink-200); background-color: $pink-200; } +.bd-pink-300 { color: color-contrast($pink-300); background-color: $pink-300; } +.bd-pink-400 { color: color-contrast($pink-400); background-color: $pink-400; } +.bd-pink-500 { color: color-contrast($pink-500); background-color: $pink-500; } +.bd-pink-600 { color: color-contrast($pink-600); background-color: $pink-600; } +.bd-pink-700 { color: color-contrast($pink-700); background-color: $pink-700; } +.bd-pink-800 { color: color-contrast($pink-800); background-color: $pink-800; } +.bd-pink-900 { color: color-contrast($pink-900); background-color: $pink-900; } + +.bd-red-100 { color: color-contrast($red-100); background-color: $red-100; } +.bd-red-200 { color: color-contrast($red-200); background-color: $red-200; } +.bd-red-300 { color: color-contrast($red-300); background-color: $red-300; } +.bd-red-400 { color: color-contrast($red-400); background-color: $red-400; } +.bd-red-500 { color: color-contrast($red-500); background-color: $red-500; } +.bd-red-600 { color: color-contrast($red-600); background-color: $red-600; } +.bd-red-700 { color: color-contrast($red-700); background-color: $red-700; } +.bd-red-800 { color: color-contrast($red-800); background-color: $red-800; } +.bd-red-900 { color: color-contrast($red-900); background-color: $red-900; } + +.bd-orange-100 { color: color-contrast($orange-100); background-color: $orange-100; } +.bd-orange-200 { color: color-contrast($orange-200); background-color: $orange-200; } +.bd-orange-300 { color: color-contrast($orange-300); background-color: $orange-300; } +.bd-orange-400 { color: color-contrast($orange-400); background-color: $orange-400; } +.bd-orange-500 { color: color-contrast($orange-500); background-color: $orange-500; } +.bd-orange-600 { color: color-contrast($orange-600); background-color: $orange-600; } +.bd-orange-700 { color: color-contrast($orange-700); background-color: $orange-700; } +.bd-orange-800 { color: color-contrast($orange-800); background-color: $orange-800; } +.bd-orange-900 { color: color-contrast($orange-900); background-color: $orange-900; } + +.bd-yellow-100 { color: color-contrast($yellow-100); background-color: $yellow-100; } +.bd-yellow-200 { color: color-contrast($yellow-200); background-color: $yellow-200; } +.bd-yellow-300 { color: color-contrast($yellow-300); background-color: $yellow-300; } +.bd-yellow-400 { color: color-contrast($yellow-400); background-color: $yellow-400; } +.bd-yellow-500 { color: color-contrast($yellow-500); background-color: $yellow-500; } +.bd-yellow-600 { color: color-contrast($yellow-600); background-color: $yellow-600; } +.bd-yellow-700 { color: color-contrast($yellow-700); background-color: $yellow-700; } +.bd-yellow-800 { color: color-contrast($yellow-800); background-color: $yellow-800; } +.bd-yellow-900 { color: color-contrast($yellow-900); background-color: $yellow-900; } + +.bd-green-100 { color: color-contrast($green-100); background-color: $green-100; } +.bd-green-200 { color: color-contrast($green-200); background-color: $green-200; } +.bd-green-300 { color: color-contrast($green-300); background-color: $green-300; } +.bd-green-400 { color: color-contrast($green-400); background-color: $green-400; } +.bd-green-500 { color: color-contrast($green-500); background-color: $green-500; } +.bd-green-600 { color: color-contrast($green-600); background-color: $green-600; } +.bd-green-700 { color: color-contrast($green-700); background-color: $green-700; } +.bd-green-800 { color: color-contrast($green-800); background-color: $green-800; } +.bd-green-900 { color: color-contrast($green-900); background-color: $green-900; } + +.bd-teal-100 { color: color-contrast($teal-100); background-color: $teal-100; } +.bd-teal-200 { color: color-contrast($teal-200); background-color: $teal-200; } +.bd-teal-300 { color: color-contrast($teal-300); background-color: $teal-300; } +.bd-teal-400 { color: color-contrast($teal-400); background-color: $teal-400; } +.bd-teal-500 { color: color-contrast($teal-500); background-color: $teal-500; } +.bd-teal-600 { color: color-contrast($teal-600); background-color: $teal-600; } +.bd-teal-700 { color: color-contrast($teal-700); background-color: $teal-700; } +.bd-teal-800 { color: color-contrast($teal-800); background-color: $teal-800; } +.bd-teal-900 { color: color-contrast($teal-900); background-color: $teal-900; } + +.bd-cyan-100 { color: color-contrast($cyan-100); background-color: $cyan-100; } +.bd-cyan-200 { color: color-contrast($cyan-200); background-color: $cyan-200; } +.bd-cyan-300 { color: color-contrast($cyan-300); background-color: $cyan-300; } +.bd-cyan-400 { color: color-contrast($cyan-400); background-color: $cyan-400; } +.bd-cyan-500 { color: color-contrast($cyan-500); background-color: $cyan-500; } +.bd-cyan-600 { color: color-contrast($cyan-600); background-color: $cyan-600; } +.bd-cyan-700 { color: color-contrast($cyan-700); background-color: $cyan-700; } +.bd-cyan-800 { color: color-contrast($cyan-800); background-color: $cyan-800; } +.bd-cyan-900 { color: color-contrast($cyan-900); background-color: $cyan-900; } + +.bd-gray-100 { color: color-contrast($gray-100); background-color: $gray-100; } +.bd-gray-200 { color: color-contrast($gray-200); background-color: $gray-200; } +.bd-gray-300 { color: color-contrast($gray-300); background-color: $gray-300; } +.bd-gray-400 { color: color-contrast($gray-400); background-color: $gray-400; } +.bd-gray-500 { color: color-contrast($gray-500); background-color: $gray-500; } +.bd-gray-600 { color: color-contrast($gray-600); background-color: $gray-600; } +.bd-gray-700 { color: color-contrast($gray-700); background-color: $gray-700; } +.bd-gray-800 { color: color-contrast($gray-800); background-color: $gray-800; } +.bd-gray-900 { color: color-contrast($gray-900); background-color: $gray-900; } + +.bd-white { color: color-contrast($white); background-color: $white; border: 2px solid $body-color;} +.bd-black { color: color-contrast($black); background-color: $black; } +.bd-foreground { color: $body-bg; background-color: $body-color; } +.bd-background { color: $body-color; background-color: $body-bg; border: 2px solid $body-color;} \ No newline at end of file diff --git a/examples/python/brand/about.txt b/examples/python/brand/about.txt new file mode 100644 index 00000000..9b2d5023 --- /dev/null +++ b/examples/python/brand/about.txt @@ -0,0 +1,2 @@ +Branded Theming +Using brand.yml diff --git a/examples/python/brand/app.py b/examples/python/brand/app.py new file mode 100644 index 00000000..9a2a2f15 --- /dev/null +++ b/examples/python/brand/app.py @@ -0,0 +1,349 @@ +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +from shiny import App, render, ui +from shiny.ui._theme_brand import bootstrap_colors + +# Learn more: https://posit-dev.github.io/brand-yml +theme = ui.Theme.from_brand(__file__) + +# theme = ui.Theme() # Uncomment this line to compare with the standard Shiny theme + +theme.add_rules((Path(__file__).parent / "_colors.scss").read_text()) + +app_ui = ui.page_navbar( + ui.nav_panel( + "Input Output Demo", + ui.layout_sidebar( + ui.sidebar( + ui.input_slider("slider1", "Numeric Slider Input", 0, 11, 11), + ui.input_numeric("numeric1", "Numeric Input Widget", 30), + ui.input_date("date1", "Date Input Component", value="2024-01-01"), + ui.input_switch("switch1", "Binary Switch Input", True), + ui.input_radio_buttons( + "radio1", + "Radio Button Group", + choices=["Option A", "Option B", "Option C", "Option D"], + ), + ui.input_action_button("action1", "Action Button"), + ), + ui.div( + "This example uses ", + ui.tags.a("brand.yml", href="https://posit-dev.github.io/brand-yml"), + " for a brand-themed app. Try editing the ", + ui.tags.code("_brand.yml"), + " file to change the theme.", + class_="alert alert-info", + ), + ui.layout_columns( + ui.value_box( + "Metric 1", + "100", + theme="primary", + ), + ui.value_box( + "Metric 2", + "200", + theme="secondary", + ), + ui.value_box( + "Metric 3", + "300", + theme="info", + ), + ), + ui.card( + ui.card_header("Plot Output"), + ui.output_plot("plot1"), + ), + ui.card( + ui.card_header("Text Output"), + ui.output_text_verbatim("out_text1"), + ), + ), + ), + ui.nav_panel( + "Widget Gallery", + ui.layout_column_wrap( + ui.card( + ui.card_header("Button Variants"), + ui.input_action_button("btn1", "Default"), + ui.input_action_button("btn2", "Primary", class_="btn-primary"), + ui.input_action_button("btn3", "Secondary", class_="btn-secondary"), + ui.input_action_button("btn4", "Info", class_="btn-info"), + ui.input_action_button("btn5", "Success", class_="btn-success"), + ui.input_action_button("btn6", "Warning", class_="btn-warning"), + ui.input_action_button("btn7", "Danger", class_="btn-danger"), + ), + ui.card( + ui.card_header("Radio Button Examples"), + ui.input_radio_buttons( + "radio2", + "Standard Radio Group", + ["Selection 1", "Selection 2", "Selection 3"], + ), + ui.input_radio_buttons( + "radio3", + "Inline Radio Group", + ["Option 1", "Option 2", "Option 3"], + inline=True, + ), + ), + ui.card( + ui.card_header("Checkbox Examples"), + ui.input_checkbox_group( + "check1", + "Standard Checkbox Group", + ["Item 1", "Item 2", "Item 3"], + ), + ui.input_checkbox_group( + "check2", + "Inline Checkbox Group", + ["Choice A", "Choice B", "Choice C"], + inline=True, + ), + ), + ui.card( + ui.card_header("Select Input Widgets"), + ui.input_selectize( + "select1", + "Selectize Input", + ["Selection A", "Selection B", "Selection C"], + ), + ui.input_select( + "select2", + "Multiple Select Input", + ["Item X", "Item Y", "Item Z"], + multiple=True, + ), + ), + ui.card( + ui.card_header("Text Input Widgets"), + ui.input_text("text1", "Text Input"), + ui.input_text_area( + "textarea1", + "Text Area Input", + "Default text content for the text area widget", + ), + ui.input_password("password1", "Password Input"), + ), + width=300, + heights_equal=False, + ), + ), + ui.nav_panel( + "Colors", + ui.fill.as_fill_item( + ui.div( + ui.div(ui.output_ui("ui_colors"), class_="container-sm"), + class_="overflow-y-auto", + ) + ), + ), + ui.nav_panel( + "Documentation", + ui.fill.as_fill_item( + ui.div( + ui.div( + ui.markdown( + """ + _Just in case it isn't obvious, this text was written by an LLM._ + + # Component Documentation + + The Shiny for Python framework, available at [shiny.posit.co/py](https://shiny.posit.co/py/), + provides a comprehensive set of UI components for building interactive web applications. These + components are designed with **consistency and usability** in mind, making it easier for + developers to create professional-grade applications. + + Our framework implements the `ui.page_navbar()` component as the primary navigation structure, + allowing for intuitive organization of content across multiple views. Each view can contain + various input and output elements, managed through the `ui.card()` container system. + + ## Component Architecture + + *The architecture of our application framework* emphasizes modularity and reusability. Key + components like `ui.value_box()` and `ui.layout_column_wrap()` work together to create + structured, responsive layouts that adapt to different screen sizes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentImplementationUse CaseStatus
Value Boxui.value_box()Metric DisplayProduction Ready
Cardui.card()Content ContainerProduction Ready
Layoutui.layout_column_wrap()Component OrganizationProduction Ready
Navigationui.page_navbar()Page StructureProduction Ready
+ + ## Implementation Best Practices + + When implementing components, maintain consistent patterns in your code. Use the + `@render` decorators for output functions and follow the reactive programming model + with `@reactive.effect` for side effects. + + Error handling should be implemented at both the UI and server levels. For input + validation, use the `req()` function to ensure all required values are present + before processing. + + ## Corporate Brand Guidelines + + Effective corporate brand guidelines should accomplish several key objectives: + + 1. **Visual Consistency**: Establish a clear color palette using our theming system. + Primary colors should be defined using `class_="btn-primary"` and similar Bootstrap + classes. + + 2. *Typography Standards*: Maintain consistent font usage across all text elements. + Headers should use the built-in styling provided by the `ui.card_header()` component. + + 3. `Component Styling`: Apply consistent styling to UI elements such as buttons, + cards, and value boxes. Use the theme parameter in components like + `ui.value_box(theme="primary")`. + + 4. **Layout Principles**: Follow a grid-based layout system using + `ui.layout_column_wrap()` with appropriate width parameters to ensure consistent + spacing and alignment. + + 5. *Responsive Design*: Implement layouts that adapt gracefully to different screen + sizes using the `fillable` parameter in page components. + + Remember that brand guidelines should serve as a framework for consistency while + remaining flexible enough to accommodate future updates and modifications to the + application interface. + """ + ), + class_="container-sm ", + ), + class_="overflow-y-auto", + ) + ), + ), + ui.nav_spacer(), + ui.nav_control(ui.input_dark_mode(id="color_mode")), + title="brand.yml Demo", + fillable=True, + theme=theme, +) + + +def server(input, output, session): + @render.plot + def plot1(): + colors = { + "foreground": "#000000", + "background": "#FFFFFF", + "primary": "#007BC2", + } + + if hasattr(theme, "brand") and theme.brand.color: + colors.update(theme.brand.color.to_dict("theme")) + + if input.color_mode() == "dark": + bg = colors["foreground"] + fg = colors["background"] + colors.update({"foreground": fg, "background": bg}) + + x = np.linspace(0, input.numeric1(), 100) + y = np.sin(x) * input.slider1() + fig, ax = plt.subplots(facecolor=colors["background"]) + ax.plot(x, y, color=colors["primary"]) + ax.set_title("Sine Wave Output", color=colors["foreground"]) + ax.set_facecolor(colors["background"]) + ax.tick_params(colors=colors["foreground"]) + for spine in ax.spines.values(): + spine.set_edgecolor(colors["foreground"]) + spine.set_alpha(0.25) + return fig + + @render.text + def out_text1(): + return "\n".join( + ["def example_function():", ' return "Function output text"'] + ) + + @render.ui + def ui_colors(): + colors = [] + # Replicates: https://getbootstrap.com/docs/5.3/customize/color/#all-colors + # Source: https://github.com/twbs/bootstrap/blob/6e1f75f4/site/content/docs/5.3/customize/color.md?plain=1#L395-L409 + for color in ["gray", *bootstrap_colors]: + if color in ["white", "black"]: + continue + + colors += [ + ui.div( + ui.div(color, class_=f"p-3 mb-2 position-relative bd-{color}-500"), + *[ + ui.div(f"{color}-{r}", class_=f"p-3 bd-{color}-{r}") + for r in range(100, 1000, 100) + ], + class_="mb-3", + ) + ] + + return ui.TagList( + ui.div( + *[ + ui.div( + ui.div( + color, class_=f"p-3 mb-2 position-relative text-bg-{color}" + ), + class_="col-md-3 mb-3", + ) + for color in [ + "primary", + "secondary", + "dark", + "light", + "info", + "success", + "warning", + "danger", + ] + ], + class_="row font-monospace", + ), + ui.div( + *[ + ui.div( + ui.div(color, class_=f"p-3 mb-2 position-relative bd-{color}"), + class_="col-md-3 mb-3", + ) + for color in ["black", "white", "foreground", "background"] + ], + class_="row font-monospace", + ), + ui.layout_column_wrap(*colors, class_="font-monospace"), + ) + + +app = App(app_ui, server) diff --git a/examples/python/brand/requirements.txt b/examples/python/brand/requirements.txt new file mode 100644 index 00000000..430504d8 --- /dev/null +++ b/examples/python/brand/requirements.txt @@ -0,0 +1,3 @@ +shiny[theme] +matplotlib +numpy diff --git a/examples/python/extra_packages/requirements.txt b/examples/python/extra_packages/requirements.txt index 5030efdb..591fc53b 100644 --- a/examples/python/extra_packages/requirements.txt +++ b/examples/python/extra_packages/requirements.txt @@ -6,4 +6,5 @@ # wheel file. isodate attrs==21.4.0 +httpx [socks,http2] https://files.pythonhosted.org/packages/ca/80/7c0cad11bd99985cfe7c09427ee0b4f9bd6b048bd13d4ffb32c6db237dfb/tabulate-0.8.9-py3-none-any.whl diff --git a/package-lock.json b/package-lock.json index 360d3f1b..12254e1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shinylive", - "version": "0.7.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shinylive", - "version": "0.7.0", + "version": "0.9.0", "license": "MIT", "devDependencies": { "@codemirror/autocomplete": "^6.4.2", @@ -15,7 +15,9 @@ "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-python": "^6.1.1", + "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.7.0", + "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.6.0", "@codemirror/legacy-modes": "^6.3.1", "@codemirror/lint": "^6.1.1", @@ -832,6 +834,20 @@ "@lezer/python": "^1.1.4" } }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, "node_modules/@codemirror/lang-sql": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.7.0.tgz", @@ -846,6 +862,21 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, "node_modules/@codemirror/language": { "version": "6.9.3", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.3.tgz", @@ -2130,10 +2161,11 @@ } }, "node_modules/@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } @@ -2148,6 +2180,30 @@ "@lezer/lr": "^1.0.0" } }, + "node_modules/@lezer/sass": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.7.tgz", + "integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", diff --git a/package.json b/package.json index c4750a0d..a637909f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-python": "^6.1.1", + "@codemirror/lang-sass": "^6.0.2", "@codemirror/lang-sql": "^6.7.0", + "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.6.0", "@codemirror/legacy-modes": "^6.3.1", "@codemirror/lint": "^6.1.1", diff --git a/scripts/build_examples_json.ts b/scripts/build_examples_json.ts index dfae2a3f..f5a52f4c 100644 --- a/scripts/build_examples_json.ts +++ b/scripts/build_examples_json.ts @@ -22,7 +22,7 @@ export default function buildExamples(examplesDir: string, buildDir: string) { console.log("Regenerating examples.json"); const ordering: ExampleCategoryIndexJson[] = JSON.parse( - fs.readFileSync(orderingFile).toString() + fs.readFileSync(orderingFile).toString(), ); function parseApp(exampleDir: string, engine: string) { @@ -30,7 +30,7 @@ export default function buildExamples(examplesDir: string, buildDir: string) { if (!fs.existsSync(appPath)) { throw new Error( - `The requested example directory: ${appPath} does not exist. Check spelling.` + `The requested example directory: ${appPath} does not exist. Check spelling.`, ); } @@ -59,10 +59,25 @@ export default function buildExamples(examplesDir: string, buildDir: string) { return true; }) .sort((a: string, b: string) => { - // Sort files, with "app.py" first, and other files in normal sorted - // order. - if (a === "app.py" || a === "app.R" || a === "server.R") return -1; - if (b === "app.py" || b === "app.R" || b === "server.R") return 1; + // Sort files, with app related files first, followed by common + // support files, other files in normal sorted order. + const primary = ["app.py", "app.R", "server.R"]; + const secondary = ["_brand.yml", "requirements.txt"]; + + const aPrimary = primary.includes(a); + const bPrimary = primary.includes(b); + if (aPrimary && !bPrimary) return -1; + if (!aPrimary && bPrimary) return 1; + + const aIsScript = a.endsWith(".py") || a.endsWith(".R"); + const bIsScript = b.endsWith(".py") || b.endsWith(".R"); + if (aIsScript && !bIsScript) return -1; + if (!aIsScript && bIsScript) return 1; + + const aSecondary = secondary.includes(a); + const bSecondary = secondary.includes(b); + if (aSecondary && !bSecondary) return -1; + if (!aSecondary && bSecondary) return 1; if (a < b) return -1; if (a > b) return 1; @@ -101,8 +116,8 @@ export default function buildExamples(examplesDir: string, buildDir: string) { }), })), null, - 2 - ) + 2, + ), ); } diff --git a/shinylive_lock.json b/shinylive_lock.json index 6c3a2853..cf32ef1d 100644 --- a/shinylive_lock.json +++ b/shinylive_lock.json @@ -512,5 +512,16 @@ "imports": [ "narwhals" ] + }, + "libsass": { + "name": "libsass", + "version": "0.23.0", + "filename": "libsass-0.23.0-cp312-abi3-pyodide_2024_0_wasm32.whl", + "sha256": null, + "url": null, + "depends": [], + "imports": [ + "libsass" + ] } } \ No newline at end of file diff --git a/shinylive_requirements.json b/shinylive_requirements.json index d4f0a6a9..21369ece 100644 --- a/shinylive_requirements.json +++ b/shinylive_requirements.json @@ -23,5 +23,11 @@ { "name": "siuba", "source": "pypi", "version": "latest" }, { "name": "palmerpenguins", "source": "pypi", "version": "latest" }, { "name": "black", "source": "pypi", "version": "latest" }, - { "name": "qrcode", "source": "pypi", "version": "latest" } + { "name": "qrcode", "source": "pypi", "version": "latest" }, + { + "name": "libsass", + "source": "local", + "version": "latest", + "comment": "Built for pyodide in https://github.com/gadenbuie/libsass-python/tree/dev" + } ] diff --git a/src/Components/codeMirror/extensions.ts b/src/Components/codeMirror/extensions.ts index e7df7267..5bf8e8ef 100644 --- a/src/Components/codeMirror/extensions.ts +++ b/src/Components/codeMirror/extensions.ts @@ -15,7 +15,9 @@ import { css } from "@codemirror/lang-css"; import { html } from "@codemirror/lang-html"; import { javascript } from "@codemirror/lang-javascript"; import { python } from "@codemirror/lang-python"; +import { sass } from "@codemirror/lang-sass"; import { sql } from "@codemirror/lang-sql"; +import { yaml } from "@codemirror/lang-yaml"; import { StreamLanguage, bracketMatching, @@ -117,6 +119,8 @@ const LANG_EXTENSIONS: Record Extension> = { html: html, css: css, sql: sql, + sass: sass, + yaml: yaml, r: () => StreamLanguage.define(r), }; diff --git a/src/hooks/usePyodide.tsx b/src/hooks/usePyodide.tsx index fc26fe7b..59e9fadd 100644 --- a/src/hooks/usePyodide.tsx +++ b/src/hooks/usePyodide.tsx @@ -381,6 +381,7 @@ async def _install_requirements_from_dir(dir: str) -> None: import re import micropip import sys + import importlib.metadata files = os.listdir(dir) if "requirements.txt" not in files: @@ -390,6 +391,8 @@ async def _install_requirements_from_dir(dir: str) -> None: for req in reqs: req = req.strip() + extras = set() + if req == "" or req.startswith("#"): continue # If it's a URL, then it must be a wheel file. @@ -397,13 +400,54 @@ async def _install_requirements_from_dir(dir: str) -> None: pkg_name = re.sub(r"^.+/(.*)-\\d.*$", r"\\1", req) else: # If we got here, it's a package specification. + # https://peps.python.org/pep-0508/#examples # Remove any trailing version info: "my-package (>= 1.0.0)" -> "my-package" - pkg_name = re.sub(r"([a-zA-Z0-9._-]+)(.*)", r"\\1", req).strip() + # or "shiny [theme] == 1.2.0" -> "shiny[theme]" + pkg_name = re.sub(r" \\[", "[", req) + pkg_name = re.sub(r"([a-zA-Z0-9._,\\[\\]-]+)(.*)", r"\\1", pkg_name).strip() + + match_extras = re.match(r"([^\\[]+)(?:\\[(.*)\\])?", pkg_name) + if match_extras and match_extras.group(2): + pkg_name = match_extras.group(1) + extras.update({e.strip() for e in match_extras.group(2).split(",")}) - if pkg_name in micropip.list(): + if pkg_name not in micropip.list(): + print(f"Installing {pkg_name} ...") + await micropip.install(pkg_name) + + if len(extras) == 0: continue - print(f"Installing {req} ...") - await micropip.install(req) + else: + # Because micropip.install() doesn't install extras, we have to + # find the package requirements of each extra and install them + # if they aren't already installed. + dist = importlib.metadata.distribution(pkg_name) + + provided_extras = set(dist.metadata.get_all("Provides-Extra") or []) + valid_extras = extras & provided_extras + invalid_extras = extras - valid_extras + if len(invalid_extras): + raise ValueError( + f"Invalid extras for package {pkg_name}: {','.join(invalid_extras)}. " + f"Found in '{req}' in requirements.txt." + ) + + pkg_reqs = dist.requires or [] + + for extra in valid_extras: + # Convert requires records : 'libsass>=0.23.0; extra == "theme"' + # into just the package name: 'libsass' + extra_reqs = [ + r for r in pkg_reqs + if f'extra == "{extra}"' in str(r) + or f"extra == '{extra}'" in str(r) + ] + + for extra_req in extra_reqs: + extra_req_name = re.sub(r"([a-zA-Z0-9._,-]+)(.*)", r"\\1", extra_req).strip() + if extra_req_name not in micropip.list(): + print(f"Installing {extra_req_name} for {pkg_name}[{extra}]") + await micropip.install(extra_req_name) async def _load_packages_from_dir(dir: str) -> None: diff --git a/src/utils.ts b/src/utils.ts index 93aaa0a9..196c9c12 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -86,6 +86,10 @@ const FILE_EXTENSIONS: Record = { csv: "csv", r: "r", sql: "sql", + sass: "sass", + scss: "sass", + yaml: "yaml", + yml: "yaml", }; export function isApplePlatform(): boolean {